"use client"; import { useState } from "react"; import { api } from "@/lib/api/client"; import { useToast } from "@/components/Toast"; import { KimiCodeOauth } from "@/components/settings/KimiCodeOauth"; import { API_KEY_PROVIDERS, KNOWN_PROVIDERS, TIER_LABELS, applyProviderChange, draftsToRoutingInput, toRoutingDrafts, type RoutingDraft, } from "@/lib/settings/providers"; import type { CapabilitiesView, ProvidersResponse, } from "@/lib/api/types"; interface ProvidersSettingsProps { initial: ProvidersResponse; // Kimi Code OAuth 连接态(GET .../oauth/status,进页 Server Component 取)。 kimiOauth: { connected: boolean; expiresAt: string | null }; } interface TestResult { ok: boolean; capabilities: CapabilitiesView; } // 设置页主体(UX §6.10):档位路由(可编辑)+ API-key 凭据行 + Kimi Code OAuth 连接区。 export function ProvidersSettings({ initial, kimiOauth, }: ProvidersSettingsProps) { const toast = useToast(); const [providers, setProviders] = useState(initial.providers ?? []); const [routing, setRouting] = useState( toRoutingDrafts(initial.tier_routing ?? []), ); const [savingRouting, setSavingRouting] = useState(false); const [drafts, setDrafts] = useState>({}); const [savingId, setSavingId] = useState(null); const [testingId, setTestingId] = useState(null); const [results, setResults] = useState>({}); const maskedFor = (id: string): string | null => providers.find((p) => p.provider === id)?.masked_key ?? null; const updateRouting = (tier: string, providerId: string): void => { setRouting((prev) => prev.map((d) => d.tier === tier ? applyProviderChange(d, providerId) : d, ), ); }; const updateRoutingModel = (tier: string, model: string): void => { setRouting((prev) => prev.map((d) => (d.tier === tier ? { ...d, model } : d)), ); }; const saveRouting = async (): Promise => { setSavingRouting(true); const { data, error } = await api.PUT("/settings/providers", { body: { tier_routing: draftsToRoutingInput(routing) }, }); setSavingRouting(false); if (error || !data) { toast("保存档位路由失败,请重试", "error"); return; } setProviders(data.providers ?? []); setRouting(toRoutingDrafts(data.tier_routing ?? [])); toast("档位路由已保存", "success"); }; const saveCredential = async (providerId: string): Promise => { const apiKey = (drafts[providerId] ?? "").trim(); if (!apiKey) { toast("请先输入 API Key", "error"); return; } setSavingId(providerId); const { data, error } = await api.PUT("/settings/providers", { body: { credentials: [{ provider: providerId, api_key: apiKey }] }, }); setSavingId(null); if (error || !data) { toast("保存凭据失败,请重试", "error"); return; } setProviders(data.providers ?? []); setDrafts((prev) => ({ ...prev, [providerId]: "" })); toast("凭据已保存", "success"); }; const testConnection = async (providerId: string): Promise => { setTestingId(providerId); const { data, error } = await api.POST("/settings/providers/test", { body: { provider: providerId }, }); setTestingId(null); if (error || !data) { toast("测试连接失败", "error"); return; } setResults((prev) => ({ ...prev, [providerId]: { ok: data.ok, capabilities: data.capabilities }, })); toast(data.ok ? "连接成功" : "连接未通过", data.ok ? "success" : "error"); }; return (

能力档位路由

    {routing.map((row) => (
  • {TIER_LABELS[row.tier] ?? row.tier} updateRoutingModel(row.tier, e.target.value)} placeholder="model" className="min-w-[10rem] flex-1 rounded border border-line bg-bg px-3 py-1.5 font-mono text-sm text-ink focus:border-cinnabar focus:outline-none" />
  • ))}

提供商凭据

{providers.length === 0 ? (

至少连接一个提供商即可开始写作(求质量选 Anthropic / 求性价比选 DeepSeek)。

) : null}
    {API_KEY_PROVIDERS.map((prov) => { const masked = maskedFor(prov.id); const result = results[prov.id]; return (
  • {result ? (
    {result.ok ? "✓ 已连接" : "✗ 未连接"}
    ) : null}
  • ); })}
); } function CapabilityBadges({ caps }: { caps: CapabilitiesView }) { const badges: { on: boolean; label: string }[] = [ { on: caps.structured_output, label: "结构化" }, { on: caps.prefix_cache, label: "前缀缓存" }, { on: caps.thinking, label: "思考" }, ]; return (
{badges.map((b) => ( {b.label} ))}
); }