```
feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能 - 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避 - 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐 - 增加运行组件心跳检测和readiness就绪检查机制 - 实现app_ticket事件的安全轮换和验证处理 - 添加生产环境运行编排和fail-closed安全机制 - 支持webhook快速确认和长连接独立进程处理 - 完善个人数据擦除时的待处理事件清理功能 ```
This commit is contained in:
@@ -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、失败重试、租约回收、并发重复只执行一次、成功后不重复。
|
||||
|
||||
@@ -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 标记最终失败、保存最小错误摘要并提供运行指标,
|
||||
不得记录密钥或完整敏感消息内容。
|
||||
|
||||
@@ -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 事件主体"]
|
||||
|
||||
Reference in New Issue
Block a user