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

@@ -0,0 +1,97 @@
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
import {
ALIYUN_QWEN_BACKEND,
isReadyAliyunQwenBackend,
} from "@/lib/execution/backends/aliyun-qwen-backend";
import {
MASTER_CODEX_NODE_BACKEND,
isReadyMasterCodexNodeBackend,
} from "@/lib/execution/backends/master-codex-node-backend";
import { OPENAI_BACKEND, isReadyOpenAiBackend } from "@/lib/execution/backends/openai-backend";
export interface ExecutionBackendSelectionInput {
primary: {
provider: AiProvider;
status: AiAccountStatus;
};
backups: Array<{
provider: AiProvider;
status: AiAccountStatus;
}>;
}
export type ExecutionBackendChoice =
| typeof MASTER_CODEX_NODE_BACKEND
| typeof OPENAI_BACKEND
| typeof ALIYUN_QWEN_BACKEND;
function resolveBackendByProvider(provider: AiProvider): ExecutionBackendChoice {
switch (provider) {
case "master_codex_node":
return MASTER_CODEX_NODE_BACKEND;
case "openai_api":
return OPENAI_BACKEND;
case "aliyun_qwen_api":
return ALIYUN_QWEN_BACKEND;
default:
return MASTER_CODEX_NODE_BACKEND;
}
}
function isReadyBackend(choice: ExecutionBackendChoice, input: ExecutionBackendSelectionInput) {
const candidates = [
...(input.primary.provider === choice.provider ? [input.primary] : []),
...input.backups.filter((item) => item.provider === choice.provider),
];
return candidates.some((candidate) => {
switch (choice.backendId) {
case "aliyun-qwen":
return isReadyAliyunQwenBackend(candidate);
case "openai-api":
return isReadyOpenAiBackend(candidate);
case "master-codex-node":
return isReadyMasterCodexNodeBackend(candidate);
default:
return false;
}
});
}
export async function selectExecutionBackend(
input: ExecutionBackendSelectionInput,
): Promise<ExecutionBackendChoice> {
return listExecutionBackendChoices(input)[0] ?? resolveBackendByProvider(input.primary.provider);
}
export function listExecutionBackendChoices(
input: ExecutionBackendSelectionInput,
): ExecutionBackendChoice[] {
const primaryBackend = resolveBackendByProvider(input.primary.provider);
const ordered: ExecutionBackendChoice[] = [];
const seen = new Set<string>();
const pushBackend = (backend: ExecutionBackendChoice) => {
if (seen.has(backend.backendId)) {
return;
}
ordered.push(backend);
seen.add(backend.backendId);
};
if (input.primary.status === "ready") {
pushBackend(primaryBackend);
}
const orderedBackends: ExecutionBackendChoice[] = [ALIYUN_QWEN_BACKEND, OPENAI_BACKEND, MASTER_CODEX_NODE_BACKEND];
for (const backend of orderedBackends) {
if (isReadyBackend(backend, input)) {
pushBackend(backend);
}
}
pushBackend(primaryBackend);
return ordered;
}
export const selectExecutionBackendForTesting = selectExecutionBackend;

View File

@@ -0,0 +1,22 @@
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
export interface ExecutionBackendDescriptor {
backendId: string;
provider: AiProvider;
label: string;
mode: "local" | "api";
}
export const ALIYUN_QWEN_BACKEND = {
backendId: "aliyun-qwen",
provider: "aliyun_qwen_api",
label: "阿里百炼 Qwen",
mode: "api",
} as const satisfies ExecutionBackendDescriptor;
export function isReadyAliyunQwenBackend(input: {
provider: AiProvider;
status: AiAccountStatus;
}) {
return input.provider === ALIYUN_QWEN_BACKEND.provider && input.status === "ready";
}

View File

@@ -0,0 +1,22 @@
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
export interface ExecutionBackendDescriptor {
backendId: string;
provider: AiProvider;
label: string;
mode: "local" | "api";
}
export const MASTER_CODEX_NODE_BACKEND = {
backendId: "master-codex-node",
provider: "master_codex_node",
label: "Master Codex Node",
mode: "local",
} as const satisfies ExecutionBackendDescriptor;
export function isReadyMasterCodexNodeBackend(input: {
provider: AiProvider;
status: AiAccountStatus;
}) {
return input.provider === MASTER_CODEX_NODE_BACKEND.provider && input.status === "ready";
}

View File

@@ -0,0 +1,19 @@
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
export interface ExecutionBackendDescriptor {
backendId: string;
provider: AiProvider;
label: string;
mode: "local" | "api";
}
export const OPENAI_BACKEND = {
backendId: "openai-api",
provider: "openai_api",
label: "OpenAI API",
mode: "api",
} as const satisfies ExecutionBackendDescriptor;
export function isReadyOpenAiBackend(input: { provider: AiProvider; status: AiAccountStatus }) {
return input.provider === OPENAI_BACKEND.provider && input.status === "ready";
}