feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
12 KiB
设计文档
概述
本设计在现有 FastAPI 模块化单体内增加飞书终端用户身份、个人画像与持久化订阅三个
业务域。内部 API 继续使用服务 API Key;飞书终端权限只能来自已经通过验签或 SDK
验证的事件,身份主键固定为 (tenant_key, open_id)。
平台只写自身数据库。旧业务 MySQL 保持 SELECT-only。个人定时提示词不允许调用工具、 读取公司报表或写入问答历史;群订阅也不加载创建者个人画像。
架构设计
系统架构图
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"]
数据流图
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_idrole(user|admin),status(active|disabled)timezone,quiet_hours_start,quiet_hours_endlast_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_idprompt,schedule_type,schedule_config,timezone,next_run_atstatus,consented_at,last_run_at,created_at,updated_at- 群目标仅从当前已验证群事件写入。
PushDelivery
id,code,subscription_id,scheduled_for,idempotency_key,message_uuidstatus,attempt_count,next_attempt_at,provider_message_idlast_error,sent_at,created_at,updated_atidempotency_key和message_uuid全局唯一。
FeishuAppTicket
id,app_id,app_ticket,received_at,updated_atapp_id全局唯一;只保留当前有效票据,不保留票据历史。
业务流程
身份与权限
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 审计。降级或停用前锁定目标并统计有效管理员,禁止移除最后
一个。
问答与偏好
- 验证 principal 并按用户 + chat 建立会话。
- 加载启用的公司规则、当前用户个人规则、偏好/兴趣、相关个人记忆和最近历史。
- 按固定顺序传给 AI;使用由用户与会话派生的 provider session id。
- 真实 AI 成功后保存本轮消息,再做一次受限偏好提取;定时任务跳过两步。
- 每次读写裁剪到最近 20 轮并删除 30 天前消息。
计划解析
支持:
- 单次:今天/明天 HH:mm,
YYYY-MM-DD HH:mm - 周期:每天、工作日、每周一至周日、每月 1-31 号
- 间隔:每隔 N 分钟/小时,折算后不得短于 15 分钟
解析输出 schedule_type + schedule_config + timezone + next_run_at(UTC)。日期不存在、时间
已过、模糊表达、无效 IANA 时区均拒绝。月末没有目标日期时跳到下个有效月份。
扫描、投递与重试
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 与元数据一致,旧规则/记忆/自选按既定策略迁移。
- 回归测试:现有固定群报表调度与现有内部接口继续工作。