Skill: youyoude/feishu-task (version 1.0.0)

--- name: feishu-task description: | 飞书任务管理 - 创建、查询、更新和删除飞书任务。Use when user mentions 飞书任务、创建任务、指派任务、任务管理。 Triggers: - "创建一个飞书任务" - "帮我在飞书里添加任务" - "查看我的飞书任务" - "更新/修改/删除飞书任务" - "给XXX指派任务" --- # 飞书任务管理 (Feishu Task) 通过飞书开放平台 API 管理任务，支持创建、查询、更新和删除任务。 ## 前置要求 ### 1. 飞书应用权限 确保飞书应用已申请以下权限： - `task:task:write` - 创建、更新、删除任务 - `task:task:readonly` - 读取任务信息 ### 2. 环境变量 设置以下环境变量（或在调用时传入）： ```bash export FEISHU_APP_ID="cli_xxxxx" export FEISHU_APP_SECRET="xxxxxxxx" ``` ## 使用方法 ### Python 代码调用 ```python from scripts.task_client import FeishuTaskClient # 初始化客户端 client = FeishuTaskClient() # 创建任务 result = client.create_task( summary="完成月度报告", description="整理2月份数据并生成报告", due_time="2026-03-10T18:00:00+08:00", assignee_id="ou_xxxxx", priority=2 # 0=默认, 1=低, 2=中, 3=高 ) print(f"任务创建成功: {result['data']['task']['id']}") # 获取任务列表 tasks = client.list_tasks() # 获取任务详情 task = client.get_task("task_id_here") # 更新任务 client.update_task( "task_id_here", summary="新的标题", completed=True ) # 删除任务 client.delete_task("task_id_here") ``` ### 命令行使用 ```bash # 创建任务 python3 scripts/task_client.py create "完成月度报告" --due "2026-03-10T18:00:00+08:00" --assignee "ou_xxxxx" --priority 2 # 获取任务详情 python3 scripts/task_client.py get "task_id" # 列出任务 python3 scripts/task_client.py list # 更新任务 python3 scripts/task_client.py update "task_id" --completed true # 删除任务 python3 scripts/task_client.py delete "task_id" ``` ## API 参数说明 ### 创建任务 (create_task) | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | summary | str | ✅ | 任务标题 | | description | str | ❌ | 任务描述 | | due_time | str | ❌ | 截止时间 (ISO 8601格式) | | assignee_id | str | ❌ | 执行者用户ID (ou_xxxxx) | | follower_ids | list | ❌ | 关注者用户ID列表 | | priority | int | ❌ | 优先级: 0=默认, 1=低, 2=中, 3=高 | ### 时间格式示例 - `2026-03-10T18:00:00+08:00` - 北京时间 - `2026-03-10T10:00:00Z` - UTC时间 ### 获取用户ID 用户ID (open_id) 格式为 `ou_xxxxx`，可以通过以下方式获取： 1. 飞书管理后台 → 组织架构 → 用户详情 2. 使用飞书 `contact:user.employee_id:readonly` 权限查询 ## 注意事项 1. **认证**：首次调用会自动获取 access_token，token 有效期约 2 小时 2. **时区**：建议使用带时区的时间格式，如 `+08:00` 表示北京时间 3. **权限**：确保应用已被用户或组织授权，否则接口会返回权限错误 4. **频率限制**：注意飞书 API 的调用频率限制 ## 相关链接 - [飞书任务 API 文档](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/task-v2/overview)

path: README.md
content: # Feishu Task Skill for OpenClaw

飞书任务管理 Skill - 通过 OpenClaw 自动化创建、指派和管理飞书任务

## 功能特性

- ✅ 创建任务（支持标题、描述、截止时间、优先级）
- ✅ 指派执行者（通过邮箱或 Open ID）
- ✅ 添加关注者
- ✅ 查询任务列表和详情
- ✅ 更新任务信息
- ✅ 删除任务

## 安装

### 1. 下载 Skill

```bash
git clone https://github.com/youyoude/feishu-task.git
```

### 2. 复制到 OpenClaw Skills 目录

```bash
cp -r feishu-task ~/.openclaw/skills/
# 或安装打包文件
cp feishu-task.skill ~/.openclaw/skills/
```

### 3. 配置环境变量

在 ~/.openclaw/openclaw.json 中添加：

```json
{
  "skills": {
    "entries": {
      "feishu-task": {
        "enabled": true,
        "env": {
          "FEISHU_APP_ID": "cli_xxxxx",
          "FEISHU_APP_SECRET": "xxxxxx"
        }
      }
    }
  }
}
```

### 4. 重启 OpenClaw

```bash
openclaw gateway restart
```

---

## ⚠️ 重要：飞书权限配置

### 必需权限

使用此 Skill 前，必须在 **飞书开放平台** 为你的应用申请以下权限：

#### 核心权限（必须）

| 权限 | 权限码 | 说明 |
|------|--------|------|
| **任务读写** | task:task:write | 创建、更新、删除任务 |
| **任务读取** | task:task:readonly | 查询任务列表和详情 |

#### 用户查询权限（可选但推荐）

| 权限 | 权限码 | 说明 |
|------|--------|------|
| **通过邮箱查用户** | contact:user.id:readonly | 通过邮箱获取用户 Open ID |
| **获取用户ID** | contact:user.employee_id:readonly | 获取员工ID信息 |

### 申请权限步骤

1. 登录 [飞书开放平台](https://open.feishu.cn/)
2. 进入你的应用 → **权限管理**
3. 搜索并添加上述权限
4. 发布版本（需要管理员审核）

### 权限说明

#### 为什么需要 task:task:write？
- 创建新任务
- 更新任务信息（标题、描述、截止时间等）
- 删除任务

#### 为什么需要 task:task:readonly？
- 获取任务列表
- 查询任务详情
- 检查任务状态

#### 为什么需要 contact:user.id:readonly？
- 通过邮箱地址（如 [REDACTED_SECRET]）获取用户 Open ID
- 如果没有此权限，需要手动提供用户的 Open ID（格式：ou_xxxxx）

---

## 使用方法

### 在 OpenClaw 中使用

安装并配置完成后，可以直接对 OpenClaw 说：

```
给李四创建一个飞书任务，AI机器人集成方案，截止下周二
```

```
创建一个任务：完成月度报告，截止3月15日，指派给张三，关注人是李四
```

### Python API 调用

```python
from scripts.task_client import FeishuTaskClient

client = FeishuTaskClient()

# 创建任务
result = client.create_task(
    summary="完成月度报告",
    description="整理2月份数据并生成报告",
    due_time="2026-03-15T18:00:00+08:00",
    assignee_id="ou_xxxxx",           # 执行者
    follower_ids=["ou_xxxxx"],         # 关注者列表
    priority=2                         # 0=默认, 1=低, 2=中, 3=高
)
```

### 命令行使用

```bash
# 创建任务
python3 scripts/task_client.py create "任务标题" \\
  --due "2026-03-15T18:00:00+08:00" \\
  --assignee "ou_xxxxx" \\
  --priority 2
```

---

## 时间格式说明

### 支持的格式

- ISO 8601: 2026-03-15T18:00:00+08:00（带时区）
- ISO 8601: 2026-03-15T10:00:00Z（UTC时间）
- 时间戳: 毫秒级时间戳（如 1773568800000）

### 北京时间示例

```python
# 2026年3月15日 18:00（北京时间）
due_time="2026-03-15T18:00:00+08:00"
```

---

## 获取用户 Open ID

### 方法一：通过邮箱查询（需要权限）

使用 contact:user.id:readonly 权限，Skill 会自动将邮箱转换为 Open ID

### 方法二：飞书管理后台

1. 登录飞书管理后台
2. 进入 **组织架构** → **成员管理**
3. 点击用户 → 查看 **Open ID**

---

## 常见问题

### Q: 创建任务时提示权限不足？

A: 请检查：
1. 应用是否申请了 task:task:write 权限
2. 权限是否已发布并通过审核
3. 用户是否已授权应用访问其任务数据

### Q: 无法通过邮箱指派任务？

A: 请检查：
1. 是否申请了 contact:user.id:readonly 权限
2. 邮箱地址是否正确
3. 该用户是否在企业通讯录中

### Q: 截止时间设置后不显示？

A: 请确保：
1. 时间格式正确（ISO 8601 或毫秒时间戳）
2. 时区信息正确（建议用 +08:00 表示北京时间）

---

## API 参考

- [飞书任务 API 文档](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/task-v2/overview)
- [飞书权限说明](https://open.feishu.cn/document/ukTMukTMukTM/uQjN3QjL0YzN04CN2cDN)

---

## 版本历史

### v1.0.0 (2026-03-05)

- ✨ 初始版本发布
- 支持创建、查询、更新、删除任务
- 支持指派执行者和关注者
- 支持截止时间设置（自动处理时区）

---

## 贡献

欢迎提交 Issue 和 Pull Request！

## 许可证

MIT License

---

**作者**: youyoude  
**项目地址**: https://github.com/youyoude/feishu-task


path: scripts/task_client.py
content: #!/usr/bin/env python3
"""
Feishu Task API Client
飞书任务管理 API 客户端

API 文档: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/task-v2/overview
"""

import os
import sys
import json
import urllib.request
import urllib.error
from typing import Optional, Dict, List, Any


class FeishuTaskClient:
    def __init__(self, app_id: Optional[str] = None, app_secret: Optional[str] = None):
        self.app_id = app_id or os.environ.get('FEISHU_APP_ID')
        self.app_secret = app_secret or os.environ.get('FEISHU_APP_SECRET')
        self.access_token = None
        self.base_url = 'https://open.feishu.cn/open-apis'
        
    def get_access_token(self) -> str:
        """获取飞书访问令牌"""
        url = f'{self.base_url}/auth/v3/tenant_access_token/internal'
        data = json.dumps({
            'app_id': self.app_id,
            'app_secret': self.app_secret
        }).encode('utf-8')
        
        req = urllib.request.Request(
            url,
            data=data,
            headers={'Content-Type': 'application/json'},
            method='POST'
        )
        
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode('utf-8'))
            if result.get('code') != 0:
                raise Exception(f"Failed to get access [REDACTED_SECRET]'msg')}")
            self.access_token = result['tenant_access_token']
            return self.access_token
    
    def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict:
        """发送 HTTP 请求"""
        if not self.access_token:
            self.get_access_token()
            
        url = f'{self.base_url}{endpoint}'
        headers = {
            'Authorization': f'Bearer {self.access_token}',
            'Content-Type': 'application/json'
        }
        
        req_data = json.dumps(data).encode('utf-8') if data else None
        req = urllib.request.Request(url, data=req_data, headers=headers, method=method)
        
        try:
            with urllib.request.urlopen(req) as response:
                return json.loads(response.read().decode('utf-8'))
        except urllib.error.HTTPError as e:
            error_body = e.read().decode('utf-8')
            raise Exception(f"HTTP Error {e.code}: {error_body}")
    
    def create_task(self, summary: str, **kwargs) -> Dict:
        """
        创建任务
        
        Args:
            summary: 任务标题（必填）
            description: 任务描述
            due_time: 截止时间（ISO 8601格式，如：2024-03-10T18:00:00+08:00）
            assignee_id: 执行者用户ID
            follower_ids: 关注者用户ID列表
            priority: 优先级（0=默认，1=低，2=中，3=高）
            creator_id: 创建者用户ID（如不传，则为自己创建）
            
        Returns:
            创建的任务信息
        """
        endpoint = '/task/v2/tasks'
        
        data = {'summary': summary}
        
        # 处理成员信息（可选，不传也能创建任务）
        # 注意：飞书任务API如果不传members，默认创建者为当前应用/用户
        # 如果传members，则每个member必须有id和type字段
        members = []
        
        # 如果有执行者ID，添加为执行者
        if 'assignee_id' in kwargs and kwargs['assignee_id']:
            members.append({'id': kwargs['assignee_id'], 'type': 'user', 'role': 'assignee'})
        
        # 添加关注者
        if 'follower_ids' in kwargs and kwargs['follower_ids']:
            for fid in kwargs['follower_ids']:
                members.append({'id': fid, 'type': 'user', 'role': 'follower'})
        
        # 只有当有成员时才添加 members 字段
        if members:
            data['members'] = members
        
        # 其他可选参数
        if 'description' in kwargs:
            data['description'] = kwargs['description']
        if 'due_time' in kwargs:
            # 支持 ISO 8601 格式或时间戳
            due_val = kwargs['due_time']
            if isinstance(due_val, str):
                # ISO 8601 格式，转换为时间戳（毫秒）
                import datetime
                # 解析 ISO 8601
                if '+' in due_val:
                    due_val = due_val.replace('+08:00', '')
                dt = datetime.datetime.fromisoformat(due_val)
                timestamp = int(dt.timestamp() * 1000)  # 转换为毫秒
            else:
                timestamp = int(due_val)
            data['due'] = {'timestamp': str(timestamp), 'timezone': 'Asia/Shanghai'}
        if 'priority' in kwargs:
            data['priority'] = kwargs['priority']
            
        return self._make_request('POST', endpoint, data)
    
    def get_task(self, task_id: str) -> Dict:
        """获取任务详情"""
        endpoint = f'/task/v2/tasks/{task_id}'
        return self._make_request('GET', endpoint)
    
    def update_task(self, task_id: str, **kwargs) -> Dict:
        """
        更新任务
        
        Args:
            task_id: 任务ID
            summary: 任务标题
            description: 任务描述
            due_time: 截止时间 (ISO 8601 或时间戳)
            priority: 优先级
            completed: 是否完成
        """
        endpoint = f'/task/v2/tasks/{task_id}'
        
        task_data = {}
        update_fields = []
        
        if 'summary' in kwargs:
            task_data['summary'] = kwargs['summary']
            update_fields.append('summary')
        if 'description' in kwargs:
            task_data['description'] = kwargs['description']
            update_fields.append('description')
        if 'due_time' in kwargs:
            # 转换 due_time 格式（毫秒时间戳）
            due_val = kwargs['due_time']
            if isinstance(due_val, str):
                import datetime
                if '+' in due_val:
                    due_val = due_val.replace('+08:00', '')
                dt = datetime.datetime.fromisoformat(due_val)
                timestamp = int(dt.timestamp() * 1000)  # 转换为毫秒
            else:
                timestamp = int(due_val)
            task_data['due'] = {'timestamp': str(timestamp), 'timezone': 'Asia/Shanghai'}
            update_fields.append('due')
        if 'priority' in kwargs:
            task_data['priority'] = kwargs['priority']
            update_fields.append('priority')
        if 'completed' in kwargs:
            task_data['completed'] = kwargs['completed']
            update_fields.append('completed')
        
        data = {'task': task_data, 'update_fields': update_fields}
        return self._make_request('PATCH', endpoint, data)
    
    def delete_task(self, task_id: str) -> Dict:
        """删除任务"""
        endpoint = f'/task/v2/tasks/{task_id}'
        return self._make_request('DELETE', endpoint)
    
    def list_tasks(self, user_id: Optional[str] = None, completed: Optional[bool] = None) -> Dict:
        """
        获取任务列表
        
        Args:
            user_id: 指定用户的任务（不传则获取当前用户）
            completed: 筛选完成状态
        """
        endpoint = '/task/v2/tasks'
        params = []
        if user_id:
            params.append(f'user_id={user_id}')
        if completed is not None:
            params.append(f'completed={"true" if completed else "false"}')
            
        if params:
            endpoint += '?' + '&'.join(params)
            
        return self._make_request('GET', endpoint)


def main():
    """CLI 入口"""
    import argparse
    
    parser = argparse.ArgumentParser(description='Feishu Task CLI')
    parser.add_argument('--app-id', help='Feishu App ID')
    parser.add_argument('--app-secret', help='Feishu App Secret')
    
    subparsers = parser.add_subparsers(dest='command', help='Commands')
    
    # Create task
    create_parser = subparsers.add_parser('create', help='Create a task')
    create_parser.add_argument('summary', help='Task title')
    create_parser.add_argument('--description', help='Task description')
    create_parser.add_argument('--due', help='Due time (ISO 8601)')
    create_parser.add_argument('--assignee', help='Assignee user ID')
    create_parser.add_argument('--priority', type=int, choices=[0,1,2,3], help='Priority (0=default,1=low,2=medium,3=high)')
    
    # Get task
    get_parser = subparsers.add_parser('get', help='Get task details')
    get_parser.add_argument('task_id', help='Task ID')
    
    # List tasks
    list_parser = subparsers.add_parser('list', help='List tasks')
    list_parser.add_argument('--user', help='User ID')
    list_parser.add_argument('--completed', type=bool, help='Filter by completion status')
    
    # Update task
    update_parser = subparsers.add_parser('update', help='Update a task')
    update_parser.add_argument('task_id', help='Task ID')
    update_parser.add_argument('--summary', help='New title')
    update_parser.add_argument('--description', help='New description')
    update_parser.add_argument('--due', help='New due time')
    update_parser.add_argument('--priority', type=int, choices=[0,1,2,3])
    update_parser.add_argument('--completed', type=bool, help='Mark as completed')
    
    # Delete task
    delete_parser = subparsers.add_parser('delete', help='Delete a task')
    delete_parser.add_argument('task_id', help='Task ID')
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        sys.exit(1)
    
    # Initialize client
    client = FeishuTaskClient(app_id=args.app_id, app_secret=args.app_secret)
    
    try:
        if args.command == 'create':
            result = client.create_task(
                summary=args.summary,
                description=args.description,
                due_time=args.due,
                assignee_id=args.assignee,
                priority=args.priority
            )
            print(json.dumps(result, indent=2, ensure_ascii=False))
            
        elif args.command == 'get':
            result = client.get_task(args.task_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))
            
        elif args.command == 'list':
            result = client.list_tasks(user_id=args.user, completed=args.completed)
            print(json.dumps(result, indent=2, ensure_ascii=False))
            
        elif args.command == 'update':
            kwargs = {}
            if args.summary: kwargs['summary'] = args.summary
            if args.description: kwargs['description'] = args.description
            if args.due: kwargs['due_time'] = args.due
            if args.priority is not None: kwargs['priority'] = args.priority
            if args.completed is not None: kwargs['completed'] = args.completed
            result = client.update_task(args.task_id, **kwargs)
            print(json.dumps(result, indent=2, ensure_ascii=False))
            
        elif args.command == 'delete':
            result = client.delete_task(args.task_id)
            print(json.dumps(result, indent=2, ensure_ascii=False))
            
    except Exception as e:
        print(f'Error: {e}', file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)