feat(agent): web.search / web.fetch via Brave Search (WEB-1)
- adapters/websearch: Brave client (X-Subscription-Token, error classes), result normalization with redaction/truncation, SSRF guard, HTML→text fetch (10s, 512KB, text/html|text/plain only) - modules/websearch: 10-min PG cache (region websearch), cross-process per-minute limit, web.fetch allowlist = URLs from the caller's latest web.search; POST /websearch/test - settings.agent.webSearch (key write-only → secret_ref); settings card under Agent; web.* offered to the model only when enabled, plus a system prompt rule; outputs marked untrusted - capability count 27 → 29; e2e E13; runbook docs/runbook/websearch.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
40dd0e69e4
commit
d53df25fc8
32 files changed
+1854
-39
No files matched your search
@@ -105,6 +105,42 @@ export const PiSettings = z.object({
|
||||
});
|
||||
export type PiSettings = z.infer<typeof PiSettings>;
|
||||
|
||||
/**
|
||||
* Agent 网页搜索(web.search / web.fetch,REQ §10.2、§10.5):默认关闭(provider = none,不向模型提供这两个能力)。
|
||||
* Key 只存 secret 引用(apiKeySecretRef);搜索结果与网页正文按不可信数据交给模型。
|
||||
*/
|
||||
export const WebSearchProvider = z.enum(["brave", "none"]);
|
||||
export type WebSearchProvider = z.infer<typeof WebSearchProvider>;
|
||||
export const WebSearchSafesearch = z.enum(["off", "moderate", "strict"]);
|
||||
export type WebSearchSafesearch = z.infer<typeof WebSearchSafesearch>;
|
||||
export const WebSearchSettings = z.object({
|
||||
provider: WebSearchProvider,
|
||||
apiKeySecretRef: Id.nullable(),
|
||||
/** 搜索地区:两位国家码(如 CN、US)或 ALL */
|
||||
country: z.string().regex(/^([A-Z]{2}|ALL)$/, "两位大写国家码或 ALL"),
|
||||
/** 搜索语言(Brave search_lang,如 zh-hans、en);null = 不指定 */
|
||||
searchLang: z
|
||||
.string()
|
||||
.regex(/^[a-z]{2}(-[a-z]{2,4})?$/, "语言码,如 zh-hans、en")
|
||||
.nullable(),
|
||||
safesearch: WebSearchSafesearch,
|
||||
/** 每次搜索返回的条数(模型可以要更少,不能更多) */
|
||||
maxResults: z.number().int().min(1).max(10),
|
||||
/** 每分钟最多调用几次搜索接口(三个进程共用;缓存命中不计) */
|
||||
requestsPerMinute: z.number().int().min(1).max(600),
|
||||
});
|
||||
export type WebSearchSettings = z.infer<typeof WebSearchSettings>;
|
||||
|
||||
export const DEFAULT_WEB_SEARCH: WebSearchSettings = {
|
||||
provider: "none",
|
||||
apiKeySecretRef: null,
|
||||
country: "ALL",
|
||||
searchLang: null,
|
||||
safesearch: "moderate",
|
||||
maxResults: 5,
|
||||
requestsPerMinute: 30,
|
||||
};
|
||||
|
||||
/**
|
||||
* Agent 运行参数(REQ §10.5、§10.7):单次运行预算、后台自动调查、fake 演示脚本。
|
||||
* 上游/模型/思考档位在 pi_upstream(见 contracts/pi.ts);provider = "fake" 的上游使用脚本化假模型(不联网,演示与 e2e 用)。
|
||||
@@ -168,6 +204,8 @@ export const AgentRuntimeSettings = z.object({
|
||||
* null = 部署默认(环境变量 AGENT_SESSION_DIR,缺省 DATA_DIR/agent-sessions)
|
||||
*/
|
||||
sessionDir: z.string().max(1024).regex(/^\//, "必须是绝对路径").nullable(),
|
||||
/** 网页搜索(web.search / web.fetch) */
|
||||
webSearch: WebSearchSettings,
|
||||
});
|
||||
export type AgentRuntimeSettings = z.infer<typeof AgentRuntimeSettings>;
|
||||
|
||||
@@ -389,6 +427,7 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||
approvalTimeoutSec: 86400,
|
||||
fakeScript: null,
|
||||
sessionDir: null,
|
||||
webSearch: DEFAULT_WEB_SEARCH,
|
||||
},
|
||||
metadata: { tmdbApiKeySecretRef: null, tmdbBaseUrl: null },
|
||||
sites: {
|
||||
@@ -426,7 +465,18 @@ export const GlobalSettingsUpdate = z.object({
|
||||
})
|
||||
.optional(),
|
||||
budget: BudgetSettings.partial().optional(),
|
||||
agent: AgentRuntimeSettings.partial().optional(),
|
||||
agent: AgentRuntimeSettings.omit({ webSearch: true })
|
||||
.partial()
|
||||
.extend({
|
||||
webSearch: WebSearchSettings.omit({ apiKeySecretRef: true })
|
||||
.partial()
|
||||
.extend({
|
||||
/** 只写:搜索服务 API Key,服务端存为 secret 并回填 apiKeySecretRef;null 清除 */
|
||||
apiKey: z.string().min(1).max(4096).nullable().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
metadata: MetadataSettings.omit({ tmdbApiKeySecretRef: true })
|
||||
.partial()
|
||||
.extend({
|
||||
@@ -443,6 +493,8 @@ export type GlobalSettingsUpdate = z.infer<typeof GlobalSettingsUpdate>;
|
||||
export function mergeSettings(current: GlobalSettings, patch: GlobalSettingsUpdate): GlobalSettings {
|
||||
const { compaction, subagents, ...pi } = patch.pi ?? {};
|
||||
const { tmdbApiKey: _tmdbApiKey, ...metadata } = patch.metadata ?? {};
|
||||
const { webSearch: wsPatch, ...agent } = patch.agent ?? {};
|
||||
const { apiKey: _wsApiKey, ...webSearch } = wsPatch ?? {};
|
||||
return {
|
||||
automation: { ...current.automation, ...patch.automation },
|
||||
cache: { ...current.cache, ...patch.cache },
|
||||
@@ -450,7 +502,11 @@ export function mergeSettings(current: GlobalSettings, patch: GlobalSettingsUpda
|
||||
retention: { ...current.retention, ...patch.retention },
|
||||
pi: mergePi(current.pi, pi, compaction, subagents),
|
||||
budget: { ...current.budget, ...patch.budget },
|
||||
agent: { ...current.agent, ...patch.agent },
|
||||
agent: {
|
||||
...current.agent,
|
||||
...agent,
|
||||
webSearch: { ...DEFAULT_WEB_SEARCH, ...current.agent.webSearch, ...webSearch },
|
||||
},
|
||||
metadata: { ...current.metadata, ...metadata },
|
||||
sites: { ...(current.sites ?? DEFAULT_SETTINGS.sites), ...patch.sites },
|
||||
naming: { ...(current.naming ?? DEFAULT_SETTINGS.naming), ...patch.naming },
|
||||
@@ -631,13 +687,14 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
{
|
||||
id: "agent",
|
||||
label: "Agent",
|
||||
description: "Agent 上游、运行预算、自动调查",
|
||||
description: "Agent 上游、运行预算、自动调查、网页搜索",
|
||||
legacyTabs: [{ tab: "pi" }],
|
||||
anchors: [
|
||||
{ id: "upstream", label: "上游" },
|
||||
{ id: "runtime", label: "上下文与子会话" },
|
||||
{ id: "budget", label: "预算" },
|
||||
{ id: "investigate", label: "自动调查" },
|
||||
{ id: "websearch", label: "网页搜索" },
|
||||
{ id: "advanced", label: "高级" },
|
||||
{ id: "mcp", label: "MCP 服务端" },
|
||||
{ id: "capabilities", label: "能力清单" },
|
||||
@@ -666,6 +723,7 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
"agent.approvalTimeoutSec",
|
||||
"agent.fakeScript",
|
||||
"agent.sessionDir",
|
||||
"agent.webSearch",
|
||||
],
|
||||
resources: ["pi-upstreams"],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user