refactor: add execution backend selection

This commit is contained in:
kris
2026-04-03 00:21:19 +08:00
parent a3a4f3e980
commit 8a62e72fd5
11 changed files with 1067 additions and 318 deletions

View File

@@ -14,13 +14,22 @@ import {
getMasterAgentTask,
queueMasterAgentTask,
readState,
reassignMasterAgentTaskExecution,
isDispatchableThreadProject,
touchUserMasterMemories,
updateAttachmentAnalysisResult,
updateAiAccountHealth,
} from "@/lib/boss-data";
import type { AiProvider, DispatchPlanTarget, Project, ProjectAgentControls, ReasoningEffort } from "@/lib/boss-data";
import type {
AiAccount,
AiProvider,
DispatchPlanTarget,
Project,
ProjectAgentControls,
ReasoningEffort,
} from "@/lib/boss-data";
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
import { listExecutionBackendChoices, selectExecutionBackend } from "@/lib/execution/backend-selector";
import { resolveRuntimeRelevantMemories } from "@/lib/execution/memory-resolver";
import type { RelevantMemory } from "@/lib/execution/memory-resolver";
import { buildExecutionPrompt } from "@/lib/execution/prompt-assembler";
@@ -363,38 +372,140 @@ function normalizeApiProviderFetchFailure(provider: ApiCompatibleProvider, error
return normalizeApiProviderError(provider, String(error));
}
function fallbackAiRolePriority(role: "primary" | "backup" | "api_fallback") {
switch (role) {
case "primary":
return 0;
case "backup":
return 1;
case "api_fallback":
return 2;
default:
return 9;
}
function isUsableApiAccount(account: AiAccount, provider: ApiCompatibleProvider) {
return (
account.enabled &&
account.provider === provider &&
(account.status === "ready" || account.status === "degraded") &&
Boolean(account.apiKey?.trim())
);
}
async function findFallbackApiAccount(excludedAccountId?: string) {
const state = await readState();
return [...state.aiAccounts]
.filter(
(account) =>
account.accountId !== excludedAccountId &&
account.enabled &&
isApiCompatibleProvider(account.provider) &&
Boolean(account.apiKey?.trim()),
)
.sort((left, right) => {
const roleDelta = fallbackAiRolePriority(left.role) - fallbackAiRolePriority(right.role);
if (roleDelta !== 0) return roleDelta;
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
})[0];
function isUsableMasterNodeAccount(account: AiAccount) {
return (
account.enabled &&
account.provider === "master_codex_node" &&
account.status === "ready" &&
Boolean(account.nodeId?.trim())
);
}
function isOnlineMasterNodeAccount(
state: Awaited<ReturnType<typeof readState>>,
account: AiAccount,
) {
if (!isUsableMasterNodeAccount(account)) {
return false;
}
const deviceId = account.nodeId?.trim();
if (!deviceId) {
return false;
}
const device = state.devices.find((item) => item.id === deviceId);
return Boolean(device && device.status === "online");
}
function sortSelectableAccounts(left: AiAccount, right: AiAccount) {
if (left.isActive !== right.isActive) {
return left.isActive ? -1 : 1;
}
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
}
function sortApiSelectableAccounts(left: AiAccount, right: AiAccount) {
if (left.status !== right.status) {
return left.status === "ready" ? -1 : 1;
}
return sortSelectableAccounts(left, right);
}
async function resolveAccountForSelectedBackend(
selectedBackendProvider: AiProvider,
runtimeAccount: AiAccount,
) {
if (selectedBackendProvider === "master_codex_node") {
const state = await readState();
if (isOnlineMasterNodeAccount(state, runtimeAccount)) {
return runtimeAccount;
}
return state.aiAccounts
.filter((account) => isOnlineMasterNodeAccount(state, account))
.sort(sortSelectableAccounts)[0];
}
if (selectedBackendProvider === "openai_api" || selectedBackendProvider === "aliyun_qwen_api") {
const state = await readState();
const candidates = [
...(isUsableApiAccount(runtimeAccount, selectedBackendProvider) ? [runtimeAccount] : []),
...state.aiAccounts.filter((account): account is AiAccount =>
account.accountId !== runtimeAccount.accountId && isUsableApiAccount(account, selectedBackendProvider),
),
];
return candidates.sort(sortApiSelectableAccounts)[0];
}
return null;
}
interface ApiExecutionCandidate {
provider: ApiCompatibleProvider;
account: AiAccount;
deviceId: string;
model: string;
}
async function buildApiExecutionCandidates(params: {
backendChoices: Array<{ provider: AiProvider }>;
runtimeAccount: AiAccount;
agentControls?: ProjectAgentControls | null;
}) {
const candidates: ApiExecutionCandidate[] = [];
const seenAccountIds = new Set<string>();
for (const backend of params.backendChoices) {
if (!isApiCompatibleProvider(backend.provider)) {
continue;
}
const account = await resolveAccountForSelectedBackend(backend.provider, params.runtimeAccount);
if (!account || !isApiCompatibleProvider(account.provider) || !account.apiKey?.trim()) {
continue;
}
if (seenAccountIds.has(account.accountId)) {
continue;
}
seenAccountIds.add(account.accountId);
candidates.push({
provider: account.provider,
account,
deviceId: account.provider === "aliyun_qwen_api" ? ALIYUN_QWEN_DEVICE_ID : OPENAI_MASTER_AGENT_DEVICE_ID,
model:
params.agentControls?.modelOverride ||
account.model ||
apiProviderConfig(account.provider).defaultModel,
});
}
return candidates;
}
async function resolveMasterNodeExecutionCandidate(params: {
backendChoices: Array<{ backendId: string; provider: AiProvider }>;
runtimeAccount: AiAccount;
}) {
const wantsMasterNode = params.backendChoices.some((backend) => backend.backendId === "master-codex-node");
if (!wantsMasterNode) {
return null;
}
const account = await resolveAccountForSelectedBackend("master_codex_node", params.runtimeAccount);
return account && account.provider === "master_codex_node" ? account : null;
}
async function replyViaOpenAiAccount(params: {
account: Awaited<ReturnType<typeof findFallbackApiAccount>>;
account: AiAccount;
requestText: string;
currentSessionExpiresAt?: string;
senderLabel: string;
@@ -431,6 +542,10 @@ async function replyViaOpenAiAccount(params: {
status: "ready",
lastValidatedAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
activate: !params.account.isActive,
switchReason: params.account.isActive
? params.account.switchReason
: `主 Agent 回复时自动切换到 ${params.account.label}`,
});
return {
@@ -554,80 +669,146 @@ function buildMasterOpenAiReplyPrompt(
}
async function queueAndStartOpenAiMasterAgentReply(params: {
provider: ApiCompatibleProvider;
candidates: ApiExecutionCandidate[];
taskId: string;
deviceId: string;
requestText: string;
currentSessionExpiresAt?: string;
apiKey: string;
model: string;
reasoningEffort: ReasoningEffort;
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: RelevantMemory[];
userMemories?: RelevantMemory[];
masterFallback?: {
account: AiAccount;
executionPrompt: string;
} | null;
}) {
const completeTaskSafely = async (payload: Parameters<typeof completeMasterAgentTask>[0]) => {
try {
await completeMasterAgentTask(payload);
} catch (error) {
if (error instanceof Error && error.message === "MASTER_AGENT_TASK_NOT_FOUND") {
return;
}
throw error;
}
};
const timer = setTimeout(() => {
void (async () => {
const task = await getMasterAgentTask(params.taskId);
if (!task || task.status !== "queued") {
let lastErrorMessage = "主 Agent 当前调用模型失败。";
for (const candidate of params.candidates) {
const task = await getMasterAgentTask(params.taskId);
if (!task || task.status !== "queued") {
return;
}
if (task.accountId !== candidate.account.accountId || task.deviceId !== candidate.deviceId) {
await reassignMasterAgentTaskExecution({
taskId: params.taskId,
deviceId: candidate.deviceId,
accountId: candidate.account.accountId,
accountLabel: candidate.account.label,
});
}
try {
const generated = await generateApiProviderReply({
provider: candidate.provider,
apiKey: candidate.account.apiKey ?? "",
model: candidate.model,
reasoningEffort: params.reasoningEffort,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
promptPolicy: params.promptPolicy,
userPrompt: params.userPrompt,
projectMemories: params.projectMemories,
userMemories: params.userMemories,
});
await completeTaskSafely({
taskId: params.taskId,
deviceId: candidate.deviceId,
status: "completed",
replyBody: generated.content,
requestId: generated.requestId,
});
await updateAiAccountHealth({
accountId: candidate.account.accountId,
status: "ready",
lastValidatedAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
activate: !candidate.account.isActive,
switchReason: candidate.account.isActive
? candidate.account.switchReason
: `主 Agent 回复时自动切换到 ${candidate.account.label}`,
});
return;
} catch (error) {
if (error instanceof Error && error.message === "MASTER_AGENT_TASK_NOT_FOUND") {
return;
}
lastErrorMessage = error instanceof Error ? error.message : "主 Agent 当前调用模型失败。";
await updateAiAccountHealth({
accountId: candidate.account.accountId,
status: "degraded",
lastError: lastErrorMessage,
lastValidatedAt: new Date().toISOString(),
});
}
}
if (params.masterFallback) {
const fallbackTask = await getMasterAgentTask(params.taskId);
if (!fallbackTask || fallbackTask.status !== "queued") {
return;
}
await reassignMasterAgentTaskExecution({
taskId: params.taskId,
deviceId: params.masterFallback.account.nodeId || "mac-studio",
accountId: params.masterFallback.account.accountId,
accountLabel: params.masterFallback.account.label,
executionPrompt: params.masterFallback.executionPrompt,
});
return;
}
try {
const generated = await generateApiProviderReply({
provider: params.provider,
apiKey: params.apiKey,
model: params.model,
reasoningEffort: params.reasoningEffort,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
promptPolicy: params.promptPolicy,
userPrompt: params.userPrompt,
projectMemories: params.projectMemories,
userMemories: params.userMemories,
});
await completeMasterAgentTask({
taskId: params.taskId,
deviceId: params.deviceId,
status: "completed",
replyBody: generated.content,
requestId: generated.requestId,
});
} catch (error) {
await completeMasterAgentTask({
taskId: params.taskId,
deviceId: params.deviceId,
status: "failed",
errorMessage: error instanceof Error ? error.message : "主 Agent 当前调用模型失败。",
});
}
await completeTaskSafely({
taskId: params.taskId,
deviceId: params.candidates[0]?.deviceId ?? OPENAI_MASTER_AGENT_DEVICE_ID,
status: "failed",
errorMessage: lastErrorMessage,
});
})();
}, 0);
timer.unref?.();
}
async function enqueueOpenAiMasterAgentReply(params: {
provider: ApiCompatibleProvider;
accountId: string;
accountLabel: string;
candidates: ApiExecutionCandidate[];
requestMessageId?: string;
requestText: string;
requestedBy: string;
requestedByAccount: string;
currentSessionExpiresAt?: string;
apiKey: string;
model: string;
reasoningEffort: ReasoningEffort;
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: RelevantMemory[];
userMemories?: RelevantMemory[];
masterFallback?: {
account: AiAccount;
executionPrompt: string;
} | null;
}) {
const primaryCandidate = params.candidates[0];
if (!primaryCandidate) {
throw new Error("MASTER_AGENT_API_BACKEND_NOT_AVAILABLE");
}
const state = await readState();
const task = await queueMasterAgentTask({
requestMessageId: params.requestMessageId ?? "master-agent-manual",
@@ -644,29 +825,27 @@ async function enqueueOpenAiMasterAgentReply(params: {
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId: params.provider === "aliyun_qwen_api" ? ALIYUN_QWEN_DEVICE_ID : OPENAI_MASTER_AGENT_DEVICE_ID,
accountId: params.accountId,
accountLabel: params.accountLabel,
deviceId: primaryCandidate.deviceId,
accountId: primaryCandidate.account.accountId,
accountLabel: primaryCandidate.account.label,
});
void queueAndStartOpenAiMasterAgentReply({
provider: params.provider,
candidates: params.candidates,
taskId: task.taskId,
deviceId: params.provider === "aliyun_qwen_api" ? ALIYUN_QWEN_DEVICE_ID : OPENAI_MASTER_AGENT_DEVICE_ID,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: params.apiKey,
model: params.model,
reasoningEffort: params.reasoningEffort,
agentControls: params.agentControls,
promptPolicy: params.promptPolicy,
userPrompt: params.userPrompt,
projectMemories: params.projectMemories,
userMemories: params.userMemories,
masterFallback: params.masterFallback,
});
const queuedReply: QueuedMasterAgentReplyEnvelope = {
ok: true as const,
accountId: params.accountId,
accountId: primaryCandidate.account.accountId,
taskId: task.taskId,
masterReplyState: "queued" as const,
task: {
@@ -1390,82 +1569,98 @@ export async function replyToMasterAgentUserMessage(params: {
params.requestedByAccount,
params.requestText,
);
const state = await readState();
const primaryDeviceId = runtime.account.nodeId || state.user.boundDeviceId || "mac-studio";
const primaryDevice = state.devices.find((device) => device.id === primaryDeviceId);
const primaryBackendStatus =
runtime.account.provider === "master_codex_node" && (!primaryDevice || primaryDevice.status !== "online")
? "degraded"
: runtime.account.status;
const backendSelectionInput = {
primary: {
provider: runtime.account.provider,
status: primaryBackendStatus,
},
backups: state.aiAccounts
.filter((account) => account.accountId !== runtime.account.accountId)
.map((account) => ({
provider: account.provider,
status: account.status,
})),
};
const selectedBackend = await selectExecutionBackend(backendSelectionInput);
const backendChoices = listExecutionBackendChoices(backendSelectionInput);
const agentControls = executionConfig.agentControls;
const masterExecutionPrompt = buildMasterCodexNodePrompt(
state,
params.requestText,
params.currentSessionExpiresAt,
agentControls,
executionConfig.promptPolicy,
executionConfig.userPrompt,
executionConfig.projectMemories,
executionConfig.userMemories,
);
const selectedMasterAccount = await resolveMasterNodeExecutionCandidate({
backendChoices,
runtimeAccount: runtime.account,
});
const apiExecutionCandidates = await buildApiExecutionCandidates({
backendChoices,
runtimeAccount: runtime.account,
agentControls,
});
const hasMasterFallback = backendChoices.some((backend) => backend.backendId === "master-codex-node");
if (params.mode === "enqueue") {
if (runtime.account.provider === "master_codex_node") {
const state = await readState();
const deviceId = runtime.account.nodeId || state.user.boundDeviceId || "mac-studio";
const boundDevice = state.devices.find((device) => device.id === deviceId);
const boundNodeLabel =
runtime.account.nodeLabel?.trim() ||
boundDevice?.name ||
state.user.boundCodexNodeLabel ||
deviceId;
const runMasterNodeExecution = async () => {
if (!selectedMasterAccount) {
await appendMasterAgentSystemReply(
[
`当前主控身份是 ${runtime.summary.roleLabel},目标后端是 Master Codex Node但当前没有可用的 master 节点账号。`,
"请先把可用的 Master Codex Node 重新接回,再重试。",
].join(""),
`主 Agent · ${runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MASTER_NODE_NOT_CONNECTED" };
}
if (!boundDevice || boundDevice.status !== "online") {
await updateAiAccountHealth({
accountId: runtime.account.accountId,
status: "degraded",
lastError: !boundDevice ? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND" : "MASTER_CODEX_NODE_DEVICE_OFFLINE",
lastValidatedAt: new Date().toISOString(),
});
const deviceId = selectedMasterAccount.nodeId || state.user.boundDeviceId || "mac-studio";
const boundDevice = state.devices.find((device) => device.id === deviceId);
const boundNodeLabel =
selectedMasterAccount.nodeLabel?.trim() ||
boundDevice?.name ||
state.user.boundCodexNodeLabel ||
deviceId;
const fallbackAccount = await findFallbackApiAccount(runtime.account.accountId);
if (fallbackAccount?.apiKey?.trim() && isApiCompatibleProvider(fallbackAccount.provider)) {
return enqueueOpenAiMasterAgentReply({
provider: fallbackAccount.provider,
accountId: fallbackAccount.accountId,
accountLabel: fallbackAccount.label || aiRoleLabel(fallbackAccount.role),
requestMessageId: params.requestMessageId,
requestText: params.requestText,
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: fallbackAccount.apiKey,
model:
agentControls?.modelOverride ||
fallbackAccount.model ||
apiProviderConfig(fallbackAccount.provider).defaultModel,
reasoningEffort: agentControls?.reasoningEffortOverride || "medium",
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
}
await appendMasterAgentSystemReply(
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线后再重试。`,
`主 Agent · ${runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MASTER_NODE_OFFLINE" };
}
const task = await queueMasterAgentTask({
requestMessageId: params.requestMessageId ?? "master-agent-manual",
requestText: params.requestText,
executionPrompt: buildMasterCodexNodePrompt(
state,
params.requestText,
params.currentSessionExpiresAt,
agentControls,
executionConfig.promptPolicy,
executionConfig.userPrompt,
executionConfig.projectMemories,
executionConfig.userMemories,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId,
accountId: runtime.account.accountId,
accountLabel: runtime.account.label || runtime.summary.roleLabel,
if (!boundDevice || boundDevice.status !== "online") {
await updateAiAccountHealth({
accountId: selectedMasterAccount.accountId,
status: "degraded",
lastError: !boundDevice ? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND" : "MASTER_CODEX_NODE_DEVICE_OFFLINE",
lastValidatedAt: new Date().toISOString(),
});
await appendMasterAgentSystemReply(
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线后再重试。`,
`主 Agent · ${selectedMasterAccount.label || runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MASTER_NODE_OFFLINE" };
}
const task = await queueMasterAgentTask({
requestMessageId: params.requestMessageId ?? "master-agent-manual",
requestText: params.requestText,
executionPrompt: masterExecutionPrompt,
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId,
accountId: selectedMasterAccount.accountId,
accountLabel: selectedMasterAccount.label || runtime.summary.roleLabel,
});
if (params.mode === "enqueue") {
const queuedReply: QueuedMasterAgentReplyEnvelope = {
ok: true as const,
accountId: runtime.account.accountId,
accountId: selectedMasterAccount.accountId,
taskId: task.taskId,
masterReplyState: "queued" as const,
task: {
@@ -1477,117 +1672,16 @@ export async function replyToMasterAgentUserMessage(params: {
return queuedReply;
}
if (isApiCompatibleProvider(runtime.account.provider) && runtime.account.apiKey?.trim()) {
return enqueueOpenAiMasterAgentReply({
provider: runtime.account.provider,
accountId: runtime.account.accountId,
accountLabel: runtime.account.label || runtime.summary.roleLabel,
requestMessageId: params.requestMessageId,
requestText: params.requestText,
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: runtime.account.apiKey,
model: executionConfig.model,
reasoningEffort: executionConfig.reasoningEffort,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
}
}
if (runtime.account.provider === "master_codex_node") {
const state = await readState();
const deviceId = runtime.account.nodeId || state.user.boundDeviceId || "mac-studio";
const boundDevice = state.devices.find((device) => device.id === deviceId);
const boundNodeLabel =
runtime.account.nodeLabel?.trim() ||
boundDevice?.name ||
state.user.boundCodexNodeLabel ||
deviceId;
if (!boundDevice || boundDevice.status !== "online") {
await updateAiAccountHealth({
accountId: runtime.account.accountId,
status: "degraded",
lastError: !boundDevice ? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND" : "MASTER_CODEX_NODE_DEVICE_OFFLINE",
lastValidatedAt: new Date().toISOString(),
});
const fallbackAccount = await findFallbackApiAccount(runtime.account.accountId);
if (fallbackAccount) {
try {
return await replyViaOpenAiAccount({
account: fallbackAccount,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
} catch {
// Fall through to the original offline guidance when the fallback API account cannot respond.
}
}
await appendMasterAgentSystemReply(
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线后再重试。`,
`主 Agent · ${runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MASTER_NODE_OFFLINE" };
}
const task = await queueMasterAgentTask({
requestMessageId: params.requestMessageId ?? "master-agent-manual",
requestText: params.requestText,
executionPrompt: buildMasterCodexNodePrompt(
state,
params.requestText,
params.currentSessionExpiresAt,
agentControls,
executionConfig.promptPolicy,
executionConfig.userPrompt,
executionConfig.projectMemories,
executionConfig.userMemories,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId,
accountId: runtime.account.accountId,
accountLabel: runtime.summary.roleLabel,
});
const completedTask = await waitForMasterAgentTaskCompletion(task.taskId);
if (completedTask?.status === "completed") {
return {
ok: true as const,
accountId: runtime.account.accountId,
accountId: selectedMasterAccount.accountId,
taskId: task.taskId,
requestId: completedTask.requestId,
};
}
if (completedTask?.status === "failed") {
const fallbackAccount = await findFallbackApiAccount(runtime.account.accountId);
if (fallbackAccount) {
try {
return await replyViaOpenAiAccount({
account: fallbackAccount,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
} catch {
// Preserve the original execution failure below if the fallback account also fails.
}
}
return {
ok: false as const,
reason: "MASTER_NODE_EXEC_FAILED",
@@ -1601,9 +1695,86 @@ export async function replyToMasterAgentUserMessage(params: {
`当前主控身份是 ${runtime.summary.roleLabel},任务已经转交到 ${boundNodeLabel} 的 Master Codex Node。`,
"如果本机 Codex 节点在线,回复会在稍后自动回写到当前会话。",
].join(""),
`主 Agent · ${runtime.summary.roleLabel}`,
`主 Agent · ${selectedMasterAccount.label || runtime.summary.roleLabel}`,
);
return { ok: true as const, accountId: runtime.account.accountId, taskId: task.taskId };
return { ok: true as const, accountId: selectedMasterAccount.accountId, taskId: task.taskId };
};
if (params.mode === "enqueue") {
if (selectedBackend.backendId === "master-codex-node") {
return runMasterNodeExecution();
}
if (apiExecutionCandidates.length > 0) {
return enqueueOpenAiMasterAgentReply({
candidates: apiExecutionCandidates,
requestMessageId: params.requestMessageId,
requestText: params.requestText,
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
currentSessionExpiresAt: params.currentSessionExpiresAt,
reasoningEffort: executionConfig.reasoningEffort,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
masterFallback: hasMasterFallback && selectedMasterAccount
? {
account: selectedMasterAccount,
executionPrompt: masterExecutionPrompt,
}
: null,
});
}
}
if (selectedBackend.backendId === "master-codex-node") {
return runMasterNodeExecution();
}
let lastApiFailureMessage: string | null = null;
let lastFailedAccount: AiAccount | null = null;
for (const candidate of apiExecutionCandidates) {
try {
return await replyViaOpenAiAccount({
account: candidate.account,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${candidate.account.label || aiRoleLabel(candidate.account.role)}`,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
} catch (error) {
lastApiFailureMessage = error instanceof Error ? error.message : "主 Agent 当前调用模型失败。";
lastFailedAccount = candidate.account;
if (!runtime.isEnvironmentFallback) {
await updateAiAccountHealth({
accountId: candidate.account.accountId,
status: "degraded",
lastError: lastApiFailureMessage,
lastValidatedAt: new Date().toISOString(),
});
}
}
}
if (hasMasterFallback && selectedMasterAccount) {
return runMasterNodeExecution();
}
if (lastApiFailureMessage) {
await appendMasterAgentSystemReply(
[
`我已经收到你的消息,但当前 AI 账号调用失败:${lastApiFailureMessage}`,
"请到“我的 > AI 账号”检查 API Key、模型名或切换到其他 AI 账号后重试。",
].join(""),
`主 Agent · ${lastFailedAccount?.label || runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MODEL_CALL_FAILED", message: lastApiFailureMessage };
}
if (!isApiCompatibleProvider(runtime.account.provider) || !runtime.account.apiKey?.trim()) {
@@ -1618,61 +1789,5 @@ export async function replyToMasterAgentUserMessage(params: {
return { ok: false as const, reason: "MASTER_NODE_NOT_CONNECTED" };
}
try {
const generated = await generateApiProviderReply({
provider: runtime.account.provider,
apiKey: runtime.account.apiKey,
model: executionConfig.model,
reasoningEffort: executionConfig.reasoningEffort,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls,
promptPolicy: executionConfig.promptPolicy,
userPrompt: executionConfig.userPrompt,
projectMemories: executionConfig.projectMemories,
userMemories: executionConfig.userMemories,
});
await appendMasterAgentSystemReply(
generated.content,
`主 Agent · ${runtime.summary.roleLabel}`,
);
if (!runtime.isEnvironmentFallback) {
await updateAiAccountHealth({
accountId: runtime.account.accountId,
status: "ready",
lastValidatedAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
activate: !runtime.account.isActive,
switchReason: runtime.account.isActive
? runtime.account.switchReason
: `主 Agent 回复时自动切换到 ${runtime.account.label}`,
});
}
return {
ok: true as const,
accountId: runtime.account.accountId,
requestId: generated.requestId,
};
} catch (error) {
const message = error instanceof Error ? error.message : "主 Agent 当前调用模型失败。";
if (!runtime.isEnvironmentFallback) {
await updateAiAccountHealth({
accountId: runtime.account.accountId,
status: "degraded",
lastError: message,
lastValidatedAt: new Date().toISOString(),
});
}
await appendMasterAgentSystemReply(
[
`我已经收到你的消息,但当前 AI 账号调用失败:${message}`,
"请到“我的 > AI 账号”检查 API Key、模型名或切换到其他 AI 账号后重试。",
].join(""),
`主 Agent · ${runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MODEL_CALL_FAILED", message };
}
return { ok: false as const, reason: "MASTER_NODE_NOT_CONNECTED" };
}