LTJT 生产控制平面
这是围绕现有 LTJT 浏览器适配器增加的生产级控制层。它不新增 ERP 业务路线,也不替换 LTJT;它负责把现有任务、解析结果、确认、插件接管、执行回查和审计变成可持久化、可恢复的服务端状态。
服务边界
- PostgreSQL 是任务、事件、幂等、审计和回查状态的唯一事实源。
- 管理员使用原生账号登录;服务端会话使用 HttpOnly/Secure/SameSite Cookie。
- 登录会话默认跨浏览器重启持续有效,不按空闲时间或绝对时长自动失效;用户退出、修改密码、账号停用或管理员强制撤销时由服务端立即撤销。
- 业务页面通过 REST 创建任务;Agent 返回结构化结果后,管理员在选中任务上一次点击“确认并提交到 ERP 插件”,再通过 SSE 或轮询读取服务端状态。
- 手工与 AgentBus 的每个新任务都会按同一组织、同一 18 项业务路由固化解析策略及配置 revision;来源不能覆盖模式。其中两项名单 route 固定为
program_only,其余 16 项可配置ai / shadow / auto / program。全消息中的唯一已登记指令可以确定 route;没有指令时,只有全部标签都属于唯一 route、至少两个不同标签且业务定位必填项完整的字段签名才可确定 route。未知、冲突或多 route 输入不猜测。后续补充轮次沿用原任务快照,设置变化只影响新任务和新会话。 - 名单 route 创建后先进入
awaiting_attachment,只接收一份.xls/.xlsx14 列模板。附件在内存中失败关闭地规范化为 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 会话中工作;
chrome.storage.local只是临时缓存。 confirmation_export先按对象范围选择 ERP 源:独立团/散拼具体子单的游客名单使用did+tid,散拼母团的整团游客信息只使用tid,且母团只开放这一单一文件类型。附件在TaskArtifactStore写入前按类型处理:游客信息保留原始 ERP.xls作为源归档,同时由平台通过 LibreOffice 生成真实.xlsx;AgentBus/微信只投递.xlsx,若 XLSX 转换失败则阻断外部附件交付。其他类型由平台转换为 PDF,转换失败回退对应 ERP 源文件。生产附件写入 OSS;PostgreSQL 只保存任务归属、文件元数据和 OSS object key。OSS Bucket 按当前生产策略公共可读,但只有后端凭据可写/删;对象 URL 使用随机执行 UUID,不包含客户名或团号。- ERP 写入继续遵循预检、管理员确认、单次提交、ERP 回查;不确定结果禁止自动重试。
- 由于插件不使用独立凭据,执行期间必须保持管理员业务页面和已登录 ERP 浏览器会话可用;页面断开不会触发自动补偿或重复提交。
- 服务端先创建唯一 ERP execution,再允许页面向插件下发;同一任务只有一个
erpattempt。刷新、重连、超时和迟到回执都不能创建第二次执行。 - 插件回执必须携带服务端
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、文件字节或名单内容。 - AgentBus 入站消息会复用
TaskService的任务/会话/解析队列,解析完成后通过同一 WebSocket 返回一次task.result。组织级“全自动化”关闭时,手工与 AgentBus 新任务都要求管理员确认;开启后,两种来源的合法解析结果都自动进入 ERP 队列,不再按来源或创建、名单、安排、修改、取消/恢复、导出等业务类型保留人工例外。操作台在 EventSource 建连/重连、30 秒后台刷新以及页面重新可见或聚焦时重新读取数据库权威开关,避免后台变更后按钮仍显示旧值。缺资料、解析失败、歧义、插件校验失败或 ERP 回查不确定时仍会停止,不会绕过校验或重试不确定写入。 - 一个组织可以维护多个“用户渠道”。渠道代表外部 AgentBus 用户身份,不等同于平台管理员账号;管理员在独立根路径
/channels的“AgentBus 渠道”目录中创建、停用、启用、轮换或删除渠道。删除会停止对应 listener、移除服务端保存的 key 和该渠道尚存的持久化回执;历史任务本体保留,其channel_id按数据库契约置空。每个渠道独立保存加密后的 AgentBus key,并建立独立 WebSocket listener;列表和日志都不会回显 key。AGENTBUS_WS_URL、重连策略和客户端类型仍是全局连接配置,AGENTBUS_BOT_ADDRESS可作为渠道 bot address 的默认值。仍由完整旧环境变量托管的兼容渠道会自动重建,必须先移除环境配置并重启服务,才允许删除其数据库记录。 /history支持逐条彻底删除,以及勾选当前页后批量删除。单条使用DELETE /api/tasks/:taskId,批量使用POST /api/tasks/bulk-delete(一次 1–100 个且不能重复);两者都要求管理员 mutation 会话、same-origin 与 CSRF 门禁。批量删除会在同一数据库事务中按组织锁定并核对全部目标,任一任务不存在或不属于当前组织时整批回滚;成功后任务、生命周期、尝试、会话、附件元数据和 AgentBus 回执按外键级联删除,task-scoped audit/outbox 行显式删除。OSS 附件对象在事务提交后使用已冻结的 storage key 逐一清理,清理异常写入服务日志。对已确认或正在插件流程中的任务,页面会明确警告:停止插件只是尽力而为,已经发生的 ERP 写入及已投递到外部渠道的副本不会因删除平台历史而撤回。- 使用数据库渠道时设置
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时显示文字补充框,在awaiting_attachment时显示单文件.xls/.xlsx补交入口,提交时携带当前task_id。文件字节只保留在浏览器内存到请求完成,不进入sessionStorage或日志;左侧输入区仍始终代表新任务。全消息中的唯一已登记业务指令会开启新任务;字段签名只为已经确定是新任务的输入选择 route,不会单独改变补充消息的会话归属。任务生命周期日志继续展示缺失信息、Agent 阶段、控制面事件和插件回执,原始 JSON 按事件缩进追加在黑框内:
POST /api/messages
Content-Type: application/json
{
"message": "补充后的用户信息",
"task_id": "TASK-...",
"conversation_id": "channel-user-or-thread",
"idempotency_key": "..."
}
名单附件补交使用同一接口,正文可以为空:
{
"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 是 18 项路由的唯一机器注册表,手工与 AgentBus 共用同一个全消息 route resolver 和任务级模式快照;program-parser.ts 按“规范化 → 路由 → 字段拆分 → 类型解析 → 业务 builder → 统一 operation 校验”确定性编译输入。AI 和程序结果都经过 external-agent-client.mjs 导出的同一最终契约校验;operation Schema、插件执行契约和 ERP 写入逻辑不因解析模式或任务来源改变。
两项名单 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 链路后不再自动重解析。尚未进入插件且明确 no_plugin_dispatch=true / no_erp_write=true 的解析失败任务,可由管理员授权一次 AI 重解析。
操作台根路径 /parser-routing 提供逐业务观察统计、即时切换检查、revision 乐观并发控制和事务级“全部切回 AI”。连续天数、任务数量和 fallback 比率不作为等待门槛:18 项已实现业务均可独立进入 Shadow,多个 Shadow 可并行测试且不受固定前序约束;已出现的差异全部判定且本业务程序关键、契约、运行错误为零即可进入 Auto;程序下游失败和上述错误为零即可进入 Program。同一时间最多一个业务处于 Auto,晋级仍只能 AI → Shadow → Auto → Program 逐级进行,回退可直接执行。7/30 日统计仅供观察。组织已开启全自动化时,切入 Auto/Program 不增加人工确认期。
主要接口:
GET /api/settings/parser-routingPUT /api/settings/parser-routing/:routeIdPOST /api/settings/parser-routing/emergency-aiPOST /api/tasks/:taskId/reparsePUT /api/parser-decisions/:decisionId/review
迁移 013_business_parser_modes 增加组织路由设置、任务快照和加密的 parse_decisions;迁移 014_task_input_attachments 增加名单输入附件元数据、加密 canonical TSV 与 awaiting_attachment 索引。原文、完整程序/AI 候选、名单 canonical 中间文本和人工说明使用字段加密保存;统计、审计和日志只使用代码、哈希、字段路径和计数。
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 是否建立。
对微信来源,listener 在调用 TaskService.ingestMessage() 前执行上述严格信封解包,因此手工正文与 AgentBus 正文进入同一个业务 route resolver、任务级 mode snapshot 和 parser orchestrator;Conversation 只属于传输路由,不会再污染业务字段签名。
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 用户回复契约 生成业务回执。操作台保留完整结构供内部展示,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。
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 时,服务也会在启动前检查必需迁移 014_task_input_attachments,缺失时拒绝监听端口。db:migrate 和管理员初始化需要可连接的 PostgreSQL。开发机没有数据库时,可以运行 npm run test:control-plane 完成无数据库静态/健康烟测。
/health/ready 同时检查 PostgreSQL 可用性和必需 schema 版本;迁移未完成时返回 503,并标明 required_migration,避免任务在数据库结构未升级时进入解析队列。
生产部署
- 复制
.env.production.example为部署机受保护的.env.production,填入 PostgreSQL URL、DATABASE_SCHEMA、字段加密密钥、外部解析 Key 和 OSS 凭据。生产数据库可使用现有 PostgreSQL 实例中的新 Schema;迁移程序会创建 Schema,不会触碰其他 Schema 的测试表。 - 在正式数据库上线前执行并验证备份:
infra/backup-postgres.sh。 - 使用
docker compose --env-file .env.production up -d --build启动;Compose 会先执行数据库迁移,再启动控制平面。Compose 中的本地 PostgreSQL 仅用于--profile local,生产DATABASE_URL指向受保护的远程数据库。 - 首次启动后在容器内通过
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。 - 配置
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。