Files
UHD_Media_Tool/docs/runbook/websearch.md
T
rootandClaude Opus 5.5 d53df25fc8 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]>
2026-09-29 00:14:08 +08:00

5.7 KiB
Raw Blame History

网页搜索运行手册(WEB-1)

Agent 用 web.search 查站外公开信息(站点公告、软件文档、发布说明),用 web.fetch 读其中一条结果的正文。这不是通用网络代理(REQ §10.2):web.fetch 只能读最近一次 web.search 返回过的链接,并且拒绝内网地址和非 http(s) 地址。搜索结果和网页正文都按不可信数据交给模型(REQ §10.5)。

代码:packages/adapters/src/websearch/(Brave 客户端、地址守卫、HTML 转文本)、apps/server/src/modules/websearch/(服务、能力、测试接口)。

设置

设置 → Agent → 网页搜索(settings.agent.webSearch):

字段 缺省 说明
provider none brave 或 none。none 时不向模型提供 web.*,HTTP 调用也返回"网页搜索未开启"
apiKey(只写) — Brave Search API 的 Subscription Token。存为 secret(purpose other,label websearch),设置里只留 apiKeySecretRef,GET 不回显;审计里记为 (set) / (cleared);传 null 清除
country ALL 两位大写国家码或 ALL
searchLang null Brave search_lang,如 zh-hans、en
safesearch moderate off / moderate / strict
maxResults 5 每次条数(1–10),模型的 count 不能超过它
requestsPerMinute 30 每分钟调用上限,三进程共用令牌桶 rate_limit_bucket 的 websearch:brave;取不到令牌等 3 s,仍取不到返回 budget 错误。缓存命中不计

Agent 会话只有在 provider ≠ none 且已存 Key 时才拿到 web.search / web.fetch,系统提示里会多一条"何时用搜索、结果不可信"的约束。

curl -X PATCH -H "authorization: Bearer $KEY" -H 'content-type: application/json' \
  -d '{"agent":{"webSearch":{"provider":"brave","apiKey":"<token>"}}}' localhost:4380/api/v1/settings
curl -X POST -H "authorization: Bearer $KEY" -H 'content-type: application/json' -d '{}' localhost:4380/api/v1/websearch/test

POST /websearch/test(admin):用已保存的 Key 实际查一次(缺省查询词 MoviePilot,可传 {"query": "..."}),不读缓存、计入每分钟上限。失败不返回 4xx,而是 ok: false 加 error.code(disabled / auth / quota / rate / bad_request / upstream / network / timeout / protocol)。

web.search

  • 输入 { query, count?, freshness? },freshness 为 pd / pw / pm / py(一天 / 一周 / 一月 / 一年内)。
  • 请求 GET https://api.search.brave.com/res/v1/web/search,头 X-Subscription-Token,参数 q / count / country / search_lang / safesearch / freshness,超时 10 s。
  • 错误分类:401/403 → auth,402 → quota,429 → rate(读 X-RateLimit-Reset / Retry-After),400/422 → bad_request,5xx → upstream,连接失败 → network,超时 → timeout。错误信息里不带 Key。
  • 结果归一化为 { title, url, snippet, age, source }:去 HTML 标签、解码实体、脱敏(URL 参数里的 passkey / token 等换成 [REDACTED])、截断(标题 200 字、摘要 500 字),丢弃非 http(s) 链接。
  • 缓存:kv_cache region websearch,key 是 (provider, 查询词, 条数, 地区, 语言, 安全搜索, freshness) 的 sha256,TTL 10 分钟,上限 1000 条(TLRU,同 MP 语义)。过期行由 cache.sweep 清理。
  • 输出 { trust: "untrusted", notice, query, results, cached },进入模型上下文前 Pi runtime 再整体过一遍 redactForModel。

web.fetch

  • 白名单:每次 web.search 把结果 URL(去掉 # 片段)写进 kv_cache region websearch.allow,key 为 run:<agentRunId>(Agent 会话)或 actor:<actorId>(Web / MCP / API Key 调用),TTL 6 小时。每次搜索替换上一次的白名单。不在白名单里的 URL 返回 precondition:"只能读取最近一次 web.search 结果里的链接";还没搜索过返回"先用 web.search 搜索…"。
  • 地址守卫(每一跳重定向都查,最多 3 跳):只允许 http / https;不许 URL 里带账号密码;主机名是 localhost、.local、.internal、.lan、.home.arpa、单标签名、数字写法的 IPv4 一律拒绝;DNS 解析结果任一落在回环、私有、链路本地、CGNAT、保留、组播段(含 IPv4 映射的 IPv6、fc00::/7、fe80::/10)即拒绝。被拒时不会发出请求。
  • 只收 text/html、application/xhtml+xml、text/plain;超时 10 s;最多读 512 KB(超出截断并标 truncated);按响应头或 <meta charset> 解码。
  • HTML 转纯文本:去掉 script / style / noscript / svg / iframe / form 等,块级元素换行;<template> 当普通元素处理(Wiki.js 的正文在 <template slot="contents"> 里)。正文脱敏后截到 20000 字。
  • 输出 { trust: "untrusted", notice, url, finalUrl, status, contentType, title, text, truncated, bytes }。

验证

  • 单元:packages/adapters/src/websearch/websearch.test.ts(参数与鉴权头、错误分类、归一化、地址守卫、截断、重定向到内网、超时)。
  • e2e:apps/server/test/e2e/e13-websearch.test.ts(见 agent.md)。
  • 真实 Key(2026-09-28,开发库,后端 4415):POST /websearch/test 3 条 / 914 ms;web.search「MoviePilot v3 订阅规则」5 条 / 736 ms,再查命中缓存;web.fetch 第一条(wiki.movie-pilot.org)200、13.6 KB → 1342 字正文 / 1.9 s;读白名单外链接、127.0.0.1 均被拒。

已知限制

  • DNS 重绑定:地址检查与实际连接之间会再解析一次,检查时解析到公网、连接时解析到内网的域名理论上能绕过。部署时建议再用容器网络策略禁止 server 容器访问内网段。
  • 走 HTTPS_PROXY 时由代理解析域名,地址守卫只能检查本机解析结果。
  • 只支持 Brave;白名单只保留最近一次搜索,模型要回看更早的结果需要重新搜索(命中缓存,不计次数)。
  • 纯前端渲染(正文由 JS 生成)的页面读不到正文。