Files
company-ai-platform/.claude/specs/feishu-account-personalization/design.md
JiuContinent d7db84571d ```
feat: 添加飞书用户模块和订阅功能支持

- 新增feishu_users模块用于处理飞书用户身份验证和权限管理
- 新增subscriptions模块用于处理订阅相关功能
- 新增personalization模块用于个性化服务
- 在alembic迁移配置中注册新的模型模块
- 在API路由器中添加feishu_users和subscriptions路由
- 实现事件调度服务的改进,包括错误处理和状态更新优化
- 添加飞书命令处理的权限检查机制
- 实现飞书应用票据事件处理
- 改进审计日志记录功能
```
2026-07-27 08:02:17 +08:00

252 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 设计文档
## 概述
本设计在现有 FastAPI 模块化单体内增加飞书终端用户身份、个人画像与持久化订阅三个
业务域。内部 API 继续使用服务 API Key飞书终端权限只能来自已经通过验签或 SDK
验证的事件,身份主键固定为 `(tenant_key, open_id)`
平台只写自身数据库。旧业务 MySQL 保持 SELECT-only。个人定时提示词不允许调用工具、
读取公司报表或写入问答历史;群订阅也不加载创建者个人画像。
## 架构设计
### 系统架构图
```mermaid
flowchart LR
F["飞书 webhook / 长连接"] --> V["事件验证与解密边界"]
V --> I["feishu_users<br/>身份与权限"]
I --> C["飞书命令编排"]
C --> P["personalization<br/>规则/偏好/会话"]
C --> S["subscriptions<br/>计划/投递"]
C --> A["ai_agent<br/>受限问答"]
API["内部 API Key"] --> I
API --> S
DB[("平台数据库")] --- I
DB --- P
DB --- S
T["每分钟持久化扫描"] --> S
S --> A
S --> FC["FeishuClient"]
```
### 数据流图
```mermaid
sequenceDiagram
participant F as 飞书
participant E as 事件服务
participant U as 用户服务
participant C as 命令服务
participant P as 个性化服务
participant A as AI
F->>E: 已签名消息事件
E->>E: 验签/解密、事件去重
E->>U: tenant_key + sender open_id
U->>U: 查找或自动注册普通用户
U-->>E: FeishuPrincipal
E->>C: principal + chat context + mentions
C->>P: 仅加载 principal 所有的数据
P->>A: 安全规则→公司规则→个人规则→请求→偏好/记忆→历史
A-->>C: 回答或明确不可用
C-->>F: 原会话回复
```
## 组件与接口
### `app/modules/feishu_users`
- `models.py` 定义 `FeishuUser`,公开随机 `code`,内部使用整数主键。
- `principal.py` 定义不可伪造的 `FeishuPrincipal`包含用户编号、租户、open_id、角色、
状态、当前 chat_id/chat_type 和结构化 mentions。
- `services/identity.py` 只接收验证边界传入的身份,负责首次注册、初始管理员引导、
最后活跃时间和旧自选认领。
- `services/management.py` 负责列表、角色/状态修改、最后管理员保护和审计。
- `routes.py` 暴露服务 API Key 保护的用户管理接口;正文中的 actor/open_id 不参与授权。
### `app/modules/personalization`
- `UserPreference` 保存白名单类别:`language``tone``detail``topic``interest`
- `AIConversation` 唯一标识“用户 + 私聊/群聊”;`AIConversationMessage` 保存消息,
每次读写时清理 30 天外记录并裁剪到最近 20 轮。
- `services/preferences.py` 提供显式偏好管理和真实 AI 可用时的结构化提取;敏感类别及
密钥样式在入库前拒绝。
- `services/conversations.py` 提供历史加载、追加、重置。
- `services/context.py` 按固定优先级组装问答上下文。
- `services/erasure.py` 实现一次性确认码和事务性“忘记我”,审计只保留随机匿名主体。
### `app/modules/subscriptions`
- `services/schedule_parser.py` 实现受控中文语法,不使用不确定的自由文本推断。
- `services/subscriptions.py` 管理创建、列表、暂停、恢复、退订、时区和安静时段。
- `services/scanner.py` 每分钟领取到期订阅,使用行锁(支持时)与唯一投递键创建投递,
并推进订阅的下一执行时间。
- `services/delivery.py` 生成受限 AI 内容并发送;个人订阅仅使用个人上下文,群订阅仅使用
系统/公司规则。HTTP 错误、429、5xx 和飞书业务 `code != 0` 都是失败。
- `tasks/``task_queue/` 仅作为执行适配层;关闭 Celery 时由扫描器直接处理持久化投递。
### 共享集成
- `FeishuEventService` 在验证后、注册事件前解析 `tenant_key/open_id`;缺失身份不创建数据。
- `FeishuCommandService.handle_text` 接收 `FeishuPrincipal | None`。内部预览接口不构造终端
用户身份,因此只能预览不涉及个人数据的安全路径。
- 管理员能力由统一权限守卫保护:公司规则、公司/财务/风险/考勤命令、用户管理、群订阅。
- `FeishuClient.send_message` 接收租户键和可选稳定 `uuid`,并把非零业务码转换为可分类失败。
- 自建应用使用 `/auth/v3/tenant_access_token/internal`;商店应用先使用最近一次已验证的
`app_ticket` 获取 `app_access_token`,再按目标 `tenant_key` 获取
`tenant_access_token`。应用令牌按应用缓存,租户令牌按 `(app_id, tenant_key)` 隔离缓存。
- 只有通过 webhook 验真或长连接 SDK 验证的 `app_ticket` 事件可以轮换持久化票据;
环境变量票据仅作为启动兜底,票据和访问令牌不得进入日志、审计或响应。
- 命令回复、卡片、图片和个人/群订阅都显式携带事件主体的 `tenant_key`;固定默认群报表
使用 `FEISHU_DEFAULT_TENANT_KEY`,不得复用另一租户的令牌。
- AI adapter context 增加每用户 session id`noop` 明确返回不可用且不写会话、偏好或记忆。
## 数据模型
### `FeishuUser`
- `id`, `code`, `tenant_key`, `open_id`, `union_id`, `user_id`
- `role` (`user|admin`), `status` (`active|disabled`)
- `timezone`, `quiet_hours_start`, `quiet_hours_end`
- `last_active_at`, `created_at`, `updated_at`
- 唯一约束:`(tenant_key, open_id)``code` 全局唯一。
### `FeishuAdminBootstrapTombstone`
- 仅保存带域隔离的 `tenant_key + open_id` 不可逆摘要和创建时间。
- 不保存原始飞书身份、owner、角色或其他画像仅用于阻止已执行“忘记我”的
配置初始管理员在重新联系时被自动重授管理员。
### `UserPreference`
- `id`, `code`, `owner_id`, `category`, `value`, `source`, `created_at`, `updated_at`
- 唯一约束:`(owner_id, category, normalized_value)`
### `AIConversation` / `AIConversationMessage`
- 会话:`id`, `code`, `owner_id`, `chat_type`, `chat_key`, `created_at`, `updated_at`
- 消息:`id`, `conversation_id`, `role`, `content`, `created_at`
- 唯一约束:`(owner_id, chat_type, chat_key)`
### 现有模型扩展
- `AIMemoryEntry.owner_id` 可空;公司规则 `owner_id=NULL`,个人规则/记忆必须有 owner。
- `AIMemoryEntry.kind` 区分 `company_rule|personal_rule|memory`,旧显式规则迁为
`company_rule/legacy_company`,旧无所有者自动记忆归档。
- 指纹唯一性改为 `(owner_id, fingerprint)`,公司数据使用空 owner 的独立范围。
- `MarketWatchlist.owner_id` 可空;旧 actor 暂存为 legacy claim key首次注册时认领
认领前个人查询不可见。
### `PushSubscription`
- `id`, `code`, `owner_id`, `target_type` (`user|chat`), `target_id`
- `prompt`, `schedule_type`, `schedule_config`, `timezone`, `next_run_at`
- `status`, `consented_at`, `last_run_at`, `created_at`, `updated_at`
- 群目标仅从当前已验证群事件写入。
### `PushDelivery`
- `id`, `code`, `subscription_id`, `scheduled_for`, `idempotency_key`, `message_uuid`
- `status`, `attempt_count`, `next_attempt_at`, `provider_message_id`
- `last_error`, `sent_at`, `created_at`, `updated_at`
- `idempotency_key``message_uuid` 全局唯一。
### `FeishuAppTicket`
- `id`, `app_id`, `app_ticket`, `received_at`, `updated_at`
- `app_id` 全局唯一;只保留当前有效票据,不保留票据历史。
## 业务流程
### 身份与权限
```mermaid
flowchart TD
E["收到事件"] --> V{"来源已验证?"}
V -- 否 --> R["拒绝且不创建个人数据"]
V -- 是 --> K{"tenant_key/open_id 完整?"}
K -- 否 --> R
K -- 是 --> U["查找或注册普通用户"]
U --> B["应用 FEISHU_ADMIN_IDENTITIES 引导"]
B --> S{"用户有效且有权限?"}
S -- 否 --> D["拒绝并审计"]
S -- 是 --> C["执行命令"]
```
初始管理员配置按 `tenant_key:open_id` 精确匹配,多条配置均可各自完成首次引导。若该
配置身份曾执行“忘记我”,系统在同一删除事务内保留不可逆 bootstrap tombstone再次
联系时只注册为普通用户。飞书管理员命令只读取事件中的结构化 mention open_id内部
API 修改由服务 principal 审计。降级或停用前锁定目标并统计有效管理员,禁止移除最后
一个。
### 问答与偏好
1. 验证 principal 并按用户 + chat 建立会话。
2. 加载启用的公司规则、当前用户个人规则、偏好/兴趣、相关个人记忆和最近历史。
3. 按固定顺序传给 AI使用由用户与会话派生的 provider session id。
4. 真实 AI 成功后保存本轮消息,再做一次受限偏好提取;定时任务跳过两步。
5. 每次读写裁剪到最近 20 轮并删除 30 天前消息。
### 计划解析
支持:
- 单次:今天/明天 HH:mm`YYYY-MM-DD HH:mm`
- 周期:每天、工作日、每周一至周日、每月 1-31 号
- 间隔:每隔 N 分钟/小时,折算后不得短于 15 分钟
解析输出 `schedule_type + schedule_config + timezone + next_run_at(UTC)`。日期不存在、时间
已过、模糊表达、无效 IANA 时区均拒绝。月末没有目标日期时跳到下个有效月份。
### 扫描、投递与重试
```mermaid
flowchart TD
S["每分钟扫描 next_run_at<=now"] --> L["领取订阅并创建唯一投递"]
L --> Q{"用户/订阅/频控/安静时段允许?"}
Q -- 否 --> P["跳过或延后,并推进计划"]
Q -- 是 --> G["生成受限内容"]
G --> F["以 delivery UUID 发送飞书"]
F --> O{"HTTP 与业务 code 成功?"}
O -- 是 --> X["标记 sent"]
O -- 可重试 --> B["1/5/15 分钟后 retry"]
O -- 最终失败 --> Z["标记 failed"]
```
投递尝试总计最多四次(初次 + 三次重试)。扫描器重启后继续处理
`pending/retry` 状态。唯一投递键由 `subscription_id + scheduled_for` 派生;同一键重复任务
返回已有结果,不再次调用飞书。
### 忘记我
首次命令仅保存哈希确认码与短期过期时间。确认后在一个事务内删除个人规则/记忆、偏好、
兴趣、会话、订阅及投递中的个人内容,删除身份映射;相关审计 actor/target 替换为随机
匿名标识,并清空目标、请求、响应和请求 ID。若被删除身份仍属于初始管理员配置仅保留
不可逆 bootstrap tombstone 防止重新授予;用户再次联系时按新普通用户注册。删除成功
确认消息不再额外写入包含目标摘要或提供方响应的发送审计。
## 错误处理
- 未验证事件、缺身份、停用用户、权限不足:拒绝且只审计最小元数据。
- AI 未配置或 `noop`返回“AI 当前不可用”,不创建虚假回答、历史、偏好或记忆。
- 计划解析失败、额度超限、手填群 id使用可操作示例响应且不部分写入。
- 飞书 429/5xx/超时及业务非零码:分类为可重试;其他 4xx 为最终失败。
- 缺少飞书凭据且存在启用订阅readiness 返回 degraded不影响基础 health。
- 商店应用缺少可用 `app_ticket` 或默认租户时 readiness 返回 degraded自建应用若启用订阅
涉及多个租户时 readiness 返回 degraded避免把单租户令牌错误用于其他租户。
- 数据库竞争:依赖唯一约束兜底;冲突后回滚到保存点并读取已存在投递。
## 测试策略
- 单元测试:身份解析、权限矩阵、敏感偏好过滤、上下文顺序、会话裁剪、计划解析、
安静时段、下一执行时间和重试分类。
- 服务测试:两租户/两用户隔离、最后管理员保护、群订阅绑定、忘记我级联与匿名审计。
- 并发/幂等测试:多扫描器只产生一条投递、相同 UUID 不重复发送、业务非零码不成功。
- API/事件测试:验签失败不注册、内部 API Key 保护、正文 actor 不参与权限。
- 多租户认证测试A/B 租户令牌缓存隔离、`app_ticket` 只能由已验证事件更新、文本/卡片/
图片/订阅均使用目标租户、固定群任务使用默认租户。
- 迁移测试Alembic head 与元数据一致,旧规则/记忆/自选按既定策略迁移。
- 回归测试:现有固定群报表调度与现有内部接口继续工作。