- 零第三方依赖,Node >=20 原生 ESM - 团队式编排引擎:拆解/路由/执行/审查/合并全真实 LLM - 阶段心跳、单一权威清单守卫、all-keys-failed 如实上报 - H4 会话视图/amend/watchdog 有界重试/产物区 artifacts.json - H5 零依赖三栏控制台
399 lines
18 KiB
Markdown
399 lines
18 KiB
Markdown
# 调度网关第一阶段统一开发任务书
|
||
|
||
**任务书版本**: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=<timestamp>,v1=<hmac>`,密钥从任务 `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。
|
||
- 所有任务终态和异常路径都有审计证据。
|
||
- 进程重启、节点掉线和迟到回报不会造成重复终态。
|
||
- 现有面板、插件、看板桥和团队编排功能未被破坏。
|
||
- 文档、测试数字和实际代码状态一致。
|