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 { 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(initialState?.providers ?? []); const [listState, setListState] = useState(initialState?.listState ?? "loading"); const [listError, setListError] = useState(initialState?.listError ?? ""); const [selectedId, setSelectedId] = useState(initialState?.selectedId ?? ""); const [filter, setFilter] = useState("all"); const [form, setForm] = useState(() => aiProviderToForm(initialSelectedProvider)); const [formMode, setFormMode] = useState<"create" | "edit" | null>(initialSelectedProvider ? "edit" : null); const [viewState, setViewState] = useState(initialState?.source ?? "local-development"); const [action, setAction] = useState(initialState?.action ?? null); const [confirmRetire, setConfirmRetire] = useState(null); const [confirmStatus, setConfirmStatus] = useState(null); const [confirmBusy, setConfirmBusy] = useState(false); const [busyProviderIds, setBusyProviderIds] = useState>(() => new Set()); const [expandedProviderId, setExpandedProviderId] = useState(null); const [formCheck, setFormCheck] = useState(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(key: K, value: AiProviderFormState[K]) { setForm((current) => ({ ...current, [key]: value })); } function handleInput(event: ChangeEvent) { 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) { 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 { if (!canManage) { return false; } setExpandedProviderId(null); const nextStatus: Extract = 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 (

模型

AI 提供商管理

{!canManage ? (viewState === "local-development" ? "本地开发 / 只读" : "只读模式") : viewStateLabel(viewState)}
提供商
{listState === "ready" ? metrics.total : "--"}
启用
{listState === "ready" ? metrics.active : "--"}
模型
{listState === "ready" ? metrics.models : "--"}
{(["all", "active", "disabled", "error"] as AiProviderFilter[]).map((item) => ( ))}
{!canManage && } {action && } {listState === "loading" && } {listState === "error" && void refreshProviders()} />} {listState === "ready" && providers.length === 0 && ( } title="暂无 AI 提供商" description="平台还没有返回任何提供商。创建第一个平台托管的 AI 提供商后,这里会显示 API 数据。" actionLabel="新增提供商" onAction={startCreate} /> )} {listState === "ready" && providers.length > 0 && (
{filteredProviders.map((provider) => ( ))}
名称 状态 类型 模式 模型 密钥状态 操作
{provider.id} {statusLabel(provider.status)} {provider.kind} {provider.relayMode} {provider.models.length} {provider.apiKeyConfigured ? "已配置" : "未配置"}
{expandedProviderId === provider.id && (
)}
{filteredProviders.length === 0 && }
)} { if (viewState !== "saving") closeForm(); }}>
void handleSubmit(event)}>
{providerPresets.map((preset) => ( ))}
系统 ID {generatedAiProviderId(form)} 自动生成,不需要手填。
高级设置:Base URL、模型、模式、超时
{formCheck && }
{ if (!confirmBusy) setConfirmRetire(null); }} onConfirm={() => void (confirmRetire ? handleRetire(confirmRetire) : undefined)} /> { if (!confirmStatus || !busyProviderIds.has(confirmStatus.id)) setConfirmStatus(null); }} onConfirm={() => { if (confirmStatus) { void handleStatus(confirmStatus).then((succeeded) => { if (succeeded) setConfirmStatus(null); }); } }} />
); } function ProviderSetupGuide() { return (
配置流程 先选厂商,再填写平台侧 secret 引用。ID、Base URL、模型和超时会按厂商默认值生成,需要时再展开高级设置。
); } 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 "错误"; } }