Files
UHD_Media_Tool/apps/web/app/components/settings/pi-test.tsx
T
rootandClaude Opus 5.5 4480fc346d refactor(copy): 术语统一——收录版本、模型服务、智能体等(DEAI-2)
- 版本目标 → 收录版本(前后端用户可见字符串、能力描述、给模型的提示)
- Agent 上游 / 上游 → 模型服务;界面上的 Agent / Agent 会话 → 智能体 / 智能体会话
- 身份歧义 / 识别有歧义 / 无法确定作品 → 没识别出作品;待识别(池) → 未识别
- 建档 → 加入媒体库;资产 → 文件;库根 → 媒体库目录;纳管 → 本工具 / 外部任务;
  哨兵 → 标记文件 / 检查目录;站点快照 → 抓取;Feed / RSS 订阅 / 来源健康 → RSS
- 代码标识符、API 字段名、迁移文件不改;glossary.md 同步;smoke 选择器跟随

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 20:35:43 +08:00

85 lines
3.5 KiB
TypeScript

import type { PiUpstream, PiUpstreamTestResult } from "@umt/contracts";
import { LoaderCircle, PlugZap, Send } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { useTestPiUpstream } from "~/api/pi.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
// 上游测试:默认只验凭据与模型(不产生费用);"发一次最小补全"真实调用一次模型
const ERROR_KIND: Record<string, string> = {
auth: "凭证被拒",
oauth: "登录失效",
model: "模型不可用",
network: "网络错误",
config: "配置错误",
other: "失败",
};
const SOURCE_TEXT: Record<string, string> = { "stored credential": "已存凭证" };
/** 上游返回的多是英文原文:先给一句怎么办,原文留作排查 */
const ERROR_HINT: Record<string, string> = {
network: "连不上模型服务,检查 Base URL、端口和网络",
auth: "凭证被拒,检查 API Key 或重新订阅登录",
model: "模型不可用,换一个模型 id",
};
export function PiTestPanel({ upstream, model }: { upstream: PiUpstream; model?: string | null }) {
const test = useTestPiUpstream();
const [result, setResult] = useState<PiUpstreamTestResult | null>(null);
const [running, setRunning] = useState<"auth" | "completion" | null>(null);
const run = async (mode: "auth" | "completion") => {
setRunning(mode);
try {
const r = await test.mutateAsync({ id: upstream.id, body: { mode, ...(model ? { model } : {}) } });
setResult(r);
if (r.ok) toast.success(mode === "auth" ? "凭证可用" : "补全成功");
else toast.error(r.message ?? "测试失败");
} catch (err) {
setResult(null);
toast.error(errorMessage(err, "测试失败"));
} finally {
setRunning(null);
}
};
return (
<div className="grid gap-2">
<div className="flex flex-wrap gap-2">
<Button type="button" size="sm" variant="outline" disabled={!!running} onClick={() => run("auth")}>
{running === "auth" ? <LoaderCircle className="animate-spin" /> : <PlugZap />}
测试
</Button>
<Button type="button" size="sm" variant="ghost" disabled={!!running} onClick={() => run("completion")}>
{running === "completion" ? <LoaderCircle className="animate-spin" /> : <Send />}
发一次最小补全
</Button>
</div>
<p className="text-meta text-muted-foreground">
「测试」只检查凭证和模型,不产生费用。最小补全会调用一次模型(最多 16 个 token)。
</p>
{result ? (
<div className="flex flex-wrap items-center gap-2 text-caption">
<StatusBadge tone={result.ok ? "success" : "destructive"}>
{result.ok ? "通过" : ERROR_KIND[result.errorKind ?? "other"]}
</StatusBadge>
<span className="text-muted-foreground">
{result.latencyMs} ms{result.model ? ` · ${result.model}` : ""}
{result.source ? ` · ${SOURCE_TEXT[result.source] ?? result.source}` : ""}
</span>
{!result.ok && result.message ? (
<span className="w-full break-words text-destructive">
{ERROR_HINT[result.errorKind ?? ""] ? `${ERROR_HINT[result.errorKind ?? ""]}。原始错误:` : ""}
{result.message}
</span>
) : null}
</div>
) : null}
</div>
);
}