- 零第三方依赖,Node >=20 原生 ESM - 团队式编排引擎:拆解/路由/执行/审查/合并全真实 LLM - 阶段心跳、单一权威清单守卫、all-keys-failed 如实上报 - H4 会话视图/amend/watchdog 有界重试/产物区 artifacts.json - H5 零依赖三栏控制台
123 lines
3.9 KiB
JavaScript
123 lines
3.9 KiB
JavaScript
/**
|
||
* mock 任务看板服务器:内存实现,语义严格对齐真实 Host v3 API:
|
||
* GET 全量文档;PUT 体内 revision 必须匹配(同时认 If-Match 头),
|
||
* 冲突 409 并回带当前文档;成功 revision+1 返回新文档。
|
||
* 可强制前 N 次 PUT 返回 409(forceConflicts),用于验证客户端重试。
|
||
* 零依赖,仅供测试/演练。
|
||
*/
|
||
import { createServer } from "node:http";
|
||
import { randomUUID } from "node:crypto";
|
||
|
||
export function makeTask(overrides = {}) {
|
||
const now = Date.now();
|
||
return {
|
||
id: overrides.id || `task-${now}-${Math.random().toString(36).slice(2, 7)}`,
|
||
title: "示例任务",
|
||
description: "",
|
||
prompt: "在工作区创建 hello.txt",
|
||
status: "todo",
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
executions: [],
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function initialState(seedTasks = []) {
|
||
return {
|
||
schemaVersion: 3,
|
||
revision: 1,
|
||
updatedAt: Date.now(),
|
||
projects: [],
|
||
tasks: seedTasks,
|
||
evidences: [],
|
||
};
|
||
}
|
||
|
||
export function startMockBoard({ port = 0, seedTasks = [], forceConflicts = 0 } = {}) {
|
||
let state = initialState(seedTasks);
|
||
let forcedLeft = forceConflicts;
|
||
const conflictCount = { value: 0 };
|
||
|
||
const server = createServer((req, res) => {
|
||
if (req.url.endsWith("/events")) {
|
||
res.writeHead(200, { "content-type": "text/event-stream" });
|
||
res.write(`data: ${JSON.stringify({ type: "ready", revision: state.revision })}\n\n`);
|
||
// 200ms 后主动断开,用于验证客户端 SSE 自动重连
|
||
const t = setTimeout(() => res.end(), 200);
|
||
req.on("close", () => {
|
||
clearTimeout(t);
|
||
res.end();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (req.method === "GET") {
|
||
res.writeHead(200, { "content-type": "application/json" });
|
||
res.end(JSON.stringify(state));
|
||
return;
|
||
}
|
||
|
||
if (req.method === "PUT") {
|
||
const chunks = [];
|
||
req.on("data", (c) => chunks.push(c));
|
||
req.on("end", () => {
|
||
let body;
|
||
try {
|
||
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||
} catch {
|
||
res.writeHead(400).end();
|
||
return;
|
||
}
|
||
const forced = forcedLeft > 0;
|
||
if (forced) forcedLeft -= 1;
|
||
if (forced || body.revision !== state.revision) {
|
||
conflictCount.value += 1;
|
||
res.writeHead(409, { "content-type": "application/json" });
|
||
res.end(
|
||
JSON.stringify({ ok: false, code: "task-board-revision-conflict", document: state }),
|
||
);
|
||
return;
|
||
}
|
||
state = {
|
||
schemaVersion: 3,
|
||
revision: state.revision + 1,
|
||
updatedAt: Date.now(),
|
||
projects: body.projects ?? [],
|
||
tasks: body.tasks ?? [],
|
||
evidences: body.evidences ?? [],
|
||
};
|
||
res.writeHead(200, { "content-type": "application/json" });
|
||
res.end(JSON.stringify(state));
|
||
});
|
||
return;
|
||
}
|
||
|
||
res.writeHead(405).end();
|
||
});
|
||
|
||
return new Promise((resolve) => {
|
||
server.listen(port, "127.0.0.1", () => {
|
||
const addr = server.address();
|
||
resolve({
|
||
url: `http://127.0.0.1:${addr.port}/api/dsh-task-board/v3`,
|
||
port: addr.port,
|
||
conflictCount,
|
||
getState: () => structuredClone(state),
|
||
close: () => new Promise((r) => server.close(r)),
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 可直接运行:node test/mock-board-server.js [port] */
|
||
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, "/")}`) {
|
||
const port = Number(process.argv[2]) || 32999;
|
||
const seed = [
|
||
makeTask({ id: "demo-todo", title: "演示待办", status: "todo" }),
|
||
makeTask({ id: "demo-backlog", title: "演示 backlog", status: "backlog" }),
|
||
];
|
||
const mock = await startMockBoard({ port, seedTasks: seed });
|
||
console.log(`mock board: ${mock.url}(Ctrl+C 退出)`);
|
||
}
|