Files
browser/platform_web/pages/AiProvidersPage.tsx
T
2026-07-20 16:42:33 +08:00

689 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Candy, FlaskConical, MoreHorizontal, Power, Sparkles, WandSparkles, UserRoundMinus } from "lucide-react";
import { type ChangeEvent, type FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
import type { AiProviderKind, AiProviderResponse, AiProviderStatus } from "../api/types";
import { ConfirmDialog, ManagementDialog } from "../components/OperationControls";
import { AIConfigDiffReviewPanel } from "../components/AIConfigDiffReviewPanel";
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
import type { PageComponentProps } from "../contracts/page";
import { isPlatformAdmin } from "../contracts/workspace";
import {
aiProviderKindDefaults,
applyAiProviderKindDefaults,
completeAiProviderForm,
generatedAiProviderId,
aiProviderToForm,
emptyAiProviderForm,
type AiProviderActionState,
type AiProviderFilter,
type AiProviderFormState,
type AiProviderListState,
type AiProviderPageInitialState,
type AiProviderViewState,
summarizeAiProviders
} from "../contracts/aiProviders";
import { aiProviderCreateRequestFromForm, aiProviderRetireRequest, aiProviderUpdateRequestFromForm } from "../schemas/aiProviders";
import { cx } from "../utils/classes";
interface AiProvidersPageProps extends Partial<PageComponentProps> {
initialState?: AiProviderPageInitialState;
}
interface ProviderPreset {
kind: AiProviderKind;
label: string;
}
type FormCheckState = { status: "pending" | "succeeded" | "failed"; message: string };
const providerPresets: ProviderPreset[] = [
{ kind: "openai", label: "OpenAI" },
{ kind: "claude", label: "Claude" },
{ kind: "gemini", label: "Gemini" },
{ kind: "ollama", label: "Ollama" },
{ kind: "openai-compatible", label: "兼容网关" },
{ kind: "custom", label: "自定义" }
];
export function AiProvidersPage({ initialState, session, operations }: AiProvidersPageProps = {}) {
const initialSelectedProvider = initialState?.selectedId ? initialState.providers?.find((provider) => provider.id === initialState.selectedId) : undefined;
const [providers, setProviders] = useState<AiProviderResponse[]>(initialState?.providers ?? []);
const [listState, setListState] = useState<AiProviderListState>(initialState?.listState ?? "loading");
const [listError, setListError] = useState(initialState?.listError ?? "");
const [selectedId, setSelectedId] = useState(initialState?.selectedId ?? "");
const [filter, setFilter] = useState<AiProviderFilter>("all");
const [form, setForm] = useState<AiProviderFormState>(() => aiProviderToForm(initialSelectedProvider));
const [formMode, setFormMode] = useState<"create" | "edit" | null>(initialSelectedProvider ? "edit" : null);
const [viewState, setViewState] = useState<AiProviderViewState>(initialState?.source ?? "local-development");
const [action, setAction] = useState<AiProviderActionState | null>(initialState?.action ?? null);
const [confirmRetire, setConfirmRetire] = useState<AiProviderResponse | null>(null);
const [confirmStatus, setConfirmStatus] = useState<AiProviderResponse | null>(null);
const [confirmBusy, setConfirmBusy] = useState(false);
const [busyProviderIds, setBusyProviderIds] = useState<Set<string>>(() => new Set());
const [expandedProviderId, setExpandedProviderId] = useState<string | null>(null);
const [formCheck, setFormCheck] = useState<FormCheckState | null>(null);
const refreshProviders = useCallback(async () => {
if (initialState?.providers) {
setListState(initialState.listState ?? "ready");
setViewState(initialState.source ?? "local-development");
return;
}
setListState("loading");
setListError("");
try {
const response = await platformApiClient.listAiProviders();
const nextSelectedId = initialState?.selectedId && response.items.some((provider) => provider.id === initialState.selectedId) ? initialState.selectedId : "";
const nextSelectedProvider = response.items.find((provider) => provider.id === nextSelectedId);
setProviders(response.items);
setSelectedId(nextSelectedId);
setForm(nextSelectedProvider ? aiProviderToForm(nextSelectedProvider) : emptyAiProviderForm());
setFormMode(nextSelectedProvider ? "edit" : null);
setListState("ready");
setListError("");
setViewState("api");
setFormCheck(null);
} catch (error: unknown) {
setProviders([]);
setSelectedId("");
setForm(emptyAiProviderForm());
setListState("error");
setListError(error instanceof Error ? error.message : "AI 提供商 API 加载失败");
setViewState("error");
setFormCheck(null);
}
}, [initialState]);
useEffect(() => {
void refreshProviders();
}, [refreshProviders]);
const metrics = useMemo(() => summarizeAiProviders(providers), [providers]);
const filteredProviders = useMemo(() => providers.filter((provider) => filter === "all" || provider.status === filter), [filter, providers]);
const canManage = Boolean(session && isPlatformAdmin(session));
function setProviderBusy(providerId: string, busy: boolean) {
setBusyProviderIds((current) => {
const next = new Set(current);
if (busy) next.add(providerId);
else next.delete(providerId);
return next;
});
}
function updateForm<K extends keyof AiProviderFormState>(key: K, value: AiProviderFormState[K]) {
setForm((current) => ({ ...current, [key]: value }));
}
function handleInput(event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
const key = event.target.name as keyof AiProviderFormState;
if (key === "kind") {
setForm((current) => applyAiProviderKindDefaults(current, event.target.value as AiProviderKind));
} else if (key === "apiKeyRef") {
setForm((current) => ({ ...current, apiKeyRef: event.target.value, apiKeyConfigured: current.apiKeyConfigured || Boolean(event.target.value.trim()) }));
} else if (key === "baseUrl") {
setForm((current) => ({ ...current, baseUrl: event.target.value, baseUrlConfigured: current.baseUrlConfigured || Boolean(event.target.value.trim()) }));
} else {
updateForm(key, event.target.value);
}
setFormCheck(null);
}
function selectProvider(provider: AiProviderResponse) {
if (!canManage) {
return;
}
setSelectedId(provider.id);
setForm(aiProviderToForm(provider));
setFormMode("edit");
setAction(null);
setFormCheck(null);
setExpandedProviderId(null);
}
function startCreate() {
if (!canManage) {
return;
}
setSelectedId("");
setForm(emptyAiProviderForm());
setFormMode("create");
setAction(null);
setConfirmRetire(null);
setFormCheck(null);
setExpandedProviderId(null);
}
function closeForm() {
setFormMode(null);
setFormCheck(null);
}
function applyProviderPreset(preset: ProviderPreset) {
setForm((current) => applyAiProviderKindDefaults(current, preset.kind));
setFormCheck({ status: "pending", message: `已选择 ${preset.label}。常规配置只需要平台 secret 引用;高级参数已按厂商默认值填入。` });
}
function runFormPreflight() {
const missing: string[] = [];
const completed = completeAiProviderForm(form);
const models = completed.modelsText
.split(",")
.map((model) => model.trim())
.filter(Boolean);
if (!completed.name.trim()) {
missing.push("名称");
}
if (!completed.baseUrl.trim() && !completed.baseUrlConfigured) {
missing.push("Base URL");
}
if (completed.relayMode !== "local" && !completed.apiKeyConfigured && !completed.apiKeyRef.trim().startsWith("secret://providers/")) {
missing.push("secret://providers/... 密钥引用");
}
if (models.length === 0) {
missing.push("至少一个模型");
}
if (!Number.isFinite(Number.parseInt(completed.timeoutMs, 10)) || Number.parseInt(completed.timeoutMs, 10) <= 0) {
missing.push("有效超时");
}
setFormCheck(
missing.length > 0
? { status: "failed", message: `保存前检查未通过:请补充 ${missing.join("、")}。` }
: { status: "succeeded", message: "保存前检查通过。保存后先测试配置,测试成功再启用。" }
);
}
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!canManage) {
return;
}
setViewState("saving");
const completed = completeAiProviderForm(form);
const providerId = generatedAiProviderId(completed);
const existing = providers.some((provider) => provider.id === providerId);
const intent = existing ? "更新 AI 提供商" : "创建 AI 提供商";
const operationId = operations?.begin({ intent, targetKind: "llm", targetId: providerId, requester: session?.displayName });
try {
const saved = existing
? await platformApiClient.updateAiProvider(providerId, aiProviderUpdateRequestFromForm(completed))
: await platformApiClient.createAiProvider(aiProviderCreateRequestFromForm(completed));
upsertProvider(saved);
setSelectedId(saved.id);
setForm(aiProviderToForm(saved));
setViewState("api");
setListState("ready");
setListError("");
setAction({ providerId: saved.id, label: "save", success: true, message: "已保存" });
if (operationId) operations?.succeed(operationId, `${saved.name} 已保存`);
setFormCheck(null);
setFormMode(null);
} catch (error) {
setViewState("error");
const message = errorMessage(error, "保存失败");
setAction({ providerId: generatedAiProviderId(form) || selectedId, label: "save", success: false, message });
setFormCheck({ status: "failed", message });
if (operationId) operations?.fail(operationId, message, operationId);
}
}
async function handleStatus(provider: AiProviderResponse): Promise<boolean> {
if (!canManage) {
return false;
}
setExpandedProviderId(null);
const nextStatus: Extract<AiProviderStatus, "active" | "disabled"> = provider.status === "active" ? "disabled" : "active";
const intent = nextStatus === "active" ? "启用 AI 提供商" : "停用 AI 提供商";
const operationId = operations?.begin({ intent, targetKind: "llm", targetId: provider.id, requester: session?.displayName });
setProviderBusy(provider.id, true);
try {
const updated = await platformApiClient.setAiProviderStatus(provider.id, { status: nextStatus });
upsertProvider(updated);
setAction({ providerId: provider.id, label: "status", success: true, message: updated.status === "disabled" ? "已停用" : "已启用" });
if (operationId) operations?.succeed(operationId, `${updated.name}${updated.status === "disabled" ? "停用" : "启用"}`);
return true;
} catch (error) {
const message = errorMessage(error, "状态更新失败");
setAction({ providerId: provider.id, label: "status", success: false, message });
if (operationId) operations?.fail(operationId, message, operationId);
return false;
} finally {
setProviderBusy(provider.id, false);
}
}
async function handleRetire(provider: AiProviderResponse) {
if (!canManage) {
return;
}
setConfirmBusy(true);
const operationId = operations?.begin({ intent: "退役 AI 提供商", targetKind: "llm", targetId: provider.id, requester: session?.displayName });
setProviderBusy(provider.id, true);
try {
const retired = await platformApiClient.setAiProviderStatus(provider.id, aiProviderRetireRequest());
upsertProvider(retired);
setAction({ providerId: provider.id, label: "retire", success: true, message: "已退役" });
if (operationId) operations?.succeed(operationId, `${retired.name} 已退役`);
setConfirmRetire(null);
} catch (error) {
const message = errorMessage(error, "退役失败,可能仍被引用");
setAction({ providerId: provider.id, label: "retire", success: false, message });
if (operationId) operations?.fail(operationId, message, operationId);
} finally {
setConfirmBusy(false);
setProviderBusy(provider.id, false);
}
}
async function handleTest(provider: AiProviderResponse) {
if (!canManage) {
return;
}
setExpandedProviderId(null);
setProviderBusy(provider.id, true);
try {
const result = await platformApiClient.testAiProvider(provider.id);
setAction({ providerId: provider.id, label: "test", success: result.success, message: result.message });
} catch (error) {
setAction({ providerId: provider.id, label: "test", success: false, message: errorMessage(error, "测试失败") });
} finally {
setProviderBusy(provider.id, false);
}
}
async function handleModels(provider: AiProviderResponse) {
if (!canManage) {
return;
}
setExpandedProviderId(null);
setProviderBusy(provider.id, true);
try {
const result = await platformApiClient.listAiProviderModels(provider.id);
setAction({ providerId: provider.id, label: "models", success: true, message: `${result.models.length} 个模型` });
} catch (error) {
setAction({ providerId: provider.id, label: "models", success: false, message: errorMessage(error, "模型刷新失败") });
} finally {
setProviderBusy(provider.id, false);
}
}
function requestStatusChange(provider: AiProviderResponse) {
if (!canManage || busyProviderIds.has(provider.id)) {
return;
}
setExpandedProviderId(null);
setConfirmStatus(provider);
}
async function handleSavedFormTest() {
const providerId = form.id.trim() || selectedId;
if (formMode !== "edit" || !providerId) {
setFormCheck({ status: "failed", message: "请先保存提供商,再测试已保存配置。" });
return;
}
setFormCheck({ status: "pending", message: "正在测试已保存配置…" });
try {
const result = await platformApiClient.testAiProvider(providerId);
setAction({ providerId, label: "test", success: result.success, message: result.message });
setFormCheck({ status: result.success ? "succeeded" : "failed", message: result.success ? `${result.message}。测试成功后可在更多菜单启用。` : result.message });
} catch (error) {
const message = errorMessage(error, "测试失败");
setAction({ providerId, label: "test", success: false, message });
setFormCheck({ status: "failed", message });
}
}
async function handleDiscoverModelsForForm() {
const providerId = form.id.trim() || selectedId;
if (formMode !== "edit" || !providerId) {
setFormCheck({ status: "failed", message: "请先保存提供商,再从平台发现模型。" });
return;
}
setFormCheck({ status: "pending", message: "正在发现模型…" });
try {
const result = await platformApiClient.listAiProviderModels(providerId);
setForm((current) => ({
...current,
modelsText: result.models.join(", "),
defaultModel: result.defaultModel ?? result.models[0] ?? current.defaultModel
}));
setAction({ providerId, label: "models", success: true, message: `${result.models.length} 个模型` });
setFormCheck({ status: "succeeded", message: `已填入 ${result.models.length} 个模型;保存后生效。` });
} catch (error) {
const message = errorMessage(error, "模型发现失败");
setAction({ providerId, label: "models", success: false, message });
setFormCheck({ status: "failed", message });
}
}
function upsertProvider(provider: AiProviderResponse) {
setProviders((current) => {
const exists = current.some((item) => item.id === provider.id);
return exists ? current.map((item) => (item.id === provider.id ? provider : item)) : [...current, provider];
});
}
const completedForm = completeAiProviderForm(form);
const formDefaults = aiProviderKindDefaults[form.kind];
const secretRequired = completedForm.relayMode !== "local";
return (
<section className="ai-providers-page" aria-labelledby="ai-provider-title">
<header className="page-header ai-provider-header">
<div>
<p className="page-kicker">模型</p>
<h1 id="ai-provider-title" className="page-title">
AI 提供商管理
</h1>
</div>
<span className={cx("page-status", viewState === "api" && canManage && "page-status-ready")}>{!canManage ? (viewState === "local-development" ? "本地开发 / 只读" : "只读模式") : viewStateLabel(viewState)}</span>
</header>
<dl className="page-summary-strip ai-provider-summary" aria-label="AI 提供商快速状态">
<div className="page-summary-chip summary-tone-neutral">
<dt>提供商</dt>
<dd>{listState === "ready" ? metrics.total : "--"}</dd>
</div>
<div className="page-summary-chip summary-tone-success">
<dt>启用</dt>
<dd>{listState === "ready" ? metrics.active : "--"}</dd>
</div>
<div className="page-summary-chip summary-tone-warning">
<dt>模型</dt>
<dd>{listState === "ready" ? metrics.models : "--"}</dd>
</div>
</dl>
<div className="ai-provider-toolbar" aria-label="provider filters">
{(["all", "active", "disabled", "error"] as AiProviderFilter[]).map((item) => (
<button key={item} type="button" className={cx("segmented-button", filter === item && "segmented-button-active")} onClick={() => setFilter(item)}>
{filterLabel(item)}
</button>
))}
<button type="button" className="icon-command" disabled={!canManage} title={canManage ? "新增提供商" : "当前账号没有管理权限"} onClick={startCreate}>
<Candy size={16} />
<span>新增</span>
</button>
</div>
{!canManage && <ResultBadge status="pending" label="当前账号为只读模式,保存、状态和测试动作需要平台管理员权限" />}
{action && <ResultBadge status={action.success ? "succeeded" : "failed"} label={action.message} />}
{listState === "loading" && <LoadingState label="正在加载 AI 提供商…" />}
{listState === "error" && <ErrorState title="AI 提供商加载失败" reason={listError} diagnosticId="ai-provider:list" onRetry={() => void refreshProviders()} />}
{listState === "ready" && providers.length === 0 && (
<EmptyState icon={<FlaskConical size={26} />} title="暂无 AI 提供商" description="平台还没有返回任何提供商。创建第一个平台托管的 AI 提供商后,这里会显示 API 数据。" actionLabel="新增提供商" onAction={startCreate} />
)}
{listState === "ready" && providers.length > 0 && (
<div className="provider-table-wrap">
<table className="provider-table">
<thead>
<tr>
<th>名称</th>
<th>状态</th>
<th>类型</th>
<th>模式</th>
<th>模型</th>
<th>密钥状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{filteredProviders.map((provider) => (
<tr key={provider.id} className={cx(provider.id === selectedId && "provider-row-active")}>
<td>
<button type="button" className="table-link-button" disabled={!canManage} onClick={() => selectProvider(provider)}>
{provider.name}
</button>
<span className="provider-id">{provider.id}</span>
</td>
<td>
<span className={cx("status-pill", `status-${provider.status}`)}>{statusLabel(provider.status)}</span>
</td>
<td>{provider.kind}</td>
<td>{provider.relayMode}</td>
<td>{provider.models.length}</td>
<td>
<span className={cx("status-pill", provider.apiKeyConfigured ? "status-active" : "status-disabled")}>{provider.apiKeyConfigured ? "已配置" : "未配置"}</span>
</td>
<td className="provider-actions-cell">
<div className="row-actions human-row-actions" aria-label={`${provider.name} 操作`}>
<button type="button" disabled={!canManage || busyProviderIds.has(provider.id)} aria-label={`${provider.name} 测试配置`} onClick={() => void handleTest(provider)}>
<FlaskConical size={15} />
<span>{busyProviderIds.has(provider.id) ? "处理中" : "测试"}</span>
</button>
<button type="button" disabled={!canManage || busyProviderIds.has(provider.id)} aria-label={`${provider.name} 刷新模型`} onClick={() => void handleModels(provider)}>
<Sparkles size={15} />
<span>{busyProviderIds.has(provider.id) ? "处理中" : "模型"}</span>
</button>
<button type="button" disabled={!canManage || busyProviderIds.has(provider.id)} aria-label={`编辑 ${provider.name}`} onClick={() => selectProvider(provider)}>
<WandSparkles size={15} />
<span>编辑</span>
</button>
<button
type="button"
className={cx(expandedProviderId === provider.id && "row-action-button-active")}
aria-expanded={expandedProviderId === provider.id}
aria-label={`${provider.name} 更多操作`}
disabled={!canManage || busyProviderIds.has(provider.id)}
onClick={() => setExpandedProviderId((current) => (current === provider.id ? null : provider.id))}
>
<MoreHorizontal size={15} />
<span>更多</span>
</button>
</div>
{expandedProviderId === provider.id && (
<div className="inline-action-menu" role="menu" aria-label={`${provider.name} 更多操作`}>
<button type="button" role="menuitem" onClick={() => requestStatusChange(provider)}>
<Power size={14} />
<span>{provider.status === "active" ? "停用提供商" : "启用提供商"}</span>
</button>
<button
type="button"
role="menuitem"
className="danger-command"
onClick={() => {
setExpandedProviderId(null);
if (canManage) setConfirmRetire(provider);
}}
>
<UserRoundMinus size={14} />
<span>退役提供商</span>
</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
{filteredProviders.length === 0 && <EmptyState title="暂无匹配提供商" description="调整状态筛选,或点击新增配置一个平台托管的 AI 提供商。" />}
</div>
)}
<AIConfigDiffReviewPanel />
<ManagementDialog open={formMode !== null} title={formMode === "edit" ? "编辑提供商" : "新增提供商"} wide onClose={() => { if (viewState !== "saving") closeForm(); }}>
<form className="provider-form dialog-form" onSubmit={(event) => void handleSubmit(event)}>
<ProviderSetupGuide />
<div className="provider-preset-grid" aria-label="提供商预设">
{providerPresets.map((preset) => (
<button key={preset.kind} type="button" className={cx("provider-preset-option", form.kind === preset.kind && "provider-preset-option-active")} onClick={() => applyProviderPreset(preset)}>
<strong>{preset.label}</strong>
<span>{aiProviderKindDefaults[preset.kind].requirement}</span>
</button>
))}
</div>
<label>
<span>名称</span>
<input name="name" value={form.name} onChange={handleInput} />
<small className="field-help">可选;留空时使用厂商默认名称。系统 ID 会自动生成。</small>
</label>
<label>
<span>{secretRequired ? "平台密钥引用" : "密钥引用(本地模式可留空)"}</span>
<input name="apiKeyRef" value={form.apiKeyRef} onChange={handleInput} placeholder={form.apiKeyConfigured ? "已配置;留空保持不变" : secretRequired ? "secret://providers/..." : "本地服务通常不需要"} />
<small className="field-help">
{secretRequired ? "填写 secret://providers/...;真实密钥进入平台 secret store,不粘贴到页面。" : "Ollama 本地模式默认只需要 Base URL 和模型名。"}
</small>
</label>
<div className="provider-generated-id" aria-label="自动生成的提供商 ID">
<span>系统 ID</span>
<code>{generatedAiProviderId(form)}</code>
<small>自动生成,不需要手填。</small>
</div>
<details className="provider-advanced-settings">
<summary>高级设置:Base URL、模型、模式、超时</summary>
<div className="form-grid">
<label>
<span>类型</span>
<select name="kind" value={form.kind} onChange={handleInput}>
<option value="openai-compatible">OpenAI Compatible</option>
<option value="openai">OpenAI</option>
<option value="claude">Claude</option>
<option value="gemini">Gemini</option>
<option value="ollama">Ollama</option>
<option value="custom">Custom</option>
</select>
</label>
<label>
<span>模式</span>
<select name="relayMode" value={form.relayMode} onChange={handleInput}>
<option value="direct">Direct</option>
<option value="relay">Relay</option>
<option value="local">Local</option>
</select>
</label>
</div>
<label>
<span>Base URL</span>
<input name="baseUrl" value={form.baseUrl} onChange={handleInput} placeholder={form.baseUrlConfigured ? "已配置;留空保持不变" : formDefaults.baseUrl} />
<small className="field-help">{formDefaults.advancedNote}</small>
</label>
<label>
<span>模型</span>
<input name="modelsText" value={form.modelsText} onChange={handleInput} />
<small className="field-help">逗号分隔;编辑已保存配置时可以自动发现并填入。</small>
</label>
<div className="form-grid">
<label>
<span>默认模型</span>
<input name="defaultModel" value={form.defaultModel} onChange={handleInput} />
</label>
<label>
<span>超时 ms</span>
<input name="timeoutMs" value={form.timeoutMs} onChange={handleInput} inputMode="numeric" />
</label>
</div>
<label>
<span>脱敏策略</span>
<input name="redactionPolicy" value={form.redactionPolicy} onChange={handleInput} />
<small className="field-help">默认 default 会隐藏密钥、Bearer token 和敏感配置片段。</small>
</label>
</details>
<div className="form-helper-actions">
<button type="button" className="theme-upload" onClick={runFormPreflight}>
保存前检查
</button>
<button type="button" className="theme-upload" disabled={formMode !== "edit"} onClick={() => void handleSavedFormTest()}>
{formMode === "edit" ? "测试已保存配置" : "保存后可测试"}
</button>
<button type="button" className="theme-upload" disabled={formMode !== "edit"} onClick={() => void handleDiscoverModelsForForm()}>
{formMode === "edit" ? "发现模型并填入" : "保存后可发现模型"}
</button>
</div>
{formCheck && <ResultBadge status={formCheck.status} label={formCheck.message} />}
<div className="confirm-actions">
<button type="button" disabled={viewState === "saving"} onClick={closeForm}>
取消
</button>
<button type="submit" className="confirm-primary" disabled={!canManage || viewState === "saving"}>
<WandSparkles size={16} />
<span>{viewState === "saving" ? "保存中…" : "保存配置"}</span>
</button>
</div>
</form>
</ManagementDialog>
<ConfirmDialog
open={confirmRetire !== null}
title="退役 AI 提供商"
description={`确认将 ${confirmRetire?.name ?? ""}${confirmRetire?.id ?? ""})退役为停用状态?如果仍被引用,平台会拒绝这次变更。`}
confirmLabel="确认退役"
danger
busy={confirmBusy}
onCancel={() => { if (!confirmBusy) setConfirmRetire(null); }}
onConfirm={() => void (confirmRetire ? handleRetire(confirmRetire) : undefined)}
/>
<ConfirmDialog
open={confirmStatus !== null}
title="确认提供商状态变更"
description={`确认将 ${confirmStatus?.name ?? "该提供商"} ${confirmStatus?.status === "active" ? "停用" : "启用"}?平台会返回持久状态,失败时保持当前状态。`}
confirmLabel={confirmStatus?.status === "active" ? "确认停用" : "确认启用"}
danger={confirmStatus?.status === "active"}
busy={confirmStatus ? busyProviderIds.has(confirmStatus.id) : false}
onCancel={() => { if (!confirmStatus || !busyProviderIds.has(confirmStatus.id)) setConfirmStatus(null); }}
onConfirm={() => { if (confirmStatus) { void handleStatus(confirmStatus).then((succeeded) => { if (succeeded) setConfirmStatus(null); }); } }}
/>
</section>
);
}
function ProviderSetupGuide() {
return (
<div className="form-guidance provider-setup-guide">
<strong>配置流程</strong>
<span>先选厂商,再填写平台侧 secret 引用。IDBase URL、模型和超时会按厂商默认值生成,需要时再展开高级设置。</span>
</div>
);
}
function errorMessage(error: unknown, fallback: string): string {
if (!(error instanceof Error)) {
return fallback;
}
if (error.message.toLowerCase().includes("reference")) {
return `${fallback}${error.message}`;
}
return error.message || fallback;
}
function viewStateLabel(viewState: AiProviderViewState): string {
switch (viewState) {
case "api":
return "平台 API";
case "local-development":
return "本地开发";
case "saving":
return "保存中";
default:
return "连接失败";
}
}
function filterLabel(filter: AiProviderFilter): string {
switch (filter) {
case "active":
return "启用";
case "disabled":
return "停用";
case "error":
return "错误";
default:
return "全部";
}
}
function statusLabel(status: AiProviderStatus): string {
switch (status) {
case "active":
return "启用";
case "disabled":
return "停用";
default:
return "错误";
}
}