外观
第 12 章 · 高级扩展与自动化
第 5 章讲的 Agents / Commands / Skills 解决「固化提示与姿态」,但有些需求它们做不到:定义全新的工具、从代码层面拦截事件、在 CI 里无人值守调用、让 Zed 等编辑器接入。本章讲 OpenCode 更底层的五类能力:Plugins、Policies、无头模式、References、ACP。
本章包含 5 个知识点:
- Plugins:用代码扩展 OpenCode
- Policies:只收紧、不放宽的集中策略
- 无头模式:
opencode run与自动化 / CI- References:按名引用项目外目录
- ACP:让编辑器 / 图形客户端接入
知识点 1:Plugins:用代码扩展 OpenCode
① 定位
当 Markdown 定义的 Agent / Command / Skill 都不够——你需要运行真正的代码、注册自定义工具、拦截运行时事件——就轮到 Plugin。它是五层扩展机制中最底层、最强的一个。
② 是什么(What)
Plugin 是用 TypeScript / JavaScript 写的代码模块,在 opencode.jsonc 的 plugins 数组中声明:
jsonc
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
"opencode-acme-plugin", // 已发布包
"opencode-acme-plugin@1.2.0", // 指定版本
"@acme/opencode-plugin", // scoped 包
"./plugins/local", // 本地目录(相对配置文件解析)
"../shared/plugin.ts", // 直接引用 .ts/.js 文件
{
"package": "@acme/opencode-plugin", // 带选项
"options": { "agent": "reviewer", "strict": true }
}
]
}两种自动发现方式(无需写进配置):
- 把
.ts/.js文件或插件包目录直接放进.opencode/plugins/(项目级); - 全局插件放在
~/.config/opencode/plugins/。
注意:项目根的 plugins/(不在 .opencode/ 下)不会被自动发现,要么显式配置、要么挪进 .opencode/。多个配置文件的 plugins 数组按优先级叠加(不是互相替换)。
③ 怎么做(How)
- 先判断是否真需要 Plugin——能靠 Agent / Command / Skill 解决的,不必写代码;
- 简单需求先在
.opencode/plugins/放一个单文件.ts试水; - 复杂 / 需分发的逻辑做成包,在
plugins中声明并可用options传配置; - 插件的具体 API(注册工具、事件钩子、RPC)以官方 Build / Plugins 文档为准;
- 改插件后按需重启服务使其生效。
④ 为什么这么做(Why)
- Markdown 只能塑造提示,代码才能塑造能力。 Agent / Command 本质是预设文本,无法引入新的工具动作或改变运行时行为;Plugin 运行在 OpenCode 进程内,可以做到前三者做不到的事。分层是为了「用复杂度匹配需求」。
- 以包的形式分发,让插件形成生态。 可以像普通 npm 包一样发布、版本锁定、scoped 管理;
@1.2.0锁版本避免上游突变。 - 自动发现(.opencode/plugins)降低试验成本:放个文件即可,不必改配置;同时全局与项目目录分层,个人插件与团队插件互不干扰。
- plugins 数组叠加而非替换,保证全局、项目、子包的插件都能共存,延续配置层的合并哲学(第 8 章)。
- Plugin 是最后手段而非首选:它引入真正的代码依赖与维护成本,且运行权限高——官方的五层扩展是递进的,应当先用简单层。
⑤ 这样做的好处
- 能力几乎无上限:自定义工具、事件拦截、集成内部系统。
- 可版本化、可分发、可传选项。
- 试验成本低:单文件自动发现。
- 多层插件叠加共存。
⑥ 不这么做的问题 / 坏处
- 把插件放进根目录 plugins/ 却不配置 → 不生效,困惑。
- 不锁版本 → 上游更新导致行为突变。
- 能用简单扩展却写 Plugin → 过度工程、维护负担。
- 忽视插件的高权限 → 第三方插件可访问进程内资源,需像依赖一样审查。
⑦ 举一反三:三个例子
例 1(基础):单文件插件试水
📌 场景:你想在每次工具调用前后加一条自定义日志。
✅ 做法:在 .opencode/plugins/log.ts 放一个单文件插件实现该钩子,无需改配置即被发现,验证后再决定是否做成包。
🔍 讲解:自动发现就是为这种轻量尝试设计的。先单文件验证想法,避免一上来就搭包。
例 2(进阶):团队共享一个带配置的插件
📌 场景:团队需要一个连接内部工单系统的插件,各项目配置不同。
✅ 做法:发布成 scoped 包,在项目配置用 { "package": ..., "options": {...} } 传入各项目参数,锁定主版本。
🔍 讲解:包 + options 让同一份插件代码适配不同项目;版本锁定保证团队行为一致。
例 3(挑战):判断该用 Plugin 还是 Skill
📌 场景:你需要「让 AI 遵循一套复杂的部署检查流程」。
✅ 判断:若流程本质是「知识 / 步骤说明」→ 用 Skill(渐进加载说明);若需要「真正调用一个新工具、拦截事件」→ 才用 Plugin。
🔍 讲解:五层扩展的选择看「需不需要运行新代码」。不要因为 Plugin 强就什么都用它——多数流程性需求,Skill / Command 已足够。
⑧ 练习题 ×2
练习 1(巩固型)
📝 题目:插件有哪两种引入方式?.opencode/plugins/ 与项目根 plugins/ 在自动发现上有何区别?
💡 思路提示:配置数组 vs 自动发现;根目录不被发现。
✅ 参考答案:
text
① 在 opencode.jsonc 的 plugins 数组声明(包 / 路径 / 带选项对象);
② 放进 .opencode/plugins/(项目)或 ~/.config/opencode/plugins/
(全局)自动发现。
区别:.opencode/plugins 会被自动发现;项目根的 plugins/
不会,需显式配置或挪位置。🔍 讲解:记住自动发现的目录限定,可避免最常见的「插件不生效」。
练习 2(迁移型)
📝 题目:迁移到浏览器扩展:浏览器为什么同时支持「装扩展(可运行代码、改浏览器行为)」和「用户脚本 / 样式(只改页面表现)」?这与 Plugin vs Skill 的分工有何共性?
✅ 参考答案(示例):
text
- 浏览器扩展能运行代码、改变浏览器本身行为(≈ Plugin);
用户脚本 / 样式只影响页面表现(≈ Skill / 提示层);
- 能力越强、权限越大、越需要审查与版本管理;
共性:OpenCode 也按「改表现 → 改能力」分层,
按需选择、强能力最后用——这是最小复杂度原则。知识点 2:Policies:只收紧、不放宽的集中策略
① 定位
Permissions(第 7 章)是「可以 allow / deny / ask 的、按 Agent 的工具规则」。但团队 / 企业还需要一种更高优先级、不可被下游绕过、且永不弹窗的硬约束——那就是 Policies。它也可由连接的 OpenCode Console 工作统一下发。
② 是什么(What)
Policies 写在 experimental.policies 下:
jsonc
{
"$schema": "https://opencode.ai/config.json",
"experimental": {
"policies": [
{ "action": "provider.use", "resource": "openai", "effect": "deny" }
]
}
}每条语句三字段:
| 字段 | 取值 | 含义 |
|---|---|---|
action | provider.use、permission 等 | 被控制的操作 |
resource | 字符串或通配模式 | 作用对象 |
effect | allow / deny | 匹配时的决定 |
Policies 与 Permissions 的本质区别(务必记住):
- Policies 是二元的(allow/deny)、永不弹窗(没有 ask);
- Policies 只会收紧 permissions / providers 本来允许的东西,绝不会放宽;
- 被 deny 的供应商即使凭证有效,也会从模型选择列表中消失。
匹配与顺序:* / ? 同 permissions;多条匹配时最后一条生效(无特异性规则),宽语句在前、例外在后;什么都没匹配时默认允许。未通过校验的语句会被丢弃并在服务日志告警,其余照常生效。
③ 怎么做(How)
- 用 Policies 表达「组织级红线」:如禁止某供应商、禁止某类权限;
- 宽 deny 在前,再用后置 allow 开精确例外(如「全禁供应商、只留 anthropic」);
- 编辑后查服务日志确认没有语句因校验失败被丢弃;
- 团队可由 Console 工作区统一下发,避免每台机器手改;
- 不要用 Policies 去做需要询问用户的场景(它不会 ask)。
④ 为什么这么做(Why)
- 企业需要「不可被项目 / 个人绕过」的天花板。 permissions 可被各层配置叠加、还能 ask;Policies 站在更高处只收紧,保证组织红线(数据合规、供应商准入)不被下游配置意外打开。
- 二元、不弹窗,适合无人值守与强制场景。 CI / 自动化中没有人回答 ask;Policies 给确定的 allow/deny,行为可预测。
- 被 deny 的供应商从目录消失,是从源头消除选择,而非每次拦截——更彻底,也防止模型调用被禁渠道。
- 「只收紧、不放宽」是安全语义的关键。 这保证 Policies 永远不可能成为削弱安全的通道;它的存在只让边界更严。
- 默认允许 + 最后匹配生效,让规则既能精确开例外又保持简单,但也要求你把宽语句放前面(顺序错误会吞掉例外)。
⑤ 这样做的好处
- 组织红线强制生效、不可绕过。
- 行为确定、适合自动化。
- 可集中(Console)下发、统一治理。
- 语义安全:只严不松。
⑥ 不这么做的问题 / 坏处
- 用 permissions 承担组织红线 → 可能被下游配置叠加 / ask 而失守。
- 顺序写反 → 宽规则最后生效,例外失效。
- 编辑后不查日志 → 某条策略校验失败被悄悄丢弃。
- 指望 Policies 弹窗确认 → 它只会直接 allow/deny。
⑦ 举一反三:三个例子
例 1(基础):全禁供应商、只留一家
📌 场景:公司只允许数据发给 Anthropic。
✅ 策略:先 {provider.use, *, deny},再 {provider.use, anthropic, allow}——最后一条生效,只有 Anthropic 可用。
🔍 讲解:宽 deny + 后置精确 allow 是标准「白名单」写法,顺序不能反。
例 2(进阶):禁止一批公司自定义供应商
📌 场景:命名为 company-us、company-eu 的内部渠道要停用。
✅ 策略:{provider.use, company-*, deny},* 同时匹配两者及不带后缀的值。
🔍 讲解:通配结尾也匹配「无后缀」值,一条覆盖整组同源渠道。
例 3(挑战):发现某条策略没生效
📌 场景:配了 deny 但该供应商仍出现在列表。
✅ 排查:① 查服务日志,该语句是否校验失败被丢弃;② 检查顺序,是否后面有一条 allow 把它覆盖;③ 字段 / action 名是否写对。
🔍 讲解:Policies 失效通常是「校验丢弃 / 顺序被覆盖 / 拼写」三类,日志是第一现场。
⑧ 练习题 ×2
练习 1(巩固型)
📝 题目:说出 Policies 与 Permissions 的三个核心区别。多条策略匹配时如何裁决?
💡 思路提示:二元不弹窗、只收紧、供应商从目录消失;最后一条生效。
✅ 参考答案:
text
区别:① Policies 只有 allow/deny、没有 ask(不弹窗);
② 只会收紧、绝不放宽 permissions/providers 的允许;
③ 被 deny 的供应商直接从模型选择目录消失。
裁决:多条匹配时最后一条生效(无特异性),宽语句在前。🔍 讲解:理解「只收紧的硬天花板」这一角色,是区分两套机制的关键。
练习 2(迁移型)
📝 题目:迁移到公司财务制度:总部下发的「不可逾越的费用红线」与部门内「可分级审批」的规则,如何对应 Policies vs Permissions?为什么红线必须由更高层制定、且不可被部门放宽?
✅ 参考答案(示例):
text
- 总部红线:二元、强制、不可被部门放宽(≈ Policies);
- 部门审批:可按金额分级、可询问(≈ Permissions 的 ask);
- 红线必须高一层制定,否则各部门自行放宽会架空整体管控。
共性:Policies 就是 OpenCode 里由组织 / Console 下发、
只收紧不放宽的「红线层」。知识点 3:无头模式:opencode run 与自动化 / CI
① 定位
前面所有操作都假设「人坐在 TUI 前」。但脚本、CI、定时任务里没有人操作界面——需要一种提交一句 Prompt、直接拿到结果的方式。这就是无头(headless)模式,是从「人机交互」迈向「自动化集成」的关键一跃。
② 是什么(What)
核心命令是 opencode run:
bash
opencode run "Explain this repository" # 不开界面,直接输出结果常用参数(来自命令 help):
| 参数 | 作用 |
|---|---|
-m, --model | 指定模型,provider/model#variant 格式 |
--agent | 指定 Agent |
-c, --continue | 继续上一个会话 |
-s, --session | 继续指定会话 ID |
--fork | 继续前先分叉(fork)该会话 |
-f, --file | 附加一个文件 |
--format | 输出格式:default / json(脚本解析用 json) |
--title | 会话标题 |
--thinking | 显示思考块 |
--auto | 自动批准「未被显式 deny」的权限(CI 常用,但 deny 仍生效) |
--standalone / --server | 用私有服务 / 连指定服务,而非共享后台服务 |
另有 opencode mini:极简交互界面(非全屏),适合轻量场景。
③ 怎么做(How)
- 脚本里用
opencode run+--format json,把结果交给程序解析; - CI 中需要跑工具时加
--auto(自动批准非 deny 操作),但关键动作仍可用 Policies / deny 硬性拦住; - 想复用已有上下文 →
--continue或--session <id>;要避免污染原会话 →--fork; - 需要模型读文件 →
--file <路径>或在消息里用路径; - 凭证用环境变量注入(第 6、8 章),CI 中不要依赖交互登录。
④ 为什么这么做(Why)
- 同一个引擎,两种用法。 交互式 TUI 服务于「人主导的探索」,
run服务于「程序主导的调用」;底层会话、配置、工具、权限完全一致,所以你在 TUI 里调好的规则在自动化里同样生效。 --format json让输出可被机器消费。 脚本可以稳定解析结果文本、usage 等字段,而不必去解析 TUI 的屏幕字符。--continue/--session/--fork复用上下文。 自动化任务可以接着已有会话做,--fork则在不污染主线的前提下尝试——与会话分叉语义一致(第 2 章)。--auto+ Policies 的组合兼顾效率与安全。 CI 里无人应答 ask,--auto放行未被 deny 的操作,而组织红线仍由 Policies / 显式 deny 硬性兜底,不会失控。
⑤ 这样做的好处
- 可嵌入脚本、CI、定时任务、Git hooks。
- 结果可解析(json)、退出码可判断成败。
- 复用既有会话与全部配置。
--fork安全试验、不污染主线。
⑥ 不这么做的问题 / 坏处
- 在 CI 里硬开 TUI → 没有 TTY 直接挂起或失败。
- 不加
--auto又无人值守 → 遇到 ask 永远卡住。 - 不用 json 而解析人类可读输出 → 格式一变脚本就崩。
- 凭证靠交互登录 → CI 环境无法登录,任务直接鉴权失败。
⑦ 举一反三:三个例子
例 1(基础):命令行快速提问
📌 场景:你想在终端直接问一句「这个仓库是干什么的」,不进界面。
✅ 答案:
bash
opencode run "用三句话说明这个仓库的用途"🔍 讲解:run 提交一句 Prompt 后把结果打到标准输出,适合一次性、可管道化的调用。
例 2(进阶):CI 中让 AI 审查改动
📌 场景:PR 流水线里想自动审查本次 diff,并以 JSON 供后续步骤判断。
✅ 答案:
bash
opencode run --auto --format json \
"审查本次 git diff 中的潜在 bug,只报告高置信问题"🔍 讲解:--auto 让读文件 / 跑只读命令不被 ask 卡住,--format json 让流水线稳定解析;危险写操作仍可被 deny 拦住。
例 3(挑战):基于已有会话做一次不落地的尝试
📌 场景:你想让自动化接着上个会话分析,但绝不能改动原会话。
✅ 答案:
bash
opencode run --continue --fork "在分叉里尝试另一种重构思路,只做分析"🔍 讲解:--continue 复用上下文、--fork 先分叉,原会话保持干净——把「安全试验」从交互场景延伸到自动化。
⑧ 练习题 ×2
练习 1(巩固型)
📝 题目:要在无人值守的 CI 中调用 OpenCode,至少需要哪两个参数 / 做法?为什么?
💡 思路提示:让非 deny 权限不弹窗 + 让输出可解析。
✅ 参考答案:
text
① --auto:自动批准未被显式 deny 的权限,避免卡在 ask;
② --format json:输出结构化结果,供脚本稳定解析;
另外凭证应通过环境变量注入,不能依赖交互登录。🔍 讲解:无人场景的两个核心痛点是「没人点确认」和「程序要读结果」,分别由 --auto 与 --format json 解决。
练习 2(迁移型)
📝 题目:迁移到数据库 CLI:像 psql 既有交互式 REPL、又有 psql -c "SQL" 的非交互模式,这与 TUI / opencode run 的双形态有何共性?什么场景该选哪种?
✅ 参考答案(示例):
text
- REPL / TUI:适合人探索、多轮迭代、即时反馈;
- -c / run:适合脚本、批量、自动化、可被其它程序调用;
共性:同一引擎提供「人用」与「机器用」两个入口,
能力一致、按交互对象选择;
需要反复探索用人,需要编排进流程用 run。知识点 4:References:按名引用项目外目录
① 定位
@文件 只能引用当前项目内的东西。但你常需要让 AI 看隔壁仓库的文档、共享的组件库、另一个 repo 的源码。References 给这些项目外目录起一个别名(alias),之后按名挂载、按需让 Agent 查阅,而不必把外部代码复制进项目。
② 是什么(What)
在 opencode.jsonc 的 references 中声明「别名 → 位置」:
jsonc
{
"$schema": "https://opencode.ai/config.json",
"references": {
"docs": {
"path": "../product-docs",
"description": "产品行为与术语,需要时查阅"
},
"shared": "~/work/shared"
}
}支持两类位置:
1. 本地目录(path)
- 相对路径相对配置文件解析;支持绝对路径与
~/开头; - 字符串简写只有以
.、/、~开头时才被当作本地路径——用./docs,裸写docs会被当成 Git 仓库。
2. 远程 Git 仓库(repository)
jsonc
{
"references": {
"effect": { "repository": "Effect-TS/effect", "branch": "main" },
"internal-sdk": { "repository": "git@gitlab.example.com:platform/sdk.git" }
}
}- 支持 GitHub
owner/repo简写、Git URL、SCP 式 remote;不支持本地file:仓库; - 不指定 branch 时跟随远程默认分支;checkout 缓存在全局数据目录,如
~/.local/share/opencode/repos/github.com/Effect-TS/effect。
另外几个关键字段:
| 字段 | 作用 |
|---|---|
description | 告诉 Agent「这个引用何时相关」,有描述才会被自动推荐,无描述只能手动挂载 |
hidden: true | 从交互式选择器中隐藏,但仍可按名使用 |
远程引用的刷新是异步、后台、约 24 小时一次:缺失时后台克隆,存在时后台 fetch + reset;不要手动改缓存目录里的 checkout,刷新会把它重置。
③ 怎么做(How)
- 在配置里给外部资料起别名,尽量写清
description; - 需要时在客户端把该别名作为引用挂载,再让 Agent 读取其中具体文件;
- 本地共享资料用
path(注意./前缀),跨仓库 / 第三方源码用repository; - 需要固定版本就显式指定
branch,否则接受默认分支的后台刷新; - 仅想程序化使用、不出现在选择器里时设
hidden: true。
④ 为什么这么做(Why)
- 保持项目边界,又能跨界取材。 外部资料不必复制 / 软链进仓库,避免污染源码与 diff;通过别名「按需挂载」,项目保持干净。
- description 让引用「可被发现」而非「死配置」。 Agent 只有在知道「这个别名何时相关」时才会主动想到它,否则你声明了也白声明。
- Git 引用统一缓存 + 后台刷新,兼顾新鲜与零等待。 每个 remote/branch 只存一份、多位置共享;刷新是异步的,Prompt 不会被克隆阻塞(代价是可能先读到稍旧内容)。
- 24 小时节流避免频繁打扰远程。 时间戳跨重启持久、失败也计入节流,防止反复打远程。
- 显式 branch 锁定可复现,默认 branch 追求最新,两种诉求都被满足。
⑤ 这样做的好处
- 外部文档 / 共享库 / 其它仓库可按名查阅,不污染项目。
- 缓存一份、多处共享、后台自动更新。
- 有描述即可被 Agent 主动利用。
- 支持本地与远程两种来源、可锁定分支。
⑥ 不这么做的问题 / 坏处
- 把外部代码复制进项目 → 很快过期、产生同步负担与脏 diff。
- 裸写
docs(不带./) → 被误判成 Git 仓库,行为与预期完全不同。 - 不写 description → 引用不会被自动推荐,容易被遗忘。
- 手改缓存 checkout → 下次后台刷新直接重置,改动丢失。
- 假设引用实时最新 → 异步刷新可能让你先读到旧内容,需留意。
⑦ 举一反三:三个例子
例 1(基础):引用隔壁的产品文档
📌 场景:写代码时常要查放在另一个目录的产品文档。
✅ 答案:
jsonc
{ "references": { "docs": { "path": "../product-docs",
"description": "查产品行为和术语时使用" } } }挂载 docs 后让 Agent 读取其中具体文件。
🔍 讲解:本地 path + 清晰 description 是最常用形态,文档留在原处、版本单一。
例 2(进阶):参考一个开源库的官方源码
📌 场景:你想让 AI 参照 Effect 的真实用法来写代码。
✅ 答案:
jsonc
{ "references": { "effect": { "repository": "Effect-TS/effect", "branch": "main" } } }🔍 讲解:远程引用会被克隆到全局缓存并后台刷新;显式 main 让跟随的分支明确。
例 3(挑战):发现 Agent 从不主动使用某个引用
📌 场景:你声明了引用,但 AI 从来想不起用它。
✅ 排查:检查是否漏了 description——没有描述的引用不会被自动推荐,只能手动挂载。补上「何时该用它」的说明。
🔍 讲解:引用「存在」不等于「会被用」。description 是把它接进 Agent 认知的开关。
⑧ 练习题 ×2
练习 1(巩固型)
📝 题目:"docs": "../docs" 与 "docs": "docs" 的解析结果有何不同?远程引用多久刷新一次、是否阻塞 Prompt?
💡 思路提示:路径前缀决定本地 / Git;24h、异步。
✅ 参考答案:
text
"../docs" 以 . 开头 → 本地目录(相对配置文件解析);
"docs" 裸写 → 被解释为 Git 仓库。
远程引用约 24 小时后台刷新一次(缺失时异步克隆),
不阻塞 Prompt,因此可能先读到稍旧内容。🔍 讲解:前缀规则和异步刷新是 References 最容易踩的两个点,记住可避免误判与「内容怎么是旧的」的困惑。
练习 2(迁移型)
📝 题目:迁移到包管理器:npm 依赖装在项目外的全局 / 缓存目录、项目只按「名字 + 版本」引用,而不复制源码进来。这与 References 的思路有何共性?为什么「引用而非复制」通常更好?
✅ 参考答案(示例):
text
- 包管理器:源码在外部缓存,项目只声明名称 / 版本(≈ 别名 + branch);
- References:外部资料在原处 / 全局缓存,项目只声明别名;
共性:单一副本、集中维护、按需取用、不污染项目;
好处:避免复制导致的过期与不一致,升级只需改一处。知识点 5:ACP:让编辑器 / 图形客户端接入
① 定位
TUI 是 OpenCode 自带的终端界面,但你可能更想在 Zed 等编辑器 / 图形客户端里使用同一个 OpenCode 引擎。ACP(Agent Client Protocol)就是让这些外部客户端把 OpenCode 当作「agent 后端」接入的标准协议。
② 是什么(What)
客户端通过启动 opencode acp 子进程来接入。例如在 Zed 的 ~/.config/zed/settings.json 中:
json
{
"agent_servers": {
"OpenCode": { "command": "opencode", "args": ["acp"] }
}
}传输与进程模型(关键事实):
- 客户端把
opencode acp作为子进程启动,通过 stdin / stdout 交换换行分隔的 JSON-RPC,使用 ACP 协议第 1 版; - 该命令为 ACP 进程启动私有 OpenCode 服务——不连共享后台服务、也不暴露网络端口;
- 一个进程可同时服务多个 ACP 会话,直到客户端关闭 stdin 才退出。
会话与能力:
- 客户端创建会话时提供工作目录,OpenCode 会先加载该目录的配置、插件、模型、Agent、命令、技能、指令与 MCP;
- 支持会话的创建 / 列出 / 载入 / 恢复 / 分叉 / 关闭 / 删除,以及取消正在执行的 Prompt、流式输出文本 / 推理 / 工具调用 / 权限请求 / usage;
- 已存在会话里记录的目录是权威的,载入 / 恢复 / 分叉都以它为准。
③ 怎么做(How)
- 在支持 ACP 的客户端里配置启动命令
opencode acp; - 若图形客户端找不到
opencode,把command改成which opencode给出的绝对路径; - 通过客户端在目标项目目录创建会话,沿用该目录已有的全部配置;
- 需要并行试验时使用「分叉」,需要终止但保留记录时「关闭」而非「删除」;
- 界面差异以各客户端为准,但底层调用的是同一个引擎。
④ 为什么这么做(Why)
- 引擎与界面解耦。 OpenCode 专注做「agent 引擎」,通过开放协议让任何客户端接入;你在 TUI 里沉淀的配置、规则、插件在 Zed 等客户端里原样生效,不必为不同工具重配。
- 子进程 + stdio,安全且免端口。 不监听网络端口,攻击面小;随客户端启停、客户端关 stdin 即退出,生命周期干净、无残留进程。
- 私有服务隔离 ACP 流量。 与共享后台服务分开,避免图形客户端与终端客户端互相干扰,行为更可预测。
- 一进程多会话 + 分叉,复用进程开销的同时保留「安全试验 / 恢复 / 删除」等完整生命周期。
- 以会话存储目录为权威,避免客户端传入不同目录导致「同一会话看到不同项目」的错乱。
⑤ 这样做的好处
- 在熟悉的编辑器里用上同一个 OpenCode,配置全复用。
- 无网络端口、随客户端生命周期、安全干净。
- 多会话复用单进程,支持分叉 / 恢复 / 删除。
- 协议开放,生态客户端可广泛接入。
⑥ 不这么做的问题 / 坏处
- 客户端 PATH 中找不到 opencode → 启动失败,需写绝对路径。
- 以为 ACP 走共享后台服务 → 排障方向错误(它用的是私有服务)。
- 误删会话 → 与「关闭」不同,删除会真正移除存储记录。
- 期待各客户端界面完全一致 → 界面能力取决于客户端,引擎一致但表现不同。
⑦ 举一反三:三个例子
例 1(基础):把 OpenCode 接进 Zed
📌 场景:你想在 Zed 里直接使用 OpenCode。
✅ 答案:在 Zed settings.json 的 agent_servers 中配置 command: "opencode", args: ["acp"]。
🔍 讲解:所有 ACP 客户端都是同一个套路:启动 opencode acp,区别只在配置文件与字段名。
例 2(进阶):图形客户端报「找不到 opencode」
📌 场景:GUI 应用的 PATH 与终端不同,启动失败。
✅ 答案:运行 which opencode 取绝对路径,填到 command 字段。
🔍 讲解:图形程序常常不加载 shell 的 PATH 配置,绝对路径是最稳妥的解决方式。
例 3(挑战):在编辑器里安全尝试一个冒险改动
📌 场景:你想试激进重构,但保留当前稳定状态。
✅ 答案:对当前会话使用「分叉」,在分叉里尝试;不满意就回到原会话(关闭分叉而非删除原记录)。
🔍 讲解:ACP 完整暴露了会话分叉 / 关闭 / 删除语义,安全试验能力与 TUI 一致。
⑧ 练习题 ×2
练习 1(巩固型)
📝 题目:opencode acp 使用什么传输、连的是共享后台服务还是私有服务?客户端如何终止它?
💡 思路提示:stdio JSON-RPC;私有;关 stdin。
✅ 参考答案:
text
通过子进程的 stdin/stdout 交换换行分隔的 JSON-RPC(ACP v1);
启动的是私有 OpenCode 服务,不连共享后台、不监听网络端口;
客户端关闭 stdin 后进程退出并停止其私有服务。🔍 讲解:记住「子进程 + stdio + 私有服务」这个模型,接入与排障都会顺。
练习 2(迁移型)
📝 题目:迁移到 LSP:语言服务器也是「编辑器以子进程启动、通过标准输入输出收发协议消息、不自己开界面」。这与 ACP 的架构有何共性?这种「编辑器做界面、独立进程做能力」的分离带来什么好处?
✅ 参考答案(示例):
text
- LSP / ACP:编辑器是前端,独立语言 / agent 进程提供能力,
经 stdio 用标准协议通信;
好处:① 引擎与界面解耦,可独立升级、多客户端复用;
② 无网络端口、随编辑器启停,安全且生命周期清晰;
③ 能力只需实现一次,任何兼容客户端都能接入。🔍 讲解:ACP 正是「agent 版的 LSP」。理解这套成熟的「协议 + 子进程」架构,你就掌握了 OpenCode 融入各类工具的方式。
本章小结
| 能力 | 一句话定位 | 何时使用 |
|---|---|---|
| Plugins | 用代码扩展、注册工具 / 钩子 | Markdown 扩展不够、需运行新代码 |
| Policies | 只收紧、不弹窗的组织红线 | 企业治理、Console 统一下发 |
| 无头模式 | opencode run 提交即返回 | 脚本、CI、自动化 |
| References | 按名引用项目外目录 / 仓库 | 查外部文档、共享库、其它 repo |
| ACP | 让编辑器 / GUI 接入同一引擎 | 想在 Zed 等客户端里使用 |
这五类能力共同把 OpenCode 从「一个 TUI 工具」扩展成「可编排、可治理、可嵌入、可互联」的 agent 平台。掌握它们的关键不是记全细节,而是清楚各自解决哪一类问题、以及选择顺序(能用简单层就不动用强机制)。