import { type PiAuthKind, type PiProviderInfo, type PiThinkingLevel, type PiUpstream, PiUpstreamCreate, } from "@umt/contracts"; import { ChevronLeft, LoaderCircle, Search } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useCreatePiUpstream, usePiProviders, usePiUpstreamModels, useProbePiModels, useRefreshPiModels, useUpdatePiUpstream, } from "~/api/pi.ts"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; import { PiOAuthLogin } from "./pi-oauth.tsx"; import { API_OPTIONS, AUTH_LABEL, GROUP_LABEL, GROUP_ORDER, listedFromInfo, ModelFetchBar, ModelInput, THINKING_OPTIONS, } from "./pi-shared.tsx"; import { PiTestPanel } from "./pi-test.tsx"; import { FieldControl } from "./resource-section.tsx"; // 新建 Agent 上游:① 选 provider → ② 填凭证与模型并创建 → ③ 登录 / 测试,可设为默认 type Step = "provider" | "config" | "verify"; export function PiUpstreamWizard({ open, onClose }: { open: boolean; onClose: () => void }) { const [step, setStep] = useState("provider"); const [provider, setProvider] = useState(null); const [created, setCreated] = useState(null); const [fetched, setFetched] = useState(false); const close = () => { onClose(); // 关闭动画后再复位,避免闪回第一步 setTimeout(() => { setStep("provider"); setProvider(null); setCreated(null); }, 200); }; const title = step === "provider" ? "新建 Agent 上游" : step === "config" ? `新建上游 · ${provider?.name ?? ""}` : (created?.name ?? ""); const description = step === "provider" ? "第 1 步,共 3 步:选 provider" : step === "config" ? "第 2 步,共 3 步:凭证与模型" : created?.authKind === "oauth" ? "第 3 步,共 3 步:登录并测试。上游已创建。" : "第 3 步,共 3 步:测试。上游已创建。"; return ( !o && close()} title={title} description={description} width="lg"> {step === "provider" ? ( { setProvider(p); setStep("config"); }} /> ) : step === "config" && provider ? ( setStep("provider")} onCreated={(u, f) => { setCreated(u); setFetched(f); setStep("verify"); }} /> ) : created ? ( ) : null} ); } function ProviderPicker({ onPick }: { onPick: (p: PiProviderInfo) => void }) { const q = usePiProviders(false); const [filter, setFilter] = useState(""); if (q.isPending) return ; if (q.isError) return q.refetch()} />; const f = filter.trim().toLowerCase(); const list = q.data.providers.filter((p) => !f || p.name.toLowerCase().includes(f) || p.id.includes(f)); return (
setFilter(e.target.value)} className="pl-9" />
{GROUP_ORDER.map((g) => { const items = list.filter((p) => p.group === g); if (!items.length) return null; return (
    {items.map((p) => (
  • ))}
); })} {list.length === 0 ?

没有匹配的 provider。

: null}
); } function ConfigStep({ provider, onBack, onCreated, }: { provider: PiProviderInfo; onBack: () => void; /** fetched = 创建前已成功试拉过模型列表(下一步自动获取并存库) */ onCreated: (u: PiUpstream, fetched: boolean) => void; }) { const full = usePiProviders(true); const create = useCreatePiUpstream(); const probe = useProbePiModels(); const staticModels = useMemo( () => listedFromInfo(full.data?.providers.find((p) => p.id === provider.id)?.models ?? []), [full.data, provider.id], ); const models = probe.data?.models ?? staticModels; const custom = provider.id === "openai-compatible"; const [authKind, setAuthKind] = useState(provider.authKinds[0] ?? "none"); const [name, setName] = useState(provider.name); const [apiKey, setApiKey] = useState(""); const [baseUrl, setBaseUrl] = useState(""); const [model, setModel] = useState(""); const [thinking, setThinking] = useState("medium"); const [api, setApi] = useState("openai-completions"); const [contextWindow, setContextWindow] = useState(""); const [env, setEnv] = useState>({}); const [errors, setErrors] = useState>({}); const showBaseUrl = provider.baseUrlOverridable && authKind !== "oauth"; // 凭证填好才能获取(订阅登录要先创建再登录,在下一步获取) const fetchBlocked = authKind === "oauth" ? "登录后在下一步获取" : custom && !baseUrl.trim() ? "填好 Base URL 后可获取" : authKind === "api_key" && !custom && !apiKey.trim() ? "填好 API Key 后可获取" : null; const fetchModels = async () => { try { const r = await probe.mutateAsync({ provider: provider.id, authKind, ...(authKind === "api_key" && apiKey.trim() ? { apiKey: apiKey.trim() } : {}), baseUrl: showBaseUrl && baseUrl.trim() ? baseUrl.trim() : null, ...(custom ? { extra: { api: api as never } } : {}), }); if (r.source === "remote") toast.success(`获取到 ${r.models.filter((m) => m.origin === "remote").length} 个模型`); } catch (err) { toast.error(errorMessage(err, "获取模型列表失败")); } }; const submit = async (e: React.FormEvent) => { e.preventDefault(); const envVals = Object.fromEntries(Object.entries(env).filter(([, v]) => v.trim())); const extra = { ...(Object.keys(envVals).length ? { env: envVals } : {}), ...(custom ? { api: api as never } : {}), ...(custom && contextWindow.trim() ? { contextWindow: Number(contextWindow) } : {}), }; const parsed = PiUpstreamCreate.safeParse({ name: name.trim(), provider: provider.id, authKind, ...(authKind === "api_key" && apiKey.trim() ? { apiKey: apiKey.trim() } : {}), baseUrl: showBaseUrl && baseUrl.trim() ? baseUrl.trim() : null, defaultModel: model.trim() || null, ...(provider.supportsThinking || custom ? { thinkingLevel: thinking } : {}), extra, }); const fe: Record = parsed.success ? {} : zodFieldErrors(parsed.error.issues); for (const f of provider.envFields) if (f.required && !env[f.name]?.trim()) fe[`env.${f.name}`] = `${f.label}必填`; if (authKind === "api_key" && !apiKey.trim() && !custom) fe.apiKey = "填写 API Key"; if (fe.name) fe.name = "名称必填,64 字以内"; if (fe["extra.contextWindow"]) fe.contextWindow = "填 1024 以上的整数"; if (!parsed.success || Object.keys(fe).length) { setErrors(fe); return; } setErrors({}); try { const u = await create.mutateAsync(parsed.data); toast.success(`${u.name}已创建`); onCreated(u, probe.data?.source === "remote"); } catch (err) { toast.error(errorMessage(err, "创建失败")); } }; const pending = create.isPending; return (
{provider.note && !custom ?

{provider.note}

: null}
setName(e.target.value)} disabled={pending} /> {provider.authKinds.length > 1 ? ( ({ value: k, label: AUTH_LABEL[k] })), wide: true, }} value={authKind} onChange={(v) => setAuthKind(v as PiAuthKind)} disabled={pending} /> ) : null} {authKind === "oauth" && provider.termsNote ? (

{provider.termsNote}

) : null} {authKind === "api_key" ? ( setApiKey(String(v))} disabled={pending} /> ) : null} {authKind === "none" && provider.group === "cloud" ? (

不存密钥,运行时读服务端进程的环境凭证(AWS 凭证链 / Google ADC)。

) : null} {showBaseUrl ? ( setBaseUrl(String(v))} disabled={pending} /> ) : null} {custom ? ( <> setApi(String(v))} disabled={pending} /> setContextWindow(String(v))} disabled={pending} /> ) : null} {provider.envFields.map((f) => ( setEnv((s) => ({ ...s, [f.name]: String(v) }))} disabled={pending} /> ))} {provider.supportsThinking || custom ? ( setThinking(v as PiThinkingLevel)} disabled={pending} /> ) : null}
); } function VerifyStep({ upstream, provider, autoFetch, onDone, }: { upstream: PiUpstream; provider: PiProviderInfo | null; autoFetch: boolean; onDone: () => void; }) { const update = useUpdatePiUpstream(); const models = usePiUpstreamModels(upstream.id); const refresh = useRefreshPiModels(); // 创建前试拉成功过:创建后用已存的凭证再拉一次并存库(只触发一次) const autoFetched = useRef(false); useEffect(() => { if (!autoFetch || autoFetched.current) return; autoFetched.current = true; refresh.mutate(upstream.id); }, [autoFetch, upstream.id, refresh.mutate]); const [model, setModel] = useState(upstream.defaultModel ?? ""); const [modelError, setModelError] = useState(); const [makeDefault, setMakeDefault] = useState(upstream.isDefault); const [loggedIn, setLoggedIn] = useState(upstream.authKind !== "oauth"); const finish = async () => { if (makeDefault && !model.trim()) { setModelError("设为默认上游前先选默认模型"); return; } const body = { ...(model.trim() !== (upstream.defaultModel ?? "") ? { defaultModel: model.trim() || null } : {}), ...(makeDefault && !upstream.isDefault ? { makeDefault: true } : {}), }; try { if (Object.keys(body).length) await update.mutateAsync({ id: upstream.id, body }); if (makeDefault && !upstream.isDefault) toast.success(`${upstream.name}已设为默认上游`); onDone(); } catch (err) { toast.error(errorMessage(err, "保存失败")); } }; return (
{upstream.authKind === "oauth" ? ( setLoggedIn(true)} /> ) : null} refresh.mutate(upstream.id, { onError: (err) => toast.error(errorMessage(err, "获取失败")) }) } disabledReason={loggedIn ? null : "登录后可获取"} /> { setModel(v); setModelError(undefined); }} models={models.data?.models ?? []} invalid={!!modelError} /> {loggedIn ? ( ) : (

登录后再测试。

)}
); }