Files
LWLT-AIBOT/control-plane/README.md
inman 295409bff0 merge: integrate leader summary webhook delivery
# Conflicts:
#	control-plane/README.md
#	control-plane/src/db.ts
#	control-plane/test/control-plane.test.ts
2026-09-09 17:25:23 +08:00

162 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LTJT 生产控制平面
这是围绕现有 LTJT 浏览器适配器增加的生产级控制层。它不新增 ERP 业务路线,也不替换 LTJT它负责把现有任务、解析结果、确认、插件接管、执行回查和审计变成可持久化、可恢复的服务端状态。
## 服务边界
- PostgreSQL 是任务、事件、幂等、审计和回查状态的唯一事实源。
- 平台账号使用 `admin`(管理员)、`team_lead`(组长)和 `user`(普通用户)三种固定角色登录;服务端会话使用 HttpOnly/Secure/SameSite Cookie。管理员维护账号、角色、状态、密码重置、会话撤销和员工可执行任务类型但自身不进入任务数据面平台不提供组织或租户选择。
- 登录会话默认跨浏览器重启持续有效,不按空闲时间或绝对时长自动失效;用户退出、修改密码、账号停用或管理员强制撤销时由服务端立即撤销。
- 业务页面通过 REST 创建任务Agent 返回结构化结果后,任务归属人在自己的任务上一次点击“确认并提交到 ERP 插件”,再通过 SSE 或轮询读取服务端状态。普通用户与组长的任务 API、SSE、附件和插件执行权限只覆盖分配给本人的人工任务与 AgentBus 任务;管理员是纯管理面身份,不能创建、查看、修改、领取、接收事件或提交任何业务任务。
- 手工与 AgentBus 的每个新任务都会按同一组织、同一 19 项业务路由固化解析策略及配置 revision来源不能覆盖模式。其中两项名单 route 与散拼批量子单 route 固定为 `program_only`,其余 16 项可配置 `ai / shadow / auto / program`。全消息中的唯一已登记指令可以确定 route没有指令时只有全部标签都属于唯一 route、至少两个不同标签且业务定位必填项完整的字段签名才可确定 route。未知、冲突或多 route 输入不猜测。后续补充轮次沿用原任务快照,设置变化只影响新任务和新会话。
- 组长和普通用户使用逐账号白名单,新账号默认没有任何可执行任务类型;管理员自身的任务白名单固定为空,只能在 `/accounts` 为员工逐项授权。已登记但未授权的业务返回 `business_not_authorized`;无法唯一确认 route 的员工输入返回 `business_type_unresolved`。两种拒绝都发生在解析器和 ERP 插件之前,并记录不含明文的授权拒绝审计。权限在补充输入、人工确认、全自动确认和插件领取前再次校验;取消授权后的任务不会进入 ERP 队列。AgentBus 入站使用渠道绑定员工的同一白名单,管理员账号不得成为员工渠道归属人。
- 名单 route 创建后先进入 `awaiting_attachment`,只接收一份 `.xls/.xlsx`;控制面在前 100 行中自动定位唯一的 ERP 名单字段表头,按精确字段语义从任意表头行和列顺序中只选择 12 个必需源字段,并把表头下方连续名单数据在内存中规范化为 13 列 canonical TSV。身份证、年龄、源证件类型和其他普通额外列不参与名单行识别、字段校验或输出必需字段缺失/重复、多个候选表头、必需字段中的非法公式及宏、外链等主动内容仍失败关闭。原始工作簿不入库,文件名和 canonical TSV 使用字段加密,解析通过或终止后清除 canonical 中间文本。附件到齐前不会领取解析任务,也不会进入 ERP。独立团初始 16 行、散拼子单初始 31 行仅为 ERP 动态扩行基线5000 为技术上限。
- 平台的任务 ID、Agent 会话 ID、确认状态、重要摘要、事件和用户通讯内容只存在于平台任务/会话/结果信封中,不回写到 Agent `operation`,也不成为 ERP 业务字段。
- Agent `operation` 只保存解析态业务事实。插件领取已确认任务后先做无需 ERP 查询的前门禁,再在 ERP 内只读唯一解析对象、资源和当前状态,最后对内部 execution operation 执行严格写前门禁。
- Chrome 插件仍在当前平台账号已登录的浏览器/ERP 会话中工作;每个员工平台账号必须配置唯一 ERP 账号,同一平台账号在 90 秒新鲜期内只允许一台云电脑保持执行 worker。心跳携带扩展对期望 ERP 账号的只读匹配结果;账号不匹配、心跳过期、第二台并发云电脑或任务归属不一致都会在领取和回执两端失败关闭。`chrome.storage.local` 只是临时缓存。
- `confirmation_export` 先按对象范围选择 ERP 源:独立团/散拼具体子单的游客名单使用 `did+tid`,散拼母团的整团游客信息只使用 `tid`,且母团只开放这一单一文件类型。附件在 `TaskArtifactStore` 写入前按类型处理:游客信息保留原始 ERP `.xls` 作为源归档,同时由平台通过 LibreOffice 生成真实 `.xlsx`AgentBus/微信只投递 `.xlsx`,若 XLSX 转换失败则阻断外部附件交付。其他类型由平台转换为 PDF转换失败回退对应 ERP 源文件。生产附件写入 OSSPostgreSQL 只保存任务归属、文件元数据和 OSS object key。OSS Bucket 按当前生产策略公共可读,但只有后端凭据可写/删;对象 URL 使用随机执行 UUID不包含客户名或团号。
- ERP 写入继续遵循预检、任务归属人确认或管理员配置的全自动化、单次提交、ERP 回查;不确定结果禁止自动重试。
- 由于插件不使用独立凭据,执行期间必须保持任务归属人的业务页面和已登录 ERP 浏览器会话可用;页面断开不会触发自动补偿或重复提交。
- 服务端先创建唯一 ERP execution再允许页面向插件下发同一任务只有一个 `erp` attempt。刷新、重连、超时和迟到回执都不能创建第二次执行。
- 插件回执必须携带服务端 `execution_id` 和领取连接;完成、阻断及待回查状态不可被后续 `running` 回执覆盖。执行租约过期会进入待回查,不会重新入队。
- Chrome 插件最低兼容版本由正式操作台与根目录发布清单共同门禁。插件包含分段前门禁、ERP 只读唯一解析、严格写前门禁、当前窄生命周期适配、写入前 `write_started` 持久化和写后回查;扩展后台重启后也不会重跑同一任务。本次双轨解析不修改插件执行契约或扩展版本。
- 渠道 Adapter 由 AgentBus 负责;控制平面只作为 AgentBus Bot 连接到文档中的 WebSocket不实现微信、个人微信或其他渠道协议。
- 微信桥接器把正文放在严格的 `New WeChat message` / `Conversation:` / `Text:` 三行传输信封中AgentBus listener 会在任务快照前只解开这一已知信封,把 `Text:` 同行值及后续行作为业务正文,并在帧没有显式 `conversation_id` 时使用信封中的 `Conversation:` 值。显式字段仍优先;近似、缺失字段或空正文的包装保持原文,不能通过忽略任意未知标签来绕过 Program parser 的失败关闭。
- 微信侧的 `[WeChat attachment: 文件名]` 只是一段传输占位文字,不代表控制面已经收到文件。若同一帧没有符合契约的 `payload.attachments[]`listener 会在进入任务服务前失败关闭、保留原名单任务的等待状态并返回“附件内容未传到平台”不会把占位文字创建成新业务任务。附件元数据、HTTPS URL、DNS、大小或摘要校验失败时返回对应的安全摘要仍不回显 URL、文件字节或名单内容。当前生产部署位于受信内网入站附件 URL 可以使用内网域名、私网 IPv4/IPv6 或 localhost因此 AgentBus 渠道和上游桥接器必须被视为受信输入边界。
- AgentBus 入站消息会复用 `TaskService` 的任务/会话/解析队列,并以渠道绑定员工写入 `created_by` 与不可变的 `assigned_user_id`,解析完成后通过同一 WebSocket 返回一次 `task.result`。组织级“全自动化”关闭时,手工与 AgentBus 新任务都需要人工确认;开启后,两种来源的合法解析结果都自动进入 ERP 队列,不再按来源或创建、名单、安排、修改、取消/恢复、导出等业务类型保留人工例外。历史未归属 AgentBus 任务不会自动执行。操作台在 EventSource 建连/重连、30 秒后台刷新以及页面重新可见或聚焦时重新读取数据库权威开关。缺资料、解析失败、歧义、插件校验失败或 ERP 回查不确定时仍会停止,不会绕过校验或重试不确定写入。
- 单一部署范围可以维护多个“用户渠道”。每个渠道代表一个外部 AgentBus 用户身份,并且必须一对一绑定一个有效的非管理员平台账号;一个平台账号也只能绑定一个渠道。管理员在 `/channels` 创建、绑定、停用、启用、轮换或删除渠道。只有绑定账号有效且已配置 ERP 账号的启用渠道才启动 listener未绑定渠道失败关闭。删除会停止对应 listener、移除服务端保存的 key 和该渠道尚存的持久化回执;历史任务本体保留,其 `channel_id` 置空而 `assigned_user_id` 不变。每个渠道独立保存加密后的 AgentBus key同一 key 不能被多个渠道复用;列表和日志都不会回显 key。`AGENTBUS_WS_URL`、重连策略和客户端类型仍是全局连接配置,`AGENTBUS_BOT_ADDRESS` 可作为渠道 bot address 的默认值。
- `/history` 同时提供可恢复的归档/恢复与显式的永久强制删除。`POST /api/tasks/:taskId/archive``POST /api/tasks/:taskId/restore``POST /api/tasks/bulk-archive` 保留归档语义及运行状态门禁;`DELETE /api/tasks/:taskId``POST /api/tasks/bulk-delete` 会绕过任务状态门禁并物理删除任务及其输入、事件、尝试、会话、投递和附件记录,同时清理任务 outbox并在提交后尽力清理 OSS 对象。普通用户和组长只能操作本人任务;管理员不能进入该页面或调用这些接口。不可逆删除仍保留最小化的删除审计事件。
- `/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实时执行事件、插件领取、执行回执以及强制删除后的浏览器清理命令只发送或接受符合角色约束的员工账号。组长摘要是独立的只读外部 Webhook 投影,不授予任何任务或 ERP 权限。已开始写入但结果不确定的任务只阻塞同一账户的后续领取,直到归属账号人工回查收敛或任务被明确强制删除。
## 任务级会话续接
- `agent_sessions` 将一个业务任务绑定到一个 Superagent 会话;`agent_session_messages` 加密保存每一轮用户/Agent 消息并使用独立幂等键。
- 解析缺少可由用户补充的信息时,任务进入 `awaiting_user_input`,不会进入 ERP 或 Chrome 插件执行链。不带业务指令的补充信息通过同一任务重新排队,沿用原会话;新任务始终创建新会话。若 `/api/messages` 的新消息首个非空行命中已知业务指令,则按新任务处理,不会被旧的等待任务或旧会话吸收。
- 外部渠道或系统集成可使用已认证、CSRF 保护的 `POST /api/messages` 续接任务;操作台在 `awaiting_user_input` 时显示文字补充框,在 `awaiting_attachment` 时显示单文件 `.xls/.xlsx` 补交入口,提交时携带当前 `task_id`。文件字节只保留在浏览器内存到请求完成,不进入 `sessionStorage` 或日志;左侧输入区仍始终代表新任务。全消息中的唯一已登记业务指令会开启新任务;字段签名只为已经确定是新任务的输入选择 route不会单独改变补充消息的会话归属。任务生命周期日志继续展示缺失信息、Agent 阶段、控制面事件和插件回执,原始 JSON 按事件缩进追加在黑框内:
```http
POST /api/messages
Content-Type: application/json
{
"message": "补充后的用户信息",
"task_id": "TASK-...",
"conversation_id": "channel-user-or-thread",
"idempotency_key": "..."
}
```
名单附件补交使用同一接口,正文可以为空:
```json
{
"message": "",
"task_id": "TASK-...",
"attachments": [{
"name": "passenger-list.xlsx",
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size": 12345,
"content_base64": "..."
}]
}
```
不带业务指令时,有 `task_id` 才精确续接;没有 `task_id` 时只有同一 `conversation_id` 下唯一一个等待补充任务才会自动匹配,多任务返回 `task_selection_required`,没有待补充任务则创建新任务。首个非空行带已知业务指令时,无论是否携带旧 `task_id`/`conversation_id` 都创建新任务和新会话。普通接口和任务卡不会暴露原始外部 `session_id`。微信接入和线上 Profile 发布属于后续接入工作。
## 双轨解析与渐进切换
`business-routes.ts` 是 19 项路由的唯一机器注册表,手工与 AgentBus 共用同一个全消息 route resolver 和任务级模式快照;`program-parser.ts` 按“规范化 → 路由 → 字段拆分 → 类型解析 → 业务 builder → Program operation 校验”确定性编译输入。新增散拼批量子单只由 Program 校验入口接受,外部 Agent 仍保持原 18 项指令路由;两者下游继续进入同一标准 operation Schema 与插件执行契约。
两项名单 route 与散拼批量子单 route 是硬性例外:`allowed_modes=['program']``mode_locked=true`,组织设置、紧急全部切 AI 和一次性 AI 重解析都不能覆盖;紧急开关只作用于其余 16 项可配置业务。
- `ai`:只运行原 AI 路径;迁移后的设置和历史任务均从该模式开始。
- `shadow`AI 是唯一权威结果,程序并行解析;两份候选只加密保存,任务详情只按差异路径解密展示 operation 字段,不公开完整候选或传输元数据。
- `auto`:程序优先;仅 `program_unsupported_syntax``program_unknown_field``program_internal_error``program_timeout``program_contract_invalid` 可整体切到 AI。缺字段、非法值、冲突、歧义、多动作和业务规则阻断不会调用 AI。
- `program`:只运行程序;失败时追问或阻断,不调用 AI。
Auto 一旦发生 AI fallback任务会永久绑定原 AI 会话。每次解析只有一个完整权威结果,不拼接候选;任务进入确认、插件领取或 ERP 链路后不再自动重解析。管理员任务隔离后不再从任务详情执行一次性 AI 重解析或解析差异判定。
操作台根路径 `/parser-routing` 提供逐业务观察统计、即时切换检查、revision 乐观并发控制和事务级“全部切回 AI”。连续天数、任务数量和 fallback 比率不作为等待门槛16 项可配置业务均可独立进入 Shadow多个 Shadow 可并行测试且不受固定前序约束;已出现的差异全部判定且本业务程序关键、契约、运行错误为零即可进入 Auto程序下游失败和上述错误为零即可进入 Program。同一时间最多一个业务处于 Auto晋级仍只能 `AI → Shadow → Auto → Program` 逐级进行回退可直接执行。7/30 日统计仅供观察。组织已开启全自动化时,切入 Auto/Program 不增加人工确认期。
主要管理接口:
- `GET /api/settings/parser-routing`
- `PUT /api/settings/parser-routing/:routeId`
- `POST /api/settings/parser-routing/emergency-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` 保存旧 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 接入
数据库渠道模式在服务端受保护的环境文件中配置全局 `AGENTBUS_WS_URL` 并设置 `AGENTBUS_ENABLED=true`。管理员先在 `/accounts` 为员工配置唯一 ERP 账号和允许的业务类型,再在 `/channels` 创建渠道、填写该渠道自己的 AgentBus key并选择对应员工账号。员工随后在自己的云电脑同时登录该平台账号与所绑定 ERP 账号并打开扩展。只有这四项就绪的启用渠道才启动长期 listener。
旧单渠道环境变量仍可创建一个兼容渠道记录,但该记录初始未绑定、停用且不启动;管理员必须完成员工绑定后手动启用。`AGENTBUS_ENABLED=auto` 只用于识别完整的旧环境连接字段,不会绕过账号绑定。
每个启用渠道会连接 `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` 页面只读展示组长摘要的外部 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` 只属于传输路由,不会再污染业务字段签名。
AgentBus 全链路日志使用控制平面 stdout/Pino 输出,同时带 `diagnostic_event=agentbus.<agentbus_event>` 与原有 `agentbus_event`。日志覆盖连接尝试、socket 生命周期、session.ready、每个收发帧、帧忽略原因、任务入队、解析队列、持久化回执出队、最终回复和发送错误。名单附件另外记录元数据存在性、DNS 开始/通过、解析地址数量与 IP family、HTTPS 状态、重定向次数、接收字节数、大小/摘要校验和各阶段耗时;不记录 URL、hostname、IP、文件名、附件字节或名单值。开发/测试环境可临时设置 `AGENTBUS_LOG_PAYLOADS=true` 记录最多 2,000 个字符的正文预览,生产环境会拒绝以该值启动。渠道 key、WebSocket Token、Invoke Token 和 Authorization header 永不写入日志。
监听器只使用每个渠道的 WebSocket key文档中的 Invoke Token 仅用于另一服务通过 Function Call API 主动向 Bot 投递任务,本服务的监听链路不会使用它。
任务失败时,公共任务响应的 `failure` 字段和最终生命周期事件会同时给出 `error_code``failure_stage``failure_source``failure_message``agent_returned``plugin_dispatch_started``erp_write_started``no_plugin_dispatch``no_erp_write`。因此可以区分“Agent 未返回”“Agent 已返回但契约校验失败”“插件已接管但 ERP 未写入”和“ERP 已写入但回执待回查”等状态,不再只显示笼统的“外部解析失败”。
任务响应统一提供 `important_message`,作为“需返回/交互用户的重要消息”出口:`awaiting_user_input` 返回 Superagent 的 `reply`,错误优先返回 ERP 业务反馈、否则返回简短错误摘要,成功且有可验证 ERP 证据时按 [AgentBus 用户回复契约](../agent设计规范/agentbus-reply-contract.md) 生成业务回执。操作台保留完整结构供内部展示AgentBus `task.result` 发往渠道时携带 `kind`、用户可读 `text` 和符合微信渠道适配器契约的 HTTPS URL-only 附件元数据;不发送 `content_base64` 或后台鉴权下载地址。外部文件投递要求生产附件使用 OSS 存储,且 URL 域名已加入适配器白名单。错误码、执行阶段、回执校验状态、隐藏 ID 和哈希不外发。`awaiting_confirmation` 不进入该栏。没有 `reply` 的 awaiting 结果会在解析阶段转为错误;没有可验证回执的 `completed` 执行会被归类为 `reconciliation_pending`,不会返回成功回执。
## 服务端诊断日志
控制面、迁移、数据保留、数据库异常和管理员命令都输出一行一个 JSON 的结构化日志。公共字段包括 `time``level``service``environment``deployment_revision``pid``hostname``diagnostic_event``diagnostic_stage`;链路字段按事件提供 `request_id``task_id``inbound_frame_id``conversation_id``channel_id`、状态、结果与 `duration_ms`。覆盖范围包括服务启动/关闭、HTTP 开始/完成/拒绝、任务状态事件、数据库 audit event、解析队列和 Worker、AgentBus、附件入口、OSS 清理、数据库连接/回滚以及未捕获进程错误。
未知异常只记录受限 `error_code``error_name`、稳定的 `error_fingerprint`、errno/syscall 和最多 12 个清理后的代码栈帧;不把第三方异常原文复制进日志。请求 query/body、Cookie、Authorization、CSRF、密码、API key、Token、附件 URL/字节和业务原文不进入生产日志。每个 HTTP 响应返回同一个 `x-request-id`,可直接用于跨开始、错误和完成事件检索。部署时应把 `DEPLOYMENT_REVISION` 设置为当前提交或发布编号,避免不同镜像的日志混淆。
Docker Compose 使用 `json-file`,默认每个容器保留 10 个、每个 20 MB 的轮转文件;可在 `.env.production``LOG_MAX_SIZE``LOG_MAX_FILES` 调整。服务端只读排障命令会显示容器状态、`/health/ready` 和最近日志,不读取或打印环境文件:
```bash
sh infra/diagnose-server.sh
sh infra/diagnose-server.sh --since 2h --match TASK-20260830071915-g7INIT0
sh infra/diagnose-server.sh --since 2h --match roster_attachment_metadata_missing
sh infra/diagnose-server.sh --since 30m --match request:example:12345678
sh infra/diagnose-server.sh --since 30m --all-services
```
`--match` 是字面量过滤,可使用 request/task/frame/conversation/channel ID、`diagnostic_event``error_code`。脚本默认最多读取 1,000 行控制面日志,`--tail` 上限为 50,000且不在仓库生成日志文件。
## 本地命令
本地命令会自动读取项目根目录 `.env`,不需要先手动 `export` 环境变量。首次部署或换环境时,只需替换该文件;生产 Docker Compose 使用 `.env.production`
```bash
npm install
npm run check
npm run build
npm run db:migrate
ADMIN_USERNAME=admin ADMIN_PASSWORD='replace-with-password' npm run admin -- bootstrap
npm run data:retention
npm run dev
```
`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`。启用组长摘要时,再填写服务提供方确认后的完整 `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。
5. 配置 `infra/Caddyfile` 中的正式域名和 HTTPS然后在 Chrome 插件中加载对应生产业务页面。
6. 启动后运行 `sh infra/diagnose-server.sh --since 10m`确认容器、readiness、`service.listening`、AgentBus session 与当前 `deployment_revision`
正式环境没有独立 staging。上线前必须完成离线测试、迁移预检查和迁移前备份恢复检查使用 `infra/restore-check.sh` 指向一次性恢复数据库。
生产容器中的保留策略命令为:`docker compose exec control-plane node .build/control-plane/src/retention.js`。当前生产模板默认 `DATA_RETENTION_ENABLED=false`;启用后,符合期限的终态任务只会转为归档,并清理已撤销或已过期超过 30 天的登录会话,不会物理删除任务或审计记录。本地开发则使用上面的 `npm run data:retention`