DeepSeek Harness 源码解析 · 第 19 讲
DeepSeek Harness 源码解析系列 第 19 讲:Web 能力(搜索/抓取) 基于 DeepSeek Harness 源码 · 2026-09-03 💡 本讲一句话: Web 能力是"能力缝 + provider"范式的又一次完整落地—— ctx.web 这条缝只拥有 选择、限额与错误语义 ,搜索/抓取怎么跑全交给 provider。读完你能带走五件事: provider 选择为什么"绝不依赖注册顺序" 、 web_search 本质是一次模型回合而非 API 调用 、 跨源重定向为什么被当作"需要重新授权的新请求"拒绝 、 字节上限与字符上限为什么要分两层 ,以及 HTML→Markdown 转换里那个防 DoS 的嵌套深度守卫 。 一、一条缝、三个搜索 provider、一个抓取 provider packages/web/ 下有六个包,分工和前面讲过的 shell、fs、subagent 完全同构: 🔹 dsh-web ( web/ ):能力缝 Service Definition,注册为 ctx.web ——provider 注册表 + 执行期选择 + 结果限额。 🔹 dsh-web-search-deepseek / -exa / -perplexity :三个搜索 provider,各自封装一种后端的 wire format,互相不知道对方存在。 🔹 dsh-web-fetch-http :本地匿名 HTTP(S) 抓取 provider——URL 校验、同源重定向、字节/字符限额、解码,全在这一层。 🔹 dsh-tool-web :模型可见的 web_search / web_fetch 两个工具——schema、参数校验、system prompt 引导、结果渲染, 从不碰网络 。 先看这条缝对 provider 的契约( packages/web/web/src/types.ts ,第 101-119 行): 📄 packages/web/web/src/types.ts(第 101-119 行) export interface WebSearchProvider { // 搜索后端契约:id + 本地可用性检查 + 一次搜索 readonly id: string // 稳定标识,作注册表键;重复 id 在注册时就被拒绝(WEB_DUPLICATE_PROVIDER) available(): boolean // 廉价的本地可用性检查(如"有没有 API key"),明确禁止发网络请求 search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> // 执行一次搜索,signal 用于协作式取消 export interface WebFetchProvider { // 抓取后端契约:与搜索同形,只是请求/结果类型不同 readonly id: string // 稳定标识;在 fetch 能力域内唯一 available(): boolean // 本地可用性检查;匿名公共抓取器恒返回 true(没有凭证可查) fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> // 取一个 URL,signal 用于协作式取消 为什么这样设计: 两个接口刻意保持同形( id + available() + 一个带 AbortSignal 的方法),这样缝层的选择逻辑可以写成 一份泛型代码同时服务搜索和抓取 ——后面第二节的 resolveProvider<P> 就是靠这个同形性成立的。 available() 被要求"不发网络请求",是因为它会在每次执行时被调用(选择期),如果它自己发请求,取消语义和超时预算就全乱了。而 WebFetchRequest 只有 url 一个字段——超时、格式、提示词这些"上层关切"被刻意排除在请求类型之外:取消走 signal,呈现归 tool-web,缝层保持最小。 二、provider 选择:绝不依赖注册顺序 packages/web/web/src/index.ts …