@@ -139,11 +139,137 @@ function getDeferredToolsCacheKey(deferredTools: Tools): string {
139139
140140AI 的信息获取不局限于本地代码:
141141
142- - ** WebSearch** :搜索互联网获取最新信息
143- - ** WebFetch** :抓取特定网页内容 ,转换为 Markdown 供 AI 阅读
142+ - ** WebSearch** ( ` src/tools/WebSearchTool/ ` ):调用 Anthropic API 的 ` web_search_20250305 ` server tool 搜索互联网
143+ - ** WebFetch** ( ` src/tools/WebFetchTool/ ` ):抓取特定 URL 内容 ,转换为 Markdown 供 AI 阅读
144144
145145这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
146146
147+ ### WebSearch 实现机制
148+
149+ WebSearch 通过适配器模式支持两种搜索后端,由 ` src/tools/WebSearchTool/adapters/ ` 中的工厂函数 ` createAdapter() ` 选择:
150+
151+ ```
152+ 适配器架构:
153+ WebSearchTool.call()
154+ → createAdapter() 选择后端
155+ ├─ ApiSearchAdapter — Anthropic API 服务端搜索(需官方 API 密钥)
156+ └─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥)
157+ → adapter.search(query, options)
158+ → 转换为统一 SearchResult[] 格式返回
159+ ```
160+
161+ #### 适配器选择逻辑
162+
163+ ` adapters/index.ts ` 中的工厂函数按以下优先级选择后端:
164+
165+ | 优先级 | 条件 | 适配器 |
166+ | --------| ------| --------|
167+ | 1 | 环境变量 ` WEB_SEARCH_ADAPTER=api ` | ` ApiSearchAdapter ` |
168+ | 2 | 环境变量 ` WEB_SEARCH_ADAPTER=bing ` | ` BingSearchAdapter ` |
169+ | 3 | API Base URL 指向 Anthropic 官方 | ` ApiSearchAdapter ` |
170+ | 4 | 第三方代理 / 非官方端点 | ` BingSearchAdapter ` |
171+
172+ 适配器是无状态的,同一会话内缓存复用。
173+
174+ #### ApiSearchAdapter — API 服务端搜索
175+
176+ 将搜索请求委托给 Anthropic API 的 ` web_search_20250305 ` server tool:
177+
178+ ```
179+ 调用链:
180+ ApiSearchAdapter.search(query, options)
181+ → queryModelWithStreaming() 发起独立的 API 调用
182+ → 携带 extraToolSchemas: [BetaWebSearchTool20250305]
183+ → API 服务端执行搜索,返回流式事件
184+ → server_tool_use / web_search_tool_result / text 交替返回
185+ → extractSearchResults() 从 content blocks 提取 SearchResult[]
186+ ```
187+
188+ | 特性 | 实现 |
189+ | ------| ------|
190+ | ** 模型选择** | Feature flag ` tengu_plum_vx3 ` 控制用 Haiku(强制 tool_choice)还是主模型 |
191+ | ** 搜索上限** | 每次调用最多 8 次搜索(` max_uses: 8 ` ) |
192+ | ** 域过滤** | 支持 ` allowedDomains ` / ` blockedDomains ` |
193+ | ** 进度追踪** | 流式解析 ` input_json_delta ` 提取 query,实时回调 ` onProgress ` |
194+
195+ #### BingSearchAdapter — Bing 搜索页面解析
196+
197+ 直接抓取 Bing 搜索 HTML 并用正则提取结果,无需 API 密钥:
198+
199+ ```
200+ 调用链:
201+ BingSearchAdapter.search(query, options)
202+ → axios.get(bing.com/search?q=...) — 使用浏览器级别 headers 绕过反爬
203+ → extractBingResults(html)
204+ → 正则匹配 <li class="b_algo"> 块
205+ → 提取 <h2><a> 标题和 URL
206+ → resolveBingUrl() 解码 Bing 重定向链接
207+ → extractSnippet() 三级降级提取摘要
208+ → 客户端域过滤 (allowedDomains / blockedDomains)
209+ → 返回 SearchResult[]
210+ ```
211+
212+ ** 反爬策略** :Bing 对非浏览器 UA 返回需要 JS 渲染的空页面。适配器使用完整的 Edge 浏览器请求头(包含 ` Sec-Ch-Ua ` 、` Sec-Fetch-* ` 等现代浏览器标头)确保获得完整 HTML。同时使用 ` setmkt=en-US ` 参数统一市场定位,避免 Bing 基于用户 IP 做区域化定向(如跳转到德语/新加坡市场导致结果不相关)。
213+
214+ ** URL 解码** :Bing 搜索结果中的 URL 为重定向格式(` bing.com/ck/a?...&u=a1aHR0cHM6Ly9... ` ),` resolveBingUrl() ` 从 ` u ` 参数中 base64 解码出真实目标 URL(` a1 ` 前缀 = https,` a0 ` = http)。
215+
216+ ** 摘要提取** (` extractSnippet() ` )按优先级尝试三个来源:
217+ 1 . ` <p class="b_lineclamp..."> ` — 带行截断的摘要段落
218+ 2 . ` <div class="b_caption"> ` 内的 ` <p> ` — 普通摘要段落
219+ 3 . ` <div class="b_caption"> ` 的直接文本内容 — 兜底方案
220+
221+ | 特性 | 实现 |
222+ | ------| ------|
223+ | ** 超时** | 30 秒(` FETCH_TIMEOUT_MS ` ) |
224+ | ** 域过滤** | 支持 ` allowedDomains ` / ` blockedDomains ` ,含子域名匹配 |
225+ | ** 进度追踪** | 发送 query_update 和 search_results_received 回调 |
226+ | ** 中止支持** | 外部 AbortSignal 传播到 axios 请求 |
227+
228+ ### WebSearchTool 统一接口
229+
230+ ` WebSearchTool ` (` src/tools/WebSearchTool/WebSearchTool.ts ` )是面向主循环的工具定义,所有 provider 均可使用(` isEnabled() ` 始终返回 true)。它将适配器返回的 ` SearchResult[] ` 转换为内部 ` Output ` 格式,` mapToolResultToToolResultBlockParam ` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
231+
232+ ### WebFetch 实现机制
233+
234+ WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:
235+
236+ ```
237+ 调用链:
238+ WebFetchTool.call({ url, prompt })
239+ → getURLMarkdownContent(url)
240+ → validateURL() — 长度≤2000、无用户名密码、公网域名
241+ → URL_CACHE 命中检查(15 分钟 TTL LRU,50MB 上限)
242+ → checkDomainBlocklist() — 调用 api.anthropic.com/api/web/domain_info 预检
243+ → getWithPermittedRedirects() — axios 请求,自定义重定向处理
244+ → HTML → Turndown 转 Markdown(懒加载单例,~1.4MB)
245+ → 非 HTML → 原始文本
246+ → 二进制(PDF 等)→ persistBinaryContent() 保存到磁盘
247+ → applyPromptToMarkdown()
248+ → 截断到 100K 字符
249+ → queryHaiku() 用小模型按 prompt 提取信息
250+ → 返回处理后的结果
251+ ```
252+
253+ 安全防护多层设计:
254+
255+ | 层级 | 机制 | 说明 |
256+ | ------| ------| ------|
257+ | ** 域名预检** | ` checkDomainBlocklist() ` | 调用 ` api.anthropic.com/api/web/domain_info?domain=… ` ,5 分钟缓存 |
258+ | ** 重定向控制** | ` isPermittedRedirect() ` | 仅允许同 host(±www)重定向,跨域重定向返回提示让 AI 重新调用 |
259+ | ** 重定向深度** | ` MAX_REDIRECTS = 10 ` | 防止重定向循环无限挂起 |
260+ | ** 内容大小** | ` MAX_HTTP_CONTENT_LENGTH = 10MB ` | 单次响应上限 |
261+ | ** 请求超时** | ` FETCH_TIMEOUT_MS = 60s ` | 主请求超时;域名预检 10s |
262+ | ** URL 验证** | ` validateURL() ` | 长度、协议、用户名密码、公网域名检查 |
263+ | ** egress 检测** | ` X-Proxy-Error: blocked-by-allowlist ` | 检测企业代理拦截 |
264+
265+ 预批准域名(` src/tools/WebFetchTool/preapproved.ts ` ):
266+
267+ 用户无需手动授权即可抓取的域名列表,包含 ~ 90 个主流技术文档站点(MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。
268+
269+ 对预批准域名,WebFetch 跳过 Haiku 摘要步骤(如果内容是 Markdown 且 < 100K 字符),直接返回原文——因为技术文档本身的结构化程度已经足够好。
270+
271+ 权限模型方面,WebFetch 按 hostname 生成 ` domain:xxx ` 规则匹配用户的 allow/deny/ask 规则,支持用户对特定域名配置永久允许或拒绝。
272+
147273### ripgrep 的流式输出
148274
149275对于交互式场景(如 QuickOpen),ripgrep 支持** 流式输出** (` ripGrepStream() ` ):
0 commit comments