夜雨聆风学习资料网

ARTICLE · 1276239

OpenCode V2 也要一切皆插件?

OpenCode V2 也要一切皆插件?

打开 OpenCode V2,敲一句话。

此刻有 90 个插件在同时工作——模型供应商、配置加载、工具、提示词、联网搜索,全是插件。

这些不是第三方装的,是它自己带的。

所以问题就来了:OpenCode V2 到底有多少东西是插件?为什么要把自己拆成这样?官方管这套架构叫什么?

这篇一次讲清楚。

一、先看 V1:同样的事,它是怎么做的

要理解 V2 为什么这么设计,得先看 V1 的代码长什么样。

V1 最新版本是 1.19.4。它的模型供应商适配,全在一个文件里:

// packages/opencode/src/provider/provider.ts
import { createAmazonBedrock } from"@ai-sdk/amazon-bedrock"
import { createAnthropic } from"@ai-sdk/anthropic"
import { createAzure } from"@ai-sdk/azure"
import { createGoogleGenerativeAI } from"@ai-sdk/google"
import { createVertex } from"@ai-sdk/google-vertex"
import { createOpenAI } from"@ai-sdk/openai"
import { createOpenRouter } from"@openrouter/ai-sdk-provider"
import { createXai } from"@ai-sdk/xai"
import { createMistral } from"@ai-sdk/mistral"
import { createGroq } from"@ai-sdk/groq"
// ……还有十几个

然后是 BUNDLED_PROVIDERS 映射表,再然后是一个叫 custom() 的函数——近 800 行,按 anthropic、openai、azure、amazon-bedrock 一个个分支写特例。

整个文件 2097 行。

同一时期,其他核心文件也不小:

文件
行数
负责什么
provider/provider.ts
2097
所有模型供应商
config/config.ts
701
所有配置
tool/registry.ts
455
所有工具
session/system.ts
154
所有提示词注入

system.ts 最典型,就是个 if-else 链:

exportfunctionprovider(model: Provider.Model) {
if (model.api.id.includes("muse")) return [PROMPT_META]
if (model.api.id.includes("gpt-4") || ...) return [PROMPT_BEAST]
if (model.api.id.includes("gpt")) {
if (model.api.id.includes("gpt-6")) return [PROMPT_ASTRA]
if (model.api.id.includes("codex")) return [PROMPT_CODEX]
return [PROMPT_GPT]
  }
if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
// ……
return [PROMPT_DEFAULT]
}

想支持一个新模型,就在这里加一行 includes。

V1 的插件系统也帮不上忙。 它只有一套钩子:

exporttypePlugin = (input: PluginInput, options?: PluginOptions) =>Promise<Hooks>

exportinterfaceHooks {
  dispose?: () =>Promise<void>
  event?: (input: { event: Event }) =>Promise<void>
  config?: (input: Config) =>Promise<void>
  tool?: { ... }
  auth?: AuthHook
  provider?: ProviderHook
}

插件的职责是「在事件前后插一脚」,不能提供功能。所以 V1 内置的 10 个插件全是认证类:Codex、Copilot、GitLab、Poe、Cloudflare、Azure、DigitalOcean、xAI、Snowflake。

真正的功能——33 家供应商、14 个工具、18 类配置——全在核心代码里硬编码。

加一家供应商要做什么?改 import、改映射表、改 custom() 分支、处理特例。四步全在同一个文件里。

二、V2 的 90 个内置插件

V2 把这些全拆了。

启动时可以通过本地接口拿到完整清单,90 个内置插件,分六类。

模型供应商:33 个

opencode.provider.*,每个供应商对应一个插件:

插件
做什么
能否关闭
amazon.bedrock
AWS Bedrock
可
azure
Azure OpenAI
可
cerebras
Cerebras 超快推理
可
chatgpt
ChatGPT 订阅登录
可
cloudflare.ai.gateway
Cloudflare AI Gateway
可
cloudflare.workers.ai
Cloudflare Workers AI
可
cohere
Cohere
可
databricks
Databricks
可
digitalocean
DigitalOcean
可
github.copilot
GitHub Copilot
可
gitlab
GitLab Duo
可
google.vertex
Google Vertex AI
可
kilo
Kilo Code
可
llmgateway
LLM Gateway
可
lmstudio
LM Studio(本地)
可
modal
Modal
可
neon
Neon
可
nvidia
NVIDIA NIM
可
ollama
Ollama(本地)
可
opencode
OpenCode 官方 Zen
不可
openai
OpenAI
可
openai-context-caps
ChatGPT 上下文上限修正
可
openrouter
OpenRouter
可
perplexity
Perplexity
可
poe
Poe
可
prompt-cache-key
提示词缓存 key
可
sap.ai.core
SAP AI Core
可
snowflake.cortex
Snowflake Cortex
可
vercel
Vercel AI Gateway
可
vllm
vLLM(本地)
可
xai
xAI Grok
可
zenmux
ZenMux
可
dynamic
动态供应商
可

「能否关闭」这一列,对应的是 plugins 配置里的负号写法:

{"plugins":["-opencode.provider.openai-context-caps"]}

标「不可」的两个——opencode.provider.opencode 和 opencode.config.policy——是官方特意保护的:前者是 Console 连接,负责下发组织策略;后者负责策略执行。仓库配置不能把它们关掉,否则有人提交一个配置文件就能绕过组织策略。

配置加载器:18 个

opencode.config.*,每个配置段落对应一个插件:

插件
做什么
能否关闭
agent
Agent 定义
可
command
自定义命令
可
compaction
压缩策略
可
compatibility
版本兼容迁移
可
formatter
格式化器
可
image
图像处理
可
instruction
指令文件
可
location-watcher
目录监听
可
mcp
MCP 服务器
可
policy
权限策略
不可
provider
供应商配置
可
reference
@ 引用
可
shell
Shell
可
skill
技能
可
snapshot
快照
可
tool-output
输出限额
可
websearch
联网搜索
可
worktree
工作树
可

工具:14 个

opencode.tool.*,每个工具一个插件:

插件
做什么
能否关闭
read
读文件
可
write
写文件
可
edit
改文件
可
glob
文件名匹配
可
grep
内容搜索
可
shell
执行命令
可
patch
打补丁
可
question
向用户提问
可
skill
加载技能
可
subagent
子代理
可
todo
待办
可
webfetch
抓网页
可
websearch
联网搜索
可
input-repair
修复模型生成的畸形参数
可

提示词:7 个

opencode.prompt.*:

插件
做什么
能否关闭
openai
GPT 系提示词
可
anthropic
Claude 系提示词
可
kimi
Kimi 提示词
可
arcee
Trinity 提示词
可
meta
Muse 提示词
可
verbosity
详略控制
可
identity
身份注入
可

搜索后端:5 个

opencode.websearch.*,每个搜索服务一个插件:

插件
做什么
能否关闭
exa
Exa 搜索
可
firecrawl
Firecrawl 抓取
可
parallel
Parallel 搜索
可
tavily
Tavily 搜索
可
tinyfish
TinyFish 搜索
可

运行时能力:13 个

插件
做什么
能否关闭
agent
Agent 运行时
可
plan
计划模式
可
command
命令运行时
可
skill
技能运行时
可
vcs.git
Git 集成
可
vcs.hg
Mercurial 集成
可
models.dev
模型目录同步
可
mcp.codemode
代码模式默认值
可
browser
浏览器能力
可
warming
会话预热
可
wellknown
远程配置拉取
可
tools
工具注册表
可
mcp-resources
MCP 资源读取
可

三、和 V1 的区别

同样一件事,两代的实现方式完全不同:

维度
V1.19.4
V2.2.0
供应商
1 个文件 2097 行,22 个静态 import
33 个插件文件 + 573 行协调层
配置
1 个 schema 701 行,一次解析
18 个插件,分级加载
工具
静态数组 [...builtin, ...custom]
可变注册表,支持增删改
提示词
1 个 if-else 函数
7 个独立插件
搜索
硬编码 2 家
5 个后端插件
插件能做什么
只能挂钩子
能改状态、加工具、加供应商
内置插件数量
10 个,全是认证
90 个,覆盖全部功能

最直观的差别是「加一家供应商」这件事。

V1 要改四步,全在别人的文件里。

V2 只要新建一个文件,写几十行:

exportconstOpenAIPlugin = define({
id: "opencode.provider.openai",
effect: Effect.fn(function* (ctx) {
yield* ctx.integration.transform((editor) => {
      editor.method.update(browser(ctx.app))   // 浏览器登录
      editor.method.update(headless(ctx.app))  // 设备码登录
    })
yield* ctx.provider.transform((providers) => {
      providers.update("openai", (provider) => {
        provider.headers = Provider.mergeHeaders(provider.headers, { originator: "opencode" })
      })
    })
yield* ctx.model.transform((models) => { /* 按账号启停模型 */ })
yield* ctx.session.hook("model.request", (evt) => { /* 改请求头 */ },
      { providerID: "openai" })
  }),
})

不碰别人的代码,只在注册表里加一行 import。

四、90 个插件怎么不打架

这是 V2 最核心的设计。

90 个插件都要改同一份状态:改供应商列表、改工具注册表、改配置。如果每个插件都直接改,顺序一变就全乱。

V2 的做法,官方文档叫 transforms(变换):

Transforms are synchronous edits to OpenCode's domain state, and each builds on earlier registrations in its domain.

翻译过来:变换是对 OpenCode 领域状态的同步编辑,每一个都建立在同领域更早的注册之上。

关键在于它不是「改」,而是「重放」:

Any registration, removal, or reload() marks the registry changed; the next read rebuilds it by replaying every active transform in registration order onto a fresh value.

任何注册、移除或 reload 都会标记注册表已变更;下一次读取时,会按注册顺序把所有活跃的 transform 重放到一个全新的值上。

代码层面是这样:

// 每次读取,从空状态开始,把所有 transform 按顺序重放
constget = () => {
const next = options.initial()      // 空状态
const editor = options.editor(next)
for (const transform of transforms) {
    transform.run(editor)             // 每个插件贡献的变换
  }
return next                          // 全新值,旧值不受影响
}

插件不持有状态,只注册一个变换函数:

yield* ctx.tool.transform((editor) => {
  editor.add({ name: "todowrite", ... })
})

这个设计带来四个好处:

1. 热重载

改插件 → 卸掉它的 transform → 下次读取自然重建。不用重启进程。

2. 失败隔离

单个插件抛错,把这一组的 transform 同步剥离,其余插件不受影响:

try { transform.run(editor) }
catch (cause) {
if (!transform.group) throw cause
disable(transform.group, { state, cause })
}

插件状态变成 failed,UI 里显示错误,OpenCode 还能用。

3. 顺序可编排

transform 按注册顺序重放,所以内置插件分成 pre 和 post 两阶段:

const pre = [
ToolInputRepairPlugin,
ConfigWorktreePlugin,
ConfigMcpPlugin,
  ...
  ...ProviderPlugins,
  ...WebSearchPlugins,
  ...OptimizePlugin.Plugins,
  ...各种 Tool 插件,
]

const post = [
ConfigInstructionPlugin,
ConfigReferencePlugin,
  ...
ConfigPolicyPlugin,
]

为什么要分两阶段?因为配置加载本身也是插件。需要配置才能初始化的东西,只能排在后面。

4. 状态可回滚

每个 transform 绑在作用域上,作用域关闭即移除,不需要手写 undo。

官方文档还特别提醒插件作者:

Keep transforms cheap and repeatable. A value you have already read is never modified by later rebuilds.

变换要轻、要可重复。你已经读到的值,不会被后续重建修改。

用个比喻:V1 是大家轮流改同一张纸;V2 是每个人写一张透明胶片,读的时候按顺序叠起来看。

五、和 DSH 的区别

同样是把产品拆成插件,DeepSeek 官方的 agent 框架 DSH(DeepSeek Harness)走了另一条路。

先说结论:

  • OpenCode V2:插件化是为了方便自己开发扩展
  • DSH:插件化是为了让用户自由搭建

这两种目标的差别,直接体现在组合方式上。

OpenCode:代码里的数组

V2 的 90 个内置插件,顺序写死在源码的 pre / post 数组里。

用户可以启用或禁用(plugins 里加 -id),但不能重排、不能替换某一行实现。

插件通过 transforms 修改状态,本质是「内核的一部分」。

DSH:配置里的树

DSH 把运行中的应用描述成一棵由有序层叠出来的插件树:

profile 的 bundles
  → cordis.patch.yml
    → home patch
      → --patch 覆盖层
  • profile:一套命名组合(web、headless、sdk、acp)
  • bundle:配置行 + 挂载代码的发行包
  • patch:按行 id 替换整行配置,或插入新行

整棵树可以直接打印:

dsh --profile web --dump-config

输出里的每一行,都能被上层 patch 换掉。

插件通过注册服务、监听事件工作,本质是「往清单上写一行」。

放在一起看

OpenCode V2
DSH
插件化目的
方便自己开发扩展
让用户自由搭建
组合方式
代码里的 pre/post 数组
YAML 层叠(profile/bundle/patch)
发行后能改吗
只能启用/禁用
可以按行 patch
扩展方式
变换状态
注册服务 + 监听事件
会话级隔离
无(进程级全局)
有(isolate realm)
技术底座
Effect(Layer/Scope)
Cordis(Service/Fiber)

一个把「可组装」做到了代码层:插件是数组里的元素,顺序由源码决定。

一个做到了产品层:整个应用是一棵 YAML 树,启动时按层叠出来。

对普通用户来说这个差别几乎无感——装哪个都是个能用的编程工具。但对想改它的人来说,差别很大。

六、为什么这么设计?官方怎么说

先说动机。

V1 的 provider.ts 已经 2097 行,tool/registry.ts 455 行,config.ts 701 行还在往外拆包。当单文件逼近两千行、每加一家供应商都要改同一个文件时,插件化就不再是架构偏好,而是唯一出路。

至于好处,从前面的拆解能看到四条:

  1. 改动隔离:加供应商只动自己的文件
  2. 可单独关闭:任何一个内置插件都能用 -id 禁用
  3. 失败隔离:一个插件坏了,不影响其他
  4. 插件 API 不落后:内置功能用同一套接口,第三方能做的,官方自己也做

第四条最关键。只有自己的功能也跑在同一套接口上,插件 API 才不会永远落后于产品一个版本。

那官方管这套架构叫什么?

我查了 V2 的文档、changelog 和代码,没有找到「一切皆插件」这种提法。

官方的用词是:

1. built-in plugins(内置插件)

文档里明确写了:

Two built-in plugins ignore removals so that a repository cannot switch off policy enforcement

CLI 里也有对应的命令:

opencode plugin list --builtin

2. transforms(变换)

官方对插件工作机制的描述:

Transforms are synchronous edits to OpenCode's domain state

3. replay(重放)

the next read rebuilds it by replaying every active transform in registration order onto a fresh value

所以更准确的说法是:

OpenCode V2 把内置功能做成了 built-in plugins,插件通过 transforms 修改领域状态,状态由 replay 重建。

「一切皆插件」是我们对这种结构的概括,不是官方口号。

但它确实做到了——90 个内置插件,覆盖供应商、配置、工具、提示词、搜索、运行时,一个不落。

总结

回头看 V1 到 V2 的变化:

  • provider.ts 从 2097 行变成 33 个插件
  • system.ts 从 if-else 链变成 7 个插件
  • 工具注册表从静态数组变成可变注册表
  • 插件从「只能挂钩子」变成「能改状态」

这不是简单的代码搬家,而是把「核心 + 分支」换成了「宿主 + 变换」。

宿主只负责提供状态容器和生命周期,功能由 90 个插件拼出来。

这大概是 V2 这一轮重构里最值得记住的决策。


注:本文数据来自 OpenCode v1.19.4 与 v2.2.0 源码、OpenCode V2 官方插件文档,以及运行时 /api/plugin 接口返回。DSH 处于 developer preview,接口可能变化。

相关学习资料