feat: add list_user skill

This commit is contained in:
insleker 2026-01-19 13:41:16 +08:00
parent 0db7c83733
commit 501b9f5ddd
4 changed files with 654 additions and 4 deletions

View File

@ -0,0 +1,331 @@
---
name: list_user
description: "User management and name mapping utility. Provides nickname-to-formal-name conversions, user validation, and metadata retrieval. Designed to support multiple report generation skills with consistent user information handling."
license: MIT
---
# User Manager Skill
## Overview
This skill provides centralized user management and name mapping functionality. It's designed to support report generation and other skills that need to transform user nicknames (informal names) into formal Chinese names or other user metadata.
## When to Use This Skill
Use this skill when:
- You need to map user nicknames to formal names in reports
- You want to validate user existence and metadata
- You need to filter users by team or department
- You're building report generators that require consistent user information
- You need case-insensitive user name lookup
## Key Features
- **Name Mapping**: Convert nicknames to formal names with configurable fallback behavior
- **User Metadata**: Store and retrieve user information (team, department, etc.)
- **Flexible Lookup**: Case-insensitive name matching with configurable behavior
- **Batch Operations**: Map multiple names at once
- **User Validation**: Check if users exist and identify missing entries
- **Filtering**: Query users by team or department
- **Reloadable**: Update mappings without restarting the application
## File Structure
```
.claude/skills/list_user/
├── user_manager.py # Main module with UserManager class
├── SKILL.md # This file
├── references/
│ └── user_mapping.json # User data and name mappings
└── README.md # Usage documentation
```
## User Mapping Format
The `user_mapping.json` file contains user data in the following format:
```json
{
"users": [
{
"nickname": "ben sung", # Informal/English name
"formal_name": "宋柏昆", # Formal Chinese name
"english_name": "Ben Sung", # Full English name
"team": "智能控制組", # Team assignment
"department": "軟體部" # Department
}
],
"mapping_config": {
"default_field": "formal_name", # Field returned by get_formal_name()
"case_sensitive": false, # Whether lookup is case-sensitive
"fallback_behavior": "keep_original" # 'keep_original', 'strict', or 'none'
}
}
```
## Usage Guide
### Basic Usage
```python
from user_manager import UserManager
# Initialize (uses default path if not specified)
manager = UserManager()
# Map a single nickname to formal name
formal_name = manager.get_formal_name("ben sung")
# Returns: "宋柏昆"
# Map multiple names
names = ["ben sung", "Ryan Hsueh"]
formal_names = manager.map_names(names)
# Returns: ["宋柏昆", "徐瑞恩"]
```
### Get User Information
```python
# Get complete user record
user = manager.get_user_info("ben sung")
# Returns: {
# "nickname": "ben sung",
# "formal_name": "宋柏昆",
# "english_name": "Ben Sung",
# "team": "智能控制組",
# "department": "軟體部"
# }
# Get all users
all_users = manager.get_all_users()
# Filter by team
team_users = manager.get_users_by_team("智能控制組")
# Filter by department
dept_users = manager.get_users_by_department("軟體部")
```
### Validate Users
```python
# Check if nicknames exist in mapping
found, missing = manager.validate_users(
["ben sung", "unknown person", "Ryan Hsueh"]
)
# found: ["ben sung", "Ryan Hsueh"]
# missing: ["unknown person"]
```
### Fallback Behaviors
The `get_formal_name()` method supports three fallback behaviors when a nickname is not found:
```python
# 1. Keep original (default)
name = manager.get_formal_name("unknown", fallback='keep_original')
# Returns: "unknown"
# 2. Strict mode (raise error)
try:
name = manager.get_formal_name("unknown", fallback='strict')
except ValueError as e:
print(f"User not found: {e}")
# 3. Return None
name = manager.get_formal_name("unknown", fallback='none')
# Returns: None
```
### Using the Singleton Instance
```python
from user_manager import get_default_manager
# Get the singleton instance
manager = get_default_manager()
# Use the same manager instance throughout your application
formal_name = manager.get_formal_name("ben sung")
```
## Integration with Other Skills
### Example: Integrating with week_report_gen
```python
from user_manager import get_default_manager
# In your report generation code
user_manager = get_default_manager()
# Transform participant list from nicknames to formal names
participants_nicknames = ["ben sung", "Ryan Hsueh"]
participants_formal = user_manager.map_names(participants_nicknames)
# Use in report
print(f"Team: {', '.join(participants_formal)}")
# Output: Team: 宋柏昆, 徐瑞恩
```
## Adding New Users
To add new users to the system:
1. Edit `references/user_mapping.json`
2. Add a new entry to the `users` array:
```json
{
"nickname": "new user",
"formal_name": "新用户",
"english_name": "New User",
"team": "智能控制組",
"department": "軟體部"
}
```
3. Reload the manager to pick up changes:
```python
manager.reload()
```
Or create a new instance:
```python
from user_manager import UserManager
manager = UserManager() # Loads the latest mapping
```
## Configuration Options
The `mapping_config` section in `user_mapping.json` controls lookup behavior:
| Option | Type | Values | Description |
|--------|------|--------|-------------|
| `default_field` | string | Any user field | Which field to return in `get_formal_name()` |
| `case_sensitive` | boolean | true/false | Whether nickname matching is case-sensitive |
| `fallback_behavior` | string | 'keep_original', 'strict', 'none' | Default behavior when user not found |
### Example Configurations
**Chinese-focused** (current default):
```json
"mapping_config": {
"default_field": "formal_name",
"case_sensitive": false,
"fallback_behavior": "keep_original"
}
```
**English-focused**:
```json
"mapping_config": {
"default_field": "english_name",
"case_sensitive": false,
"fallback_behavior": "keep_original"
}
```
**Strict validation**:
```json
"mapping_config": {
"default_field": "formal_name",
"case_sensitive": true,
"fallback_behavior": "strict"
}
```
## API Reference
### UserManager Class
#### Constructor
```python
UserManager(mapping_file: Optional[str] = None)
```
Initialize UserManager. If `mapping_file` is None, uses default location.
#### Methods
**`get_formal_name(nickname: str, fallback: str = 'keep_original') -> str`**
- Get formal name for a nickname
- Fallback options: 'keep_original', 'strict', 'none'
**`get_user_info(nickname: str) -> Optional[Dict]`**
- Get complete user record
**`map_names(nicknames: List[str], fallback: str = 'keep_original') -> List[str]`**
- Map multiple nicknames to formal names
**`get_all_users() -> List[Dict]`**
- Return all user records
**`get_users_by_team(team: str) -> List[Dict]`**
- Filter users by team
**`get_users_by_department(department: str) -> List[Dict]`**
- Filter users by department
**`validate_users(nicknames: List[str]) -> Tuple[List[str], List[str]]`**
- Validate nicknames, return (found, missing)
**`reload()`**
- Reload mappings from file
### Module Functions
**`get_default_manager(mapping_file: Optional[str] = None) -> UserManager`**
- Get or create singleton instance
## Error Handling
The skill handles several error scenarios:
| Error | When | Handling |
|-------|------|----------|
| FileNotFoundError | Mapping file not found | Raised immediately |
| JSONDecodeError | Invalid JSON syntax | Raised with message |
| User not found | Nickname lookup fails | Depends on fallback setting |
Example:
```python
try:
manager = UserManager()
except FileNotFoundError:
print("User mapping file is missing!")
except ValueError:
print("User mapping file has invalid JSON!")
```
## Testing
Run the module directly to test basic functionality:
```bash
cd .claude/skills/list_user
python user_manager.py
```
This will display:
- Name mapping examples
- User information retrieval
- Team filtering
## Future Enhancements
Potential improvements:
- Database backend (SQLite, PostgreSQL) instead of JSON
- LDAP/Active Directory integration for enterprise environments
- Role-based access control (RBAC) per user
- User status tracking (active, inactive, etc.)
- Export/import utilities for user data
- Audit logging for user lookup operations
## License
MIT License - See LICENSE file for details

View File

@ -0,0 +1,51 @@
{
"users": [
{
"nickname": "ben sung",
"formal_name": "宋柏昆",
"english_name": "Ben Sung",
"team": "智能控制組",
"department": "軟體部"
},
{
"nickname": "Ryan Hsueh",
"formal_name": "薛裕弘",
"english_name": "Ryan Hsueh",
"team": "智能控制組",
"department": "軟體部"
},
{
"nickname": "傑 羅",
"formal_name": "羅傑",
"english_name": "Jie Luo",
"team": "智能控制組",
"department": "軟體部"
},
{
"nickname": "kenta kinoshita",
"formal_name": "楊博軒",
"english_name": "Kenta Kinoshita",
"team": "智能控制組",
"department": "軟體部"
},
{
"nickname": "暉庭 洪",
"formal_name": "洪暉庭",
"english_name": "Hong Hui-Ting",
"team": "智能控制組",
"department": "軟體部"
},
{
"nickname": "庭宇 施",
"formal_name": "施庭宇",
"english_name": "Shi Ting-Yu",
"team": "智能控制組",
"department": "軟體部"
}
],
"mapping_config": {
"default_field": "formal_name",
"case_sensitive": false,
"fallback_behavior": "keep_original"
}
}

View File

@ -0,0 +1,226 @@
"""
User Manager - Handles user name mapping and lookup
This module provides utilities for managing user information, including
nickname-to-formal-name mappings, user validation, and metadata retrieval.
"""
import json
import os
from typing import Dict, List, Optional, Tuple
class UserManager:
"""
Manages user information and name mappings.
Supports:
- Looking up formal names from nicknames
- Validating user existence
- Retrieving user metadata (team, department, etc.)
- Case-insensitive name matching
"""
def __init__(self, mapping_file: Optional[str] = None):
"""
Initialize UserManager with user mapping data.
Args:
mapping_file: Path to user_mapping.json. If None, uses default location.
"""
if mapping_file is None:
# Use default location relative to this script
script_dir = os.path.dirname(os.path.abspath(__file__))
mapping_file = os.path.join(script_dir, 'references', 'user_mapping.json')
self.mapping_file = mapping_file
self.users: List[Dict] = []
self.nickname_index: Dict[str, Dict] = {}
self.config: Dict = {}
self._load_mapping()
def _load_mapping(self):
"""Load user mapping from JSON file."""
if not os.path.exists(self.mapping_file):
raise FileNotFoundError(f"User mapping file not found: {self.mapping_file}")
try:
with open(self.mapping_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self.users = data.get('users', [])
self.config = data.get('mapping_config', {})
# Build nickname index for fast lookup
self._build_index()
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in user mapping file: {e}")
def _build_index(self):
"""Build case-insensitive index of nicknames."""
case_sensitive = self.config.get('case_sensitive', False)
for user in self.users:
nickname = user.get('nickname', '')
key = nickname if case_sensitive else nickname.lower()
self.nickname_index[key] = user
def get_formal_name(self, nickname: str, fallback: str = 'keep_original') -> str:
"""
Get formal name for a given nickname.
Args:
nickname: The nickname to look up
fallback: Behavior if nickname not found:
- 'keep_original': Return original nickname
- 'strict': Raise ValueError
- 'none': Return None
Returns:
Formal name, or fallback value if not found
Raises:
ValueError: If fallback='strict' and nickname not found
"""
case_sensitive = self.config.get('case_sensitive', False)
key = nickname if case_sensitive else nickname.lower()
if key in self.nickname_index:
user = self.nickname_index[key]
field = self.config.get('default_field', 'formal_name')
return user.get(field, nickname)
# Handle fallback
if fallback == 'strict':
raise ValueError(f"User nickname not found: {nickname}")
elif fallback == 'none':
return None
else: # 'keep_original'
return nickname
def get_user_info(self, nickname: str) -> Optional[Dict]:
"""
Get complete user information.
Args:
nickname: The nickname to look up
Returns:
Dict with user info, or None if not found
"""
case_sensitive = self.config.get('case_sensitive', False)
key = nickname if case_sensitive else nickname.lower()
return self.nickname_index.get(key)
def map_names(self, nicknames: List[str], fallback: str = 'keep_original') -> List[str]:
"""
Map multiple nicknames to formal names.
Args:
nicknames: List of nicknames
fallback: Behavior if nickname not found (see get_formal_name)
Returns:
List of formal names
"""
return [self.get_formal_name(nick, fallback) for nick in nicknames]
def get_all_users(self) -> List[Dict]:
"""Get all user records."""
return self.users.copy()
def get_users_by_team(self, team: str) -> List[Dict]:
"""Get all users in a specific team."""
return [u for u in self.users if u.get('team') == team]
def get_users_by_department(self, department: str) -> List[Dict]:
"""Get all users in a specific department."""
return [u for u in self.users if u.get('department') == department]
def validate_users(self, nicknames: List[str]) -> Tuple[List[str], List[str]]:
"""
Validate that nicknames exist in the mapping.
Args:
nicknames: List of nicknames to validate
Returns:
Tuple of (found_nicknames, missing_nicknames)
"""
case_sensitive = self.config.get('case_sensitive', False)
found = []
missing = []
for nick in nicknames:
key = nick if case_sensitive else nick.lower()
if key in self.nickname_index:
found.append(nick)
else:
missing.append(nick)
return found, missing
def reload(self):
"""Reload user mapping from file."""
self._load_mapping()
# Singleton instance (optional - for convenience)
_default_manager: Optional[UserManager] = None
def get_default_manager(mapping_file: Optional[str] = None) -> UserManager:
"""
Get or create the default UserManager instance.
Args:
mapping_file: Path to user_mapping.json (only used on first call)
Returns:
UserManager instance
"""
global _default_manager
if _default_manager is None:
_default_manager = UserManager(mapping_file)
return _default_manager
if __name__ == "__main__":
# Example usage
manager = UserManager()
print("=" * 60)
print("USER MANAGER - EXAMPLE")
print("=" * 60)
print()
# Test name lookup
test_names = ["ben sung", "Ryan Hsueh", "傑 羅"]
print("Name Mapping Test:")
print("-" * 60)
for nickname in test_names:
formal = manager.get_formal_name(nickname)
print(f" {nickname:20}{formal}")
print()
# Test user info
print("User Information:")
print("-" * 60)
user_info = manager.get_user_info("ben sung")
if user_info:
for key, value in user_info.items():
print(f" {key:20} : {value}")
print()
# Test team lookup
print("Users by Team (智能控制組):")
print("-" * 60)
team_users = manager.get_users_by_team("智能控制組")
for user in team_users:
print(f" {user.get('nickname'):20} ({user.get('formal_name')})")
print()
print("=" * 60)

View File

@ -10,6 +10,22 @@ from datetime import datetime
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from copy import copy
import os
import sys
# Add list_user skill to path for user mapping
# Current structure: .../week_report_gen/generate_report.py
# Target: .../list_user/user_manager.py
script_dir = os.path.dirname(os.path.abspath(__file__))
skills_dir = os.path.dirname(script_dir) # .../skills
list_user_path = os.path.join(skills_dir, 'list_user')
if list_user_path not in sys.path:
sys.path.insert(0, list_user_path)
try:
from user_manager import UserManager
USER_MANAGER_AVAILABLE = True
except ImportError as e:
USER_MANAGER_AVAILABLE = False
# Style constants for consistent coloring
@ -156,10 +172,14 @@ def aggregate_work_hours(df):
return summary, start_date, end_date
def format_for_weekly_report(summary_df):
def format_for_weekly_report(summary_df, user_manager=None):
"""
Transform aggregated data into weekly report format.
Group entries by project and list participants.
Args:
summary_df: Aggregated work data
user_manager: Optional UserManager instance for name mapping
"""
report_data = []
@ -175,7 +195,14 @@ def format_for_weekly_report(summary_df):
for _, row in project_data.iterrows():
user_name = row['使用者']
user_hours = row['工時']
participants.append(f"{user_name}")
# Map nickname to formal name if user manager is available
if user_manager:
formal_name = user_manager.get_formal_name(user_name, fallback='keep_original')
participants.append(formal_name)
else:
participants.append(f"{user_name}")
total_hours += user_hours
if '活動' in row and row['活動'] and row['活動'] != '-' and pd.notna(row['活動']):
@ -206,10 +233,11 @@ def format_for_weekly_report(summary_df):
'下周計畫': None
})
return pd.DataFrame(report_data)
def generate_weekly_report(input_file, output_file, template_file=None, team_name="智能控制組"):
def generate_weekly_report(input_file, output_file, template_file=None, team_name="智能控制組", use_user_mapping=True):
"""
Main function to generate weekly report from cost report.
@ -218,12 +246,26 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam
output_file: Path to output weekly report Excel file
template_file: Optional path to template file (if not using default)
team_name: Team name for sheet title (default: "智能控制組")
use_user_mapping: Whether to apply user name mapping (default: True)
Returns:
Tuple of (output_file_path, report_dataframe)
"""
# Initialize user manager for name mapping
user_manager = None
if use_user_mapping and USER_MANAGER_AVAILABLE:
try:
user_manager = UserManager()
print("✓ User name mapping enabled")
except Exception as e:
print(f"⚠ Warning: Could not load user mapping: {e}")
print(" Continuing without user name mapping...")
elif use_user_mapping and not USER_MANAGER_AVAILABLE:
print("⚠ Warning: list_user skill not available, skipping user name mapping")
print(f"Reading cost report from: {input_file}")
# Read and process input
df = read_cost_report(input_file)
print(f"Loaded {len(df)} entries from cost report")
@ -232,7 +274,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)
report_df = format_for_weekly_report(summary, user_manager=user_manager)
print(f"Formatted {len(report_df)} project entries for report")
# Load template or create new workbook