feat: add omx team adapter skeleton

This commit is contained in:
kris
2026-04-03 02:22:02 +08:00
parent 8e2350e89d
commit 60f5e2d7d6
11 changed files with 582 additions and 3 deletions

View File

@@ -0,0 +1,37 @@
import type { OrchestrationBackend } from "@/lib/execution/orchestration-backend";
import {
getOmxTeamBackendAvailability,
getOmxTeamBackendConfig,
isOmxTeamBackendConfigured,
type OmxTeamBackendAvailability,
type OmxTeamBackendConfig,
} from "@/lib/execution/backends/omx-team-config";
export const OMX_TEAM_BACKEND_ID = "omx-team";
export const OMX_TEAM_BACKEND: OrchestrationBackend = {
backendId: OMX_TEAM_BACKEND_ID,
async describe() {
return {
backendId: OMX_TEAM_BACKEND_ID,
label: "OMX Team Runtime",
};
},
};
export interface OmxTeamBackendSelectionState {
enabled: boolean;
selectable: boolean;
availability: OmxTeamBackendAvailability;
}
export async function getOmxTeamBackendSelectionState(
config: OmxTeamBackendConfig = getOmxTeamBackendConfig(),
): Promise<OmxTeamBackendSelectionState> {
const availability = await getOmxTeamBackendAvailability(config);
return {
enabled: isOmxTeamBackendConfigured(config),
selectable: availability.selectable,
availability,
};
}

View File

@@ -0,0 +1,184 @@
import { constants } from "node:fs";
import { access } from "node:fs/promises";
import path from "node:path";
export interface OmxTeamBackendConfig {
enabled: boolean;
command?: string;
args: string[];
cwd?: string;
timeoutMs: number;
}
export type OmxTeamBackendAvailabilityStatus = "disabled" | "misconfigured" | "ready";
export interface OmxTeamBackendAvailability {
status: OmxTeamBackendAvailabilityStatus;
selectable: boolean;
configured: boolean;
reason:
| "disabled"
| "command_not_set"
| "command_not_found"
| "workdir_not_found"
| "script_not_found"
| "ready";
reasonLabel: string;
command?: string;
cwd?: string;
}
function parseBoolean(value: string | undefined) {
return value?.trim().toLowerCase() === "true";
}
function parseArgs(value: string | undefined) {
return String(value || "")
.trim()
.split(/\s+/)
.filter(Boolean);
}
function parseTimeoutMs(value: string | undefined) {
const parsed = Number.parseInt(value || "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 45000;
}
export function getOmxTeamBackendConfig(): OmxTeamBackendConfig {
return {
enabled: parseBoolean(process.env.BOSS_OMX_ENABLED),
command: process.env.BOSS_OMX_COMMAND?.trim() || undefined,
args: parseArgs(process.env.BOSS_OMX_ARGS),
cwd: process.env.BOSS_OMX_WORKDIR?.trim() || undefined,
timeoutMs: parseTimeoutMs(process.env.BOSS_OMX_TIMEOUT_MS),
};
}
export function isOmxTeamBackendConfigured(config: OmxTeamBackendConfig) {
return config.enabled && Boolean(config.command);
}
function commandLooksLikePath(command: string) {
return command.includes("/") || command.includes("\\");
}
async function fileExists(filePath: string, mode = constants.F_OK) {
try {
await access(filePath, mode);
return true;
} catch {
return false;
}
}
function resolveScriptCandidate(config: OmxTeamBackendConfig) {
if (!config.command || config.args.length === 0) {
return null;
}
const commandName = path.basename(config.command).toLowerCase();
const scriptRuntimes = new Set(["node", "node.exe", "tsx", "tsx.cmd", "bun", "bun.exe", "deno", "deno.exe"]);
if (!scriptRuntimes.has(commandName)) {
return null;
}
const candidate = config.args[0];
if (!candidate || candidate.startsWith("-")) {
return null;
}
return path.isAbsolute(candidate)
? candidate
: path.resolve(config.cwd?.trim() || process.cwd(), candidate);
}
async function isCommandReachable(command: string) {
if (commandLooksLikePath(command)) {
return fileExists(path.resolve(command), constants.X_OK);
}
const searchPaths = (process.env.PATH || "")
.split(path.delimiter)
.map((entry) => entry.trim())
.filter(Boolean);
for (const entry of searchPaths) {
const candidate = path.join(entry, command);
if (await fileExists(candidate, constants.X_OK)) {
return true;
}
}
return false;
}
export async function getOmxTeamBackendAvailability(
config: OmxTeamBackendConfig = getOmxTeamBackendConfig(),
): Promise<OmxTeamBackendAvailability> {
const base = {
command: config.command,
cwd: config.cwd,
configured: isOmxTeamBackendConfigured(config),
};
if (!config.enabled) {
return {
...base,
status: "disabled",
selectable: false,
reason: "disabled",
reasonLabel: "OMX Team Runtime 当前未启用。",
};
}
if (!config.command) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "command_not_set",
reasonLabel: "OMX Team Runtime 缺少启动命令。",
};
}
if (!(await isCommandReachable(config.command))) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "command_not_found",
reasonLabel: "未检测到可执行的 OMX Team 启动命令。",
};
}
if (config.cwd && !(await fileExists(config.cwd, constants.F_OK))) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "workdir_not_found",
reasonLabel: "OMX Team Runtime 工作目录不存在。",
};
}
const scriptCandidate = resolveScriptCandidate(config);
if (scriptCandidate && !(await fileExists(scriptCandidate, constants.F_OK))) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "script_not_found",
reasonLabel: "未检测到有效的 OMX Team 启动脚本,将自动回退到 Boss Native Orchestrator。",
};
}
return {
...base,
status: "ready",
selectable: true,
reason: "ready",
reasonLabel: "OMX Team Runtime 可用。",
};
}
export const getOmxTeamBackendConfigForTesting = getOmxTeamBackendConfig;
export const isOmxTeamBackendConfiguredForTesting = isOmxTeamBackendConfigured;
export const getOmxTeamBackendAvailabilityForTesting = getOmxTeamBackendAvailability;

View File

@@ -0,0 +1,63 @@
import { BOSS_NATIVE_ORCHESTRATOR } from "@/lib/execution/backends/boss-native-orchestrator";
import {
OMX_TEAM_BACKEND,
type OmxTeamBackendSelectionState,
} from "@/lib/execution/backends/omx-team-backend";
import type { OrchestrationBackend } from "@/lib/execution/orchestration-backend";
export interface OrchestrationBackendSelectionInput {
requestedBackendId?: string;
omx?: OmxTeamBackendSelectionState;
}
export type OrchestrationBackendChoice =
| typeof BOSS_NATIVE_ORCHESTRATOR
| typeof OMX_TEAM_BACKEND;
function isReadyBackend(
backend: OrchestrationBackend,
input: OrchestrationBackendSelectionInput,
) {
if (backend.backendId === OMX_TEAM_BACKEND.backendId) {
return input.omx?.selectable ?? false;
}
return true;
}
export async function selectOrchestrationBackend(
input: OrchestrationBackendSelectionInput = {},
): Promise<OrchestrationBackendChoice> {
return (await listOrchestrationBackendChoices(input))[0] ?? BOSS_NATIVE_ORCHESTRATOR;
}
export async function listOrchestrationBackendChoices(
input: OrchestrationBackendSelectionInput = {},
): Promise<OrchestrationBackendChoice[]> {
const ordered: OrchestrationBackendChoice[] = [];
const seen = new Set<string>();
const pushBackend = (backend: OrchestrationBackendChoice) => {
if (seen.has(backend.backendId)) {
return;
}
ordered.push(backend);
seen.add(backend.backendId);
};
if (
input.requestedBackendId === OMX_TEAM_BACKEND.backendId &&
isReadyBackend(OMX_TEAM_BACKEND, input)
) {
pushBackend(OMX_TEAM_BACKEND);
}
pushBackend(BOSS_NATIVE_ORCHESTRATOR);
if (isReadyBackend(OMX_TEAM_BACKEND, input)) {
pushBackend(OMX_TEAM_BACKEND);
}
return ordered;
}
export const selectOrchestrationBackendForTesting = selectOrchestrationBackend;