import { Candy, FlaskConical, MoreHorizontal, Power, Sparkles, WandSparkles, UserRoundMinus } from "lucide-react"; import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { AiProviderResponse, AiProviderStatus } from "../api/types"; import { ConfirmDialog, ManagementDialog } from "../components/OperationControls"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import type { PageComponentProps } from "../contracts/page"; import { 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 { id: string; label: string; summary: string; draft: Partial; } type FormCheckState = { status: "pending" | "succeeded" | "failed"; message: string }; const providerPresets: ProviderPreset[] = [ { id: "openai-relay", label: "OpenAI Relay", summary: "平台中转,适合公网 OpenAI 兼容网关。", draft: { id: "ai.openai", name: "OpenAI Relay", kind: "openai-compatible", baseUrl: "https://api.openai.com/v1", apiKeyRef: "secret://providers/openai", modelsText: "gpt-4.1, gpt-4.1-mini", defaultModel: "gpt-4.1-mini", relayMode: "relay", redactionPolicy: "default" } }, { id: "claude-relay", label: "Claude Relay", summary: "平台托管 Anthropic 兼容配置,密钥只留引用。", draft: { id: "ai.claude", name: "Claude Relay", kind: "claude", baseUrl: "https://api.anthropic.com/v1", apiKeyRef: "secret://providers/anthropic", modelsText: "claude-sonnet, claude-haiku", defaultModel: "claude-sonnet", relayMode: "relay", redactionPolicy: "default" } }, { id: "gemini-relay", label: "Gemini Relay", summary: "平台托管 Google Gemini 配置,保存后发现模型。", draft: { id: "ai.gemini", name: "Gemini Relay", kind: "gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", apiKeyRef: "secret://providers/gemini", modelsText: "gemini-pro", defaultModel: "gemini-pro", relayMode: "relay", redactionPolicy: "default" } }, { id: "ollama-local", label: "Ollama Local", summary: "本机或内网模型服务,默认走本地模式。", draft: { id: "ai.ollama", name: "Ollama Local", kind: "ollama", baseUrl: "http://127.0.0.1:11434/v1", apiKeyRef: "secret://providers/ollama-local", modelsText: "llama3.1, qwen2.5", defaultModel: "llama3.1", relayMode: "local", redactionPolicy: "default" } } ]; export function AiProvidersPage({ initialState }: 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 [confirmBusy, setConfirmBusy] = useState(false); const [expandedProviderId, setExpandedProviderId] = useState(null); const [formCheck, setFormCheck] = useState(null); useEffect(() => { let cancelled = false; if (initialState?.providers) { setListState(initialState.listState ?? "ready"); setViewState(initialState.source ?? "local-development"); return () => { cancelled = true; }; } platformApiClient .listAiProviders() .then((response) => { if (cancelled) { return; } 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) => { if (cancelled) { return; } setProviders([]); setSelectedId(""); setForm(emptyAiProviderForm()); setListState("error"); setListError(error instanceof Error ? error.message : "AI 提供商 API 加载失败"); setViewState("error"); setFormCheck(null); }); return () => { cancelled = true; }; }, [initialState?.listState, initialState?.providers, initialState?.selectedId, initialState?.source]); const metrics = useMemo(() => summarizeAiProviders(providers), [providers]); const filteredProviders = useMemo(() => providers.filter((provider) => filter === "all" || provider.status === filter), [filter, providers]); function updateForm(key: K, value: AiProviderFormState[K]) { setForm((current) => ({ ...current, [key]: value })); } function handleInput(event: ChangeEvent) { updateForm(event.target.name as keyof AiProviderFormState, event.target.value); setFormCheck(null); } function selectProvider(provider: AiProviderResponse) { setSelectedId(provider.id); setForm(aiProviderToForm(provider)); setFormMode("edit"); setAction(null); setFormCheck(null); setExpandedProviderId(null); } function startCreate() { 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) => ({ ...current, ...preset.draft, id: formMode === "edit" ? current.id : preset.draft.id ?? current.id })); setFormCheck({ status: "pending", message: `已套用 ${preset.label} 预设。保存前请确认 Base URL、secret 引用和默认模型。` }); } function runFormPreflight() { const missing: string[] = []; const models = form.modelsText .split(",") .map((model) => model.trim()) .filter(Boolean); if (!form.id.trim()) { missing.push("ID"); } if (!form.name.trim()) { missing.push("名称"); } if (!form.baseUrl.trim()) { missing.push("Base URL"); } if (!form.apiKeyRef.trim().startsWith("secret://providers/")) { missing.push("secret://providers/... 密钥引用"); } if (models.length === 0) { missing.push("至少一个模型"); } if (!Number.isFinite(Number.parseInt(form.timeoutMs, 10)) || Number.parseInt(form.timeoutMs, 10) <= 0) { missing.push("有效超时"); } setFormCheck( missing.length > 0 ? { status: "failed", message: `保存前检查未通过:请补充 ${missing.join("、")}。` } : { status: "succeeded", message: "保存前检查通过。保存后先测试配置,测试成功再启用。" } ); } async function handleSubmit(event: FormEvent) { event.preventDefault(); setViewState("saving"); const existing = providers.some((provider) => provider.id === form.id.trim()); try { const saved = existing ? await platformApiClient.updateAiProvider(form.id.trim(), aiProviderUpdateRequestFromForm(form)) : await platformApiClient.createAiProvider(aiProviderCreateRequestFromForm(form)); upsertProvider(saved); setSelectedId(saved.id); setForm(aiProviderToForm(saved)); setViewState("api"); setListState("ready"); setListError(""); setAction({ providerId: saved.id, label: "save", success: true, message: "已保存" }); setFormCheck(null); setFormMode(null); } catch (error) { setViewState("error"); const message = errorMessage(error, "保存失败"); setAction({ providerId: form.id.trim() || selectedId, label: "save", success: false, message }); setFormCheck({ status: "failed", message }); } } async function handleStatus(provider: AiProviderResponse) { setExpandedProviderId(null); const nextStatus: Extract = provider.status === "active" ? "disabled" : "active"; try { const updated = await platformApiClient.setAiProviderStatus(provider.id, { status: nextStatus }); upsertProvider(updated); setAction({ providerId: provider.id, label: "status", success: true, message: updated.status === "disabled" ? "已停用" : "已启用" }); } catch (error) { setAction({ providerId: provider.id, label: "status", success: false, message: errorMessage(error, "状态更新失败") }); } } async function handleRetire(provider: AiProviderResponse) { setConfirmBusy(true); try { const retired = await platformApiClient.setAiProviderStatus(provider.id, aiProviderRetireRequest()); upsertProvider(retired); setAction({ providerId: provider.id, label: "retire", success: true, message: "已退役" }); setConfirmRetire(null); } catch (error) { setAction({ providerId: provider.id, label: "retire", success: false, message: errorMessage(error, "退役失败,可能仍被引用") }); } finally { setConfirmBusy(false); } } async function handleTest(provider: AiProviderResponse) { setExpandedProviderId(null); 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, "测试失败") }); } } async function handleModels(provider: AiProviderResponse) { setExpandedProviderId(null); 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, "模型刷新失败") }); } } 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]; }); } return (

模型

AI 提供商管理

{viewStateLabel(viewState)}
提供商
{metrics.total}
启用
{metrics.active}
模型
{metrics.models}
{(["all", "active", "disabled", "error"] as AiProviderFilter[]).map((item) => ( ))}
{action && } {listState === "loading" && } {listState === "error" && window.location.reload()} />} {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.apiKeyRef}
{expandedProviderId === provider.id && (
)}
{filteredProviders.length === 0 && }
)}
void handleSubmit(event)}>
{providerPresets.map((preset) => ( ))}
{formCheck && }
setConfirmRetire(null)} onConfirm={() => void (confirmRetire ? handleRetire(confirmRetire) : undefined)} />
); } function ProviderSetupGuide() { return (
配置流程 先选预设,再填写平台侧 secret 引用。保存前做本地检查;保存后测试已保存配置,测试成功后再启用。
); } 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 "错误"; } }