feat(web): settings with 9 sidebar sections and Agent upstream management
- /console/settings#<section>[/<anchor>]; legacy ?tab= redirected in clientLoader - SETTINGS_SECTIONS in contracts with field-ownership test - Agent section: upstream list, create wizard (provider -> credentials/model -> login/test/default), edit drawer with OAuth browser/device login, test, set default, delete - sites section with site registration, snapshot/rate-limit drawer and account credentials - path mappings moved into downloader / media-server drawers; metadata and account sections Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
459991421e
commit
c67f6a9682
25 files changed
+3842
-942
No files matched your search
@@ -0,0 +1,72 @@
|
||||
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: "失败",
|
||||
};
|
||||
|
||||
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 ? ` · ${result.source}` : ""}
|
||||
</span>
|
||||
{!result.ok && result.message ? (
|
||||
<span className="w-full break-words text-destructive">{result.message}</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user