import { type PiProviderInfo, type PiThinkingLevel, type PiUpstream, PiUpstreamUpdate } from "@umt/contracts"; import { LoaderCircle, LogOut, Plus, Star, Trash2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { useLogoutPiOAuth, usePiProviders, usePiUpstreamModels, usePiUpstreams, useRefreshPiModels, useRemovePiUpstream, useUpdatePiUpstream, } from "~/api/pi.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { DataTable } from "~/components/console/data-table.tsx"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { formatRelative } from "~/lib/labels.ts"; import { maskedDisplay, secretPatch } from "~/lib/secret.ts"; import { formatDateTime } from "~/lib/utils.ts"; import { PiOAuthLogin } from "./pi-oauth.tsx"; import { AUTH_LABEL, ModelFetchBar, ModelInput, providerName, THINKING_LABEL, THINKING_OPTIONS, UpstreamStatusBadge, } from "./pi-shared.tsx"; import { PiTestPanel } from "./pi-test.tsx"; import { PiUpstreamWizard } from "./pi-wizard.tsx"; import { FieldControl } from "./resource-section.tsx"; import { SettingsBlock } from "./settings-block.tsx"; // Agent 上游列表 + 编辑抽屉(登录、测试、设默认、删除) export function PiUpstreamsBlock() { const list = usePiUpstreams(); const providers = usePiProviders(false); const [wizard, setWizard] = useState(false); const [openId, setOpenId] = useState(null); const open = list.data?.items.find((u) => u.id === openId) ?? null; const pv = providers.data?.providers; return ( setWizard(true)}> 新建模型服务 } > r.id} loading={list.isPending} error={list.error} onRetry={() => list.refetch()} onRowClick={(r) => setOpenId(r.id)} empty={{ title: "还没有模型服务" }} columns={[ { key: "name", header: "名称", cell: (r) => (

{r.name} {r.isDefault ? 默认 : null}

{providerName(pv, r.provider)}

), }, { key: "auth", header: "认证", hideBelow: "md", cell: (r) => AUTH_LABEL[r.authKind] }, { key: "model", header: "模型", hideBelow: "sm", cell: (r) => {r.defaultModel ?? "默认模型"}, }, { key: "status", header: "状态", cell: (r) => }, ]} /> p.id === open?.provider)} onClose={() => setOpenId(null)} /> setWizard(false)} />
); } function UpstreamDrawer({ upstream, provider, onClose, }: { upstream: PiUpstream | null; provider: PiProviderInfo | undefined; onClose: () => void; }) { const update = useUpdatePiUpstream(); const remove = useRemovePiUpstream(); const logout = useLogoutPiOAuth(); const models = usePiUpstreamModels(upstream?.id ?? null); const refresh = useRefreshPiModels(); const [deleting, setDeleting] = useState(false); const [v, setV] = useState(() => ({ name: upstream?.name ?? "", model: upstream?.defaultModel ?? "", thinking: (upstream?.thinkingLevel ?? "medium") as PiThinkingLevel, baseUrl: upstream?.baseUrl ?? "", apiKey: "", enabled: upstream?.enabled ?? true, env: { ...(upstream?.extra.env ?? {}) } as Record, })); const [errors, setErrors] = useState>({}); if (!upstream) return null; const custom = upstream.provider === "openai-compatible"; const showBaseUrl = (provider?.baseUrlOverridable ?? custom) && upstream.authKind !== "oauth"; const showThinking = provider?.supportsThinking || custom; const envFields = provider?.envFields ?? []; const dirty = v.name !== upstream.name || v.model !== (upstream.defaultModel ?? "") || v.thinking !== upstream.thinkingLevel || v.baseUrl !== (upstream.baseUrl ?? "") || v.apiKey !== "" || v.enabled !== upstream.enabled || envFields.some((f) => (v.env[f.name] ?? "") !== (upstream.extra.env?.[f.name] ?? "")); // 获取用已保存的配置:凭证或端点改了没保存时先保存 const fetchBlocked = upstream.authKind !== "none" && !custom && !upstream.hasCredential ? upstream.authKind === "oauth" ? "登录后可获取" : "先配置 API Key" : v.apiKey !== "" || v.baseUrl !== (upstream.baseUrl ?? "") ? "先保存 API Key / Base URL 再获取" : null; const save = async (e: React.FormEvent) => { e.preventDefault(); const env = Object.fromEntries(Object.entries(v.env).filter(([, x]) => x.trim())); const apiKey = secretPatch(v.apiKey); const parsed = PiUpstreamUpdate.safeParse({ name: v.name.trim(), defaultModel: v.model.trim() || null, ...(showThinking ? { thinkingLevel: v.thinking } : {}), ...(showBaseUrl ? { baseUrl: v.baseUrl.trim() || null } : {}), ...(apiKey !== undefined ? { apiKey } : {}), enabled: v.enabled, ...(envFields.length ? { extra: { ...upstream.extra, env } } : {}), }); if (!parsed.success) { setErrors(zodFieldErrors(parsed.error.issues)); return; } setErrors({}); try { await update.mutateAsync({ id: upstream.id, body: parsed.data }); setV((s) => ({ ...s, apiKey: "" })); toast.success(`已保存「${v.name.trim()}」`); } catch (err) { toast.error(errorMessage(err, "保存失败")); } }; const makeDefault = async () => { const model = v.model.trim(); if (!model) { setErrors((e) => ({ ...e, defaultModel: "设为默认模型服务前先选默认模型" })); return; } try { await update.mutateAsync({ id: upstream.id, body: { makeDefault: true, ...(model !== (upstream.defaultModel ?? "") ? { defaultModel: model } : {}) }, }); toast.success(`${upstream.name}已设为默认模型服务`); } catch (err) { toast.error(errorMessage(err, "设置失败")); } }; const pending = update.isPending; return ( !o && onClose()} title={upstream.name} description={ {upstream.isDefault ? 默认模型服务 : null} {provider?.name ?? upstream.provider} · {AUTH_LABEL[upstream.authKind]} } width="lg" footer={ <> {upstream.isDefault ? null : ( )} } >
{upstream.credentialMask ? ( <>
API Key
{maskedDisplay(upstream.credentialMask)}
) : null} {upstream.account ? ( <>
账号
{upstream.account}
) : null} {upstream.oauthExpiresAt ? ( <>
令牌到期
{formatDateTime(upstream.oauthExpiresAt)}(到期前自动刷新)
) : null}
最近验证
{upstream.lastVerifiedAt ? formatRelative(upstream.lastVerifiedAt) : "未验证"}
{upstream.lastError ? ( <>
最近错误
{upstream.lastError}
) : null}
{upstream.authKind === "oauth" ? ( { try { await logout.mutateAsync(upstream.id); toast.success("已退出登录"); } catch (err) { toast.error(errorMessage(err, "退出失败")); } }} > 退出登录 ) : null } > {upstream.hasCredential && upstream.status === "ok" ? (

已登录。退出只删除本地令牌,不会撤销提供方侧的授权。

) : ( )}
) : null}
setV((s) => ({ ...s, name: String(x) }))} disabled={pending} /> setV((s) => ({ ...s, enabled: x === true }))} disabled={pending} /> refresh.mutate(upstream.id, { onError: (err) => toast.error(errorMessage(err, "获取失败")) }) } disabledReason={fetchBlocked} /> { setV((s) => ({ ...s, model: x })); setErrors(({ defaultModel: _, ...rest }) => rest); }} models={models.data?.models ?? []} disabled={pending} invalid={!!errors.defaultModel} /> {showThinking ? ( setV((s) => ({ ...s, thinking: x as PiThinkingLevel }))} disabled={pending} /> ) : null} {showBaseUrl ? ( setV((s) => ({ ...s, baseUrl: String(x) }))} disabled={pending} /> ) : null} {upstream.authKind === "api_key" ? ( upstream.hasCredential, mask: () => upstream.credentialMask, clearable: true }, wide: true, }} value={v.apiKey} error={errors.apiKey} onChange={(x) => setV((s) => ({ ...s, apiKey: String(x) }))} disabled={pending} /> ) : null} {envFields.map((f) => ( setV((s) => ({ ...s, env: { ...s.env, [f.name]: String(x) } }))} disabled={pending} /> ))}
{ try { await remove.mutateAsync(upstream.id); toast.success(`${upstream.name}已删除`); onClose(); } catch (err) { toast.error(errorMessage(err, "删除失败")); throw err; } }} />
); }