refactor: add execution backend selection
This commit is contained in:
@@ -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" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user