# 调度网关第一阶段统一开发任务书 **任务书版本**:1.0 **发布日期**:2026-09-09 **适用项目**:`scheduler-gateway-epoch` 6.0.0 **执行方式**:四个模型同时执行同一份任务书 **阶段名称**:P0 生产化基线 ## 一、任务目标 在不破坏现有网关、节点、团队编排、控制台、插件和测试能力的前提下,把当前单机原型的核心任务生命周期改造成可验证的工程化基础: 1. 任务状态只能通过受控状态机迁移。 2. 存储访问通过 Repository 接口隔离,现有文件存储行为保持兼容。 3. claim、settle、retry、cancel、create 等关键操作支持幂等和租约。 4. 进程、节点和执行器异常后,任务不会永久丢失、重复完成或无限重试。 5. 所有关键行为有结构化审计和测试证据。 本阶段**不做数据库迁移、不做多租户、不做 SSH 真实接入、不重写团队编排器、不开发新模型适配器**。 ## 二、当前代码基线 重点代码: ```text src/server.js HTTP 网关、节点协议、任务 API src/store.js 任务/节点/审计/事件存储 src/scheduler-core.js 调度、领取、结算、重试、死信 src/protocol.js 状态常量、节点协议、鉴权 src/wal.js JSONL WAL src/recovery.js 崩溃恢复 src/team/orchestrate.js 团队式编排主流程 src/team/panel-server.js 团队控制台 API test/live-suite.mjs 网关、节点、调度、安全、WAL、压测测试 test/team-section.mjs 团队编排和证据链测试 ``` 现有行为、接口和测试是兼容基线。除非本任务书明确要求,不得删除、改名或改变既有 API 语义。 ## 三、统一技术约束 ### 3.1 必须遵守 - Node.js >= 20,原生 ESM,零第三方运行时依赖。 - 只使用 ASCII 新增代码和注释;已有中文文件按原编码维护。 - 不得修改 `归档-*`、`out`、`work`、`evidence` 中已有历史产物。 - 不得把密钥写入日志、测试快照、证据文件、任务结果、argv 或错误消息。 - 不得通过任意 `task.state = ...` 绕过状态机。 - 不得为了让测试通过而静默吞掉错误、伪造 REAL 结果或扩大重试上限。 - 保留现有 REAL/OFFLINE/fixture 诚实标记。 - 生产默认配置不能继续依赖 `dev-token-change-me`。 ### 3.2 推荐实现方式 - 新增模块优先放在 `src/domain`、`src/application`、`src/adapters`、`src/platform`。 - 先包裹旧实现,再逐步迁移调用方;不要一次性重写 `server.js` 或 `orchestrate.js`。 - 错误使用稳定错误码,错误消息可以变化但错误类型不能靠字符串猜测。 - 所有变更都必须有针对性测试;跨模块改动必须补集成测试。 ## 四、统一交付范围 ### 任务 A:实现任务状态机 新增任务状态机模块,例如: ```text src/domain/task-state-machine.js ``` 要求: - 定义当前任务所有合法状态和迁移。 - 提供 `canTransition(from, to)`、`transition(task, to, context)` 等明确接口。 - 统一处理 `queued`、`running`、`done/succeeded`、`failed`、`canceled`、`dead`、`rework`、`waiting_approval` 等当前代码实际使用的状态。 - 迁移失败必须抛出稳定错误码,例如 `INVALID_STATE_TRANSITION`。 - 迁移时追加历史记录,记录操作者、原因、时间、attemptId 和 requestId。 - 将 `server.js`、`store.js`、`scheduler-core.js` 中直接修改状态的关键路径迁移到状态机。 - 重复 settle、过期 lease settle、已取消任务回报等场景必须拒绝或转为幂等成功,不能产生第二个终态。 ### 任务 B:建立 Repository 抽象 新增存储接口,例如: ```text src/domain/repositories.js src/adapters/storage/file-repository.js ``` 要求: - 至少抽象任务、节点、事件、审计、死信和产物索引访问。 - 现有 `GatewayStore` 可作为兼容实现,但业务服务不得继续依赖 Map 的内部结构。 - 保留 `persist:false` 的干净内存测试模式。 - 保留现有 JSON 落盘和 WAL 能力,不能因抽象而关闭恢复逻辑。 - Repository 方法必须区分“找不到”“冲突”“非法状态”“存储失败”。 - 为未来 SQLite/PostgreSQL 实现保留异步接口,即使当前 FileRepository 内部仍是同步实现。 建议最小接口: ```text getTask(id) createTask(input, context) updateTask(id, patch, context) transitionTask(id, nextState, context) claimTask(id, lease, context) settleTask(id, result, context) listTasks(query) getNode(id) upsertNode(input) appendEvent(event) appendAudit(entry) ``` ### 任务 C:实现幂等键和租约 要求: - 创建任务支持 `Idempotency-Key` 或等价请求字段;同一项目/网关范围内重复请求返回同一任务。 - claim 生成唯一 `leaseId`,并记录 `leaseExpiresAt`、`nodeId`、`attemptNo`。 - settle 必须校验 lease;重复提交同一 `leaseId` 和相同结果应幂等返回;不同结果必须拒绝并审计。 - lease 过期后任务可重投,但旧节点迟到回报不得覆盖新结果。 - retry、cancel、approve、amend 具备幂等行为。 - 所有幂等命中和冲突都要有审计记录。 - 不得用进程内 Map 作为唯一幂等数据来源;至少要能随现有持久化状态恢复。 ### 任务 D:统一错误、审计和请求关联 要求: - 新增稳定错误码集合,例如: ```text INVALID_ARGUMENT TASK_NOT_FOUND INVALID_STATE_TRANSITION LEASE_NOT_FOUND LEASE_EXPIRED IDEMPOTENCY_CONFLICT TASK_ALREADY_TERMINAL STORAGE_ERROR ``` - 每个 HTTP 请求生成或透传 `X-Request-Id`。 - 每个任务运行生成或透传 `runId`、`taskId`、`attemptId`、`nodeId`。 - 审计记录至少包含:时间、动作、对象、结果、操作者/来源、requestId、runId、错误码。 - API 错误统一为: ```json { "error": { "code": "INVALID_STATE_TRANSITION", "message": "任务当前不可取消", "requestId": "req_xxx", "details": {} } } ``` - 保持现有接口状态码兼容;如必须变化,在兼容字段中同时提供新错误结构。 ### 任务 E:健康检查和配置校验 新增: ```text src/platform/config.js src/platform/health.js ``` 要求: - 统一读取端口、host、数据目录、WAL、节点 token、TLS、LLM provider 和超时配置。 - 新增 `gateway check` 或等价检查命令。 - 新增 `/healthz`:进程存活检查,不依赖外部服务成功。 - 新增 `/readyz`:检查数据目录、WAL/Repository 可写性、必要配置和依赖状态。 - 生产模式检测到默认节点 token 时必须给出明确警告;安全模式可直接拒绝启动。 - 检查结果必须脱敏,不能输出 key、token、密码或完整环境变量。 ### 任务 G:任务版本与乐观锁 当前 `task.state` 直接赋值,没有 revision 字段,幂等键只能去重但做不了并发冲突检测。 要求: - 每个任务带 `revision`(整数,初始 1),每次 transition/settle/update 成功后自增。 - `transitionTask`、`settleTask`、`updateTask` 必须接受 `expectedRevision`;不匹配则返回 `REVISION_CONFLICT`,不修改任务。 - 重复提交相同 `leaseId` + 相同结果时,revision 不变,幂等返回当前终态。 - 不同结果或不同 `attemptId` 的 settle 必须因 revision/lease 校验失败而拒绝。 - revision 必须随任务持久化,重启后恢复的 revision 与落盘一致。 - 审计记录每次 revision 变化,包含旧 revision、新 revision、触发动作。 测试必须覆盖: - 两个并发 settle 只有一个成功。 - 过期 lease 持有者迟到 settle 被拒绝。 - `expectedRevision` 不匹配时返回冲突错误且任务不变。 ### 任务 H:事件持久化与 SSE 重连 当前 `EventBus` 是进程内 `Set`,seq 不持久化,SSE 断线后无法补播,审计链路不完整。 要求: - 事件通过 `EventRepository` 持久化(FileRepository 先落 JSONL,与 WAL 同目录或 `state/events.jsonl`)。 - 每个事件带 `seq`(全局递增)、`type`、`aggregateId`(taskId/runId/nodeId)、`revision`、`at`、`requestId`。 - SSE 支持 `Last-Event-ID`:客户端断线重连时,从 `Last-Event-ID + 1` 开始补播。 - 没有 `Last-Event-ID` 时从当前 seq 开始推送(不回放全量)。 - 事件保留期可配置(默认 7 天或 10000 条,先到先裁);裁剪时保留最早和最新各一份用于边界测试。 - 进程重启后 seq 必须继续递增,不能回退。 测试必须覆盖: - 断线重连后能收到断线期间的事件。 - 高并发下 seq 单调递增无重复。 - 损坏的事件文件不阻塞启动(跳过损坏行并审计)。 ### 任务 I:回调机制加固 当前 `fireCallback` 是 best-effort 裸 `fetch` + 5s 超时,无重试、无签名、无死信,生产环境会丢任务结果。 要求: - 回调必须有 HMAC-SHA256 签名:`X-Signature: t=,v1=`,密钥从任务 `callbackSecret` 或网关 `GW_CALLBACK_SECRET` 读取。 - 回调失败按指数退避重试(至少 3 次:1s / 5s / 30s);全部失败后进入回调死信队列。 - 回调死信队列独立于任务死信队列;死信记录 `taskId`、`callbackUrl`、`lastError`、`attemptCount`、`createdAt`。 - 回调必须幂等:接收方可用 `X-Callback-Id`(= taskId)去重。 - 回调 payload 必须包含 `taskId`、`runId`、`state`、`revision`、`result`、`finishedAt`。 - 密钥不得出现在日志、审计、导出或错误消息中。 测试必须覆盖: - 回调成功且签名校验通过。 - 回调失败后重试 3 次进入死信。 - 回调密钥缺失时明确报错而非静默跳过。 - 重复回调不会触发多次下游副作用(通过幂等键验证)。 ### 任务 J:任务取消传播 当前 `cancel` 只改任务状态,不通知执行节点中断,节点可能继续跑已取消的任务。 要求: - `cancel` 必须将取消信号传播到持有当前 lease 的执行节点(通过 `gw-node/1` 协议新增 `cancel` 消息或复用现有 `interrupt` 机制)。 - 取消后任务进入 `canceled` 终态;执行节点的迟到回报在任务已 `canceled` 时必须被拒绝。 - 如果任务有子任务(`parentRun` / `dependencies`),取消父任务时必须级联取消未完成的子任务。 - 取消操作本身必须幂等:对已 `canceled` 的任务再次 cancel 不报错。 - 取消必须审计:记录谁取消、为什么取消、取消传播到了哪些子任务。 - 如果节点不在线或取消信号投递失败,任务仍应进入 `canceled` 终态(取消不依赖节点确认)。 测试必须覆盖: - 取消正在执行的任务后,节点的迟到回报被拒绝。 - 取消有子任务的父任务后,子任务也进入 `canceled`。 - 对已取消的任务再次调用 cancel 不报错。 - 节点离线时 cancel 仍能成功终态化任务。 ### 任务 K:死信队列加固 当前 `deadLetter` 是普通数组,无 retryCount、无 originalTaskId、无 deadReason,死信可被无限重投。 要求: - 每条死信记录必须包含:`deadLetterId`、`taskId`、`originalTaskId`(= 首次进入死信的任务 id)、`deadReason`、`retryCount`(从死信重投的次数)、`firstDeadAt`、`lastDeadAt`。 - 死信重投有上限:`maxDeadRetries`(默认 3);超过后标记 `exhausted`,不再自动重投。 - `exhausted` 死信只能通过人工 `POST /api/deadletter/:id/force-retry` 重投,且必须记录操作者。 - 死信队列支持按 `deadReason` / `retryCount` / `originalTaskId` 查询。 - 死信重投必须生成新的 `attemptId` 和 `leaseId`,不能复用旧 lease。 - 死信归档:`exhausted` 超过保留期后自动归档到 `state/archived-dead-letters.json`,归档后不再出现在活跃死信查询中。 测试必须覆盖: - 任务失败 3 次进入死信。 - 死信重投超过 `maxDeadRetries` 后标记 `exhausted`。 - `force-retry` 能重投 `exhausted` 死信并记录操作者。 - 归档后的死信不出现在活跃列表中。 ### 任务 F:补齐测试和验证脚本 新增或扩展测试: - 状态机:合法迁移、非法迁移、重复终态、取消竞态。 - Repository:内存态、文件态、WAL 开关、损坏文件、恢复后读取。 - 幂等:重复创建、重复 claim、重复 settle、冲突 settle、过期 lease。 - 故障:进程中断、节点掉线、旧节点迟到回报、回调失败、磁盘不可写。 - 安全:错误响应不泄漏密钥,requestId 和审计不泄漏敏感字段。 - 回归:现有 `npm test`、`npm run test:conversation`、`npm run e2e-team` 能继续运行。 ## 五、明确不允许的实现结果 - 只新增函数但没有接入真实任务路径。 - 只修改测试 fixture,未覆盖 server/node/orchestrator 实际入口。 - 通过放宽断言、增加无限重试、忽略异常来制造全绿。 - 把 `done`、`failed`、`dead` 等终态重新改回可执行状态但不留下 retry/rework 记录。 - 用文件名、日志文本或前端状态推断任务真实状态。 - 把数据库、Redis、消息队列作为本阶段必须依赖,导致现有零依赖模式无法运行。 - 修改归档文件或清理现有 evidence 以掩盖回归。 - 回调重试无限循环或吞掉签名错误;回调死信队列静默丢弃而不审计。 - 取消操作只改状态不传播到执行节点和子任务,导致任务状态与实际执行不一致。 - 死信队列无 retryCount 上限,允许无限重投或永久静默堆积。 - 事件持久化阻塞调度主路径,或 seq 回退导致审计链断裂。 - revision 冲突被静默忽略而非返回 `REVISION_CONFLICT`,导致并发覆盖不可见。 ## 六、完成验收标准 ### 功能验收 - [ ] 所有关键状态迁移经过统一状态机。 - [ ] 业务层不再直接依赖 `GatewayStore` 的内部 Map 结构。 - [ ] `create/claim/settle/retry/cancel/approve` 支持幂等或冲突检测。 - [ ] 任务拥有 leaseId、leaseExpiresAt、attemptId 等执行关联字段。 - [ ] 旧 lease 的迟到回报不能覆盖新 attempt。 - [ ] `/healthz`、`/readyz` 和配置检查可用。 - [ ] 错误响应包含稳定错误码和 requestId。 - [ ] 审计能够还原一次任务从创建到终态的完整过程。 - [ ] 任务带 `revision`,并发 settle 只有一个成功,`REVISION_CONFLICT` 正确返回。 - [ ] 事件持久化到 Repository,SSE 断线重连后能补播断线期间的事件。 - [ ] 回调有 HMAC 签名、失败重试 3 次、全部失败进死信队列。 - [ ] 取消信号传播到执行节点,已取消任务的迟到回报被拒绝,子任务级联取消。 - [ ] 死信带 `retryCount`/`originalTaskId`/`deadReason`,超过 `maxDeadRetries` 标记 `exhausted`。 ### 回归验收 ```powershell npm test npm run test:conversation npm run e2e-team npm run recovery-demo npm run stress ``` 要求:所有命令退出码为 0;若本机没有真实 LLM key,真实 LLM 命令不得被伪装为成功,使用项目既有 OFFLINE/fixture 方式验证。 ### 性能验收 - 在现有压测规模下,任务无重复领取、无永久丢失、无重复终态。 - 新增状态机、Repository、审计后,现有 stress 测试吞吐下降不超过 **10%**。 - 必须提供改造前后的 baseline 对比数据:吞吐(task/s)、P95/P99 排队延迟、P95/P99 执行延迟、重复领取数、丢失任务数。 - 单次状态迁移不产生不必要的全量历史文件重写。 - 事件持久化不能阻塞调度主路径;事件写入失败的降级行为必须有测试覆盖。 ### 安全验收 - 错误、审计、状态、导出、日志中均不存在 key/token/password/secret 明文。 - 默认 token、非法路径、非法状态、伪造 lease、越权 settle 均有拒绝结果。 - 不引入新的 shell 拼接、动态代码执行或不受限文件路径。 ## 七、交付物格式 每个模型必须提交以下内容: ```text 1. 修改文件清单 2. 实现摘要 3. 状态迁移表 4. Repository 接口说明 5. 幂等和 lease 语义说明 6. 测试命令与实际结果 7. 未完成项和已知风险 8. 与其它模型合并时的冲突点 ``` 禁止只提交“已完成”“测试通过”等无证据结论。 ## 八、四模型协作规则 四个模型使用完全相同的任务书,但必须各自独立工作区或分支,禁止互相覆盖文件。建议每个模型优先形成完整闭环,不按文件机械拆分: - 模型 1:重点实现状态机(A)+ 任务版本与乐观锁(G)并接入 server/scheduler。 - 模型 2:重点实现 Repository(B)+ 事件持久化(H)并接入 store/application 层。 - 模型 3:重点实现幂等/lease(C)+ 回调加固(I)+ 死信加固(K)+ 错误码和审计关联(D)。 - 模型 4:重点实现取消传播(J)+ 补测试(F)+ 健康检查和配置校验(E)。 上述分工只是减少冲突,**验收标准对四个模型完全相同**。每个模型都必须审阅并修正自己改动触及的兼容性问题,不能以“不是我的分工”为理由留下失败测试。 合并顺序建议: 1. 状态机、错误码和 revision(任务 A、D、G)。 2. Repository 兼容层和事件持久化(任务 B、H)。 3. 幂等和 lease(任务 C)。 4. server、scheduler、node-runtime 接入。 5. 回调加固、死信加固、取消传播(任务 I、K、J)。 6. 健康检查和测试补齐(任务 E、F)。 7. 全量回归与人工审阅。 ## 九、给模型的执行指令 你正在参与 `scheduler-gateway-epoch` 第一阶段 P0 生产化基线开发。请严格按照本任务书执行:先阅读 `README.md`、`ARCHITECTURE.md`、`IMPLEMENTATION.md`、`ADR.md` 和相关源码;确认现有行为后再修改。你的目标是提交可合并代码,而不是写方案。保持 Node >=20、零第三方运行时依赖、REAL/OFFLINE 诚实标记和现有 API 兼容。完成后必须运行对应测试,报告实际命令、退出码、修改文件和剩余风险。不得修改归档目录,不得删除用户已有改动,不得泄漏任何密钥。 ## 十、阶段完成定义 只有同时满足以下条件,第一阶段才算完成: - 四个模型的实现可以合并为一个统一版本。 - 全量回归测试通过,新增测试覆盖状态机、Repository、幂等和 lease。 - 所有任务终态和异常路径都有审计证据。 - 进程重启、节点掉线和迟到回报不会造成重复终态。 - 现有面板、插件、看板桥和团队编排功能未被破坏。 - 文档、测试数字和实际代码状态一致。