Files
rootandClaude Opus 5.5 fa344a7184 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]>
2026-09-30 00:10:46 +08:00

144 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { GlobalSettings } from "@umt/contracts";
import { LoaderCircle } from "lucide-react";
import { toast } from "sonner";
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";
// Agent 分区「网页搜索」:provider / Key(只写,存 secret)/ 地区 / 语言 / 安全搜索 / 每次条数 / 每分钟上限 + 测试
export const WEBSEARCH_CARD: CardSpec = {
anchor: "websearch",
title: "网页搜索",
description:
"开启后智能体可以用 web.search 搜索站外公开信息,用 web.fetch 读取搜索结果里的网页。结果和正文按不可信数据交给模型,web.fetch 不能读内网地址和结果以外的链接。",
fields: [
{
name: "agent.webSearch.provider",
label: "搜索服务",
kind: "select",
options: [
{ value: "none", label: "关闭" },
{ value: "brave", label: "Brave Search" },
],
},
{
name: "agent.webSearch.apiKey",
label: "API Key",
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",
},
{
name: "agent.webSearch.country",
label: "搜索地区",
kind: "text",
placeholder: "ALL",
hint: "两位国家码,如 CN、US;ALL 表示不限",
visible: (v) => v["agent.webSearch.provider"] !== "none",
},
{
name: "agent.webSearch.searchLang",
label: "结果语言",
kind: "text",
placeholder: "不限",
hint: "如 zh-hans、en;留空不限",
visible: (v) => v["agent.webSearch.provider"] !== "none",
},
{
name: "agent.webSearch.safesearch",
label: "安全搜索",
kind: "select",
options: [
{ value: "off", label: "关闭" },
{ value: "moderate", label: "适中" },
{ value: "strict", label: "严格" },
],
visible: (v) => v["agent.webSearch.provider"] !== "none",
},
{
name: "agent.webSearch.maxResults",
label: "每次条数",
kind: "number",
hint: "1–10,智能体可以要更少",
visible: (v) => v["agent.webSearch.provider"] !== "none",
},
{
name: "agent.webSearch.requestsPerMinute",
label: "每分钟上限",
kind: "number",
hint: "三个进程共用;10 分钟内的相同搜索走缓存,不计次数",
visible: (v) => v["agent.webSearch.provider"] !== "none",
},
],
fromSettings: (s) => {
const w = s.agent.webSearch;
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,
"agent.webSearch.maxResults": n(w.maxResults),
"agent.webSearch.requestsPerMinute": n(w.requestsPerMinute),
};
},
toPatch: (v) => {
const key = secretPatch(v["agent.webSearch.apiKey"]);
const safe = str(v["agent.webSearch.safesearch"]);
return {
agent: {
webSearch: {
provider: v["agent.webSearch.provider"] === "brave" ? "brave" : "none",
country: str(v["agent.webSearch.country"]).toUpperCase() || "ALL",
searchLang: str(v["agent.webSearch.searchLang"]).toLowerCase() || null,
safesearch: safe === "off" || safe === "strict" ? safe : "moderate",
maxResults: num(v["agent.webSearch.maxResults"]),
requestsPerMinute: num(v["agent.webSearch.requestsPerMinute"]),
...(key !== undefined ? { apiKey: key } : {}),
},
},
};
},
extra: (s) => <WebSearchKeyStatus settings={s} />,
};
/** 已保存 Key 时可以测试;Key 的设置与清除在上面的输入框里 */
function WebSearchKeyStatus({ settings }: { settings: GlobalSettings }) {
const test = useTestWebSearch();
const w = settings.agent.webSearch;
if (w.provider === "none" || !w.apiKeySecretRef) return null;
return (
<div className="mb-2 flex flex-wrap items-center gap-2">
<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
</span>
) : null}
</div>
);
}