feat: add omx orchestration backend selection
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user