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、失败重试、租约回收、并发重复只执行一次、成功后不重复。