- 版本目标 → 收录版本(前后端用户可见字符串、能力描述、给模型的提示) - Agent 上游 / 上游 → 模型服务;界面上的 Agent / Agent 会话 → 智能体 / 智能体会话 - 身份歧义 / 识别有歧义 / 无法确定作品 → 没识别出作品;待识别(池) → 未识别 - 建档 → 加入媒体库;资产 → 文件;库根 → 媒体库目录;纳管 → 本工具 / 外部任务; 哨兵 → 标记文件 / 检查目录;站点快照 → 抓取;Feed / RSS 订阅 / 来源健康 → RSS - 代码标识符、API 字段名、迁移文件不改;glossary.md 同步;smoke 选择器跟随 Co-Authored-By: Claude Opus 5.5 <[email protected]>
165 lines
5.8 KiB
TypeScript
165 lines
5.8 KiB
TypeScript
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 { Button } from "~/components/ui/button.tsx";
|
||
import { errorMessage } from "~/lib/api.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: "password",
|
||
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.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 = str(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 ? { apiKey: key } : {}),
|
||
},
|
||
},
|
||
};
|
||
},
|
||
extra: (s) => <WebSearchKeyStatus settings={s} />,
|
||
};
|
||
|
||
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;
|
||
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}
|
||
{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>
|
||
);
|
||
}
|