feat(web): token 数以 K 输入显示,已存凭证显示掩码占位(FIX-8)
- 新增 UnitInput 与 kind=tokens:上下文预算、单次 token 上限、自定义模型上下文窗口以 K 输入(允许小数),存储仍为原始 token 数;校验与提示用 K 表述;列表/详情 token 数统一 K/M 简写(formatTokens) - 新增 SecretInput 与 kind=secret:已保存时显示「已设置 ••••••••」(有服务端掩码时带末 4 位),聚焦直接输入替换,留空保持,「清除」确认后保存生效;替换模型服务、TMDB、网页搜索、下载器、媒体服务器、浏览器服务、站点账号、Passkey、本机 Token 各处 - 统一只写凭证服务端语义:省略或空串 = 不变,null = 清除(contracts secretInput);下载器/媒体服务器新增清除;站点凭证同类更新可留空保持密码/Cookie/API Key/TOTP,TOTP 可 null 清除,不再误删 TOTP 与 UA - contextBudget 最小值 4096→4000、contextWindow 1024→1000,便于按 K 填写 - 测试:换算与校验、SecretInput 渲染与状态、各接口留空不变/显式清除 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
68f53390f6
commit
fa344a7184
39 files changed
+1175
-232
No files matched your search
@@ -1,10 +1,10 @@
|
||||
import type { GlobalSettings } from "@umt/contracts";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTestWebSearch, useUpdateSettings } from "~/api/settings.ts";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { useTestWebSearch } from "~/api/settings.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { secretPatch } from "~/lib/secret.ts";
|
||||
import { num, str } from "./resource-section.tsx";
|
||||
import { type CardSpec, n } from "./settings-card.tsx";
|
||||
|
||||
@@ -28,8 +28,9 @@ export const WEBSEARCH_CARD: CardSpec = {
|
||||
{
|
||||
name: "agent.webSearch.apiKey",
|
||||
label: "API Key",
|
||||
kind: "password",
|
||||
hint: "Brave Search API 的 Subscription Token;留空不修改",
|
||||
kind: "secret",
|
||||
secret: { set: (v) => v["agent.webSearch.apiKey:set"] === true, clearable: true },
|
||||
hint: "Brave Search API 的 Subscription Token",
|
||||
visible: (v) => v["agent.webSearch.provider"] !== "none",
|
||||
},
|
||||
{
|
||||
@@ -79,6 +80,7 @@ export const WEBSEARCH_CARD: CardSpec = {
|
||||
return {
|
||||
"agent.webSearch.provider": w.provider,
|
||||
"agent.webSearch.apiKey": "",
|
||||
"agent.webSearch.apiKey:set": !!w.apiKeySecretRef,
|
||||
"agent.webSearch.country": w.country,
|
||||
"agent.webSearch.searchLang": w.searchLang ?? "",
|
||||
"agent.webSearch.safesearch": w.safesearch,
|
||||
@@ -87,7 +89,7 @@ export const WEBSEARCH_CARD: CardSpec = {
|
||||
};
|
||||
},
|
||||
toPatch: (v) => {
|
||||
const key = str(v["agent.webSearch.apiKey"]);
|
||||
const key = secretPatch(v["agent.webSearch.apiKey"]);
|
||||
const safe = str(v["agent.webSearch.safesearch"]);
|
||||
return {
|
||||
agent: {
|
||||
@@ -98,7 +100,7 @@ export const WEBSEARCH_CARD: CardSpec = {
|
||||
safesearch: safe === "off" || safe === "strict" ? safe : "moderate",
|
||||
maxResults: num(v["agent.webSearch.maxResults"]),
|
||||
requestsPerMinute: num(v["agent.webSearch.requestsPerMinute"]),
|
||||
...(key ? { apiKey: key } : {}),
|
||||
...(key !== undefined ? { apiKey: key } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -106,54 +108,31 @@ export const WEBSEARCH_CARD: CardSpec = {
|
||||
extra: (s) => <WebSearchKeyStatus settings={s} />,
|
||||
};
|
||||
|
||||
/** 已保存 Key 时可以测试;Key 的设置与清除在上面的输入框里 */
|
||||
function WebSearchKeyStatus({ settings }: { settings: GlobalSettings }) {
|
||||
const update = useUpdateSettings();
|
||||
const test = useTestWebSearch();
|
||||
const w = settings.agent.webSearch;
|
||||
if (w.provider === "none") return null;
|
||||
const saved = !!w.apiKeySecretRef;
|
||||
if (w.provider === "none" || !w.apiKeySecretRef) return null;
|
||||
return (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={saved ? "success" : "neutral"}>{saved ? "API Key 已保存" : "未保存 API Key"}</StatusBadge>
|
||||
{saved ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
disabled={test.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const r = await test.mutateAsync({});
|
||||
if (r.ok) toast.success(`搜索可用:${r.count} 条结果,用时 ${r.durationMs} ms`);
|
||||
else toast.error(r.error?.message ?? "搜索失败");
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "测试失败"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{test.isPending ? <LoaderCircle className="animate-spin" /> : null}
|
||||
测试搜索
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="hover:text-destructive"
|
||||
disabled={update.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await update.mutateAsync({ agent: { webSearch: { apiKey: null } } });
|
||||
toast.success("搜索服务 API Key 已清除");
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "清除失败"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
清除
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
disabled={test.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const r = await test.mutateAsync({});
|
||||
if (r.ok) toast.success(`搜索可用:${r.count} 条结果,用时 ${r.durationMs} ms`);
|
||||
else toast.error(r.error?.message ?? "搜索失败");
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "测试失败"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{test.isPending ? <LoaderCircle className="animate-spin" /> : null}
|
||||
测试搜索
|
||||
</Button>
|
||||
{test.data?.ok ? (
|
||||
<span className="text-meta text-muted-foreground tabular-nums">
|
||||
「{test.data.query}」{test.data.count} 条 · {test.data.durationMs} ms
|
||||
|
||||
Reference in new issue
Block a user