Add second batch master agent fast paths
This commit is contained in:
@@ -85,11 +85,33 @@ Android 主 Agent 会话页顶部标题同步显示:
|
|||||||
|
|
||||||
下一批适合继续接入 Fast Path 的问题:
|
下一批适合继续接入 Fast Path 的问题:
|
||||||
|
|
||||||
- 当前绑定设备 / 在线设备查询
|
|
||||||
- 当前会话 / 当前线程运行状态查询
|
- 当前会话 / 当前线程运行状态查询
|
||||||
- GUI / CLI 默认执行模式查询
|
|
||||||
- 当前接管开关状态查询
|
|
||||||
- 最近活跃项目 / 最近活跃线程查询
|
- 最近活跃项目 / 最近活跃线程查询
|
||||||
|
- 更细粒度的线程级接管控制与作用域切换
|
||||||
|
|
||||||
|
## 6.1 第二批已接入意图
|
||||||
|
|
||||||
|
本次继续把主 Agent 的“控制面”高频问题接入快路径:
|
||||||
|
|
||||||
|
- 全局接管状态查询
|
||||||
|
- 例:“当前有没有开启主agent接管”
|
||||||
|
- 全局接管开关
|
||||||
|
- 例:“帮我开启全局接管”
|
||||||
|
- 例:“关闭全局接管”
|
||||||
|
- 默认后端切换
|
||||||
|
- 例:“把默认后端切到 Hermes”
|
||||||
|
- 例:“切到 Claw 后端”
|
||||||
|
- 默认执行模式查询
|
||||||
|
- 例:“现在默认走 GUI 还是 CLI”
|
||||||
|
- 当前主节点 / 绑定设备在线状态查询
|
||||||
|
- 例:“当前主节点在线吗”
|
||||||
|
|
||||||
|
这批实现原则是:
|
||||||
|
|
||||||
|
- 只处理确定性、纯本地状态可回答的问题
|
||||||
|
- 不进入异步任务队列
|
||||||
|
- 先命中接管 / 后端 / 执行模式,再命中模型切换,避免“切到 Hermes 后端”被模型切换规则误判
|
||||||
|
- `GUI 还是 CLI` 与 `Hermes 还是 Claw` 分属不同意图,不再混用一个正则
|
||||||
|
|
||||||
## 7. 验证基线
|
## 7. 验证基线
|
||||||
|
|
||||||
@@ -99,3 +121,11 @@ Android 主 Agent 会话页顶部标题同步显示:
|
|||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `./gradlew :app:compileDebugJavaWithJavac :app:assembleDebug`
|
- `./gradlew :app:compileDebugJavaWithJavac :app:assembleDebug`
|
||||||
- 真机安装并验证主 Agent 名称与模型查询行为
|
- 真机安装并验证主 Agent 名称与模型查询行为
|
||||||
|
|
||||||
|
第二批补充验证点:
|
||||||
|
|
||||||
|
- `npx tsx --test tests/master-agent-message-queue.test.ts`
|
||||||
|
- 覆盖全局接管查询 / 切换
|
||||||
|
- 覆盖默认后端切换
|
||||||
|
- 覆盖 GUI/CLI 执行模式查询
|
||||||
|
- 覆盖主节点设备在线状态查询
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import {
|
|||||||
createHermesBackend,
|
createHermesBackend,
|
||||||
getHermesBackendSelectionState,
|
getHermesBackendSelectionState,
|
||||||
} from "@/lib/execution/backends/hermes-backend";
|
} from "@/lib/execution/backends/hermes-backend";
|
||||||
|
import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config";
|
||||||
|
import { getHermesBackendAvailability } from "@/lib/execution/backends/hermes-config";
|
||||||
import { getOmxTeamBackendSelectionState } from "@/lib/execution/backends/omx-team-backend";
|
import { getOmxTeamBackendSelectionState } from "@/lib/execution/backends/omx-team-backend";
|
||||||
import type { OrchestrationBackendId } from "@/lib/execution/orchestration-backend";
|
import type { OrchestrationBackendId } from "@/lib/execution/orchestration-backend";
|
||||||
import { listExecutionBackendChoices, selectExecutionBackend } from "@/lib/execution/backend-selector";
|
import { listExecutionBackendChoices, selectExecutionBackend } from "@/lib/execution/backend-selector";
|
||||||
@@ -1723,6 +1725,21 @@ type MasterAgentFastIntentContext = {
|
|||||||
effectiveDeepTaskPolicy: ReturnType<typeof resolveMasterAgentModelPolicy>;
|
effectiveDeepTaskPolicy: ReturnType<typeof resolveMasterAgentModelPolicy>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildMasterAgentRuntimeBackendLabel(context: MasterAgentFastIntentContext) {
|
||||||
|
return context.agentControls?.backendOverride?.trim() || "master-codex-node";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMasterAgentRuntimeDevice(context: MasterAgentFastIntentContext) {
|
||||||
|
const deviceId =
|
||||||
|
context.runtime.account.nodeId?.trim() ||
|
||||||
|
context.state.user.boundDeviceId?.trim() ||
|
||||||
|
"";
|
||||||
|
if (!deviceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return context.state.devices.find((device) => device.id === deviceId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function detectMasterAgentModelCommandScope(requestText: string): MasterAgentModelCommandScope {
|
function detectMasterAgentModelCommandScope(requestText: string): MasterAgentModelCommandScope {
|
||||||
const normalized = normalizeLexicalText(requestText);
|
const normalized = normalizeLexicalText(requestText);
|
||||||
if (
|
if (
|
||||||
@@ -1791,11 +1808,76 @@ function isBackendStatusRequest(requestText: string) {
|
|||||||
if (!normalized || isModelSwitchRequest(requestText)) {
|
if (!normalized || isModelSwitchRequest(requestText)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(什么后端|哪个后端|当前后端|现在后端|正在用什么后端|当前用什么后端|gui还是cli|hermes还是claw|claw还是hermes)/i.test(
|
return /(什么后端|哪个后端|当前后端|现在后端|正在用什么后端|当前用什么后端|hermes还是claw|claw还是hermes)/i.test(
|
||||||
normalized,
|
normalized,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBackendSwitchRequest(requestText: string) {
|
||||||
|
const normalized = normalizeLexicalText(requestText);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(后端|runtime)/i.test(normalized) && /(切到|切换到|换成|改成|改为|用|使用)/i.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectRequestedBackend(requestText: string) {
|
||||||
|
const normalized = normalizeLexicalText(requestText);
|
||||||
|
if (normalized.includes("hermes")) {
|
||||||
|
return HERMES_BACKEND_ID;
|
||||||
|
}
|
||||||
|
if (normalized.includes("claw")) {
|
||||||
|
return CLAW_BACKEND_ID;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTakeoverStatusRequest(requestText: string) {
|
||||||
|
const normalized = normalizeLexicalText(requestText);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(接管|协同接管)/i.test(normalized) && /(有没有开启|是否开启|开了吗|状态|现在开着吗|当前开着吗)/i.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTakeoverSwitchRequest(requestText: string) {
|
||||||
|
const normalized = normalizeLexicalText(requestText);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(有没有开启|是否开启|开了吗|状态|现在开着吗|当前开着吗)/i.test(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(接管|协同接管)/i.test(normalized) && /(开启|打开|关闭|关掉|停用|禁用)/i.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectRequestedTakeoverEnabled(requestText: string) {
|
||||||
|
const normalized = normalizeLexicalText(requestText);
|
||||||
|
if (/(关闭|关掉|停用|禁用)/i.test(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(开启|打开)/i.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecutionModeStatusRequest(requestText: string) {
|
||||||
|
const normalized = normalizeLexicalText(requestText);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(gui\s*还是\s*cli|默认走.*gui.*cli|默认执行模式|执行模式|gui\s*cli)/i.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBoundDeviceStatusRequest(requestText: string) {
|
||||||
|
const normalized = normalizeLexicalText(requestText);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(绑定设备|主节点|master节点|codex节点)/i.test(normalized) && /(在线吗|在不在线|是否在线|状态)/i.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
async function buildMasterAgentFastIntentContext(
|
async function buildMasterAgentFastIntentContext(
|
||||||
requestedByAccount: string,
|
requestedByAccount: string,
|
||||||
): Promise<MasterAgentFastIntentContext | null> {
|
): Promise<MasterAgentFastIntentContext | null> {
|
||||||
@@ -1934,6 +2016,140 @@ async function tryHandleMasterAgentBackendStatusQuery(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function tryHandleMasterAgentBackendSwitchCommand(params: {
|
||||||
|
requestText: string;
|
||||||
|
requestedByAccount: string;
|
||||||
|
context: MasterAgentFastIntentContext;
|
||||||
|
}) {
|
||||||
|
if (!isBackendSwitchRequest(params.requestText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const requestedBackend = detectRequestedBackend(params.requestText);
|
||||||
|
if (!requestedBackend) {
|
||||||
|
return appendFastPathError(
|
||||||
|
"我收到的是后端切换请求,但没有识别到具体后端。你可以直接说“切到 Hermes 后端”或“切到 Claw 后端”。",
|
||||||
|
"BACKEND_NAME_REQUIRED",
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedBackend === HERMES_BACKEND_ID) {
|
||||||
|
const availability = await getHermesBackendAvailability();
|
||||||
|
if (!availability.selectable) {
|
||||||
|
return appendFastPathError(
|
||||||
|
`Hermes Runtime 当前不可切换:${availability.reasonLabel}`,
|
||||||
|
"BACKEND_NOT_AVAILABLE",
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedBackend === CLAW_BACKEND_ID) {
|
||||||
|
const availability = await getClawBackendAvailability();
|
||||||
|
if (!availability.selectable) {
|
||||||
|
return appendFastPathError(
|
||||||
|
`Claw Runtime 当前不可切换:${availability.reasonLabel}`,
|
||||||
|
"BACKEND_NOT_AVAILABLE",
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProjectAgentControls(
|
||||||
|
"master-agent",
|
||||||
|
{ backendOverride: requestedBackend },
|
||||||
|
params.requestedByAccount,
|
||||||
|
);
|
||||||
|
return appendFastPathReply(
|
||||||
|
`已把默认后端切到 ${requestedBackend}。`,
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryHandleMasterAgentTakeoverCommand(params: {
|
||||||
|
requestText: string;
|
||||||
|
requestedByAccount: string;
|
||||||
|
context: MasterAgentFastIntentContext;
|
||||||
|
}) {
|
||||||
|
const isStatus = isTakeoverStatusRequest(params.requestText);
|
||||||
|
const isSwitch = isTakeoverSwitchRequest(params.requestText);
|
||||||
|
if (!isStatus && !isSwitch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEnabled = Boolean(params.context.agentControls?.globalTakeoverEnabled);
|
||||||
|
if (isStatus && !isSwitch) {
|
||||||
|
return appendFastPathReply(
|
||||||
|
`全局接管:${currentEnabled ? "开启" : "关闭"}。`,
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEnabled = detectRequestedTakeoverEnabled(params.requestText);
|
||||||
|
if (nextEnabled === null) {
|
||||||
|
return appendFastPathError(
|
||||||
|
"我收到的是接管切换请求,但没有识别到要开启还是关闭。你可以直接说“开启全局接管”或“关闭全局接管”。",
|
||||||
|
"TAKEOVER_ACTION_REQUIRED",
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProjectAgentControls(
|
||||||
|
"master-agent",
|
||||||
|
{ globalTakeoverEnabled: nextEnabled },
|
||||||
|
params.requestedByAccount,
|
||||||
|
);
|
||||||
|
return appendFastPathReply(
|
||||||
|
nextEnabled ? "已开启全局接管。" : "已关闭全局接管。",
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryHandleMasterAgentExecutionModeStatusQuery(params: {
|
||||||
|
requestText: string;
|
||||||
|
context: MasterAgentFastIntentContext;
|
||||||
|
}) {
|
||||||
|
if (!isExecutionModeStatusRequest(params.requestText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const device = getMasterAgentRuntimeDevice(params.context);
|
||||||
|
if (!device) {
|
||||||
|
return appendFastPathReply(
|
||||||
|
"当前没有绑定可识别的主节点设备,所以还不能判断默认执行模式。",
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const preferredMode = device.preferredExecutionMode || "unknown";
|
||||||
|
const guiStatus = device.capabilities?.gui?.connected ? "在线" : "离线";
|
||||||
|
const cliStatus = device.capabilities?.cli?.connected ? "在线" : "离线";
|
||||||
|
return appendFastPathReply(
|
||||||
|
`默认执行模式:${preferredMode}。设备:${device.name || device.id}。GUI:${guiStatus}。CLI:${cliStatus}。`,
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryHandleMasterAgentBoundDeviceStatusQuery(params: {
|
||||||
|
requestText: string;
|
||||||
|
context: MasterAgentFastIntentContext;
|
||||||
|
}) {
|
||||||
|
if (!isBoundDeviceStatusRequest(params.requestText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const device = getMasterAgentRuntimeDevice(params.context);
|
||||||
|
if (!device) {
|
||||||
|
return appendFastPathReply(
|
||||||
|
"当前没有找到绑定的主节点设备记录。",
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const guiStatus = device.capabilities?.gui?.connected ? "在线" : "离线";
|
||||||
|
const cliStatus = device.capabilities?.cli?.connected ? "在线" : "离线";
|
||||||
|
return appendFastPathReply(
|
||||||
|
`当前主节点设备:${device.name || device.id}。设备状态:${device.status}。GUI:${guiStatus}。CLI:${cliStatus}。`,
|
||||||
|
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isModelListRequest(requestText: string) {
|
function isModelListRequest(requestText: string) {
|
||||||
return /(哪些模型|什么模型|模型清单|可用模型|有哪些可用|available models?)/i.test(requestText);
|
return /(哪些模型|什么模型|模型清单|可用模型|有哪些可用|available models?)/i.test(requestText);
|
||||||
}
|
}
|
||||||
@@ -2024,6 +2240,10 @@ async function tryHandleMasterAgentFastIntent(params: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const handlers = [
|
const handlers = [
|
||||||
|
() => tryHandleMasterAgentTakeoverCommand({ ...params, context }),
|
||||||
|
() => tryHandleMasterAgentBackendSwitchCommand({ ...params, context }),
|
||||||
|
() => tryHandleMasterAgentExecutionModeStatusQuery({ requestText: params.requestText, context }),
|
||||||
|
() => tryHandleMasterAgentBoundDeviceStatusQuery({ requestText: params.requestText, context }),
|
||||||
() => tryHandleMasterAgentModelCommand({ ...params, context }),
|
() => tryHandleMasterAgentModelCommand({ ...params, context }),
|
||||||
() => tryHandleMasterAgentStatusQuery({ requestText: params.requestText, context }),
|
() => tryHandleMasterAgentStatusQuery({ requestText: params.requestText, context }),
|
||||||
() => tryHandleMasterAgentBackendStatusQuery({ requestText: params.requestText, context }),
|
() => tryHandleMasterAgentBackendStatusQuery({ requestText: params.requestText, context }),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ let POST: (typeof import("../src/app/api/v1/projects/[projectId]/messages/route"
|
|||||||
let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
|
let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
|
||||||
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
|
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
|
||||||
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
||||||
|
let updateDevice: (typeof import("../src/lib/boss-data"))["updateDevice"];
|
||||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||||
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
||||||
let AUTH_SESSION_COOKIE = "";
|
let AUTH_SESSION_COOKIE = "";
|
||||||
@@ -33,6 +34,7 @@ async function setup() {
|
|||||||
saveAiAccount = data.saveAiAccount;
|
saveAiAccount = data.saveAiAccount;
|
||||||
getProjectAgentControls = data.getProjectAgentControls;
|
getProjectAgentControls = data.getProjectAgentControls;
|
||||||
updateProjectAgentControls = data.updateProjectAgentControls;
|
updateProjectAgentControls = data.updateProjectAgentControls;
|
||||||
|
updateDevice = data.updateDevice;
|
||||||
readState = data.readState;
|
readState = data.readState;
|
||||||
createAuthSession = data.createAuthSession;
|
createAuthSession = data.createAuthSession;
|
||||||
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
|
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
|
||||||
@@ -357,6 +359,252 @@ test("master-agent 查询当前后端时直接走 fast path 返回后端摘要",
|
|||||||
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4");
|
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("master-agent 查询全局接管状态时直接走 fast path 返回当前状态", async () => {
|
||||||
|
await saveAiAccount({
|
||||||
|
accountId: "openai-takeover-status",
|
||||||
|
label: "OpenAI 主模型",
|
||||||
|
role: "primary",
|
||||||
|
provider: "openai_api",
|
||||||
|
displayName: "OpenAI 主模型",
|
||||||
|
model: "gpt-5.4-mini",
|
||||||
|
apiKey: "sk-openai-takeover-status",
|
||||||
|
enabled: true,
|
||||||
|
setActive: true,
|
||||||
|
loginStatusNote: "用于全局接管状态查询测试。",
|
||||||
|
});
|
||||||
|
await updateProjectAgentControls(
|
||||||
|
"master-agent",
|
||||||
|
{
|
||||||
|
globalTakeoverEnabled: true,
|
||||||
|
},
|
||||||
|
"17600003315",
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
await createAuthedRequest("master-agent", {
|
||||||
|
body: "当前有没有开启主agent接管",
|
||||||
|
}),
|
||||||
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
ok: boolean;
|
||||||
|
task?: { taskId: string } | null;
|
||||||
|
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||||
|
};
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.task ?? null, null);
|
||||||
|
assert.equal(payload.masterReplyState, "completed");
|
||||||
|
|
||||||
|
const state = await readState();
|
||||||
|
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||||
|
const reply = masterProject?.messages.at(-1);
|
||||||
|
assert.ok(reply);
|
||||||
|
assert.match(reply?.body ?? "", /全局接管:开启/);
|
||||||
|
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4-mini");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("master-agent 可以直接通过 fast path 开启全局接管", async () => {
|
||||||
|
await saveAiAccount({
|
||||||
|
accountId: "openai-takeover-switch",
|
||||||
|
label: "OpenAI 主模型",
|
||||||
|
role: "primary",
|
||||||
|
provider: "openai_api",
|
||||||
|
displayName: "OpenAI 主模型",
|
||||||
|
model: "gpt-5.4-mini",
|
||||||
|
apiKey: "sk-openai-takeover-switch",
|
||||||
|
enabled: true,
|
||||||
|
setActive: true,
|
||||||
|
loginStatusNote: "用于全局接管切换测试。",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
await createAuthedRequest("master-agent", {
|
||||||
|
body: "帮我开启全局接管",
|
||||||
|
}),
|
||||||
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
ok: boolean;
|
||||||
|
task?: { taskId: string } | null;
|
||||||
|
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||||
|
};
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.task ?? null, null);
|
||||||
|
assert.equal(payload.masterReplyState, "completed");
|
||||||
|
|
||||||
|
const controls = await getProjectAgentControls("master-agent", "17600003315");
|
||||||
|
assert.equal(controls?.globalTakeoverEnabled ?? null, true);
|
||||||
|
|
||||||
|
const state = await readState();
|
||||||
|
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||||
|
const reply = masterProject?.messages.at(-1);
|
||||||
|
assert.ok(reply);
|
||||||
|
assert.match(reply?.body ?? "", /已开启全局接管/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("master-agent 可以直接通过 fast path 切换默认后端到 Hermes", async () => {
|
||||||
|
const previousEnv = {
|
||||||
|
BOSS_HERMES_ENABLED: process.env.BOSS_HERMES_ENABLED,
|
||||||
|
BOSS_HERMES_COMMAND: process.env.BOSS_HERMES_COMMAND,
|
||||||
|
};
|
||||||
|
process.env.BOSS_HERMES_ENABLED = "true";
|
||||||
|
process.env.BOSS_HERMES_COMMAND = process.execPath;
|
||||||
|
|
||||||
|
await saveAiAccount({
|
||||||
|
accountId: "openai-backend-switch",
|
||||||
|
label: "OpenAI 主模型",
|
||||||
|
role: "primary",
|
||||||
|
provider: "openai_api",
|
||||||
|
displayName: "OpenAI 主模型",
|
||||||
|
model: "gpt-5.4",
|
||||||
|
apiKey: "sk-openai-backend-switch",
|
||||||
|
enabled: true,
|
||||||
|
setActive: true,
|
||||||
|
loginStatusNote: "用于后端切换测试。",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await POST(
|
||||||
|
await createAuthedRequest("master-agent", {
|
||||||
|
body: "把默认后端切到 Hermes",
|
||||||
|
}),
|
||||||
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
ok: boolean;
|
||||||
|
task?: { taskId: string } | null;
|
||||||
|
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||||
|
};
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.task ?? null, null);
|
||||||
|
assert.equal(payload.masterReplyState, "completed");
|
||||||
|
|
||||||
|
const controls = await getProjectAgentControls("master-agent", "17600003315");
|
||||||
|
assert.equal(controls?.backendOverride ?? null, "hermes-runtime");
|
||||||
|
|
||||||
|
const state = await readState();
|
||||||
|
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||||
|
const reply = masterProject?.messages.at(-1);
|
||||||
|
assert.ok(reply);
|
||||||
|
assert.match(reply?.body ?? "", /已把默认后端切到 hermes-runtime/i);
|
||||||
|
} finally {
|
||||||
|
process.env.BOSS_HERMES_ENABLED = previousEnv.BOSS_HERMES_ENABLED;
|
||||||
|
process.env.BOSS_HERMES_COMMAND = previousEnv.BOSS_HERMES_COMMAND;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("master-agent 查询默认执行模式时直接返回 GUI CLI 状态", async () => {
|
||||||
|
await saveAiAccount({
|
||||||
|
accountId: "master-codex-execution-mode",
|
||||||
|
label: "主 GPT",
|
||||||
|
role: "primary",
|
||||||
|
provider: "master_codex_node",
|
||||||
|
displayName: "Mac 上的 Master Codex Node",
|
||||||
|
nodeId: "mac-studio",
|
||||||
|
nodeLabel: "Mac Studio",
|
||||||
|
model: "gpt-5.4",
|
||||||
|
enabled: true,
|
||||||
|
setActive: true,
|
||||||
|
loginStatusNote: "用于执行模式查询测试。",
|
||||||
|
});
|
||||||
|
await updateDevice("mac-studio", {
|
||||||
|
status: "online",
|
||||||
|
preferredExecutionMode: "gui",
|
||||||
|
capabilities: {
|
||||||
|
gui: {
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
cli: {
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
await createAuthedRequest("master-agent", {
|
||||||
|
body: "现在默认走 GUI 还是 CLI",
|
||||||
|
}),
|
||||||
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
ok: boolean;
|
||||||
|
task?: { taskId: string } | null;
|
||||||
|
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||||
|
};
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.task ?? null, null);
|
||||||
|
assert.equal(payload.masterReplyState, "completed");
|
||||||
|
|
||||||
|
const state = await readState();
|
||||||
|
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||||
|
const reply = masterProject?.messages.at(-1);
|
||||||
|
assert.ok(reply);
|
||||||
|
assert.match(reply?.body ?? "", /默认执行模式:gui/i);
|
||||||
|
assert.match(reply?.body ?? "", /GUI:在线/);
|
||||||
|
assert.match(reply?.body ?? "", /CLI:在线/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("master-agent 查询当前主节点设备状态时直接返回绑定设备在线信息", async () => {
|
||||||
|
await saveAiAccount({
|
||||||
|
accountId: "master-codex-device-status",
|
||||||
|
label: "主 GPT",
|
||||||
|
role: "primary",
|
||||||
|
provider: "master_codex_node",
|
||||||
|
displayName: "Mac 上的 Master Codex Node",
|
||||||
|
nodeId: "mac-studio",
|
||||||
|
nodeLabel: "Mac Studio",
|
||||||
|
model: "gpt-5.4",
|
||||||
|
enabled: true,
|
||||||
|
setActive: true,
|
||||||
|
loginStatusNote: "用于绑定设备状态查询测试。",
|
||||||
|
});
|
||||||
|
await updateDevice("mac-studio", {
|
||||||
|
status: "online",
|
||||||
|
capabilities: {
|
||||||
|
gui: {
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
cli: {
|
||||||
|
connected: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
await createAuthedRequest("master-agent", {
|
||||||
|
body: "当前主节点在线吗",
|
||||||
|
}),
|
||||||
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
ok: boolean;
|
||||||
|
task?: { taskId: string } | null;
|
||||||
|
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||||
|
};
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.task ?? null, null);
|
||||||
|
assert.equal(payload.masterReplyState, "completed");
|
||||||
|
|
||||||
|
const state = await readState();
|
||||||
|
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||||
|
const reply = masterProject?.messages.at(-1);
|
||||||
|
assert.ok(reply);
|
||||||
|
assert.match(reply?.body ?? "", /当前主节点设备:/);
|
||||||
|
assert.match(reply?.body ?? "", /设备状态:online/);
|
||||||
|
assert.match(reply?.body ?? "", /GUI:在线/);
|
||||||
|
assert.match(reply?.body ?? "", /CLI:离线/);
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异步实际回复时继承当前会话覆盖", async () => {
|
test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异步实际回复时继承当前会话覆盖", async () => {
|
||||||
await saveAiAccount({
|
await saveAiAccount({
|
||||||
accountId: "openai-master-agent-queue",
|
accountId: "openai-master-agent-queue",
|
||||||
|
|||||||
Reference in New Issue
Block a user