feat: add claw backend adapter

This commit is contained in:
kris
2026-04-03 01:36:29 +08:00
parent 8daaea01fd
commit 39b576cc42
23 changed files with 1212 additions and 23 deletions

View File

@@ -2,7 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { NextRequest } from "next/server";
let runtimeRoot = "";
@@ -240,6 +240,99 @@ test("master-agent enqueue 在主节点离线时会自动切到 OpenAI 后台队
}
});
test("master-agent enqueue 在显式选择 claw-runtime 时会通过 Claw 异步回写回复", async () => {
const clawDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-queue-"));
const clawScriptPath = path.join(clawDir, "claw-runtime.mjs");
await writeFile(
clawScriptPath,
`
let stdin = "";
process.stdin.setEncoding("utf8");
for await (const chunk of process.stdin) {
stdin += chunk;
}
const payload = JSON.parse(stdin);
process.stdout.write(JSON.stringify({
status: "completed",
output: "Claw 已接管当前主 Agent 会话:" + payload.body
}));
`,
"utf8",
);
const previousEnv = {
BOSS_CLAW_ENABLED: process.env.BOSS_CLAW_ENABLED,
BOSS_CLAW_COMMAND: process.env.BOSS_CLAW_COMMAND,
BOSS_CLAW_ARGS: process.env.BOSS_CLAW_ARGS,
BOSS_CLAW_TIMEOUT_MS: process.env.BOSS_CLAW_TIMEOUT_MS,
};
process.env.BOSS_CLAW_ENABLED = "true";
process.env.BOSS_CLAW_COMMAND = process.execPath;
process.env.BOSS_CLAW_ARGS = clawScriptPath;
process.env.BOSS_CLAW_TIMEOUT_MS = "1000";
await saveAiAccount({
accountId: "master-codex-primary-claw",
label: "主 GPT",
role: "primary",
provider: "master_codex_node",
displayName: "Mac 上的 Master Codex Node",
nodeId: "local-codex-node",
nodeLabel: "本机 Codex",
model: "gpt-5.4",
enabled: true,
setActive: true,
loginStatusNote: "用于 Claw backend 队列测试。",
});
await updateProjectAgentControls("master-agent", {
backendOverride: "claw-runtime",
});
try {
const response = await POST(
await createAuthedRequest("master-agent", {
body: "请走 Claw runtime",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
task?: { taskId: string; status: string } | null;
masterReply?: { accountId?: string } | null;
masterReplyState?: string | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.masterReply?.accountId, "claw-runtime");
assert.equal(payload.masterReplyState, "queued");
assert.ok(payload.task?.taskId);
await waitFor(async () => {
const state = await readState();
const task = state.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
return task?.status === "completed";
});
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.equal(task?.status, "completed");
assert.equal(task?.replyBody, "Claw 已接管当前主 Agent 会话:请走 Claw runtime");
const masterProject = nextState.projects.find((project) => project.id === "master-agent");
const mirroredReply = masterProject?.messages.at(-1);
assert.match(mirroredReply?.body ?? "", /Claw 已接管当前主 Agent 会话/);
} finally {
process.env.BOSS_CLAW_ENABLED = previousEnv.BOSS_CLAW_ENABLED;
process.env.BOSS_CLAW_COMMAND = previousEnv.BOSS_CLAW_COMMAND;
process.env.BOSS_CLAW_ARGS = previousEnv.BOSS_CLAW_ARGS;
process.env.BOSS_CLAW_TIMEOUT_MS = previousEnv.BOSS_CLAW_TIMEOUT_MS;
await rm(clawDir, { recursive: true, force: true });
}
});
test("master-agent enqueue 在首选主节点离线时会回退到可用的备用主节点并返回实际账号", async () => {
await saveAiAccount({
accountId: "master-codex-primary-offline",