Files
LWLT-AIBOT/control-plane/README.md
2026-08-25 10:33:29 +08:00

90 lines
12 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 是任务、事件、幂等、审计和回查状态的唯一事实源。
- 管理员使用原生账号登录;服务端会话使用 HttpOnly/Secure/SameSite Cookie。
- 登录会话默认跨浏览器重启持续有效,不按空闲时间或绝对时长自动失效;用户退出、修改密码、账号停用或管理员强制撤销时由服务端立即撤销。
- 业务页面通过 REST 创建任务;Agent 返回结构化结果后,管理员在选中任务上一次点击“确认并提交到 ERP 插件”,再通过 SSE 或轮询读取服务端状态。
- 平台的任务 ID、Agent 会话 ID、确认状态、重要摘要、事件和用户通讯内容只存在于平台任务/会话/结果信封中,不回写到 Agent `operation`,也不成为 ERP 业务字段。
- Agent `operation` 只保存解析态业务事实。插件领取已确认任务后先做无需 ERP 查询的前门禁,再在 ERP 内只读唯一解析对象、资源和当前状态,最后对内部 execution operation 执行严格写前门禁。
- Chrome 插件仍在管理员已登录的浏览器/ERP 会话中工作;`chrome.storage.local` 只是临时缓存。
- `confirmation_export` 的附件在 `TaskArtifactStore` 写入前按类型处理:游客信息保留原始 ERP `.xls` 作为源归档,同时由平台通过 LibreOffice 生成真实 `.xlsx`;AgentBus/微信只投递 `.xlsx`,若 XLSX 转换失败则阻断外部附件交付。其他类型由平台转换为 PDF,转换失败回退对应 ERP 源文件。生产附件写入 OSS;PostgreSQL 只保存任务归属、文件元数据和 OSS object key。OSS Bucket 按当前生产策略公共可读,但只有后端凭据可写/删;对象 URL 使用随机执行 UUID,不包含客户名或团号。
- ERP 写入继续遵循预检、管理员确认、单次提交、ERP 回查;不确定结果禁止自动重试。
- 由于插件不使用独立凭据,执行期间必须保持管理员业务页面和已登录 ERP 浏览器会话可用;页面断开不会触发自动补偿或重复提交。
- 服务端先创建唯一 ERP execution,再允许页面向插件下发;同一任务只有一个 `erp` attempt。刷新、重连、超时和迟到回执都不能创建第二次执行。
- 插件回执必须携带服务端 `execution_id` 和领取连接;完成、阻断及待回查状态不可被后续 `running` 回执覆盖。执行租约过期会进入待回查,不会重新入队。
- Chrome 插件最低兼容版本为 `0.5.86`。该版本包含分段前门禁、ERP 只读唯一解析、严格写前门禁、当前窄生命周期适配、产品/客户分别唯一解析、写入前 `write_started` 持久化和写后回查;扩展后台重启后也不会重跑同一任务。
- 渠道 Adapter 由 AgentBus 负责;控制平面只作为 AgentBus Bot 连接到文档中的 WebSocket,不实现微信、个人微信或其他渠道协议。
- AgentBus 入站消息会复用 `TaskService` 的任务/会话/解析队列,解析完成后通过同一 WebSocket 返回一次 `task.result`。默认仍要求管理员确认;组织级“全自动化”开启后适用于所有业务类型,不再按创建、名单、安排、修改、取消/恢复、导出或删除区分人工例外。缺资料、解析失败、歧义、插件校验失败或 ERP 回查不确定时仍会停止,不会绕过校验或重试不确定写入。
- 一个组织可以维护多个“用户渠道”。渠道代表外部 AgentBus 用户身份,不等同于平台管理员账号;管理员在独立根路径 `/channels` 的“AgentBus 渠道”目录中创建、停用、启用或轮换渠道。每个渠道独立保存加密后的 AgentBus key,并建立独立 WebSocket listener;列表和日志都不会回显 key。`AGENTBUS_WS_URL`、重连策略和客户端类型仍是全局连接配置,`AGENTBUS_BOT_ADDRESS` 可作为渠道 bot address 的默认值。
- 使用数据库渠道时设置 `AGENTBUS_ENABLED=true`;此模式不要求 `AGENTBUS_WS_TOKEN` 或 `AGENTBUS_BOT_ADDRESS`,但启用的渠道仍需要全局 `AGENTBUS_WS_URL`,并可在渠道上覆盖 bot address。保留旧环境变量配置时,服务会按需创建“默认 AgentBus 渠道”兼容旧单渠道部署;`AGENTBUS_ENABLED=auto` 仅由完整的旧环境连接字段自动启用。
- `user_channels`、`tasks.channel_id` 和 `agentbus_deliveries` 共同保存入站归属、accepted 受理回执和最终 result 回执。回执以 `(channel_id, inbound_frame_id, delivery_kind)` 幂等,发送失败会重试,进程重启或 WebSocket 重连后仍会继续投递;因此不会因为超过原等待时长而丢掉最终回复。
- ERP 插件领取由组织级数据库锁和 FIFO confirmed 队列统一串行化:同一组织/同一 ERP 浏览器会话在任意时刻最多一个 ERP execution,其他任务留在服务端等待;已开始写入但结果不确定的任务会阻塞后续领取,直到人工回查收敛。
## 任务级会话续接
- `agent_sessions` 将一个业务任务绑定到一个 Superagent 会话;`agent_session_messages` 加密保存每一轮用户/Agent 消息并使用独立幂等键。
- 解析缺少可由用户补充的信息时,任务进入 `awaiting_user_input`,不会进入 ERP 或 Chrome 插件执行链。不带业务指令的补充信息通过同一任务重新排队,沿用原会话;新任务始终创建新会话。若 `/api/messages` 的新消息首个非空行命中已知业务指令,则按新任务处理,不会被旧的等待任务或旧会话吸收。
- 外部渠道或系统集成可使用已认证、CSRF 保护的 `POST /api/messages` 续接任务;当前操作台仅在选中任务处于 `awaiting_user_input` 时显示“补充信息”输入框,提交时携带当前 `task_id`,左侧输入区仍始终代表新任务。任务生命周期日志继续展示缺失信息、Agent 阶段、控制面事件和插件回执,原始 JSON 按事件缩进追加在黑框内:
```http
POST /api/messages
Content-Type: application/json
{
"message": "补充后的用户信息",
"task_id": "TASK-...",
"conversation_id": "channel-user-or-thread",
"idempotency_key": "..."
}
```
不带业务指令时,有 `task_id` 才精确续接;没有 `task_id` 时只有同一 `conversation_id` 下唯一一个等待补充任务才会自动匹配,多任务返回 `task_selection_required`,没有待补充任务则创建新任务。首个非空行带已知业务指令时,无论是否携带旧 `task_id`/`conversation_id` 都创建新任务和新会话。普通接口和任务卡不会暴露原始外部 `session_id`。微信接入和线上 Profile 发布属于后续接入工作。
## AgentBus Bot 接入
将 onboarding 文档中的 WebSocket 地址、WebSocket Token、Bot Address 和 Worker Address 写入服务端受保护的环境文件。配置 `AGENTBUS_ENABLED=auto` 时,只要填写 AgentBus 连接字段,控制平面就会自动启动长期监听;保持这些字段为空则不启动监听。
每个启用渠道会连接 `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 是否建立。
AgentBus 全链路日志使用现有控制平面 stdout/Pino 输出,并统一带 `agentbus_event` 字段,可用 `rg 'agentbus_event'` 过滤。日志覆盖连接尝试、socket 生命周期、session.ready、每个收发帧、帧忽略原因、任务入队、解析队列、持久化回执出队、最终回复和发送错误。默认只记录消息正文的长度与摘要;本地排障可在受保护的环境文件中设置 `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`,不会返回成功回执。
## 本地命令
本地命令会自动读取项目根目录 `.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-12-plus-chars' npm run admin -- bootstrap
npm run data:retention
npm run dev
```
`npm run dev` 和 `npm start` 会先执行数据库迁移,再启动控制平面;直接运行 `control-plane/src/server.ts` 或构建后的 `server.js` 时,服务也会在启动前检查必需迁移 `012_agentbus_user_channels`,缺失时拒绝监听端口。`db:migrate` 和管理员初始化需要可连接的 PostgreSQL。开发机没有数据库时,可以运行 `npm run test:control-plane` 完成无数据库静态/健康烟测。
`/health/ready` 同时检查 PostgreSQL 可用性和必需 schema 版本;迁移未完成时返回 503,并标明 `required_migration`,避免任务在数据库结构未升级时进入解析队列。
## 生产部署
1. 复制 `.env.production.example` 为部署机受保护的 `.env.production`,填入 PostgreSQL URL、`DATABASE_SCHEMA`、字段加密密钥、外部解析 Key 和 OSS 凭据。生产数据库可使用现有 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-12-plus-chars' control-plane node .build/control-plane/src/admin-cli.js bootstrap` 创建管理员;不要把密码写入镜像或 Git。
5. 配置 `infra/Caddyfile` 中的正式域名和 HTTPS,然后在 Chrome 插件中加载对应生产业务页面。
正式环境没有独立 staging。上线前必须完成离线测试、迁移预检查和迁移前备份;恢复检查使用 `infra/restore-check.sh` 指向一次性恢复数据库。
生产容器中的清理命令为:`docker compose exec control-plane node .build/control-plane/src/retention.js`。当前生产模板默认 `DATA_RETENTION_ENABLED=false`,命令只会返回零删除结果,待正式保留期限确认后再启用。本地开发则使用上面的 `npm run data:retention`。