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, 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 { formatDateTime } from "~/lib/utils.ts"; import { PiOAuthLogin } from "./pi-oauth.tsx"; import { AUTH_LABEL, 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 ?? "provider 默认"}, }, { 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 [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 save = async (e: React.FormEvent) => { e.preventDefault(); const env = Object.fromEntries(Object.entries(v.env).filter(([, x]) => x.trim())); const parsed = PiUpstreamUpdate.safeParse({ name: v.name.trim(), defaultModel: v.model.trim() || null, ...(showThinking ? { thinkingLevel: v.thinking } : {}), ...(showBaseUrl ? { baseUrl: v.baseUrl.trim() || null } : {}), ...(v.apiKey.trim() ? { apiKey: v.apiKey.trim() } : {}), 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 () => { try { await update.mutateAsync({ id: upstream.id, body: { makeDefault: true } }); 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
{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} /> setV((s) => ({ ...s, model: x }))} models={models.data?.items ?? []} disabled={pending} /> {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" ? ( 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; } }} />
); }