merge: integrate leader summary webhook delivery

# Conflicts:
#	control-plane/README.md
#	control-plane/src/db.ts
#	control-plane/test/control-plane.test.ts
This commit is contained in:
inman committed 2026-09-09 17:25:23 +08:00
commit 295409bff0
21 files changed
+1260 -1269

No files matched your search

+5 -5
View File
@@ -29,7 +29,7 @@
- `/operations-dashboard` 是组长专用的只读业务操作看板。它以不可变的任务归属账号作为员工口径,纳入同一固定组织中所有已归属的人工与 AgentBus 任务,并支持从结果状态、员工、上海业务日期和业务类型逐层穿透;历史上无法安全归属员工的 AgentBus 任务不在人员看板中被猜测归属。列表和详情只回答“谁负责什么任务、收到什么指令、完成了什么结果”:详情返回员工、业务类型、完整指令轮次、输入附件名称/行数和可读业务结果,不返回任务生命周期、解析/执行 JSON、技术阶段、错误码或产物地址。关键词查询先受日期、人员、业务和状态约束,单次解密匹配候选最多 2,000 条,超过时要求继续缩小范围。该路径不授予他人任务修改、ERP 执行、SSE、产物下载、账号维护或全局安全审计权限;管理员同样无权访问。
- 使用数据库渠道时设置 `AGENTBUS_ENABLED=true`;此模式不要求 `AGENTBUS_WS_TOKEN` 或 `AGENTBUS_BOT_ADDRESS`,但启用的渠道仍需要全局 `AGENTBUS_WS_URL`,并可在渠道上覆盖 bot address。保留旧环境变量配置时,服务会按需创建“默认 AgentBus 渠道”兼容旧单渠道部署;兼容渠道初始为未绑定且不启动,管理员必须在 `/channels` 绑定员工账号后再启用。`AGENTBUS_ENABLED=auto` 仅由完整的旧环境连接字段自动启用。
- `user_channels`、`tasks.channel_id` 和 `agentbus_deliveries` 共同保存入站归属、accepted 受理回执和最终 result 回执。回执以 `(channel_id, inbound_frame_id, delivery_kind)` 幂等,发送失败会重试,进程重启或 WebSocket 重连后仍会继续投递;因此不会因为超过原等待时长而丢掉最终回复。
- ERP 插件领取按任务 `assigned_user_id` 使用账户级数据库锁和 FIFO confirmed 队列:同一平台/ERP 账户在任意时刻最多一个 ERP execution,该账户的其他任务留在服务端等待;不同账户的活跃或待执行任务互不占用队列位置、可独立领取执行。管理员不能成为 `assigned_user_id`、渠道 owner、浏览器 worker 或组长任务摘要接收人,也不能打开任务 SSE;实时执行事件、任务摘要、插件领取、执行回执以及强制删除后的浏览器清理命令只发送或接受符合角色约束的员工账号。已开始写入但结果不确定的任务只阻塞同一账户的后续领取,直到归属账号人工回查收敛或任务被明确强制删除。
- ERP 插件领取按任务 `assigned_user_id` 使用账户级数据库锁和 FIFO confirmed 队列:同一平台/ERP 账户在任意时刻最多一个 ERP execution,该账户的其他任务留在服务端等待;不同账户的活跃或待执行任务互不占用队列位置、可独立领取执行。管理员不能成为 `assigned_user_id`、渠道 owner 或浏览器 worker,也不能打开任务 SSE;实时执行事件、插件领取、执行回执以及强制删除后的浏览器清理命令只发送或接受符合角色约束的员工账号。组长摘要是独立的只读外部 Webhook 投影,不授予任何任务或 ERP 权限。已开始写入但结果不确定的任务只阻塞同一账户的后续领取,直到归属账号人工回查收敛或任务被明确强制删除。
## 任务级会话续接
@@ -89,7 +89,7 @@ Auto 一旦发生 AI fallback,任务会永久绑定原 AI 会话。每次解
旧的任务详情重解析和解析差异判定路由仍保留兼容响应,但已纳入任务数据面统一门禁;管理员调用会返回 `task_access_forbidden`,管理页面不再提供这两个任务级入口。
迁移 `013_business_parser_modes` 增加内部固定范围的路由设置、任务快照和加密的 `parse_decisions`;迁移 `014_task_input_attachments` 增加名单输入附件元数据、加密 canonical TSV 与 `awaiting_attachment` 索引;迁移 `015_account_roles_and_task_audit` 增加账号角色、密码更新时间、输入/附件操作者、任务归档和账号级幂等(历史 `must_change_password` 列仅保留兼容,当前流程不启用首次强制改密);迁移 `016_team_lead_operations_dashboard` 增加组长角色与人工指令看板索引;迁移 `017_user_business_route_authorizations` 增加逐账号业务白名单、授权人和乐观并发 revision;迁移 `018_agentbus_account_workers` 增加 ERP 账号、渠道归属、任务执行归属、唯一在线 worker 与 ERP 身份核验字段;迁移 `020_leader_task_summary_notifications` 增加内部失败关闭的组长路由快照和独立加密通知 outbox;迁移 `021_admin_task_data_plane_isolation` 清除管理员的遗留任务执行绑定和待发组长摘要,并以数据库触发器阻止管理员成为任务创建人/归属人、渠道 owner、浏览器 worker、业务授权对象或任务摘要接收人;迁移 `022_shared_child_order_batch_create` 只扩展 Program-only 散拼批量子单的解析设置与员工授权 route 约束,不自动授予任何账号。原文、完整程序/AI 候选、名单 canonical 中间文本、人工说明和待发组长摘要使用字段加密保存;统计、全局审计和运行日志不复制明文业务输入。
迁移 `013_business_parser_modes` 增加内部固定范围的路由设置、任务快照和加密的 `parse_decisions`;迁移 `014_task_input_attachments` 增加名单输入附件元数据、加密 canonical TSV 与 `awaiting_attachment` 索引;迁移 `015_account_roles_and_task_audit` 增加账号角色、密码更新时间、输入/附件操作者、任务归档和账号级幂等(历史 `must_change_password` 列仅保留兼容,当前流程不启用首次强制改密);迁移 `016_team_lead_operations_dashboard` 增加组长角色与人工指令看板索引;迁移 `017_user_business_route_authorizations` 增加逐账号业务白名单、授权人和乐观并发 revision;迁移 `018_agentbus_account_workers` 增加 ERP 账号、渠道归属、任务执行归属、唯一在线 worker 与 ERP 身份核验字段;迁移 `020_leader_task_summary_notifications` 保存旧 AgentBus 组长摘要订阅与投递历史;迁移 `021_admin_task_data_plane_isolation` 清除管理员的遗留任务执行绑定和待发组长摘要,并以数据库触发器阻止管理员成为任务创建人/归属人、渠道 owner、浏览器 worker、业务授权对象或任务摘要接收人;迁移 `022_shared_child_order_batch_create` 只扩展 Program-only 散拼批量子单的解析设置与员工授权 route 约束,不自动授予任何账号;迁移 `023_leader_summary_webhook_delivery` 停用旧 AgentBus 组长摘要并创建组织级外部 Webhook 状态与加密投递 outbox。原文、完整程序/AI 候选、名单 canonical 中间文本、人工说明和待发组长摘要使用字段加密保存;统计、全局审计和运行日志不复制明文业务输入。
## AgentBus Bot 接入
@@ -99,7 +99,7 @@ Auto 一旦发生 AI fallback,任务会永久绑定原 AI 会话。每次解
每个启用渠道会连接 `AGENTBUS_WS_URL?ready=1`,使用该渠道自己的 `Authorization: Bearer <channel-agentbus-key>`,等待 `session.ready` 后接收普通 `event` 消息。普通任务发送一次持久化受理通知(`task.progress`,`status=accepted`)并在完成时返回一次 `task.result`。名单任务的首次文字指令改为返回明确的等待附件提示,附件入站改为返回“名单附件已收到,正在校验并处理”;最终结果只归属触发解析的最新附件消息,因此不会因文字与附件两条入站帧重复发送成功回执。解析完成、等待确认或进入 ERP 等内部进度不外发。`GET /health/ready` 和 `GET /api/status` 的 `agentbus.channels` 字段可用于确认每个 listener 与 session 是否建立。
`/channels` 只读展示组长任务摘要抄送状态,不再维护单独的订阅开关、收件地址或微信会话 ID。有效 `team_lead` 账号拥有已启用且可连接的 AgentBus 渠道时,服务会自动启用组织范围摘要;优先采用该账号经此渠道最近一次有效入站帧的 `from` 与 `conversation_id`,尚无入站记录时使用渠道 `external_user_ref` 并按普通回复规则生成会话键。没有任何可用路由时保持等待,首条有效组长消息到达后自动学习;渠道换绑账号时会清空旧外部用户标识,且历史入站路由只有任务归属仍是当前组长时才能复用,避免抄送到原渠道所有者。`GET /api/settings/leader-summary-subscriptions` 只返回不含明文目标的投递健康状态,没有人工修改或真实测试发送接口。角色、账号、渠道归属、渠道启停或路由变化会建立新的生效时间与 revision,取消旧路由未发送摘要,且永不回补历史。摘要固定读取同组织其他非管理员归属人的人工和 AgentBus 任务,只从自动生效后的 `task.updated` 稳定结果投影。主动帧为无 `reply_to` 的 `task.summary`,员工回执始终优先。正文只含员工、业务、稳定状态、公共任务编号、上海提交时间以及白名单团号/订单号,不含原始指令、人员资料、附件或技术错误;路由与正文加密,日志只留指纹。发送为至少一次语义且微信消息不可撤回。
`/channels` 页面只读展示组长摘要的外部 Webhook 状态;该投递不再依赖组长 AgentBus 账号、渠道或微信会话。运行环境同时配置完整 `WEBHOOK_SEND_URL` 与 32 位 `WEBHOOK_EXTERNAL_TOKEN` 后自动启用,请求固定为 `POST`、原始 `x-token`、JSON 字符串 ID `9999` 和非空 `content`。配置缺失或无效只会关闭这个摘要 worker 并显示安全错误,不会阻断普通用户任务、员工 AgentBus、解析、确认或 ERP 执行。`GET /api/settings/leader-summary-webhook` 只返回不含 URL/token/正文的状态与队列计数,没有人工修改或真实测试发送接口。配置首次生效或变化会建立新的时间边界和 revision,只投影此后的已明确归属、非管理员人工/AgentBus 任务稳定结果,永不回补历史。正文只含员工、业务、稳定状态、公共任务编号、上海提交时间以及白名单团号/订单号,并在数据库中字段加密。只有 HTTP 200、`code=0`、`data=true` 同时成立才记为“已受理”,不代表微信已送达;明确拒绝、超时、断网、5xx 或未知响应均不自动重试,以避免无幂等能力的接口产生重复群通知。完整契约见 [`leader-summary-webhook-contract.md`](../agent设计规范/leader-summary-webhook-contract.md)。
对微信来源,listener 在调用 `TaskService.ingestMessage()` 前执行上述严格信封解包,因此手工正文与 AgentBus 正文进入同一个业务 route resolver、任务级 mode snapshot 和 parser orchestrator;`Conversation` 只属于传输路由,不会再污染业务字段签名。
@@ -143,13 +143,13 @@ npm run data:retention
npm run dev
```
`npm run dev` 和 `npm start` 会先执行数据库迁移,再启动控制平面;直接运行 `control-plane/src/server.ts` 或构建后的 `server.js` 时,服务也会在启动前检查必需迁移 `022_shared_child_order_batch_create`,缺失时拒绝监听端口。`db:migrate` 和管理员初始化需要可连接的 PostgreSQL。开发机没有数据库时,可以运行 `npm run test:control-plane` 完成无数据库静态/健康烟测。
`npm run dev` 和 `npm start` 会先执行数据库迁移,再启动控制平面;直接运行 `control-plane/src/server.ts` 或构建后的 `server.js` 时,服务也会在启动前检查必需迁移 `023_leader_summary_webhook_delivery`,缺失时拒绝监听端口。`db:migrate` 和管理员初始化需要可连接的 PostgreSQL。开发机没有数据库时,可以运行 `npm run test:control-plane` 完成无数据库静态/健康烟测。
`/health/ready` 同时检查 PostgreSQL 可用性和必需 schema 版本;迁移未完成时返回 503,并标明 `required_migration`,避免任务在数据库结构未升级时进入解析队列。
## 生产部署
1. 复制 `.env.production.example` 为部署机受保护的 `.env.production`,填入 `DEPLOYMENT_REVISION`、PostgreSQL URL、`DATABASE_SCHEMA`、字段加密密钥、外部解析 Key 和 OSS 凭据,并确认 `AGENTBUS_LOG_PAYLOADS=false`。生产数据库可使用现有 PostgreSQL 实例中的新 Schema;迁移程序会创建 Schema,不会触碰其他 Schema 的测试表。
1. 复制 `.env.production.example` 为部署机受保护的 `.env.production`,填入 `DEPLOYMENT_REVISION`、PostgreSQL URL、`DATABASE_SCHEMA`、字段加密密钥、外部解析 Key 和 OSS 凭据,并确认 `AGENTBUS_LOG_PAYLOADS=false`。启用组长摘要时,再填写服务提供方确认后的完整 `WEBHOOK_SEND_URL` 与单独提供的 32 位 `WEBHOOK_EXTERNAL_TOKEN`;不得通过真实发送猜测网关路径。生产数据库可使用现有 PostgreSQL 实例中的新 Schema;迁移程序会创建 Schema,不会触碰其他 Schema 的测试表。
2. 在正式数据库上线前执行并验证备份:`infra/backup-postgres.sh`。
3. 使用 `docker compose --env-file .env.production up -d --build` 启动;Compose 会先执行数据库迁移,再启动控制平面。Compose 中的本地 PostgreSQL 仅用于 `--profile local`,生产 `DATABASE_URL` 指向受保护的远程数据库。
4. 首次启动后在容器内通过 `docker compose exec -e ADMIN_USERNAME=admin -e ADMIN_PASSWORD='replace-with-password' control-plane node .build/control-plane/src/admin-cli.js bootstrap` 创建管理员;不要把密码写入镜像或 Git。