fix: route master agent through codex device pool

This commit is contained in:
AI Bot
2026-06-06 12:31:04 +08:00
parent 684b98c5c1
commit 9e81d8a960
7 changed files with 619 additions and 32 deletions

View File

@@ -10,6 +10,9 @@ let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let updateAiAccountHealth: (typeof import("../src/lib/boss-data"))["updateAiAccountHealth"];
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
let updateDevice: (typeof import("../src/lib/boss-data"))["updateDevice"];
let upsertDeviceHeartbeat: (typeof import("../src/lib/boss-data"))["upsertDeviceHeartbeat"];
let completeMasterAgentTask: (typeof import("../src/lib/boss-data"))["completeMasterAgentTask"];
async function setup() {
if (runtimeRoot) return;
@@ -28,6 +31,9 @@ async function setup() {
readState = data.readState;
updateAiAccountHealth = data.updateAiAccountHealth;
updateProjectAgentControls = data.updateProjectAgentControls;
updateDevice = data.updateDevice;
upsertDeviceHeartbeat = data.upsertDeviceHeartbeat;
completeMasterAgentTask = data.completeMasterAgentTask;
}
async function waitFor(predicate: () => Promise<boolean>, timeoutMs = 5_000) {
@@ -54,6 +60,14 @@ test.beforeEach(async () => {
});
test("replyToMasterAgentUserMessage falls back to a runnable OpenAI API account when the master node is offline", async () => {
await updateDevice("mac-studio", {
capabilities: {
gui: { connected: false },
cli: { connected: false },
codexAppServer: { connected: false },
},
});
await saveAiAccount({
accountId: "master-codex-primary",
label: "主 GPT",
@@ -253,6 +267,14 @@ test("replyToMasterAgentUserMessage uses active DeepSeek API accounts directly",
});
test("replyToMasterAgentUserMessage falls back to a runnable aliyun qwen backup account when the master node is offline", async () => {
await updateDevice("mac-studio", {
capabilities: {
gui: { connected: false },
cli: { connected: false },
codexAppServer: { connected: false },
},
});
await saveAiAccount({
accountId: "master-codex-primary",
label: "主 GPT",
@@ -508,3 +530,256 @@ test("replyToMasterAgentUserMessage falls back to a ready backup master node acc
assert.equal(task?.accountId, "master-codex-backup-ready");
assert.equal(task?.deviceId, "mac-studio");
});
test("replyToMasterAgentUserMessage routes to another bound Codex device when the active node has no usable Codex channel", async () => {
await updateDevice("mac-studio", {
status: "online",
capabilities: {
gui: { connected: false },
cli: { connected: false },
codexAppServer: { connected: false },
},
});
await upsertDeviceHeartbeat({
deviceId: "macbook-air-codex",
token: "boss-macbook-air-codex-token",
name: "MacBook Air",
avatar: "A",
account: "krisolo",
status: "online",
quota5h: 90,
quota7d: 90,
projects: [],
capabilities: {
gui: { connected: true },
cli: { connected: true },
codexAppServer: { connected: true },
},
});
await saveAiAccount({
accountId: "master-codex-primary-no-channel",
label: "主 GPT",
role: "primary",
provider: "master_codex_node",
displayName: "在线但没有 Codex 通道的主节点",
nodeId: "mac-studio",
nodeLabel: "Mac Studio",
model: "gpt-5.4",
enabled: true,
setActive: true,
loginStatusNote: "模拟 boss-agent 未检测到可用 Codex 通道。",
});
const result = await replyToMasterAgentUserMessage({
requestMessageId: "msg-master-node-device-pool",
requestText: "请使用可用的 Codex 设备池。",
requestedBy: "Boss 超级管理员",
requestedByAccount: "krisolo",
mode: "enqueue",
});
assert.equal(result.ok, true);
assert.equal(result.accountId, "master-codex-device-macbook-air-codex");
assert.ok(result.taskId, "expected a queued master-agent task");
const state = await readState();
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
assert.ok(task, "expected queued task to be written into state");
assert.equal(task?.accountId, "master-codex-device-macbook-air-codex");
assert.equal(task?.deviceId, "macbook-air-codex");
});
test("replyToMasterAgentUserMessage falls back to API when no bound Codex model channel is usable", async () => {
await updateDevice("mac-studio", {
status: "online",
capabilities: {
gui: { connected: false },
cli: { connected: false },
codexAppServer: { connected: false },
},
});
await saveAiAccount({
accountId: "master-codex-primary-no-model-channel",
label: "主 GPT",
role: "primary",
provider: "master_codex_node",
displayName: "不可用的 Master Codex Node",
nodeId: "mac-studio",
nodeLabel: "Mac Studio",
model: "gpt-5.4",
enabled: true,
setActive: true,
loginStatusNote: "模拟没有任何可用 Codex 模型通道。",
});
await saveAiAccount({
accountId: "openai-api-after-codex-pool",
label: "API 备用",
role: "api_fallback",
provider: "openai_api",
displayName: "OpenAI API 备用账号",
model: "gpt-5.4-mini",
apiKey: "sk-openai-after-codex-pool",
enabled: true,
setActive: false,
loginStatusNote: "Codex 设备池不可用时兜底。",
});
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input) => {
if (typeof input === "string" && input === "https://api.openai.com/v1/responses") {
return new Response(JSON.stringify({ output_text: "已切到 API 备用链。" }), {
status: 200,
headers: {
"content-type": "application/json",
"x-request-id": "req-api-after-codex-pool",
},
});
}
throw new Error(`unexpected fetch: ${String(input)}`);
}) as typeof fetch;
try {
const result = await replyToMasterAgentUserMessage({
requestMessageId: "msg-api-after-codex-pool",
requestText: "Codex 设备都不可用时继续回复。",
requestedBy: "Boss 超级管理员",
requestedByAccount: "krisolo",
mode: "enqueue",
});
assert.equal(result.ok, true);
assert.equal(result.accountId, "openai-api-after-codex-pool");
assert.ok(result.taskId, "expected an API fallback task");
const state = await readState();
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
assert.ok(task, "expected queued task to be written into state");
assert.equal(task?.accountId, "openai-api-after-codex-pool");
assert.equal(task?.deviceId, "master-agent-openai");
await waitFor(async () => {
const nextState = await readState();
const completedTask = nextState.masterAgentTasks.find((item) => item.taskId === result.taskId);
return completedTask?.status === "completed";
});
} finally {
globalThis.fetch = originalFetch;
}
});
test("failed Master Codex Node task is requeued to the next usable Codex device before surfacing an error", async () => {
await updateDevice("mac-studio", {
status: "online",
capabilities: {
gui: { connected: true },
cli: { connected: true },
codexAppServer: { connected: true },
},
});
await upsertDeviceHeartbeat({
deviceId: "macbook-air-codex-failover",
token: "boss-macbook-air-codex-failover-token",
name: "MacBook Air",
avatar: "A",
account: "krisolo",
status: "online",
quota5h: 90,
quota7d: 90,
projects: [],
capabilities: {
gui: { connected: true },
cli: { connected: true },
codexAppServer: { connected: true },
},
});
await saveAiAccount({
accountId: "master-codex-primary-runtime-failover",
label: "主 GPT",
role: "primary",
provider: "master_codex_node",
displayName: "Mac Studio Master Codex Node",
nodeId: "mac-studio",
nodeLabel: "Mac Studio",
model: "gpt-5.4",
enabled: true,
setActive: true,
loginStatusNote: "首选 Master Codex Node。",
});
const result = await replyToMasterAgentUserMessage({
requestMessageId: "msg-master-node-runtime-failover",
requestText: "请走主节点,如果失败自动切下一台。",
requestedBy: "Boss 超级管理员",
requestedByAccount: "krisolo",
mode: "enqueue",
});
assert.equal(result.ok, true);
assert.equal(result.accountId, "master-codex-primary-runtime-failover");
assert.ok(result.taskId, "expected a queued master-agent task");
const requeued = await completeMasterAgentTask({
taskId: result.taskId,
deviceId: "mac-studio",
status: "failed",
errorMessage: "spawn codex ENOENT",
});
assert.equal(requeued.status, "queued");
assert.equal(requeued.deviceId, "macbook-air-codex-failover");
assert.equal(requeued.accountId, "master-codex-device-macbook-air-codex-failover");
assert.deepEqual(requeued.modelChannelAttemptedDeviceIds, ["mac-studio"]);
const state = await readState();
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
assert.equal(task?.status, "queued");
assert.equal(task?.deviceId, "macbook-air-codex-failover");
const masterProject = state.projects.find((project) => project.id === "master-agent");
assert.doesNotMatch(masterProject?.messages.at(-1)?.body ?? "", /Master Codex Node 执行失败/);
});
test("replyToMasterAgentUserMessage tells the user when neither Codex devices nor API keys are available", async () => {
await updateDevice("mac-studio", {
status: "online",
capabilities: {
gui: { connected: false },
cli: { connected: false },
codexAppServer: { connected: false },
},
});
await saveAiAccount({
accountId: "master-codex-primary-no-channel-no-api",
label: "主 GPT",
role: "primary",
provider: "master_codex_node",
displayName: "不可用的 Master Codex Node",
nodeId: "mac-studio",
nodeLabel: "Mac Studio",
model: "gpt-5.4",
enabled: true,
setActive: true,
loginStatusNote: "没有可用模型通道。",
});
const result = await replyToMasterAgentUserMessage({
requestMessageId: "msg-no-model-channel",
requestText: "现在还能回复吗?",
requestedBy: "Boss 超级管理员",
requestedByAccount: "krisolo",
mode: "enqueue",
});
assert.equal(result.ok, false);
assert.equal(result.reason, "MASTER_NODE_NOT_CONNECTED");
const state = await readState();
const masterProject = state.projects.find((project) => project.id === "master-agent");
const reply = masterProject?.messages.at(-1);
assert.match(reply?.body ?? "", /当前没有可用的模型渠道/);
assert.doesNotMatch(reply?.body ?? "", /master 节点账号/);
});