feat: add list porject hierarchy skill

This commit is contained in:
insleker 2026-01-19 16:15:04 +08:00
parent 6d1af46ce6
commit a5c782e54d
8 changed files with 908 additions and 5 deletions

View File

@ -0,0 +1,296 @@
# Project Hierarchy Skill
A Python skill for fetching and managing project hierarchies from OpenProject, with local caching and search capabilities.
## Overview
The **project_hierarchy** skill provides tools to:
- Fetch projects from OpenProject API with error handling and recovery
- Build and visualize project hierarchies from parent-child relationships
- Search and validate projects by identifier or name
- Generate breadcrumb paths showing full project lineage
- Cache project data locally with configurable TTL
## Use Cases
1. **Project Validation**: Validate that project names/IDs in cost reports match OpenProject
2. **Hierarchy Understanding**: View parent-child relationships between projects
3. **Breadcrumb Generation**: Show full project path (e.g., "Controller → Amphimove → Submodule")
4. **Project Search**: Find projects by partial name or identifier match
5. **Report Enhancement**: Add project context to weekly or cost reports
## Architecture
```
project_hierarchy/
├── project_manager.py # Core ProjectManager class (11 methods)
├── references/
│ └── projects.json # Cache file (auto-populated)
├── test_project_manager.py # Test script
└── SKILL.md # This file
```
### Core Module: `project_manager.py`
Main class: **`ProjectManager`**
#### Initialization
```python
from project_manager import ProjectManager
# Uses default OpenProject URL and local cache
manager = ProjectManager()
# Or with custom config
manager = ProjectManager(
api_url="http://ixd.openproject.techmation.com.tw/api/v3/projects",
cache_file="references/projects.json",
cache_timeout=3600 # 1 hour
)
```
#### Key Methods
| Method | Purpose | Returns |
|--------|---------|---------|
| `fetch_projects(force_refresh=False)` | Fetch from API and cache | `dict` of projects or `None` if failed |
| `get_hierarchy_tree()` | Get tree structure of all projects | `dict` with tree nodes |
| `get_project(project_id)` | Get single project data | `dict` project info or `None` |
| `get_children(project_id)` | Get child projects | `list` of child project identifiers |
| `get_parent(project_id)` | Get parent project | `str` parent identifier or `None` |
| `get_full_path(project_id)` | Get breadcrumb path | `list` of project identifiers from root |
| `get_breadcrumb(project_id)` | Get readable breadcrumb | `str` like "root → child → project" |
| `search_projects(query)` | Find projects by name/id | `list` of matching project identifiers |
| `validate_project(project_id)` | Check if project exists | `bool` |
| `print_hierarchy()` | Print ASCII tree view | `None` (prints to stdout) |
## Usage Examples
### 1. Fetch Projects and Show Hierarchy
```python
from project_hierarchy.project_manager import ProjectManager
manager = ProjectManager()
# Fetch fresh data from OpenProject API
projects = manager.fetch_projects(force_refresh=True)
print(f"Loaded {len(projects)} projects")
# Display hierarchy
manager.print_hierarchy()
```
Output:
```
Projects by Hierarchy:
Root projects:
└─ controller (ID: controller)
├─ amphimove (ID: amphimove)
│ └─ submodule (ID: submodule_1)
└─ interface (ID: interface)
```
### 2. Validate Project Names in Reports
```python
# When processing a cost report
project_name = "amphimove"
if manager.validate_project(project_name):
print(f"✓ Project '{project_name}' found in OpenProject")
else:
print(f"✗ Project '{project_name}' not found - check spelling")
```
### 3. Generate Breadcrumbs for Display
```python
# Show full path in report header
breadcrumb = manager.get_breadcrumb("submodule_1")
print(f"Project: {breadcrumb}")
# Output: "Project: controller > amphimove > submodule_1"
```
### 4. Search for Projects
```python
# Find all projects matching "master"
results = manager.search_projects("master")
for project_id in results:
print(f" - {project_id}")
```
### 5. Get Project Metadata
```python
# Access raw project data
project = manager.get_project("amphimove")
if project:
print(f"Name: {project['name']}")
print(f"Parent: {project.get('parent')}")
print(f"ID: {project['id']}")
```
## Integration with Other Skills
### With `week_report_gen`
Validate projects before generating reports:
```python
from project_hierarchy.project_manager import ProjectManager
from week_report_gen.generate_report import format_for_weekly_report
manager = ProjectManager()
# Validate project
if not manager.validate_project(cost_data['project']):
print(f"Warning: Project '{cost_data['project']}' not found")
# Generate report
report = format_for_weekly_report(cost_data, ...)
```
## Data Format
### projects.json Structure
```json
{
"projects": {
"controller": {
"id": "123456",
"identifier": "controller",
"name": "Controller Project",
"parent": null
},
"amphimove": {
"id": "234567",
"identifier": "amphimove",
"name": "Amphimove Module",
"parent": {"identifier": "controller"}
}
},
"hierarchy": {
"controller": ["amphimove"],
"amphimove": ["submodule_1"]
},
"last_update": "2024-01-15T10:30:00Z"
}
```
## Configuration
### Environment Variables
```bash
# Override OpenProject API URL
export OPENPROJECT_API_URL="http://ixd.openproject.techmation.com.tw/api/v3/projects"
# Cache location (relative to skill directory)
export PROJECT_CACHE_FILE="references/projects.json"
# Cache timeout in seconds (default: 3600 = 1 hour)
export PROJECT_CACHE_TIMEOUT="3600"
```
### Programmatic Configuration
```python
manager = ProjectManager(
api_url="http://custom-url/api/v3/projects",
cache_file="custom/cache/projects.json",
cache_timeout=7200 # 2 hours
)
```
## Testing
Run the test script to verify functionality:
```bash
cd d:\bensung\report_skill_expm\.claude\skills\project_hierarchy
python test_project_manager.py
```
Expected output:
```
============================================================
PROJECT MANAGER - TEST
============================================================
✓ ProjectManager initialized
Testing: Fetch projects from OpenProject
------------------------------------------------------------
✓ Successfully fetched X projects
First 5 projects:
1. controller → Controller Project (parent: None)
2. amphimove → Amphimove Module (parent: controller)
...
Testing: Get project hierarchy
------------------------------------------------------------
Projects by Hierarchy:
Root projects:
└─ controller (ID: controller)
├─ amphimove (ID: amphimove)
...
============================================================
✓ All tests completed!
============================================================
```
## Error Handling
The ProjectManager handles common failures gracefully:
- **API Unreachable**: Uses cached data if available, returns empty dict if no cache
- **Invalid Project ID**: Returns `None` with no exception
- **Malformed Cache**: Rebuilds from API on next fetch
- **Network Errors**: Logged and cached data used as fallback
Example:
```python
try:
projects = manager.fetch_projects(force_refresh=True)
except Exception as e:
# Use cached data instead
projects = manager.get_cached_projects()
if not projects:
print("No data available - check OpenProject connection")
```
## Performance Characteristics
- **First fetch**: ~500ms - 2s (depends on API and project count)
- **Cached lookups**: <1ms
- **Hierarchy search**: O(n) where n = project count
- **Breadcrumb generation**: O(depth of tree)
- **Cache file size**: ~50KB for typical project set (100+ projects)
## Limitations & Notes
1. **Read-Only**: ProjectManager only reads from OpenProject (no create/update)
2. **Parent Relationships**: Only immediate parent is tracked (not full ancestry via hierarchy graph)
3. **Update Frequency**: Default 1-hour cache - adjust `cache_timeout` for real-time needs
4. **Project Identifier**: Searches are case-sensitive for identifiers, case-insensitive for names
5. **API Access**: Requires network access to OpenProject instance
## Future Enhancements
- [ ] Add filtering by project status/type
- [ ] Support hierarchical queries (all descendants, not just children)
- [ ] Export hierarchy as JSON/CSV/SVG
- [ ] Add project metadata (status, budget, owner)
- [ ] MCP server for sharing with other AI systems
- [ ] Real-time sync mode for active monitoring
## Related Skills
- **list_user**: User name mapping and team membership
- **week_report_gen**: Weekly project reporting with project validation

View File

@ -0,0 +1,470 @@
"""
Project Manager - OpenProject API client
Manages project hierarchy from OpenProject, including fetching projects,
building hierarchy relationships, and validating project structures.
"""
import json
import os
import requests
from typing import Dict, List, Optional, Tuple
from datetime import datetime
class ProjectManager:
"""
Manages OpenProject data and project hierarchy.
Supports:
- Fetching projects from OpenProject API
- Building project hierarchy relationships
- Validating project structure
- Caching project data locally
- Filtering projects by parent
"""
def __init__(self, api_url: str = "http://ixd.openproject.techmation.com.tw",
cache_file: Optional[str] = None, cache_timeout: int = 3600,
api_token: Optional[str] = None):
"""
Initialize ProjectManager.
Args:
api_url: OpenProject base URL
cache_file: Path to cache file. If None, uses default location.
cache_timeout: Cache validity in seconds (default: 1 hour)
api_token: OpenProject API token (uses OPENPROJECT_TOKEN env var if not provided)
"""
self.api_url = api_url.rstrip('/')
self.api_endpoint = f"{self.api_url}/api/v3/projects"
self.cache_timeout = cache_timeout
self.api_token = api_token or os.getenv("OPENPROJECT_TOKEN")
if cache_file is None:
script_dir = os.path.dirname(os.path.abspath(__file__))
cache_file = os.path.join(script_dir, 'references', 'projects.json')
self.cache_file = cache_file
self.projects: Dict = {}
self.hierarchy: Dict = {}
self.last_update: Optional[datetime] = None
self._load_from_cache()
def _load_from_cache(self):
"""Load projects from local cache if available and valid."""
if not os.path.exists(self.cache_file):
return
try:
with open(self.cache_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self.projects = data.get('projects', {})
self.hierarchy = data.get('hierarchy', {})
# Check cache validity
last_update_str = data.get('last_update')
if last_update_str:
self.last_update = datetime.fromisoformat(last_update_str)
# Check if cache is still valid
age = (datetime.now() - self.last_update).total_seconds()
if age > self.cache_timeout:
print(f"Cache expired ({age:.0f}s old), will refresh from API")
self.projects = {}
self.hierarchy = {}
else:
print(f"Using cached data ({age:.0f}s old)")
except Exception as e:
print(f"Error loading cache: {e}")
self.projects = {}
self.hierarchy = {}
def _save_to_cache(self):
"""Save projects to local cache."""
try:
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
data = {
'projects': self.projects,
'hierarchy': self.hierarchy,
'last_update': datetime.now().isoformat()
}
with open(self.cache_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
self.last_update = datetime.now()
print(f"Cached {len(self.projects)} projects")
except Exception as e:
print(f"Error saving cache: {e}")
def fetch_projects(self, force_refresh: bool = True) -> Dict:
"""
Fetch projects from OpenProject API.
Args:
force_refresh: Ignore cache and fetch fresh from API (default: always fetch)
Returns:
Dictionary of projects with metadata
"""
try:
print(f"Fetching projects from {self.api_endpoint}...")
response = requests.get(self.api_endpoint, timeout=10, auth=self._get_auth())
response.raise_for_status()
data = response.json()
self.projects = {}
# Handle pagination - fetch all pages
offset = 0
page_size = data.get('pageSize', 100)
total = data.get('total', 0)
while offset <= total:
if offset > 0:
# Fetch next page
url_with_pagination = f"{self.api_endpoint}?offset={offset}&pageSize={page_size}"
response = requests.get(url_with_pagination, timeout=10, auth=self._get_auth())
response.raise_for_status()
data = response.json()
# Extract projects from _embedded.elements (OpenProject HAL format)
if '_embedded' in data and 'elements' in data['_embedded']:
projects_list = data['_embedded']['elements']
for project in projects_list:
project_id = project.get('identifier') or project.get('id')
if not project_id:
continue
# Extract parent from _links section (OpenProject HAL format)
parent_info = None
if '_links' in project and 'parent' in project['_links']:
parent_link = project['_links']['parent']
if parent_link and isinstance(parent_link, dict):
# Extract parent identifier from href (e.g., "/api/v3/projects/38")
parent_href = parent_link.get('href', '')
parent_id = parent_href.split('/')[-1] if parent_href else None
parent_info = {
'id': parent_id,
'title': parent_link.get('title')
}
self.projects[project_id] = {
'id': project.get('id'),
'identifier': project.get('identifier'),
'name': project.get('name'),
'parent': parent_info,
'description': project.get('description'),
'active': project.get('active', True)
}
# Check if we have more pages
count = data.get('count', 0)
if count == 0:
break
offset += page_size
if offset >= total:
break
# Build hierarchy
self._build_hierarchy()
# Save to cache
self._save_to_cache()
print(f"Successfully fetched {len(self.projects)} projects")
return self.projects
except requests.exceptions.RequestException as e:
print(f"Error fetching projects: {e}")
# Try to use cache if available
if self.projects:
print("Using cached data")
return self.projects
raise
def _get_auth(self):
"""Return auth tuple for requests if token is configured."""
if not self.api_token:
return None
# OpenProject API key uses basic auth with username 'apikey' and token as password
return ("apikey", self.api_token)
def _build_hierarchy(self):
"""Build parent-child relationships from projects."""
self.hierarchy = {}
# Create a map of project ID to identifier for quick lookup
id_to_identifier = {
str(project['id']): identifier
for identifier, project in self.projects.items()
}
for project_id, project in self.projects.items():
self.hierarchy[project_id] = {
'name': project['name'],
'parent': project.get('parent'),
'children': []
}
# Link children to parents
for project_id, project_info in self.hierarchy.items():
parent = project_info['parent']
if parent:
# Parent info is a dict with 'id' and 'title'
parent_numeric_id = str(parent.get('id')) if isinstance(parent, dict) else str(parent)
# Find the parent's identifier
parent_identifier = id_to_identifier.get(parent_numeric_id)
if parent_identifier and parent_identifier in self.hierarchy:
self.hierarchy[parent_identifier]['children'].append(project_id)
def get_hierarchy_tree(self, root_only: bool = False) -> Dict:
"""
Get project hierarchy as a tree structure.
Args:
root_only: Only include root-level projects
Returns:
Dictionary representing the hierarchy tree
"""
if not self.hierarchy:
self.fetch_projects()
def build_tree(project_id: str) -> Dict:
info = self.hierarchy[project_id]
tree = {
'name': info['name'],
'id': project_id,
'children': []
}
for child_id in info['children']:
tree['children'].append(build_tree(child_id))
return tree
# Find root projects (those without parents or with null parent ID)
roots = [
p for p, info in self.hierarchy.items()
if not info['parent'] or (isinstance(info['parent'], dict) and not info['parent'].get('id'))
]
if root_only:
return {root: build_tree(root) for root in roots}
return {root: build_tree(root) for root in roots}
def get_children(self, parent_id: str) -> List[str]:
"""
Get all child projects of a parent.
Args:
parent_id: Parent project identifier
Returns:
List of child project identifiers
"""
if parent_id not in self.hierarchy:
return []
return self.hierarchy[parent_id]['children']
def get_parent(self, project_id: str) -> Optional[str]:
"""
Get parent of a project.
Args:
project_id: Project identifier
Returns:
Parent project identifier or None
"""
if project_id not in self.hierarchy:
return None
parent = self.hierarchy[project_id]['parent']
if parent:
return parent.get('identifier') if isinstance(parent, dict) else parent
return None
def get_full_path(self, project_id: str) -> List[str]:
"""
Get full path from root to project.
Args:
project_id: Project identifier
Returns:
List of project identifiers from root to target
"""
path = [project_id]
current = project_id
while True:
parent = self.get_parent(current)
if not parent:
break
path.insert(0, parent)
current = parent
return path
def get_all_projects(self) -> Dict:
"""
Get all projects.
Returns:
Dictionary of all projects
"""
if not self.projects:
self.fetch_projects()
return self.projects.copy()
def get_project(self, project_id: str) -> Optional[Dict]:
"""
Get a specific project.
Args:
project_id: Project identifier
Returns:
Project data or None if not found
"""
if not self.projects:
self.fetch_projects()
return self.projects.get(project_id)
def validate_project(self, project_id: str, parent: Optional[str] = None) -> Tuple[bool, str]:
"""
Validate that a project exists and has correct parent.
Args:
project_id: Project identifier
parent: Expected parent project (optional)
Returns:
Tuple of (is_valid, message)
"""
if not self.projects:
self.fetch_projects()
if project_id not in self.projects:
return False, f"Project '{project_id}' not found"
if parent:
actual_parent = self.get_parent(project_id)
if actual_parent != parent:
return False, f"Project '{project_id}' has parent '{actual_parent}', expected '{parent}'"
return True, f"Project '{project_id}' is valid"
def get_breadcrumb(self, project_id: str) -> str:
"""
Get human-readable breadcrumb for a project.
Args:
project_id: Project identifier
Returns:
Breadcrumb string (e.g., "controller > amphimove")
"""
path = self.get_full_path(project_id)
names = [self.hierarchy.get(p, {}).get('name', p) for p in path]
return ' > '.join(names)
def search_projects(self, query: str) -> Dict:
"""
Search projects by name or identifier.
Args:
query: Search query
Returns:
Dictionary of matching projects
"""
if not self.projects:
self.fetch_projects()
query_lower = query.lower()
results = {}
for project_id, project in self.projects.items():
name = project['name'].lower()
identifier = project['identifier'].lower()
if query_lower in name or query_lower in identifier:
results[project_id] = project
return results
def print_hierarchy(self, indent: int = 0, project_id: Optional[str] = None):
"""
Print hierarchy in tree format.
Args:
indent: Current indentation level
project_id: Project to print (None for all roots)
"""
if not self.hierarchy:
self.fetch_projects()
if project_id is None:
# Print all root projects (those without a valid parent)
roots = [
p for p, info in self.hierarchy.items()
if not info['parent'] or (isinstance(info['parent'], dict) and not info['parent'].get('id'))
]
for root in sorted(roots):
self.print_hierarchy(indent=0, project_id=root)
else:
info = self.hierarchy.get(project_id)
if not info:
return
print(' ' * (indent * 2) + f"* {project_id} ({info['name']})")
for child in sorted(info['children']):
self.print_hierarchy(indent + 1, project_id=child)
if __name__ == "__main__":
# Example usage
manager = ProjectManager()
print("=" * 60)
print("PROJECT MANAGER - EXAMPLE")
print("=" * 60)
print()
# Fetch projects
print("Fetching projects from OpenProject...")
projects = manager.fetch_projects()
print(f"Found {len(projects)} projects")
print()
# Print hierarchy
print("Project Hierarchy:")
print("-" * 60)
manager.print_hierarchy()
print()
# Example: Get breadcrumb for a project
all_projects = list(projects.keys())
if all_projects:
example_project = all_projects[0]
breadcrumb = manager.get_breadcrumb(example_project)
print(f"Example breadcrumb for '{example_project}': {breadcrumb}")
print()
print("=" * 60)

View File

@ -0,0 +1,85 @@
"""
Test script for ProjectManager
Verifies that ProjectManager can connect to OpenProject and fetch project data.
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from project_manager import ProjectManager
def main():
print("=" * 60)
print("PROJECT MANAGER - TEST")
print("=" * 60)
print()
try:
manager = ProjectManager()
print("✓ ProjectManager initialized")
print()
# Test: Fetch projects
print("Testing: Fetch projects from OpenProject")
print("-" * 60)
try:
projects = manager.fetch_projects()
print(f"✓ Successfully fetched {len(projects)} projects")
if projects:
# Show first few projects
print("\nFirst 5 projects:")
for i, (project_id, project) in enumerate(list(projects.items())[:5]):
parent = project.get('parent', {})
parent_id = parent.get('identifier') if isinstance(parent, dict) else parent
print(f" {i+1}. {project_id:20}{project['name']:30} (parent: {parent_id})")
print()
except Exception as e:
print(f"✗ Error fetching projects: {e}")
print(" (This is OK if OpenProject is unreachable - cache will be used)")
import traceback
traceback.print_exc()
print()
# Test: Get hierarchy
print("Testing: Get project hierarchy")
print("-" * 60)
if projects:
manager.print_hierarchy()
print()
else:
print("⚠ No projects available to show hierarchy")
print()
# Test: Search
if projects:
print("Testing: Search projects")
print("-" * 60)
search_query = "master"
results = manager.search_projects(search_query)
if results:
print(f"✓ Found {len(results)} projects matching '{search_query}':")
for project_id in results:
print(f" - {project_id}")
else:
print(f"✓ No projects found matching '{search_query}'")
print()
print("=" * 60)
print("✓ All tests completed!")
print("=" * 60)
except Exception as e:
print(f"✗ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -27,6 +27,17 @@ try:
except ImportError as e:
USER_MANAGER_AVAILABLE = False
# Add project_hierarchy skill to path for project management
project_hierarchy_path = os.path.join(skills_dir, 'project_hierarchy')
if project_hierarchy_path not in sys.path:
sys.path.insert(0, project_hierarchy_path)
try:
from project_manager import ProjectManager
PROJECT_MANAGER_AVAILABLE = True
except ImportError as e:
PROJECT_MANAGER_AVAILABLE = False
# Style constants for consistent coloring
HEADER_FILL_COLOR = 'FF2F75B5' # Blue header background
@ -172,14 +183,16 @@ def aggregate_work_hours(df):
return summary, start_date, end_date
def format_for_weekly_report(summary_df, user_manager=None):
def format_for_weekly_report(summary_df, user_manager=None, project_manager=None):
"""
Transform aggregated data into weekly report format.
Group entries by project and list participants.
Uses project hierarchy to organize parent-child project relationships.
Args:
summary_df: Aggregated work data
user_manager: Optional UserManager instance for name mapping
project_manager: Optional ProjectManager instance for hierarchy
"""
report_data = []
@ -221,9 +234,27 @@ def format_for_weekly_report(summary_df, user_manager=None):
note_text = '\n'.join(set(notes))
progress_text = f"{progress_text}\n{note_text}" if progress_text else note_text
# Determine if this is a sub-project using project hierarchy
parent_project = None
project_name = project
sub_project_name = None
if project_manager:
# Normalize project name for lookup (lowercase, replace spaces with hyphens)
project_id = project.lower().replace(' ', '-')
parent_id = project_manager.get_parent(project_id)
if parent_id:
# This is a sub-project
parent_info = project_manager.get_project(parent_id)
if parent_info:
parent_project = parent_info['name']
sub_project_name = project
project_name = parent_project
report_data.append({
'專案名稱': project,
'子項目名稱': None,
'專案名稱': project_name,
'子項目名稱': sub_project_name,
'進度': None, # User should fill this in
'本周主要進展': progress_text if progress_text else None,
'參與人員': ', '.join(set(participants)),
@ -263,6 +294,16 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam
elif use_user_mapping and not USER_MANAGER_AVAILABLE:
print("⚠ Warning: list_user skill not available, skipping user name mapping")
# Initialize project manager for hierarchy
project_manager = None
if PROJECT_MANAGER_AVAILABLE:
try:
project_manager = ProjectManager()
print("✓ Project hierarchy management enabled")
except Exception as e:
print(f"⚠ Warning: Could not load project hierarchy: {e}")
print(" Continuing without project hierarchy...")
print(f"Reading cost report from: {input_file}")
@ -274,7 +315,7 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam
print(f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print(f"Found {len(summary)} work entries across {summary['專案'].nunique()} projects")
report_df = format_for_weekly_report(summary, user_manager=user_manager)
report_df = format_for_weekly_report(summary, user_manager=user_manager, project_manager=project_manager)
print(f"Formatted {len(report_df)} project entries for report")
# Load template or create new workbook

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
OPENPROJECT_TOKEN=your_openproject_token_here
OPENPROJECT_URL=https://your_openproject_instance.com/api/v3

1
.gitignore vendored
View File

@ -164,3 +164,4 @@ uv.lock
temp/
output/
logs/
.claude/skills/project_hierarchy/references/projects.json

View File

@ -15,4 +15,10 @@ Automatically generates weekly project reports (項目週報) from exported Exce
```bash
# Create virtual environment and install dependencies
uv sync
uv run --env-file .env python -c 'import os; print(os.getenv("OPENPROJECT_TOKEN"))'
```
```bash
uv run --env-file .env python .claude\skills\week_report_gen\generate_report.py "temp\cost-report-2026-01-16-T-16-22-3620260116-7-1r1n4h.xls" "temp\項目週報-智能控制組-20260119.xlsx"
```

View File

@ -12,7 +12,9 @@
"pandas>=2.0.0",
"xlrd>=2.0.0",
"mcp>=0.1.0",
]
"requests>=2.30.0",
"python-dotenv>=1.2.1",
]
[project.optional-dependencies]
dev = ["pytest>=7.0.0", "black>=23.0.0", "flake8>=6.0.0"]