feat: add omx orchestration backend selection

This commit is contained in:
kris
2026-04-03 03:17:12 +08:00
parent 60f5e2d7d6
commit ec45bed59f
18 changed files with 1993 additions and 20 deletions

View File

@@ -29,6 +29,8 @@ import type {
OtaUpdateLog,
OpsRepairTicket,
OpsRepairVerification,
ProjectOrchestrationBackendState,
OrchestrationBackendId,
ThreadContextSnapshot,
UserProfile,
UserSettings,
@@ -781,6 +783,172 @@ export function ProjectHeaderActions({ projectId }: { projectId: string }) {
);
}
function orchestrationBackendChoiceLabel(choice: ProjectOrchestrationBackendState["availableChoices"][number]) {
return choice.backendId === "boss-native-orchestrator"
? "Boss Native Orchestrator"
: "OMX Team Runtime";
}
function normalizeOrchestrationReasonLabel(value: string) {
const trimmed = value.trim();
if (trimmed.endsWith("。") || trimmed.endsWith(".")) {
return trimmed.slice(0, -1);
}
return trimmed;
}
function orchestrationBackendAvailabilityCopy(
state: ProjectOrchestrationBackendState,
fallbackActive: boolean,
) {
if (state.omxAvailability.selectable) {
return {
badge: "正常",
summary: "OMX Team Runtime 当前可用,当前可切换到该后端。",
};
}
return {
badge: fallbackActive ? "已回退" : "OMX 受限",
summary: fallbackActive
? `${normalizeOrchestrationReasonLabel(state.omxAvailability.reasonLabel)},当前已自动回退到 Boss Native Orchestrator。`
: `${normalizeOrchestrationReasonLabel(state.omxAvailability.reasonLabel)},切换后会自动回退到 Boss Native Orchestrator。`,
};
}
export function ProjectOrchestrationBackendCard({
projectId,
initialState,
}: {
projectId: string;
initialState: ProjectOrchestrationBackendState;
}) {
const router = useRouter();
const [state, setState] = useState(initialState);
const [savingBackendId, setSavingBackendId] = useState<OrchestrationBackendId | null>(null);
const [message, setMessage] = useState("");
const fallbackActive = state.requestedBackendId !== state.currentBackendId;
const availabilityCopy = orchestrationBackendAvailabilityCopy(state, fallbackActive);
async function saveBackend(requestedBackendId: OrchestrationBackendId) {
setSavingBackendId(requestedBackendId);
const response = await fetch(`/api/v1/projects/${projectId}/orchestration-backend`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ requestedBackendId }),
});
const result = (await response.json()) as {
ok: boolean;
message?: string;
currentBackendId?: OrchestrationBackendId;
currentBackendLabel?: string;
requestedBackendId?: OrchestrationBackendId;
requestedBackendLabel?: string;
availableChoices?: ProjectOrchestrationBackendState["availableChoices"];
omxAvailability?: ProjectOrchestrationBackendState["omxAvailability"];
};
setSavingBackendId(null);
if (
!result.ok ||
!result.currentBackendId ||
!result.currentBackendLabel ||
!result.requestedBackendId ||
!result.requestedBackendLabel ||
!result.availableChoices ||
!result.omxAvailability
) {
setMessage(result.message ?? "保存失败");
return;
}
setState({
projectId,
currentBackendId: result.currentBackendId,
currentBackendLabel: result.currentBackendLabel,
requestedBackendId: result.requestedBackendId,
requestedBackendLabel: result.requestedBackendLabel,
availableChoices: result.availableChoices,
omxAvailability: result.omxAvailability,
});
setMessage(
requestedBackendId === "omx-team"
? "已切换到 OMX Team Runtime。"
: "已切换回 Boss Native Orchestrator。",
);
router.refresh();
}
return (
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[14px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] leading-5 text-[#57606A]">
{state.currentBackendLabel}
<br />
{state.requestedBackendLabel}
</div>
</div>
<div
className={clsx(
"rounded-full px-3 py-1 text-[11px] font-semibold",
fallbackActive || !state.omxAvailability.selectable
? "bg-[#FFF7E6] text-[#D46B08]"
: "bg-[#EAF7F0] text-[#215B39]",
)}
>
{availabilityCopy.badge}
</div>
</div>
<div className="mt-3 grid gap-2">
{state.availableChoices.map((choice) => {
const active = choice.current;
const selectable = choice.selectable && savingBackendId !== choice.backendId;
return (
<button
key={choice.backendId}
type="button"
onClick={() => void saveBackend(choice.backendId)}
disabled={!selectable}
className={clsx(
"flex items-center justify-between rounded-2xl border px-4 py-3 text-left",
active ? "border-[#07C160] bg-[#F5FFF8]" : "border-[#E5E5EA] bg-[#F7F8FA]",
!choice.selectable ? "opacity-70" : "",
)}
>
<div>
<div className="text-[14px] font-semibold text-[#111111]">
{orchestrationBackendChoiceLabel(choice)}
</div>
<div className="mt-1 text-[12px] leading-5 text-[#57606A]">{choice.label}</div>
</div>
<div className="text-right text-[11px] text-[#8C8C8C]">
<div>{active ? "当前" : "切换"}</div>
{!choice.selectable ? <div></div> : null}
</div>
</button>
);
})}
</div>
<div
className={clsx(
"mt-3 rounded-2xl px-4 py-3 text-[12px] leading-6",
state.omxAvailability.selectable
? "bg-[#EAF7F0] text-[#215B39]"
: "bg-[#FFF7E6] text-[#8D5D00]",
)}
>
{availabilityCopy.summary}
</div>
{message ? (
<div className="mt-3 rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] text-[#57606A]">
{message}
</div>
) : null}
</div>
);
}
function masterIdentityPillClasses(role: MasterIdentitySummary["role"]) {
switch (role) {
case "primary":