90 lines
12 KiB
Markdown
90 lines
12 KiB
Markdown
# 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`。
|