WebFetch 与 WebSearch

两个联网工具的实现:WebFetch 的 URL 规范化、出站护栏、重定向与各项上限、手写的 HTML 转 Markdown、交给当前模型提炼与 15 分钟缓存;预批准域名与审批;WebSearch 怎样借模型提供方的原生 web_search,哪些端点支持,结果与引用怎样整理。

作者 David更新于 17 篇(共 47 篇)

联网工具只有两个,实现都在 apps/zcode-cli/packages/core/src/tool/handlersWebFetchwebfetch.ts 和十个 webfetch-*.ts 分工完成:从 Agent 所在的机器发 GET,把页面转成 Markdown,再交给当前模型按 prompt 提炼出答案。WebSearchwebsearch.tswebsearch-results.tswebsearch-support.ts)自己不碰网络,而是另发一次模型请求,让模型提供方在服务端执行 Anthropic 协议的原生 web_search 工具。

预批准域名表在 apps/zcode-cli/packages/core/src/tool/webfetch-preapproved.ts,输入输出契约在 apps/zcode-cli/packages/contracts/src/tools/webfetch.tsapps/zcode-cli/packages/contracts/src/tools/websearch.ts。WebFetch 的请求经 HttpClientPort 交给 apps/zcode-cli/packages/adapters/src/http,代理、证书与出口网络的统一处理见执行边界:子进程、环境与网络;工具怎样被调度、审批,见执行器:调度、审批、超时与结果权限模式与规则

怎么用

WebFetchWebSearch
参数urlpromptquery(至少 2 个字符),allowed_domainsblocked_domains 二选一
何时可见始终当前模型的 supportsNativeWebSearch 为真时
给模型的结果当前模型按 prompt 写出的回答摘要加至多 20 条链接
build 模式询问;预批准 URL 免询问不询问
超时60 秒60 秒
缓存按 URL 缓存 15 分钟

出处:apps/zcode-cli/packages/contracts/src/tools/webfetch.ts:13apps/zcode-cli/packages/contracts/src/tools/websearch.ts:12apps/zcode-cli/packages/core/src/tool/handlers/webfetch.ts:195apps/zcode-cli/packages/core/src/tool/handlers/websearch.ts:129apps/zcode-cli/packages/core/src/runtime/methods/config.ts:141。另外几件用户能感知的事:

  • WebFetch 的描述除了一句总述,只有三条提示:私有 URL 会失败,HTTP 升级为 HTTPS 且跨主机重定向要自己再调一次,结果按 URL 缓存 15 分钟(handlers/webfetch.ts:39)。
  • 桌面端审批框的摘要取 URL 而不是 prompt,注释说显示 prompt 会遮住真正需要确认的目标地址(packages/ui/src/ToolCallBlocks/renderers/search.tsx:69)。
  • HTTP 客户端按配置里的 network.httpProxynetwork.noProxynetwork.caCertFile 创建(apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:409)。每个请求都显式带 60 秒超时,所以 network.timeout 管不到 WebFetch。
  • 自己配置的模型可以在模型设置里打开“原生联网搜索”,WebSearch 才会出现,但只有 Anthropic Messages 协议的模型真能用上,见下文(packages/ui/src/i18n/locales/zh-CN.ts:2866,可编辑字段见 packages/provider/src/config/manual-model-config.ts:14)。

WebFetch:一次调用经过什么

图表加载中…

URLnormalizeWebFetchUrlapps/zcode-cli/packages/core/src/tool/handlers/webfetch-url.ts:10)依次检查:长度不超过 2000 字符(apps/zcode-cli/packages/core/src/tool/handlers/webfetch-constants.ts:3)、能被 URL 解析、协议只能是 http 或 https、不许带用户名密码;然后把 http 改成 https(webfetch-url.ts:39)。主机名的形态检查在 webfetch-url.ts:103:空主机名、localhost.localhost.local 结尾、没有点的单段主机名(如 intranet)都拒绝;IP 字面量在这一层放过,留给下面的出站检查。

重定向。请求以 redirect: "manual" 发出(apps/zcode-cli/packages/core/src/tool/handlers/webfetch-network.ts:77),遇到 301、302、303、307、308 由 isPermittedRedirect 判断能否自动跟随(webfetch-url.ts:63):

export function isPermittedRedirect(from: URL, to: URL): boolean {
  if (to.username || to.password) {
    return false;
  }

  if (!isPublicHost(to)) {
    return false;
  }

  if (from.protocol !== to.protocol || effectivePort(from) !== effectivePort(to)) {
    return false;
  }

  if (!sameHostModuloWww(from.hostname, to.hostname)) {
    return false;
  }

  return true;
}

只有协议、端口相同,主机名去掉开头的 www. 后也相同,才自动跟随,最多 10 次,超过报 TooManyRedirects(webfetch-constants.ts:9webfetch-network.ts:151)。其余重定向不跟,而是返回一段 “REDIRECT DETECTED” 文本,列出原 URL、目标 URL 和状态码,请模型用同一个 prompt 再调一次(handlers/webfetch.ts:128)。新的调用是一次新的工具调用,会重新过权限检查。缺少 Location 的 3xx 按 HTTP 错误处理(webfetch-network.ts:188)。

出站护栏

每次真正发 GET 之前,包括每一跳重定向,都要过 assertWebFetchLiteralEgresswebfetch-network.ts:52,实现在 apps/zcode-cli/packages/core/src/tool/handlers/webfetch-egress-guard.ts:14):

export function assertWebFetchLiteralEgress(url: URL): void {
  const hostname = normalizeHostname(url.hostname);

  if (isLocalHostname(hostname)) {
    throw webFetchError("EgressBlocked", "WebFetch cannot access private or local hostnames", {
      hostname,
      url: url.toString(),
    });
  }

  // DNS preflight 在部分网络下 1s 内无法完成,会让公网 URL 在真实 fetch 前失败。
  // 当前只保留 URL 字面量层面的本地/私网目标阻断,不对普通域名做本地 DNS 解析。
  if (!isIpLiteral(hostname)) return;
  assertPublicIpAddress(hostname, { hostname, url });
}
  • 主机名localhost.localhost 结尾的一律拦下。
  • IP 字面量:用 ipaddr.js 分类,只有归为 unicast 的地址才放行,回环、私网、链路本地(169.254.0.0/16,云主机元数据地址 169.254.169.254 就在其中)都不是;另外显式排除 198.18.0.0/15 基准测试网段和五个特殊用途的 IPv6 前缀(webfetch-egress-guard.ts:4:89:94)。
  • IPv6 里藏着的 IPv4:IPv4 映射地址和 NAT64 前缀 64:ff9b::/96 先还原出低 32 位,再按 IPv4 规则判(webfetch-egress-guard.ts:77)。
  • 出口代理:响应头带 x-proxy-error: blocked-by-allowlist 时,按出口白名单拦截处理,返回一段 JSON 错误(webfetch-network.ts:233)。

注释写得明白:普通域名不做 DNS 解析。一个解析到 10.x 或 169.254.169.254 的域名,或者做 DNS 重绑定的域名,都能通过这道检查。HTTP adapter 里其实有一套按 DNS 结果拦截私网地址的策略,只在请求带 egressPolicy: "public" 时启用(apps/zcode-cli/packages/adapters/src/http/index.ts:70apps/zcode-cli/packages/adapters/src/http/public-egress-policy.ts:79),但 WebFetch 发请求时没有带这个字段(webfetch-network.ts:70),仓库里也没有别处设置它。webfetch-network.ts:252 为这种拦截准备的“不把解析出的内网 IP 透露给模型”的文案改写,目前走不到。

超时、大小与内容类型

  • 时间:每个 GET 60 秒,整个工具调用也是 60 秒且不许调用方改(webfetch-constants.ts:2webfetch-network.ts:75handlers/webfetch.ts:239)。工具内部的模型请求在进程级准入闸门前排队时,工具的 deadline 会暂停计时(apps/zcode-cli/packages/core/src/tool/executor/timeout.ts:13)。
  • 大小:响应体不超过 10 MiB,先看 content-length,再在流式读取中累计(webfetch-constants.ts:4apps/zcode-cli/packages/adapters/src/http/response-body.ts:18:62)。
  • 请求头:User-Agent 是 ZCode-WebFetch/0.1 (+https://zcode.ai; coding-agent-cli)Accepttext/markdown 排在最前(webfetch-constants.ts:11webfetch-network.ts:274)。
  • 内容类型:只收 text/*、JSON、XML、JavaScript、+json+xml 以及缺省类型,其余报 “Unsupported WebFetch content type”,所以 PDF、图片链接抓不了(apps/zcode-cli/packages/core/src/tool/handlers/webfetch-content.ts:8:103)。正文一律按 UTF-8 解码,不看 charsetwebfetch-content.ts:14)。

HTML 转 Markdown 没有用任何库,是一串正则(webfetch-content.ts:59):删掉注释、scriptstylenoscripth1h6 换成对应层级的 #;链接换成 Markdown 链接;列表项换成 - br 与段落、表格行等块级结束标签换成换行;其余标签全部剥掉,只解码少数几个实体。最后每一行的连续空白压成一个空格并去掉首尾空白,所以 pre 里代码的缩进也会被压平。

转换后的文本超过 100000 字节时,全文写成会话级的工具结果附件(webfetch-content.ts:21),路径只出现在结构化输出里,模型看到的仍是提炼后的答案(handlers/webfetch.ts:95:257)。

交给当前模型提炼

描述里说用 “a small fast model” 回答 prompthandlers/webfetch.ts:40),实际用的是 context.modelapps/zcode-cli/packages/core/src/tool/handlers/webfetch-processing.ts:30),也就是执行器交给工具的本轮模型(apps/zcode-cli/packages/core/src/runtime/methods/turn-tools.ts:186,字段注释见 apps/zcode-cli/packages/core/src/runtime/methods/turn-loop-state.ts:95),代码里没有另配小模型。它取最低一档的推理强度,输出不超过 4096 token,不带任何工具(webfetch-processing.ts:67apps/zcode-cli/packages/core/src/model/auxiliary-model-options.ts:9)。正文先截到 100000 字符,结尾附一句截断说明(webfetch-content.ts:46)。提示词在 webfetch-processing.ts:106

function buildProcessingPrompt(content: string, prompt: string, preapprovedUrl: boolean): string {
  const instruction = preapprovedUrl
    ? "Provide a concise response based on the content above. Include relevant details, code examples, and documentation excerpts as needed."
    : [
        "Provide a concise response based only on the content above. In your response:",
        " - Enforce a strict 125-character maximum for quotes from any source document. Open Source Software is ok as long as we respect the license.",
        " - Use quotation marks for exact language from articles; any language outside of the quotation should never be word-for-word the same.",
        " - You are not a lawyer and never comment on the legality of your own prompts and responses.",
        " - Never produce or reproduce exact song lyrics.",
      ].join("\n");

  return `
Web page content:
---
${content}
---

${prompt}

${instruction}
`;
}

非预批准的页面要求引文不超过 125 个字符、不逐字复述、不评论合法性、不复现歌词;预批准的文档站则鼓励给出细节、代码示例和文档摘录。模型返回空文本时,结果换成一句固定说明(webfetch-processing.ts:80)。

缓存是模块级的 Mapapps/zcode-cli/packages/core/src/tool/handlers/webfetch-cache.ts:8),同一进程里的所有会话共用。键是模型传入的原始 URL 字符串(handlers/webfetch.ts:56),因此 http://https:// 两种写法各占一条,尽管实际请求相同。条目存活 15 分钟,总量不超过 50 MiB,命中时移到队尾,清理时先删过期再从最旧的删起(webfetch-constants.ts:7:8webfetch-cache.ts:24:45)。只有成功抓到的正文进缓存,重定向与 HTTP 错误都不缓存(handlers/webfetch.ts:71)。缓存的是转换后的正文而不是答案:换一个 prompt 再问同一个 URL,不再联网,但仍会重新调一次模型,输出里 cacheHit 为真(handlers/webfetch.ts:79:93)。

预批准域名

webfetch-preapproved.ts 里有 82 个整主机名和 4 个“主机加路径前缀”(apps/zcode-cli/packages/core/src/tool/webfetch-preapproved.ts:1:86),全是公开的技术文档站:

类别例子
MCP 与技能modelcontextprotocol.ioagentskills.io
语言与运行时docs.python.orggo.devdoc.rust-lang.orgwww.typescriptlang.orgnodejs.org
前端react.devvuejs.orgnextjs.orgtailwindcss.com
后端与数据docs.djangoproject.comfastapi.tiangolo.compandas.pydata.orgpytorch.org
数据库www.postgresql.orgredis.iowww.sqlite.org
云与运维docs.aws.amazon.comcloud.google.comkubernetes.iowww.docker.com
限定路径wordpress.org/documentationhuggingface.co/docswww.kaggle.com/docsvercel.com/docs

主机名必须完全相等,子域名和父域名都不算。限定路径的四项要求路径正好是前缀或以“前缀加斜杠”开头,并且拒绝含 %2f%5c%2e(包括多重编码)的路径,防止用编码绕出前缀(webfetch-preapproved.ts:109)。

从代码看,预批准有两层作用:一是权限上免询问,在项目 deny、ask 规则与 plan 模式判断之后、按模式询问之前放行(apps/zcode-cli/packages/core/src/permission/service.ts:189);二是内容处理上更宽松,服务器返回 text/markdown 且不足 100000 字符时原文直接返回、不经过模型,否则用上面那段宽松的提示词(webfetch-processing.ts:95)。

审批

WebFetch 的元数据是只读、needsApproval: true、副作用范围 networkhandlers/webfetch.ts:198);WebSearch 是只读、不需要审批(handlers/websearch.ts:139)。套进 checkPermission 的判定顺序(service.ts:97):

模式WebFetchWebSearch
build预批准 URL 或 allow 规则命中则放行,否则询问放行
edit同 build放行
plan按只读工具放行(service.ts:412放行
yolo放行(service.ts:136放行
auto拒绝,模式未实现(service.ts:140拒绝

build 模式下 WebFetch 落到 “Tool has side effects and requires approval” 这一支(service.ts:505),WebSearch 走的是只读直通(service.ts:460)。规则匹配时,WebFetch 的比对对象是 domain:<主机名>apps/zcode-cli/packages/core/src/permission/rule-matching.ts:9service.ts:288)。而审批时默认给出的“始终允许”建议,是把输入里的完整 url 当作规则内容(apps/zcode-cli/packages/core/src/tool/executor/permission-suggestions.ts:4:39);从代码看,这样的规则与 domain: 主体对不上,以后的请求仍会询问,本书没有在运行中验证。

WebSearch:借提供方的原生搜索

图表加载中…

WebSearch 只有在当前模型 supportsNativeWebSearch 为真时才出现在工具清单里(config.ts:268),handler 执行时再查一遍(handlers/websearch.ts:71)。它另起一次请求(handlers/websearch.ts:82):

  const request: Parameters<typeof model.streamText>[0] = {
    messages: [
      {
        role: "system",
        content: "You are an assistant for performing a web search tool use.",
      },
      {
        role: "user",
        content: `Perform a web search for the query: ${input.query}`,
      },
    ],
    tools: [createProviderNativeWebSearchContract(input)],
    // BigModel 的 Anthropic 兼容端点会拒绝 named forced web_search tool_choice(1210)。
    // 这里保持自动选择,依靠单工具请求和 prompt 触发 provider-native 搜索。
    options: {
      ...auxiliaryModelOptions(model),
      maxOutputTokens: Math.min(4096, model.optionSpecs.maxOutputTokens.max),
    },
    abortSignal: context.abortSignal,
  };

请求里唯一的工具是 provider-native 的 web_searchhandlers/websearch.ts:156)。之所以走流式,注释说 BigModel 的 Anthropic 兼容端点在非流式 JSON 里会把内部搜索结果返回成 assistant 一侧的裸 tool_result,AI SDK 校验时会报错(handlers/websearch.ts:103)。适配层只会为 Anthropic Messages 协议编码这个工具,映射成 anthropic.tools.webSearch_20260209,其他 API 形态直接报错(apps/zcode-cli/packages/adapters/src/model/tool-transform.ts:259:275)。maxUses 默认 8、上限 8,运行时接受,但不在给模型的 JSON Schema 里(apps/zcode-cli/packages/contracts/src/tools/websearch.ts:9:26:45)。

内置规则里把 supportsNativeWebSearch 设为真的端点:

端点出处
Anthropic 官方 api.anthropic.com/v1 上的 claude-*config/provider/zcode-builtin.json:4052
DeepSeek 的 /anthropic 端点zcode-builtin.json:4072
Z.ai 的 api.z.ai/api/anthropiczcode-builtin.json:4095
智谱 BigModel 的 open.bigmodel.cn/api/anthropiczcode-builtin.json:4106
ZCode Coding Plan 的 zcode-plan/anthropiczcode-builtin.json:4117

闲时计划的 off-peak/anthropic 端点显式关掉(zcode-builtin.json:4128),其余模型的默认值为假(zcode-builtin.json:884)。账号与 Coding Plan 见账号、Coding Plan 与闲时计划,规则体系见Provider 规则、模型目录与选项映射

结果与引用。收集流时只记文本、工具调用与结束事件(apps/zcode-cli/packages/core/src/tool/handlers/websearch.ts:179),流里的裸 tool_result 块又会被兼容层滤掉(apps/zcode-cli/packages/adapters/src/model/anthropic-stream-compat.ts:258),所以 results 通常为空,来源主要靠从摘要里抽取 Markdown 链接,注释也承认引用有时只出现在摘要里(apps/zcode-cli/packages/core/src/tool/handlers/websearch-results.ts:110)。给模型的内容是 “Web search results for query” 加摘要,再列至多 20 条链接,最后一句 “REMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.”(websearch-results.ts:14:42)。服务端实际搜了几次,取自用量里的 server_tool_use.web_search_requestsapps/zcode-cli/packages/adapters/src/model/runner-normalization.ts:21)。

工具描述每次读取时现算当前月份,并要求回答末尾附 “Sources:” 链接列表(handlers/websearch.ts:49:136)。描述第一句写着 “US-only”,但从代码看,能不能搜只取决于模型配置,实际后端是各家提供方自己的搜索。

错误与给模型的提示

WebFetch 自己抛的错误都经 webFetchError 构造:类型是可恢复的 ToolExecutionFailed,上下文里带一个 webFetchCode,只有两类标记为可重试(apps/zcode-cli/packages/core/src/tool/handlers/webfetch-errors.ts:20:24):

代码触发可重试
webfetch_invalid_urlURL 过长、无法解析、主机名不合法
webfetch_unsupported_protocol不是 http 或 https
webfetch_credentials_in_urlURL 带用户名或密码
webfetch_unsafe_redirectLocation 不是合法 URL
webfetch_egress_blocked本地主机名、非公网 IP 字面量,或出口代理拦截
webfetch_too_many_redirects同主机重定向超过 10 次
webfetch_response_too_large响应超过 10 MiB
webfetch_fetch_failed网络错误、不支持的内容类型
webfetch_processing_failed模型提炼失败

映射表里还有 webfetch_missing_redirect_location,但没有抛出点。不支持的内容类型也归入可重试的 fetch_failed,重试并不会有别的结果。网络错误的文案会顺着 cause 链找到最底层的原因和错误码拼上去,免得只剩一句 “fetch failed”(webfetch-network.ts:281:305)。

非 2xx 响应不算错误:工具返回一段以 “The server returned HTTP” 开头的说明,数字形式的 Retry-After 会附上,并提示需要认证的页面改用 gh 或带认证的 MCP 工具(handlers/webfetch.ts:161webfetch-network.ts:220)。每次 GET 前后还会发一条 NetworkRequestStatus 会话事件,带上是否走代理、是否用了自定义 CA 等出口信息(webfetch-network.ts:58:82apps/zcode-cli/packages/adapters/src/http/index.ts:283)。

WebSearch 的失败要少得多:没有模型、模型不支持原生搜索分别报配置错误,后者可恢复(handlers/websearch.ts:64:71);流里的错误原样抛出,非 Error 对象包成 “WebSearch stream failed”(handlers/websearch.ts:215)。

下一篇:Bash:解析、只读判定与后台任务——命令怎样被解析、哪些算只读、超时怎么定,以及长命令怎样转入后台。

本页目录