web 访问接缝
packages/web/web 定义 ctx.web——WebRuntime 服务,一个接缝、两个注册表。与单实现的 shell/fs 接缝不同,ctx.web 是 Provider 选择式的:search 与 fetch Provider 各自在稳定 id 下注册,接缝在执行期决定用哪个 Provider。所有 web 包都位于 packages/web/。
tool-web (web_search / web_fetch) (消费者)
│
┌────────────────▼─────────────────┐
│ 服务定义:ctx.web │ @deepseek-ai/dsh-web
│ WebRuntime —— 两个注册表 │
└──────────┬──────────────────┬────┘
search │ │ fetch
┌───────────┼───┐ ┌─────┴──────────────┐
▼ ▼ ▼ ▼ ▼
web-search- web- web-search- web-fetch-http
deepseek exa perplexity (id 'http')
(deepseek-official)各包版本
| 包 | Provider id | 角色 |
|---|---|---|
@deepseek-ai/dsh-web | — | 服务定义(WebRuntime、ctx.web、WebError) |
@deepseek-ai/dsh-web-fetch-http | http | Fetch Provider:匿名公共 HTTP(S) |
@deepseek-ai/dsh-web-search-deepseek | deepseek-official | Search Provider:DeepSeek Anthropic 兼容 API |
@deepseek-ai/dsh-web-search-exa | exa | Search Provider:Exa |
@deepseek-ai/dsh-web-search-perplexity | perplexity | Search Provider:Perplexity |
@deepseek-ai/dsh-tool-web | — | 消费者:web_search/web_fetch 模型工具 |
Provider 选择
WebRuntime 配置钉定每个能力哪个 Provider 胜出:searchProvider/fetchProvider,两者都可选。操作环境变量喂给同一个字段——$DSH_WEB_SEARCH_PROVIDER/$DSH_WEB_FETCH_PROVIDER 与配置键等价,不是一条隐藏的优先级链。resolveProvider 在执行期调用,绝不依赖注册顺序:
- 配置的 id 已注册且
available()→ 该 Provider。 - 配置的 id 未注册 →
WEB_PROVIDER_CONFIGURED_MISSING。 - 配置的 id 已注册但不可用 →
WEB_PROVIDER_CONFIGURED_UNAVAILABLE。 - 无 id,恰好一个可用 Provider → 该 Provider。
- 无 id,多个可用 →
WEB_PROVIDER_AMBIGUOUS。 - 无 id,无可用 →
WEB_PROVIDER_UNAVAILABLE。
available() 是廉价的本地检查,不得发网络调用(例如 Exa 检查非空 API 密钥与合法的 base URL)。WebRuntime 接缝还会在搜索结果上强制执行 maxResults(截断 sources[] 并置 truncated)。
WebRuntime 接口
WebRuntime.search(request, signal?) 与 WebRuntime.fetch(request, signal?) 是两个执行动词,解析出实现 WebSearchProvider/WebFetchProvider 的 Provider——各自为 { id, available(), search|fetch(request, signal) }。规范化结构:
// 搜索
interface WebSearchRequest { query: string; maxResults?: number }
interface WebSearchResult {
content?: string // Provider 生成的答案文本(仅 Perplexity)
sources: readonly WebSearchSource[] // { url, title?, snippet?, publishedAt? }
truncated: boolean
}
// 抓取
interface WebFetchRequest { url: string } // 刻意不设 timeout/format/prompt 旋钮
interface WebFetchResult {
url: string // 重定向后的最终 URL
statusCode: number // 非 2xx 是结果而非异常
body: WebFetchBody // { kind: 'html'|'text', content } —— 封闭联合
truncated: boolean
}注意两个刻意的契约:非 2xx 的抓取是结果,不是错误(状态码是资源状态的一部分——WebError 只留给「未能安全获取或呈现资源」的情况);以及 WebFetchBody 是 dsh-web 持有的封闭判别联合,因此新增一种 kind 是对已知包的协调变更,消费者以 assertNever 结尾 switch 于 kind。
面向模型的工具
@deepseek-ai/dsh-tool-web 注册 web_search 与 web_fetch,两者都通过配置启用(search/fetch 布尔,默认均为 true)。启用控制注册;启用的工具在 Provider 不可用时仍可见,执行期以结构化的 WebError 失败。
工具配置:searchMaxResults(默认 WEB_SEARCH_MAX_RESULTS = 8,这个上限也作为每次接缝请求的 maxResults 发送)、fetchTimeoutMs/searchTimeoutMs(默认 30_000,作为 ToolDefinition.timeoutMs 挂给工具,供工具调用超时策略使用)、fetchMaxOutputChars(默认 200_000,解码后的抓取输出与每源字符的上限)。
web_search——一个参数:query。返回规范化来源;WebError代码暴露在结构化错误元数据中。输出渲染为带围栏的来源链接 + 摘要。web_fetch——一个参数:url。返回最终 URL、状态码与解码后的 body(html经 turndown GFMD 转成 Markdown,或text),受 body 上限约束,带truncated标志。
结果如何渲染给模型
formatSearchOutput(packages/web/tool-web/src/search.ts)把规范化的 WebSearchResult 转成模型看到的 markdown,顺序稳定:
<content?> # 先放 Provider 答案文本(若存在,Perplexity)
Sources:
- [label](url) — snippet (publishedAt)
… # 每个来源一个条目,受 searchMaxResults 限制
(Showing the first N sources. Refine the query for more.) # 仅当截断时
Cite the relevant URLs above as markdown links in your answer.仅 URL 的来源在有 title 时渲染 title,否则渲染 hostname(sourceLabel)。结尾那句 "Cite the relevant URLs above…" 让出处保留在模型的回答里。
web_fetch 通过共享的 turndown 转换器(fetch.ts)把 HTML 转成 Markdown,分类 body kind,formatFetchOutput 渲染 URL + 状态码 + body,受 fetchMaxOutputChars 限制并上报 truncated。工具展示层在回放时从 presentationMeta 重新推导 { url, statusCode, truncated } 包裹,因此完成的视图无需重新抓取。
Fetch Provider:web-fetch-http
web-fetch-http 注册一个匿名公共 HTTP(S) WebFetchProvider(id: 'http',LOCAL_FETCH_PROVIDER_ID)。它是函数/命名空间插件(不是 default-export 服务),注册进接缝的 fetch 注册表。配置(全部有默认值):
| 键 | 默认 | 含义 |
|---|---|---|
maxUrlLength | 2048 | 接受的请求 URL 最大长度 |
maxResponseBytes | 5_000_000 | 响应体最大字节数 |
maxBodyChars | 100_000 | 解码 body 最大字符数 |
timeoutMs | 30_000 | 默认抓取超时 |
maxRedirects | 5 | 同源重定向跳数上限 |
userAgent | deepseek-harness/0.0.1 (+https://github.com/deepseek-ai) | 明确的产品代理,绝不是浏览器伪装 |
传输卫生策略位于 packages/web/web-fetch-http/src/policy.ts——纯且无网络的一半(validateFetchUrl、isSameOrigin、content-type 分类):
- 仅 http(s);URL 中内嵌凭据被拒(
WEB_BLOCKED_URL);长度有界(WEB_INVALID_URL)。 - 重定向必须保持同源——跨源一跳被拒绝,于是每个新源都需要一次全新的工具调用(也就有一次全新的 Provider/权限决策)。
- Content-Type 分类:
text/html/application/xhtml+xml→html;其他text/*加若干结构化文本类型 →text;二进制/不支持 → 不可解码。 - SSRF/私网阻断暂缓(尚未强制)——记录在包 Agent Note 里。
搜索 Provider
每个 Provider 住在自己的包里,在稳定 id 下注册,并把结果规范化进 WebSearchSource[](来源必有 URL;title/snippet/publishedAt 可选,因为并非每个 Provider 都返回——逼适配器编造会让接缝撒谎)。Provider 生成的答案文本(content)仅由 Perplexity 产出。
DeepSeek(web-search-deepseek,id deepseek-official)
调用 DeepSeek 的 Anthropic 兼容 Messages API——不是 LLM 层使用的 chat-completions 底座,因此它不复用 $DEEPSEEK_BASE_URL;只共享 API 密钥。默认:base https://api.deepseek.com/anthropic/v1(再追加 /messages)、模型 deepseek-v4-flash、API 版本 2023-06-01、max_tokens 4096、max_uses 5。它在一个 Messages 请求中使用 Anthropic 的 web_search_20250305 服务端工具。配置:apiKey(字面密钥)、apiKeyEnv(环境变量名,默认 DEEPSEEK_API_KEY,也可经 CredentialRef 解析)、baseURL、model、maxTokens、maxUses、apiVersion。在分发前,会把一个免密辅助请求记录到会话事件 web/deepseek-search-llm-request。
Exa(web-search-exa,id exa)
调用 Exa 的 POST /search。默认:base https://api.exa.ai、searchType 'auto'、highlightsPerResult 1。配置:apiKey(env 默认 $EXA_API_KEY)、baseURL、searchType(auto|keyword|neural)、numResults、highlightsPerResult。没有 highlight 的结果会被丢弃——接缝没有别的字段可据此推导摘要,编造就是撒谎。Exa 不返回生成的答案(content 缺席)。
Perplexity(web-search-perplexity,id perplexity)
调用 Perplexity 的 OpenAI 兼容 /chat/completions 端点,并返回生成的答案,它成为 content;其引用则成为 sources。配置:apiKey(env 默认 $PERPLEXITY_API_KEY)、baseURL、model。
限流与重试
web 包里没有接缝级的限流器或自动重试——该接缝刻意保持轻薄。Provider 的 HTTP 处理设置显式超时(web-fetch-http 经 @deepseek-ai/dsh-timeout 的 deadline(...),标签为 'WEB_FETCH_TIMEOUT'),而工具层提供协作式逐调用超时预算(fetchTimeoutMs/searchTimeoutMs),由工具调用超时策略强制。跨轮的重试/节奏是消费者/agent 循环的事,而同源重定向规则是针对无限抓取循环的安全兜底。错误是带开放字符串 code 的 WebError,并链上 cause,以结构化错误元数据暴露在工具结果上。
延伸阅读
- Shell 与终端 —— 兄弟执行器接缝,也展示
tool-web的超时如何经ToolDefinition.timeoutMs挂上。 - 文件系统工具与策略 —— 采用同样 Provider/消费者拆分的
ctx.fs接缝。 - 代码运行时 —— 程序可调用的、面向 LLM 的「tools」绑定在此定义。
- API 代理(apiproxy) —— Host API 的凭据/网络关切如何分发。
packages/web/web/src/index.ts——WebRuntime定义与选择规则。packages/web/web/src/types.ts—— 规范化的请求/结果/来源结构。