5.8 KiB
5.8 KiB
网页搜索运行手册(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_cacheregionwebsearch,key 是 (provider, 查询词, 条数, 地区, 语言, 安全搜索, freshness) 的 sha256,TTL 取storage.cacheTtl.webSearchDays(缺省 1 天,CACHE-2 前是 10 分钟),上限 1000 条(TLRU,同 MP 语义)。过期行由cache.sweep清理。 - 输出
{ trust: "untrusted", notice, query, results, cached },进入模型上下文前 Pi runtime 再整体过一遍redactForModel。
web.fetch
- 白名单:每次 web.search 把结果 URL(去掉
#片段)写进kv_cacheregionwebsearch.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/test3 条 / 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 生成)的页面读不到正文。