联网工具与 Code Mode
webfetch 的内容协商、大小与超时限制和 HTML 转 Markdown,websearch 的启用条件与 Exa、Parallel 分流,以及让模型写程序编排 MCP 工具的 Code Mode。
webfetch:抓取网页
参数有 url、format(markdown、text、html,默认 markdown)和 timeout(秒)。三个常量定下边界:
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000 // 2 minutes(packages/opencode/src/tool/webfetch.ts:9)
流程如下:
- 只接受
http://与https://开头的 URL,然后以 URL 为模式询问webfetch权限; - 按目标格式构造带 q 值的
Accept头,比如要 markdown 时依次偏好text/markdown、纯文本、HTML,User-Agent 则伪装成桌面 Chrome; - 响应头声明的
content-length与实际字节数都不得超过 5 MB; - 图片(SVG 除外)作为附件返回;HTML 在 markdown 模式下用 Turndown 转成 Markdown(ATX 标题、围栏代码块,去掉 script、style、meta、link),text 模式用 htmlparser2 抽取文本并跳过 script、style、iframe 等标签。
还有一处针对 Cloudflare 的处理(删减后):
// Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch)
const response = yield* httpOk.execute(request).pipe(
Effect.catchIf(
(err) =>
err.reason._tag === "StatusCodeError" &&
err.reason.response.status === 403 &&
err.reason.response.headers["cf-mitigated"] === "challenge",
() =>
httpOk.execute(
HttpClientRequest.get(params.url).pipe(
HttpClientRequest.setHeaders({ ...headers, "User-Agent": "opencode" }),
),
),
),
// ...
)(packages/opencode/src/tool/webfetch.ts:78)
请求头自称 Chrome,TLS 指纹却对不上,反而会被 Cloudflare 判为机器人;遇到 challenge 时就换回诚实的 opencode UA 再试一次。
webfetch 没有自己的截断逻辑,长页面交给通用截断:超过 2000 行或 50 KB 就落盘并返回预览。另外,webfetch.txt 里“HTTP 会自动升级为 HTTPS”“内容过大时可能被摘要”两句,在当前代码里都找不到对应实现。
websearch:两家搜索服务
websearch 并非总是可用,注册表每轮先判断:
export function webSearchEnabled(providerID: ProviderV2.ID, flags = { exa: false, parallel: false }) {
return (
providerID === ProviderV2.ID.opencode ||
providerID === ProviderV2.ID.make("opencode-go") ||
flags.exa ||
flags.parallel
)
}(packages/opencode/src/tool/registry.ts:58)
也就是:当前模型来自 opencode 自家的提供方(opencode、opencode-go),或者打开了 OPENCODE_ENABLE_EXA(总开关 OPENCODE_EXPERIMENTAL 与旧名 OPENCODE_EXPERIMENTAL_EXA 同样生效)、OPENCODE_ENABLE_PARALLEL 之一。
后端有 Exa 与 Parallel 两家,选择顺序是:环境变量 OPENCODE_WEBSEARCH_PROVIDER 指定的优先;否则开了 Parallel 开关用 Parallel,开了 Exa 开关用 Exa;都没开就按会话 ID 校验和的奇偶分流,同一会话始终落在同一家。
调用方式也有意思。两家都提供远程 MCP 端点(https://mcp.exa.ai/mcp、https://search.parallel.ai/mcp),但 mcp-websearch.ts 没有引入 MCP 客户端,而是手工 POST 一个 JSON-RPC 的 tools/call 请求;响应可能是 JSON 也可能是 SSE,后者逐行找 data: 前缀解析;超时 25 秒。EXA_API_KEY 与 PARALLEL_API_KEY 都是可选的。
参数上,Exa 接收 query、numResults(默认 8)、livecrawl、type(auto、fast、deep)与 contextMaxCharacters;Parallel 接收 objective、search_queries,外加会话 ID 与模型名。说明文字里的 {{year}} 每次读取时替换成当年年份,提醒模型搜“最新”信息时带对年份。
Code Mode:让模型写程序调工具
要解决的问题
MCP 服务器一多,工具定义本身就吃掉大量上下文;每个有依赖关系的调用都要模型往返一次;中间结果也全部流经上下文。packages/codemode/codemode.md 列出的目标正对着这几点:压缩大工具目录占用的上下文,免去依赖调用之间的往返,把大块中间结果留在程序里,并且只给生成的代码宿主明确授予的权限。
它是什么
packages/codemode 是一个受限的 JavaScript 解释器:先擦掉 TypeScript 语法,用 Acorn 解析,再由自有的树遍历解释器执行,不使用 eval。程序只能访问宿主提供的 tools 树(packages/codemode/README.md:58 的示例):
const order = await tools.orders.lookup({ id: "order_42" })
return { id: order.id, needsAttention: order.status !== "complete" }它支持的是为编排裁剪的子集:变量与解构、条件和循环、箭头函数、try/catch、Promise.all 等,没有模块、类、生成器、定时器和任意宿主全局对象。工具调用最多 8 个并发。工具太多时说明里只内联一部分签名(默认约 2000 个估算 token 的预算,按命名空间轮流分配),其余靠内置的 tools.$codemode.search 检索。
开启之后
设置 OPENCODE_EXPERIMENTAL_CODE_MODE=true(或总开关 OPENCODE_EXPERIMENTAL)后,V1 会发生三件事:
- 注册表动态导入
tool/code-mode.ts,多出一个execute工具,说明由一句简介加describeCatalog生成的 Code Mode 指南与 MCP 工具目录组成;没有可见的 MCP 工具时它不出现; SessionTools.resolve在添加 MCP 工具之前提前返回,MCP 工具不再逐个暴露给模型;- MCP 工具按服务器分组成命名空间,服务器名按最长前缀匹配,于是键
github_list_issues变成tools.github.list_issues。
子调用照样过权限
execute 里的每次子调用都会触发插件钩子并询问权限(删减后):
const result: CallToolResult = yield* Effect.gen(function* () {
yield* input.ctx.ask({ permission: input.entry.key, metadata: {}, patterns: ["*"], always: ["*"] })
// Deliberately mirrors McpCatalog.convertTool's transport call so the MCP service stays free of tool-loop concerns.
return yield* Effect.promise(async () => {
const raw = await input.entry.tool.client.callTool(
{ name: input.entry.tool.def.name, arguments: input.args },
CallToolResultSchema,
// ...
)
// ...(packages/opencode/src/tool/code-mode.ts:146)
权限名就是 MCP 工具的键,与直接暴露时一致,所以针对 MCP 工具的授权规则在 Code Mode 下照样生效;目录本身也先按 agent 与会话的合并规则集过滤(Permission.visibleTools)。这对应 codemode README 所说的权限边界:解释器只负责把程序关在给定的工具树里,授权始终是宿主和叶子工具的事。
子调用结果优先取 structuredContent,其次是文本;图片、音频和二进制资源不进解释器,由宿主收集后挂到 execute 的结果上。onToolCallStart、onToolCallEnd 两个钩子把每个子调用的状态写进 metadata.toolCalls,TUI 据此显示嵌套调用进度。V1 没有给解释器设超时、调用次数或输出上限:用户中止会打断整个执行,输出长度交给通用截断,README 也点名说 OpenCode 属于这种宿主。
V2 里 Code Mode 由 core 注册表统一接管,延迟加载的工具组被收进保留的 execute 工具,见 V2 工具与权限。
下一篇:权限系统——规则怎么求值、“总是允许”存在哪里,以及 --auto 到底放开了什么。