feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能

- 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避
- 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐
- 增加运行组件心跳检测和readiness就绪检查机制
- 实现app_ticket事件的安全轮换和验证处理
- 添加生产环境运行编排和fail-closed安全机制
- 支持webhook快速确认和长连接独立进程处理
- 完善个人数据擦除时的待处理事件清理功能
```
This commit is contained in:
2026-07-27 17:14:37 +08:00
parent d7db84571d
commit eb8267ed18
61 changed files with 8703 additions and 183 deletions

View File

@@ -103,6 +103,34 @@ sequenceDiagram
使用 `FEISHU_DEFAULT_TENANT_KEY`,不得复用另一租户的令牌。
- AI adapter context 增加每用户 session id`noop` 明确返回不可用且不写会话、偏好或记忆。
### 混合数据库协调与运行编排
- `app/tools/reconcile_platform_schema.py` 只面向平台 PostgreSQL。它先生成结构指纹和允许列表
dry-run获取 PostgreSQL advisory transaction lock 后执行一次性基线协调;实际结构、
行数、依赖、权限或版本任一不符即中止。
- 协调 revision 只补齐当前 metadata 缺失的表、列、索引、外键和唯一语义;除已审核为空且
无依赖的遗留表外不删除远端对象。完成后在同一事务内验证零漂移并登记 Alembic revision。
- `FEISHU_EVENT_TRANSPORT` 明确选择 `disabled|webhook|long_connection`。生产环境开启用户
功能时必须具备相应凭据;长连接使用独立受管进程,不嵌入 API 工作线程。
- API、scheduler、worker 和长连接进程使用 heartbeat 表报告存活。readiness 同时校验
Alembic head、期望组件 heartbeat、队列和飞书依赖缺失或陈旧时返回 HTTP 503。
- Compose 提供内部 PostgreSQL默认编排和不覆盖 `DATABASE_URL` 的外部数据库覆盖方式,
并为持续进程配置重启策略。运行配置只使用本地且不提交的 `.env`
### 飞书入站 inbox
- 已验证的消息事件先写入 `FeishuEventReceipt` inbox再由持久化扫描任务执行。
- inbox 保存处理所需的短期规范化载荷、状态、尝试次数、下次尝试时间、租约和最小错误摘要;
成功或最终失败后清除原始载荷,避免长期保存个人消息。
- 命令事务内通过 reply outbox 捕获唯一的文本、卡片或图片回复意图、目标租户和稳定 UUID
只有命令写入、receipt 成功状态与回复意图一起提交后才允许访问飞书网络。
- 回复使用独立状态、租约和退避时间。发送失败或发送成功后进程在状态提交前退出时,只以
同一内容和 UUID 重试回复,不重新执行用户命令;终态后清除回复载荷。
- webhook 在验真、挑战处理和 inbox 入库后立即确认;长连接 SDK 回调复用同一入库路径。
- 执行器通过唯一事件键、数据库锁、租约 token 和状态条件更新防止并发重复执行。失败进入
有界重试,进程崩溃后由过期租约重新领取;成功事件的后续重复投递只返回已有状态。
- `app_ticket` 仍在已验证边界内同步轮换,不把票据写入普通消息 inbox。
## 数据模型
### `FeishuUser`
@@ -158,6 +186,15 @@ sequenceDiagram
- `id`, `app_id`, `app_ticket`, `received_at`, `updated_at`
- `app_id` 全局唯一;只保留当前有效票据,不保留票据历史。
### `FeishuEventReceipt` inbox 扩展
- `event_key`, `source`, `event_id`, `message_id`, `received_at`
- `status`, `payload`, `attempt_count`, `next_attempt_at`
- `locked_until`, `lock_token`, `last_error`, `processed_at`, `updated_at`
- `reply_payload`, `reply_status`, `reply_attempt_count`, `reply_next_attempt_at`
- `reply_locked_until`, `reply_locked_by`, `reply_last_error`, `reply_sent_at`
- 旧 receipt 迁为已成功状态;新消息使用唯一 `event_key` 保证幂等。
## 业务流程
### 身份与权限
@@ -219,6 +256,28 @@ flowchart TD
`pending/retry` 状态。唯一投递键由 `subscription_id + scheduled_for` 派生;同一键重复任务
返回已有结果,不再次调用飞书。
### 入站事件处理
```mermaid
flowchart TD
F["飞书已验证事件"] --> C{"挑战或 app_ticket"}
C -- 是 --> S["立即安全处理并确认"]
C -- 否 --> I["幂等写入 inbox pending"]
I --> A["立即 ACK"]
W["持久化扫描器"] --> L["领取租约 processing"]
L --> E["执行身份、权限和命令"]
E --> O{"结果"}
O -- 成功 --> X["原子提交 succeeded 与 reply outbox"]
O -- 可重试 --> R["retry + 退避时间"]
O -- 超限 --> Z["failed + 最小错误并清空 payload"]
X --> Q["独立领取并发送 reply"]
Q --> Y{"发送结果"}
Y -- 成功 --> C["reply succeeded 并清空载荷"]
Y -- 可重试 --> QR["仅重试同一内容与 UUID"]
QR --> W
R --> W
```
### 忘记我
首次命令仅保存哈希确认码与短期过期时间。确认后在一个事务内删除个人规则/记忆、偏好、
@@ -237,6 +296,10 @@ flowchart TD
- 商店应用缺少可用 `app_ticket` 或默认租户时 readiness 返回 degraded自建应用若启用订阅
涉及多个租户时 readiness 返回 degraded避免把单租户令牌错误用于其他租户。
- 数据库竞争:依赖唯一约束兜底;冲突后回滚到保存点并读取已存在投递。
- 混合数据库基线不匹配:协调工具中止并输出不含凭据/数据的结构差异,不自动猜测或 stamp。
- 期望运行组件无 heartbeat、Alembic 非 head 或事件 transport 不可用readiness 返回 503。
- 入站命令失败或进程丢失租约:保留短期 inbox payload 并按退避重试;成功或最终失败后清除。
- 入站回复失败或进程在发送后退出:保留短期 reply payload 并以稳定 UUID 重试,绝不重跑命令。
## 测试策略
@@ -249,3 +312,6 @@ flowchart TD
图片/订阅均使用目标租户、固定群任务使用默认租户。
- 迁移测试Alembic head 与元数据一致,旧规则/记忆/自选按既定策略迁移。
- 回归测试:现有固定群报表调度与现有内部接口继续工作。
- 基线测试:用远端结构的脱敏快照验证 dry-run 指纹、允许列表、事务回滚和零漂移。
- 运行测试Compose 契约、transport 配置、Alembic head、组件 heartbeat 与 fail-closed。
- 入站测试:快速 ACK、失败重试、租约回收、并发重复只执行一次、成功后不重复。

View File

@@ -211,3 +211,57 @@
飞书非零业务码和数据库迁移一致性。
6. WHEN 完整验证运行 THEN Ruff、Python 编译检查、Alembic 元数据一致性测试和全部 pytest
测试 SHALL 通过。
### 需求 11混合数据库安全基线
**用户故事:** 作为维护人员,我希望现有未登记 Alembic 版本的混合平台数据库可以安全
协调到当前模型,而不丢失已有平台数据或误操作业务数据库。
#### 验收条件
1. WHEN 协调工具连接数据库 THEN 系统 SHALL 只接受平台 PostgreSQL且不得连接或修改
`LEGACY_DATABASE_URL`
2. WHEN 数据库版本为空且结构符合已审核的混合基线 THEN 系统 SHALL 在事务锁内仅执行
允许列表中的结构补齐、旧约束替换和已确认空表清理。
3. IF 实际结构指纹、数据行数、依赖关系、权限或 Alembic 版本不符合预检 THEN 系统 SHALL
中止且不得留下部分 DDL 或版本记录。
4. WHEN 协调完成 THEN 系统 SHALL 验证 SQLAlchemy metadata 无漂移,再原子登记 Alembic
版本IF 任一步失败 THEN 全部变更 SHALL 回滚。
5. WHEN 常规数据库已经位于受支持 revision THEN 系统 SHALL 继续使用标准 Alembic 升级,
不得重复执行一次性基线协调。
### 需求 12生产运行与就绪判定
**用户故事:** 作为运维人员,我希望 API、飞书事件、调度器和任务执行进程可以持续运行
且系统只在依赖和迁移真正就绪时接收流量。
#### 验收条件
1. WHEN 启用飞书用户功能 THEN 系统 SHALL 明确选择 `webhook``long_connection` 事件入口;
IF 对应凭据或进程未就绪 THEN production 配置或 readiness SHALL 失败关闭。
2. WHEN 使用长连接部署 THEN 运行编排 SHALL 启动独立飞书事件进程,并为 API、事件进程、
scheduler 和 worker 配置重启策略。
3. WHEN scheduler、worker 或长连接属于当前配置期望组件 THEN 系统 SHALL 要求其存在新鲜
heartbeat缺失或过期 SHALL 使 readiness 返回 HTTP 503。
4. WHEN readiness 检查平台数据库 THEN 系统 SHALL 验证当前 Alembic revision 与 head 一致,
而不只执行 `SELECT 1`
5. WHEN 生成部署配置样例 THEN 系统 SHALL 只包含占位符和安全默认值,不得提交真实密钥;
外部 PostgreSQL 部署 SHALL 能避免被 Compose 内部数据库 URL 强制覆盖。
### 需求 13可靠的飞书入站事件
**用户故事:** 作为飞书用户,我希望机器人在临时故障或进程重启后仍能处理我的消息,
同时不会因飞书重复投递而重复执行命令。
#### 验收条件
1. WHEN webhook 或长连接收到已经验证的事件 THEN 系统 SHALL 先以唯一事件键持久化 inbox
状态,再快速确认接收,不得在 webhook 请求内同步等待 AI 或飞书回复。
2. IF 多个进程并发接收相同事件 THEN 系统 SHALL 只创建一个 inbox 记录,并只允许一个
有效租约执行命令。
3. IF 命令执行发生可重试失败或执行进程在完成前退出 THEN 事件 SHALL 回到持久化重试状态,
不得因为 receipt 已登记而永久丢失。
4. WHEN 命令执行成功 THEN 系统 SHALL 原子标记成功;后续重复事件 SHALL 返回已接收且不得
再次执行命令。
5. WHEN 重试超过上限 THEN 系统 SHALL 标记最终失败、保存最小错误摘要并提供运行指标,
不得记录密钥或完整敏感消息内容。

View File

@@ -47,12 +47,37 @@
- _Requirements: 6.2, 9, 10.2, 10.4_
- [x] 8. 完成配置、Alembic 迁移与全量验证
- 增加安全关闭的功能开关、管理员身份配置并更新 `.env.example`
- 增加安全关闭的功能开关、管理员身份配置并更新 `.env` 配置
- 创建迁移并处理旧公司规则、旧自动记忆和旧自选延迟认领。
- 补齐身份、隔离、权限、计划、并发、投递、删除和迁移回归测试。
- 运行 Ruff、compileall、完整 pytest 和迁移一致性检查。
- _Requirements: 3.7, 10_
- [x] 9. 安全协调现有混合平台数据库
- 新增严格允许列表的 reconciliation revision 和一次性 baseline runner。
- 实现结构指纹、advisory transaction lock、权限/行数/依赖预检和原子版本登记。
- 用脱敏结构快照验证成功、漂移拒绝和失败回滚;不得连接或修改业务 MySQL。
- _Requirements: 10, 11_
- [x] 10. 补齐生产运行编排和 fail-closed readiness
- 增加明确事件 transport、生产配置校验、`.env` 配置和长连接服务编排。
- 为 API、scheduler、worker、飞书事件进程增加重启/heartbeat并校验 Alembic head。
- 支持外部 PostgreSQL 部署且不被 Compose 内部数据库 URL 意外覆盖。
- _Requirements: 12_
- [x] 11. 实现可靠飞书入站 inbox
- 扩展事件 receipt 为持久化 inbox支持状态、租约、退避重试和终态载荷清理。
- webhook 快速 ACK长连接共用入库路径并发重复和进程重启不得重复或丢失命令。
- 命令事务原子保存 reply outbox回复失败只以稳定 UUID 重试,不重新执行命令。
- 接入 scheduler/Celery/inline 执行适配并增加运行指标。
- _Requirements: 1, 9, 13_
- [ ] 12. 应用平台迁移并完成真实运行验收
- 停止相关进程,执行 dry-run、备份/事务预检、协调迁移和零漂移验证。
- 启动 API、事件入口和 scheduler验证 health/readiness、heartbeat 与无待处理错误。
- 通过真实已验证事件绑定首位管理员,并完成私聊订阅和群订阅 smoke。
- _Requirements: 11, 12, 13_
```mermaid
flowchart LR
T1["1 身份权限"] --> T2["2 事件主体"]