# 网页搜索运行手册(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,系统提示里会多一条"何时用搜索、结果不可信"的约束。 ```sh curl -X PATCH -H "authorization: Bearer $KEY" -H 'content-type: application/json' \ -d '{"agent":{"webSearch":{"provider":"brave","apiKey":""}}}' 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 取 `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_cache` region `websearch.allow`,key 为 `run:`(Agent 会话)或 `actor:`(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`);按响应头或 `` 解码。 - HTML 转纯文本:去掉 script / style / noscript / svg / iframe / form 等,块级元素换行;`