飞书个人授权
清海访问飞书个人数据的统一授权入口。用户首次向清海发送任意消息时,Bot 主动发送授权引导卡片;授权后清海可代用户身份读取妙记、云文档、多维表格、个人任务等个人数据,之后静默自动续期。 角色会话场景见 角色场景,能力定义见 Skill 清单。
背景与设计动机
飞书 API 权限分为两类 Token:
| Token 类型 | 代表身份 | 能访问什么 |
|---|---|---|
tenant_access_token | 清海 Bot(应用身份) | 被共享给 Bot 的文档、可见的通讯录、可发送的消息 |
user_access_token | 具体员工个人身份 | 该员工自己的妙记、私有云文档、个人任务、多维表格权限数据 |
问题:清海 Bot 使用 tenant_access_token 只能访问对 Bot 公开的数据。员工的个人录制妙记、私有文档、个人任务属于个人数据,租户 Token 无权访问。
解法(方案 B):用户首次使用时主动引导完成一次 OAuth 授权,获取 user_access_token。授权后清海代用户身份访问个人数据,用完即用 refresh_token 静默续期,用户只需授权一次。
触发时机
用户向清海发送第一条消息时,无论消息内容是什么,清海在正常回复用户消息的同时,额外发送一张授权引导卡片。
触发条件:MongoDB user_tokens 中不存在该 open_id 的记录
触发方式:主动推送,非阻塞——清海照常回复用户消息,卡片作为附加消息发出不需要重新引导的情况:用户已完成授权且 refresh_token 未过期(30 天内活跃),静默自动续期,无感知。
授权流程
授权卡片设计
清海在用户首次对话时发送的飞书交互消息卡片:
┌─────────────────────────────────────────┐
│ 👋 我是清海,很高兴认识你! │
│ │
│ 授权后我可以帮你: │
│ • 📝 整理会议妙记 → 自动生成会议纪要 │
│ • 📄 查找你的云文档和知识库 │
│ • ✅ 追踪你的个人任务进度 │
│ • 📊 读取你的多维表格数据 │
│ │
│ 授权仅用于执行你的指令,不会主动读取数据。 │
│ │
│ [ 点击授权 ] [ 暂不授权 ] │
└─────────────────────────────────────────┘卡片行为:
- 点击「点击授权」→ 跳转飞书 OAuth 授权页面 → 完成后 Bot 发送成功确认消息
- 点击「暂不授权」→ Bot 回复「没问题,需要时随时告诉我,我重新发授权链接」
- 忽略卡片(不点任何按钮)→ 下次发消息时不重复推送卡片,仅在触发需要个人数据的功能时再次提醒
Token 生命周期管理
| 状态 | 处理方式 | 用户感知 |
|---|---|---|
| 从未授权(首次对话) | 回复消息 + 额外发送授权卡片 | 收到一张引导卡片 |
| 点击「暂不授权」 | 记录拒绝状态,不再主动推送 | 无 |
| access_token 有效(< 2h) | 直接使用 | 无感 |
| access_token 过期,refresh_token 有效(< 30d) | 静默刷新 | 无感 |
| refresh_token 过期(> 30d 未活跃) | 再次发送授权卡片 | 收到重新授权提示 |
存储位置:MongoDB · user_tokens 集合,按 open_id 索引,字段包含 access_token、refresh_token、expires_at、refresh_expires_at、auth_declined(是否已拒绝)。
所需飞书 API 与 Scope
OAuth 授权流程 API
| 步骤 | API | 说明 |
|---|---|---|
| 1. 生成授权链接 | GET https://open.feishu.cn/open-apis/authen/v1/authorize | 拼接 app_id、redirect_uri、scope、state |
| 2. 换取 Token | POST /authen/v1/oidc/access_token | 用 code + app_id + app_secret 换取 user_access_token + refresh_token |
| 3. 刷新 Token | POST /authen/v1/oidc/refresh_access_token | 用 refresh_token 静默续期 |
| 4. 验证 Token | GET /authen/v1/user_info | 验证 token 有效性,获取用户 open_id |
申请的 OAuth Scope
| Scope | 用途 | 下游场景 |
|---|---|---|
minutes:minutes.basic:read | 读取妙记基本信息 | 妙记会议纪要 |
minutes:minutes:readonly | 读取妙记转录文本 | 妙记会议纪要 |
docx:document:readonly | 读取私有云文档 | 飞书云文档 |
drive:drive:readonly | 列出个人云盘文件 | 飞书云文档 |
task:task:read | 读取个人任务 | 个人任务(待规划) |
bitable:app:readonly | 读取个人多维表格 | 多维表格(待规划) |
⚠️ Scope 只需申请当前已规划场景所需的最小集合,后续场景上线时追加,用户重新授权一次即可。
边界与约束
| 约束 | 说明 |
|---|---|
| 非阻塞式引导 | 卡片是附加消息,不阻断正常对话;用户未授权时仍可使用不需要个人数据的功能 |
| 首次对话只推一次 | 同一用户只在第一条消息时发卡片,后续对话不重复推送(除非重新授权场景) |
| 一次授权,多场景通用 | 妙记、云文档、任务等所有个人数据场景共用同一套 token,不分别授权 |
| 仅代用户执行指令,不主动读取 | 清海不会在后台主动扫描用户数据,只在用户明确发起指令时才调用 |
| Token 不跨用户共享 | 每个 open_id 独立存储 token,绝对隔离 |
| 重定向地址 | 回调地址需注册在飞书开发者后台「安全设置 → 重定向 URL」中 |
| State 参数防 CSRF | 生成授权链接时携带随机 state,回调时校验,防止伪造回调攻击 |