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

@@ -1333,6 +1333,7 @@ export interface MasterAgentTask {
deviceId: string;
accountId?: string;
accountLabel?: string;
modelChannelAttemptedDeviceIds?: string[];
attachmentId?: string;
attachmentFileName?: string;
attachmentDownloadToken?: string;
@@ -9915,6 +9916,123 @@ function isTerminalMasterAgentTaskStatus(status: MasterAgentTaskStatus) {
return status === "completed" || status === "failed" || status === "timed_out" || status === "canceled";
}
function codexModelChannelAccountSummarySignedIn(device: Device) {
const metadata = device.capabilities?.codexAppServer?.metadata;
const accountSummary =
metadata && typeof metadata === "object"
? (metadata.accountSummary as { signedIn?: unknown } | undefined)
: undefined;
return accountSummary?.signedIn !== false;
}
function hasUsableCodexModelChannelInState(device: Device | undefined) {
if (!device || device.status !== "online" || isDeviceRevoked(device)) {
return false;
}
const capabilities = device.capabilities;
const hasCodexTransport = Boolean(
capabilities?.codexAppServer?.connected ||
capabilities?.cli?.connected ||
capabilities?.gui?.connected,
);
return hasCodexTransport && codexModelChannelAccountSummarySignedIn(device);
}
function isMasterCodexNodeTask(task: MasterAgentTask, state: BossState) {
if (task.taskType !== "conversation_reply") {
return false;
}
if (task.accountId?.startsWith("master-codex-device-")) {
return true;
}
const account = task.accountId
? state.aiAccounts.find((item) => item.accountId === task.accountId)
: undefined;
return account?.provider === "master_codex_node";
}
function isTaskAuthorizedForDevice(task: MasterAgentTask, deviceId: string) {
return (
!task.authorizedDeviceIds ||
task.authorizedDeviceIds.length === 0 ||
task.authorizedDeviceIds.includes(deviceId)
);
}
function sortMasterCodexFailoverDevices(left: Device, right: Device) {
return (right.lastSeenAt ?? "").localeCompare(left.lastSeenAt ?? "");
}
function findNextMasterCodexNodeCandidateForFailedTask(
state: BossState,
task: MasterAgentTask,
failedDeviceId: string,
) {
const excludedDeviceIds = new Set([failedDeviceId, ...(task.modelChannelAttemptedDeviceIds ?? [])]);
const seenDeviceIds = new Set<string>();
const configuredCandidates = state.aiAccounts
.filter((account) => {
const deviceId = account.nodeId?.trim();
if (
!account.enabled ||
account.provider !== "master_codex_node" ||
(account.status !== "ready" && account.status !== "degraded") ||
!deviceId ||
excludedDeviceIds.has(deviceId) ||
!isTaskAuthorizedForDevice(task, deviceId)
) {
return false;
}
const device = state.devices.find((item) => item.id === deviceId);
return hasUsableCodexModelChannelInState(device);
})
.sort((left, right) => {
if (left.isActive !== right.isActive) {
return left.isActive ? -1 : 1;
}
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
});
const configured = configuredCandidates[0];
if (configured?.nodeId) {
return {
deviceId: configured.nodeId,
accountId: configured.accountId,
accountLabel: configured.label,
};
}
for (const account of configuredCandidates) {
if (account.nodeId) {
seenDeviceIds.add(account.nodeId);
}
}
const device = state.devices
.filter((item) => {
if (
excludedDeviceIds.has(item.id) ||
seenDeviceIds.has(item.id) ||
!isTaskAuthorizedForDevice(task, item.id)
) {
return false;
}
return hasUsableCodexModelChannelInState(item);
})
.sort(sortMasterCodexFailoverDevices)[0];
if (!device) {
return null;
}
return {
deviceId: device.id,
accountId: `master-codex-device-${device.id}`,
accountLabel: `Codex · ${device.name || device.id}`,
};
}
function masterAgentTaskLeaseMs(task: MasterAgentTask) {
return task.taskType === "conversation_reply"
? MASTER_AGENT_TASK_CONVERSATION_LEASE_MS
@@ -10376,6 +10494,46 @@ export async function completeMasterAgentTask(payload: {
dispatchExecution: undefined,
};
}
if (payload.status === "failed" && isMasterCodexNodeTask(task, state)) {
const failover = findNextMasterCodexNodeCandidateForFailedTask(state, task, payload.deviceId);
if (failover) {
const failedAt = nowIso();
const failedAccount = task.accountId
? state.aiAccounts.find((item) => item.accountId === task.accountId)
: undefined;
if (failedAccount?.provider === "master_codex_node") {
failedAccount.status = "degraded";
failedAccount.lastError = payload.errorMessage?.trim() || "MASTER_CODEX_NODE_EXEC_FAILED";
failedAccount.lastValidatedAt = failedAt;
failedAccount.updatedAt = failedAt;
}
task.status = "queued";
task.deviceId = failover.deviceId;
task.accountId = failover.accountId;
task.accountLabel = failover.accountLabel;
task.modelChannelAttemptedDeviceIds = [
...new Set([...(task.modelChannelAttemptedDeviceIds ?? []), payload.deviceId]),
];
task.completedAt = undefined;
task.canceledAt = undefined;
task.canceledBy = undefined;
task.cancelReason = undefined;
task.leaseExpiresAt = undefined;
task.claimedAt = undefined;
task.lastClaimedAt = undefined;
task.lastErrorKind = "model_channel_failover";
task.errorMessage = payload.errorMessage?.trim() || "MASTER_CODEX_NODE_EXEC_FAILED";
task.replyBody = undefined;
task.requestId = undefined;
upsertTaskExecutionProgressMessageInState(state, task, "queued", payload.executionProgress);
return {
...task,
dispatchPlan: undefined,
dispatchExecution: undefined,
dialogGuardIntervention: undefined,
};
}
}
task.status = payload.status;
task.completedAt = nowIso();
task.leaseExpiresAt = undefined;

View File

@@ -25,6 +25,7 @@ import type {
AiAccount,
AiProvider,
AuthRole,
Device,
BossPermission,
DispatchPlanTarget,
ExternalReplyTarget,
@@ -1740,9 +1741,36 @@ function isUsableMasterNodeAccount(account: AiAccount) {
);
}
function codexAppServerAccountSummarySignedIn(device: Device) {
const metadata = device.capabilities?.codexAppServer?.metadata;
const accountSummary =
metadata && typeof metadata === "object"
? (metadata.accountSummary as { signedIn?: unknown } | undefined)
: undefined;
return accountSummary?.signedIn !== false;
}
function hasUsableCodexModelChannel(device: Device | undefined) {
if (!device || device.status !== "online") {
return false;
}
const capabilities = device.capabilities;
const hasCodexTransport = Boolean(
capabilities?.codexAppServer?.connected ||
capabilities?.cli?.connected ||
capabilities?.gui?.connected,
);
return hasCodexTransport && codexAppServerAccountSummarySignedIn(device);
}
function isAuthorizedDeviceId(deviceId: string, authorizedDeviceIds?: string[]) {
return !authorizedDeviceIds || authorizedDeviceIds.length === 0 || authorizedDeviceIds.includes(deviceId);
}
function isOnlineMasterNodeAccount(
state: Awaited<ReturnType<typeof readState>>,
account: AiAccount,
authorizedDeviceIds?: string[],
) {
if (!isUsableMasterNodeAccount(account)) {
return false;
@@ -1751,8 +1779,11 @@ function isOnlineMasterNodeAccount(
if (!deviceId) {
return false;
}
if (!isAuthorizedDeviceId(deviceId, authorizedDeviceIds)) {
return false;
}
const device = state.devices.find((item) => item.id === deviceId);
return Boolean(device && device.status === "online");
return hasUsableCodexModelChannel(device);
}
function sortSelectableAccounts(left: AiAccount, right: AiAccount) {
@@ -1790,19 +1821,106 @@ function supportsResponsesReasoning(provider: ApiCompatibleProvider) {
return provider === "openai_api" || provider === "hyzq_api";
}
function buildAutoDiscoveredMasterNodeAccount(device: Device): AiAccount {
const now = device.lastSeenAt || new Date(0).toISOString();
return {
accountId: `master-codex-device-${device.id}`,
label: `Codex · ${device.name || device.id}`,
role: "backup",
provider: "master_codex_node",
displayName: `${device.name || device.id} 上的 Codex`,
nodeId: device.id,
nodeLabel: device.name || device.id,
model: "Codex",
enabled: true,
isActive: false,
status: "ready",
loginStatusNote: "由绑定设备 heartbeat 自动发现的 Codex 模型通道。",
lastValidatedAt: device.lastSeenAt,
lastUsedAt: device.lastSeenAt,
createdAt: now,
updatedAt: now,
};
}
function sortAutoDiscoveredCodexDevices(left: Device, right: Device) {
return (right.lastSeenAt ?? "").localeCompare(left.lastSeenAt ?? "");
}
function resolveMasterCodexNodeExecutionCandidates(params: {
state: Awaited<ReturnType<typeof readState>>;
runtimeAccount: AiAccount;
authorizedDeviceIds?: string[];
excludeDeviceIds?: string[];
}) {
const seenAccountIds = new Set<string>();
const seenDeviceIds = new Set<string>();
const excludedDeviceIds = new Set(params.excludeDeviceIds ?? []);
const candidates: AiAccount[] = [];
const pushConfiguredAccount = (account: AiAccount | undefined) => {
if (!account || seenAccountIds.has(account.accountId)) {
return;
}
const deviceId = account.nodeId?.trim();
if (!deviceId || excludedDeviceIds.has(deviceId)) {
return;
}
if (!isOnlineMasterNodeAccount(params.state, account, params.authorizedDeviceIds)) {
return;
}
seenAccountIds.add(account.accountId);
seenDeviceIds.add(deviceId);
candidates.push(account);
};
if (params.runtimeAccount.provider === "master_codex_node") {
pushConfiguredAccount(params.runtimeAccount);
}
for (const account of params.state.aiAccounts
.filter((item) => item.accountId !== params.runtimeAccount.accountId)
.sort(sortSelectableAccounts)) {
pushConfiguredAccount(account);
}
for (const device of params.state.devices
.filter((item) => {
if (excludedDeviceIds.has(item.id) || seenDeviceIds.has(item.id)) {
return false;
}
if (!isAuthorizedDeviceId(item.id, params.authorizedDeviceIds)) {
return false;
}
return hasUsableCodexModelChannel(item);
})
.sort(sortAutoDiscoveredCodexDevices)) {
const account = buildAutoDiscoveredMasterNodeAccount(device);
if (seenAccountIds.has(account.accountId)) {
continue;
}
seenAccountIds.add(account.accountId);
seenDeviceIds.add(device.id);
candidates.push(account);
}
return candidates;
}
function isPersistedAiAccount(
state: Awaited<ReturnType<typeof readState>>,
account: AiAccount,
) {
return state.aiAccounts.some((item) => item.accountId === account.accountId);
}
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];
return resolveMasterCodexNodeExecutionCandidates({ state, runtimeAccount })[0] ?? null;
}
if (isApiCompatibleProvider(selectedBackendProvider)) {
@@ -1993,13 +2111,16 @@ export async function tryBuildLocalMasterAgentFastReply(params: {
async function resolveMasterNodeExecutionCandidate(params: {
backendChoices: Array<{ backendId: string; provider?: AiProvider }>;
runtimeAccount: AiAccount;
candidates?: 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);
const account =
params.candidates?.[0] ??
(await resolveAccountForSelectedBackend("master_codex_node", params.runtimeAccount));
return account && account.provider === "master_codex_node" ? account : null;
}
@@ -4160,10 +4281,13 @@ export async function replyToMasterAgentUserMessage(params: {
return { ok: false as const, reason: "FORBIDDEN" };
}
const replyProject = authorizedState.projects.find((project) => project.id === replyProjectId);
const primaryDeviceId = runtime.account.nodeId || state.user.boundDeviceId || "mac-studio";
const primaryDevice = state.devices.find((device) => device.id === primaryDeviceId);
const masterNodeCandidates = resolveMasterCodexNodeExecutionCandidates({
state,
runtimeAccount: runtime.account,
authorizedDeviceIds: authorizedScope.authorizedDeviceIds,
});
const primaryBackendStatus =
runtime.account.provider === "master_codex_node" && (!primaryDevice || primaryDevice.status !== "online")
runtime.account.provider === "master_codex_node" && masterNodeCandidates.length === 0
? "degraded"
: runtime.account.status;
const clawSelectionState = await getClawBackendSelectionState();
@@ -4176,7 +4300,11 @@ export async function replyToMasterAgentUserMessage(params: {
.filter((account) => account.accountId !== runtime.account.accountId)
.map((account) => ({
provider: account.provider,
status: account.status,
status:
account.provider === "master_codex_node" &&
!isOnlineMasterNodeAccount(state, account, authorizedScope.authorizedDeviceIds)
? "degraded"
: account.status,
})),
requestKind: "master_agent_reply" as const,
requestedBackendId: executionConfig.agentControls?.backendOverride,
@@ -4194,6 +4322,7 @@ export async function replyToMasterAgentUserMessage(params: {
const selectedMasterAccount = await resolveMasterNodeExecutionCandidate({
backendChoices,
runtimeAccount: runtime.account,
candidates: masterNodeCandidates,
});
const apiExecutionCandidates = await buildApiExecutionCandidates({
backendChoices,
@@ -4333,8 +4462,8 @@ export async function replyToMasterAgentUserMessage(params: {
if (!selectedMasterAccount) {
await appendMasterAgentSystemReply(
[
`当前主控身份是 ${runtime.summary.roleLabel},目标后端是 Master Codex Node但当前没有可用的 master 节点账号。`,
"请先把可用的 Master Codex Node 重新接回,再重试。",
"当前没有可用的模型渠道。",
"请先接回一台已经登录 Codex / ChatGPT 的电脑,或到“我的 > AI 账号”配置可用 API Key 后再重试。",
].join(""),
`主 Agent · ${runtime.summary.roleLabel}`,
replyProjectId,
@@ -4351,15 +4480,27 @@ export async function replyToMasterAgentUserMessage(params: {
state.user.boundCodexNodeLabel ||
deviceId;
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(),
});
if (!hasUsableCodexModelChannel(boundDevice)) {
const lastError = !boundDevice
? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND"
: boundDevice.status !== "online"
? "MASTER_CODEX_NODE_DEVICE_OFFLINE"
: "MASTER_CODEX_NODE_CHANNEL_UNAVAILABLE";
if (isPersistedAiAccount(state, selectedMasterAccount)) {
await updateAiAccountHealth({
accountId: selectedMasterAccount.accountId,
status: "degraded",
lastError,
lastValidatedAt: new Date().toISOString(),
});
}
const unavailableReason = !boundDevice
? "未找到"
: boundDevice.status !== "online"
? "不在线"
: "没有检测到可用的 Codex 模型通道";
await appendMasterAgentSystemReply(
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线后再重试。`,
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel} ${unavailableReason},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 boss-agent 在线后再重试。`,
`主 Agent · ${selectedMasterAccount.label || runtime.summary.roleLabel}`,
replyProjectId,
params.requestedByAccount,
@@ -4367,12 +4508,14 @@ export async function replyToMasterAgentUserMessage(params: {
return { ok: false as const, reason: "MASTER_NODE_OFFLINE" };
}
await updateAiAccountHealth({
accountId: selectedMasterAccount.accountId,
status: "ready",
lastValidatedAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
});
if (isPersistedAiAccount(state, selectedMasterAccount)) {
await updateAiAccountHealth({
accountId: selectedMasterAccount.accountId,
status: "ready",
lastValidatedAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
});
}
if (
controlIntent.intentCategory === "browser_control" || controlIntent.intentCategory === "desktop_control"