Skill: noir-hedgehog/feishu-api (version 1.0.0)

--- name: feishu-api description: | 飞书开放平台 API 技能。用于：(1) 调用飞书开放 API 完成插件以外的操作（如批量写入、权限管理、文件夹操作等）；(2) 实现 OAuth 用户授权流程；(3) 批量数据处理。 当用户提到飞书 API、飞书开放平台、OAuth 授权、user_access_token，或需要批量操作飞书数据（多维表格批量写入/删除、权限配置、云文档管理）时触发。 --- # 飞书开放平台 API 本技能提供直接调用飞书开放 API 的能力，作为飞书插件工具的补充。 ## 核心概念 ### 认证方式 | 类型 | 用途 | 有效期 | |------|------|--------| | `tenant_access_token` | 应用身份调用 API | 2小时 | | `user_access_token` | 代表用户操作 | 有效期短，需刷新 | **重要**：所有脚本中不得硬编码 `app_id`、`app_secret`、`access_token`。从配置文件读取或使用环境变量。 ### 读取凭据 飞书凭据存储在 `~/.openclaw/openclaw.json` 的 `channels.feishu` 下： ```python import json with open('/root/.openclaw/openclaw.json') as f: config = json.load(f) feishu_cfg = config.get('channels', {}).get('feishu', {}) APP_ID = feishu_cfg.get('appId', '') APP_SECRET = feishu_cfg.get('appSecret', '') ``` ## 典型工作流 ### 1. 获取 Token ```python import ssl, urllib.request, json def get_app_access_token(app_id, app_secret): url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' data = json.dumps({'app_id': app_id, 'app_secret': app_secret}).encode() req = urllib.request.Request(url, data=data, method='POST') req.add_header('Content-Type', 'application/json') ctx = ssl._create_unverified_context() with urllib.request.urlopen(req, context=ctx, timeout=10) as r: return json.loads(r.read()).get('tenant_access_token') ``` ### 2. 调用 API ```python def call_feishu_api(url, method, token, payload=None): ctx = ssl._create_unverified_context() data = json.dumps(payload, ensure_ascii=False).encode() if payload else None req = urllib.request.Request(url, data=data, method=method) req.add_header('Authorization', f'Bearer {token}') req.add_header('Content-Type', 'application/json') with urllib.request.urlopen(req, context=ctx, timeout=30) as r: return json.loads(r.read()) ``` ### 3. 批量操作多维表格 ```python # 批量创建记录 url = f'https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create' payload = {'records': [{'fields': {'字段名': '值'}} for item in items]} result = call_feishu_api(url, 'POST', token, payload) # 批量删除记录 url = f'https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete' payload = {'records': ['record_id_1', 'record_id_2']} result = call_feishu_api(url, 'POST', token, payload) ``` ### 4. 权限管理 ```python # 添加协作者 url = f'https://open.feishu.cn/open-apis/drive/v1/permissions/{file_token}/members?type=bitable' payload = { 'member_type': 'openid', # 或 email, userid, unionid 'member_id': 'ou_xxx', # 用户 open_id 'perm': 'edit' # view | edit | full_access } result = call_feishu_api(url, 'POST', token, payload) ``` ## 常用 API 端点 | 功能 | 端点 | |------|------| | 批量创建多维表格记录 | `POST /bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create` | | 批量删除多维表格记录 | `POST /bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete` | | 更新记录 | `PUT /bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}` | | 添加权限成员 | `POST /drive/v1/permissions/{file_token}/members?type={type}` | | 列出权限成员 | `GET /drive/v1/permissions/{file_token}/members?type={type}` | | 创建文件夹 | `POST /drive/v1/files/create_folder` | | 移动文件 | `POST /drive/v1/files/{file_token}/move` | | 上传文件 | `POST /drive/v1/files/upload_all` | ## 详细参考 - **OAuth 授权流程**：参见 [references/oauth.md](references/oauth.md) - **多维表格 API**：参见 [references/bitable.md](references/bitable.md) - **云文档管理**：参见 [references/drive.md](references/drive.md) ## 数据安全准则 1. **不硬编码凭据** - 始终从配置文件读取 2. **不输出敏感信息** - 不打印 token、secret 等 3. **最小权限** - 仅申请所需的权限范围 4. **定期刷新** - token 过期前刷新 ## 速率限制 - 普通 API：每应用每秒 10 请求 - 上传文件：每应用每分钟 60 次 - 批量接口：每批最大 50 条记录

path: references/app-creation.md
content: # 创建飞书应用

## Step 1: 注册开发者账号

1. 访问 [飞书开放平台](https://open.feishu.cn/)
2. 登录后进入「开发者后台」
3. 选择企业或自建应用

## Step 2: 创建应用

1. 点击「创建应用」
2. 选择「企业自建应用」
3. 填写应用信息：
   - 应用名称
   - 应用描述
   - 应用图标（可选）

## Step 3: 获取凭证

在「凭证与基础信息」页面获取：
- `App ID`（格式：`cli_xxx`）
- `App Secret`

**重要**：妥善保管 App Secret，不要泄露。

## Step 4: 配置权限

1. 进入「权限管理」
2. 添加所需权限：

常用权限：
- `bitable:app` - 多维表格
- `docx:document` - 文档读写
- `drive:drive` - 云文档
- `contact:user.id:readonly` - 用户 ID

## Step 5: 配置应用功能

### 启用「机器人」功能

如需使用机器人：
1. 进入「应用功能」
2. 开启「机器人」

### 配置网页

如需 OAuth 回调：
1. 进入「应用功能」→「网页」
2. 添加网页地址（需 HTTPS）

## Step 6: 发布应用

1. 进入「版本管理与发布」
2. 创建版本
3. 申请发布（企业管理员审批）

## OAuth 配置

### 设置重定向 URL

1. 进入「安全设置」
2. 「重定向 URL」中添加授权回调地址
3. 必须使用 HTTPS

### 权限范围

在 OAuth 配置页面设置应用申请的权限范围。

## 获取用户授权

应用已创建并发布后，用户访问授权 URL 完成授权。

## 注意事项

1. **权限最小化**：只申请必要的权限
2. **HTTPS 必需**：生产环境回调地址必须 HTTPS
3. **企业管理员审批**：自建应用需要管理员审批才能生效

path: references/bitable.md
content: # 多维表格（Bitable）API

## 资源结构

```
多维表格 App (app_token)
└── 表 (table_id)
    ├── 字段 (field_id)
    └── 记录 (record_id)
```

获取 app_token 和 table_id：
- URL: `https://xxx.feishu.cn/base/{app_token}?table={table_id}`
- app_token: `base` 后第一段
- table_id: `table=` 参数值

## 字段类型

| type | 类型 | 说明 |
|------|------|------|
| 1 | Text | 文本 |
| 2 | Number | 数字 |
| 3 | SingleSelect | 单选 |
| 4 | MultiSelect | 多选 |
| 5 | DateTime | 日期时间 |
| 7 | Checkbox | 复选框 |
| 11 | User | 用户 |
| 13 | Phone | 电话 |
| 15 | URL | 链接 |
| 17 | Attachment | 附件 |
| 18 | SingleLink | 单向链接 |
| 19 | Lookup | 查找 |
| 20 | Formula | 公式 |
| 21 | DuplexLink | 双向链接 |
| 22 | Location | 地理位置 |
| 23 | GroupChat | 群组 |
| 1001 | CreatedTime | 创建时间 |
| 1002 | ModifiedTime | 修改时间 |
| 1003 | CreatedUser | 创建人 |
| 1004 | ModifiedUser | 修改人 |
| 1005 | AutoNumber | 自动编号 |

## 记录操作

### 批量创建记录

```
POST /bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create
```

```python
payload = {
    'records': [
        {
            'fields': {
                '文本字段名': '值',
                '数字': 123,
                '单选': '选项1',
                '多选': ['选项1', '选项2'],
                '用户': [{'id': 'ou_xxx'}],
                '链接': {'text': '显示文本', 'link': 'https://...'}
            }
        }
    ]
}
# 响应
{'code': 0, 'data': {'records': [{'record_id': 'recxxx', ...}]}}
```

### 批量删除记录

```
POST /bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete
```

```python
payload = {'records': ['recxxx1', 'recxxx2', ...]}
# 建议每批不超过 50 条
```

### 更新记录

```
PUT /bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}
```

```python
payload = {'fields': {'字段名': '新值'}}
```

### 查询记录

```
GET /bitable/v1/apps/{app_token}/tables/{table_id}/records?page_size=100
```

## 字段操作

### 创建字段

```
POST /bitable/v1/apps/{app_token}/tables/{table_id}/fields
```

```python
payload = {
    'field_name': '新字段',
    'type': 1,  # 字段类型
    'property': {}  # 类型相关属性
}
```

### 批量创建字段

```
POST /bitable/v1/apps/{app_token}/tables/{table_id}/fields/batch_create
```

```python
payload = {
    'fields': [
        {'field_name': '字段1', 'type': 1},
        {'field_name': '字段2', 'type': 2}
    ]
}
```

## 常用字段格式

### 用户类型

```python
{'id': 'ou_xxx'}  # 单个用户
[{'id': 'ou_xxx'}, {'id': 'ou_yyy'}]  # 多个用户
```

### 日期时间

```python
# 毫秒时间戳
1699999999000
```

### 链接

```python
{'text': '链接文字', 'link': 'https://example.com'}
```

## 注意事项

1. **批量限制**：每批最多 50 条记录
2. **频率限制**：每秒 10 请求
3. **字段名匹配**：创建/更新时字段名必须与飞书表中的名称完全一致
4. **必填字段**：主键字段必须提供

path: references/drive.md
content: # 云文档（Drive）API

## 权限管理

### 添加协作者

```
POST /drive/v1/permissions/{file_token}/members?type={type}
```

参数：
- `file_token`: 文件/多维表格的 token
- `type`: bitable | doc | sheet | folder

请求体：

```python
payload = {
    'member_type': 'openid',      # openid | email | userid | unionid | openchat | opendepartmentid
    'member_id': 'ou_xxx',       # 用户 ID
    'perm': 'edit',              # view | edit | full_access
    'type': 'bitable'            # 与 URL 参数一致
}
```

响应：

```python
{'code': 0, 'msg': 'Success'}
```

常见错误码：
- `230013`: 无权限（应用没有被授予管理此文档的权限）
- `99991663`: 成员已在协作者列表中

### 移除协作者

```
DELETE /drive/v1/permissions/{file_token}/members?type={type}&member_id={member_id}&member_type={member_type}
```

### 列出协作者

```
GET /drive/v1/permissions/{file_token}/members?type={type}
```

## 文件管理

### 创建文件夹

```
POST /drive/v1/files/create_folder
```

```python
payload = {
    'name': '文件夹名',
    'folder_token': 'fldcnxxx'  # 父文件夹 token，根目录则留空
}
```

### 移动文件

```
POST /drive/v1/files/{file_token}/move
```

```python
payload = {
    'type': 'bitable',  # bitable | doc | sheet | folder
    'folder_token': 'fldcnxxx'  # 目标文件夹
}
```

### 复制文件

```
POST /drive/v1/files/{file_token}/copy
```

```python
payload = {
    'type': 'bitable',
    'folder_token': 'fldcnxxx',
    'name': '新文件名'  # 可选
}
```

### 上传文件

```
POST /drive/v1/files/upload_all
```

```python
import mimetypes, os, urllib.request, json

file_path = '/path/to/file.pdf'
file_name = os.path.basename(file_path)
mime = mimetypes.guess_type(file_name)[0] or 'application/octet-stream'

with open(file_path, 'rb') as f:
    file_data = f.read()

url = 'https://open.feishu.cn/open-apis/drive/v1/files/upload_all'
boundary = '----FormBoundary7MA4YWxkTrZu0gW'
body = f'''--{boundary}
Content-Disposition: form-data; name="file"; filename="{file_name}"
Content-Type: {mime}

{file_data.decode('latin-1')}
--{boundary}
Content-Disposition: form-data; name="parent_type"

drive
--{boundary}
Content-Disposition: form-data; name="parent_node"

{fldcnxxx}
--{boundary}
Content-Disposition: form-data; name="size"

{len(file_data)}
--{boundary}
Content-Disposition: form-data; name="file_name"

{file_name}
--{boundary}--
'''

req = urllib.request.Request(url, data=body.encode('utf-8'), method='POST')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', f'multipart/form-data; boundary={boundary}')

ctx = ssl._create_unverified_context()
with urllib.request.urlopen(req, context=ctx, timeout=60) as r:
    resp = json.loads(r.read())
# resp['data']['file_token'] 即上传文件的 token
```

## 文档类型

| type | 说明 |
|------|------|
| `docx` | 文档 |
| `sheet` | 电子表格 |
| `bitable` | 多维表格 |
| `folder` | 文件夹 |
| `mindnote` | 思维导图 |
| `file` | 文件 |

从 URL 获取：
- 文档: `https://xxx.feishu.cn/docx/ABC123` → token: `ABC123`
- 多维表格: `https://xxx.feishu.cn/base/ABC123?table=XYZ` → app_token: `ABC123`, table_id: `XYZ`

path: references/oauth.md
content: # OAuth 用户授权流程

## 概述

OAuth 2.0 用于获取代表用户的 `user_access_token`，实现"用户委托"能力。

## 授权流程

```
用户 → 授权页面 → 用户同意 → 回调获取 code → 换取 user_access_token
```

### Step 1: 构建授权 URL

```
https://open.feishu.cn/open-apis/authen/v1/authorize?app_id={app_id}&redirect_uri={encoded_redirect_uri}&state={random_state}
```

参数说明：
- `app_id`: 应用 ID（cli_xxx）
- `redirect_uri`: 授权后跳转的回调地址（需在应用配置中注册）
- `state`: 随机字符串，用于防止 CSRF

### Step 2: 用户授权

用户访问授权 URL，在飞书页面确认授权。

### Step 3: 获取 code

授权成功后会回调到 `redirect_uri`，URL 参数中携带 `code`：

```
https://your-server.com/callback?code=xxx&state=xxx
```

### Step 4: 换取 user_access_token

```python
import ssl, urllib.request, json

def exchange_code_for_token(app_id, app_secret, code):
    url = 'https://open.feishu.cn/open-apis/authen/v1/oidc/access_token'
    data = json.dumps({
        'grant_type': 'authorization_code',
        'code': code,
        'app_id': app_id,
        'app_secret': app_secret
    }).encode()
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Content-Type', 'application/json')
    ctx = ssl._create_unverified_context()
    with urllib.request.urlopen(req, context=ctx, timeout=10) as r:
        resp = json.loads(r.read())
    return resp.get('data', {}).get('access_token')
```

返回数据结构：
```json
{
  "code": 0,
  "data": {
    "access_token": "u-xxx",
    "token_type": "Bearer",
    "expires_in": 7200,
    "refresh_token": "r-xxx",
    "scope": "contact:user.id:readonly",
    "open_id": "ou_xxx",
    "union_id": "on_xxx"
  }
}
```

### Step 5: 刷新 token

`user_access_token` 有效期短，过期前用 refresh_token 刷新：

```python
def refresh_user_token(app_id, app_secret, refresh_token):
    url = 'https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token'
    data = json.dumps({
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token,
        'app_id': app_id,
        'app_secret': app_secret
    }).encode()
    # ... 同上调用
```

## 安全建议

1. **state 参数**：生成随机字符串，验证回调时比对，防止 CSRF
2. **code 一次性**：code 只能使用一次，用完即失效
3. **HTTPS**：生产环境必须使用 HTTPS
4. **Token 存储**：加密存储 refresh_token，不要明文保存
5. **最小范围**：申请最小必要的权限范围

## 权限范围

常用 scope：

| Scope | 说明 |
|-------|------|
| `docx:document:readonly` | 只读文档 |
| `docx:document` | 读写文档 |
| `bitable:app` | 多维表格 |
| `drive:drive` | 云文档 |
| `contact:user.id:readonly` | 获取用户 ID |

在飞书应用管理后台配置应用的「权限管理」，添加所需权限。

path: scripts/feishu_utils.py
content: #!/usr/bin/env python3
"""批量写入飞书多维表格记录"""
import json, sys, ssl, urllib.request, time

def get_config():
    """从配置文件读取飞书凭据"""
    with open('/root/.openclaw/openclaw.json') as f:
        config = json.load(f)
    feishu_cfg = config.get('channels', {}).get('feishu', {})
    return feishu_cfg.get('appId', ''), feishu_cfg.get('appSecret', '')

def get_access_token(app_id, app_secret):
    url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'
    data = json.dumps({'app_id': app_id, 'app_secret': app_secret}).encode()
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Content-Type', 'application/json')
    ctx = ssl._create_unverified_context()
    with urllib.request.urlopen(req, context=ctx, timeout=10) as r:
        resp = json.loads(r.read())
    return resp.get('tenant_access_token', '')

def call_api(url, method, token, payload=None):
    ctx = ssl._create_unverified_context()
    data = json.dumps(payload, ensure_ascii=False).encode() if payload else None
    req = urllib.request.Request(url, data=data, method=method)
    req.add_header('Authorization', f'Bearer {token}')
    req.add_header('Content-Type', 'application/json')
    with urllib.request.urlopen(req, context=ctx, timeout=30) as r:
        return json.loads(r.read())

def batch_create_records(app_token, table_id, records, token, batch_size=50):
    """批量创建记录"""
    url = f'https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create'
    total = len(records)
    created = 0
    for i in range(0, total, batch_size):
        batch = records[i:i+batch_size]
        payload = {'records': batch}
        resp = call_api(url, 'POST', token, payload)
        if resp.get('code') == 0:
            n = len(resp.get('data', {}).get('records', []))
            created += n
            print(f'  [{i+1}-{min(i+batch_size,total)}/{total}] OK +{n}', file=sys.stderr)
        else:
            print(f'  [{i+1}-{min(i+batch_size,total)}/{total}] FAIL: {resp.get("msg")}', file=sys.stderr)
        time.sleep(0.3)
    return created

def batch_delete_records(app_token, table_id, record_ids, token, batch_size=50):
    """批量删除记录"""
    url = f'https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete'
    total = len(record_ids)
    deleted = 0
    for i in range(0, total, batch_size):
        batch = record_ids[i:i+batch_size]
        payload = {'records': batch}
        resp = call_api(url, 'POST', token, payload)
        if resp.get('code') == 0:
            deleted += len(batch)
            print(f'  [{i+1}-{min(i+batch_size,total)}/{total}] OK +{len(batch)}', file=sys.stderr)
        else:
            print(f'  [{i+1}-{min(i+batch_size,total)}/{total}] FAIL: {resp.get("msg")}', file=sys.stderr)
        time.sleep(0.3)
    return deleted

def add_permission(file_token, member_type, member_id, perm, token, file_type='bitable'):
    """添加协作者"""
    url = f'https://open.feishu.cn/open-apis/drive/v1/permissions/{file_token}/members?type={file_type}'
    payload = {
        'member_type': member_type,  # openid | email | userid
        'member_id': member_id,
        'perm': perm  # view | edit | full_access
    }
    return call_api(url, 'POST', token, payload)

if __name__ == '__main__':
    # 示例用法
    print(__doc__)
    print('Usage: import from this script and call the functions')

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