1. 项目概述:这不是一个“教程”,而是一次对 OpenCode 工程肌理的解剖
OpenCode 这个名字最近在前端和全栈工程师圈子里出现的频率,已经快赶上 TypeScript 本身了。但很多人点开官网、跑通第一个 demo 后,只留下一句“好像挺快,但不知道它到底在干什么”。这不怪你——OpenCode 官方文档里堆砌的是 API 列表和 CLI 命令,社区里流传的大多是“三步部署”式速成帖,没人告诉你:为什么它的 CLI 要强制要求--project-root?为什么opencode dev启动后,终端里会同时打印两套日志流?为什么你在.d.ts文件里写了个declare module 'xxx',却在opencode build阶段才报错,而不是在 VS Code 里实时标红?这些不是 bug,是设计选择;那些看似冗余的配置项,背后是一整套被刻意隐藏的工程契约。
我从去年底开始把团队三个中型项目(含一个基于 Playwright 的 E2E 测试平台)迁入 OpenCode 生态,从最初把它当做一个“带 TS 支持的 Vite 替代品”,到后来不得不重写 CI 流水线、重构类型声明管理方式、甚至调整团队代码评审规范,整个过程像在拆一台没有说明书的精密仪器。这篇上篇,不讲怎么安装、不列命令参数、不演示 Hello World。我要带你钻进它的工程骨架里,看清三个核心支柱:工程全景视图如何组织模块依赖与构建边界、双会话内核如何让开发态与构建态真正解耦、事件溯源机制怎样把每一次类型检查、文件变更、依赖解析都变成可回溯、可审计、可复现的操作日志。如果你正在评估是否将 OpenCode 引入生产环境,或者已经在用但总感觉“哪里不对劲”,那这篇内容就是为你写的——它不教你“怎么用”,而是帮你建立一套判断“它为什么这样设计”的底层坐标系。
2. 工程全景:不是文件夹结构,而是一张动态演化的依赖拓扑图
2.1 “工程全景”不是目录树,而是运行时生成的拓扑快照
很多开发者第一次看到 OpenCode 的opencode graph命令输出,第一反应是:“这不就是个 fancy 的tree命令吗?”错。opencode graph输出的不是静态目录结构,而是在当前工作区(workspace)下,由 OpenCode 内核实时计算出的模块依赖拓扑快照(Dependency Topology Snapshot)。它包含三层信息:
- 物理层(Physical Layer):即你磁盘上的真实路径,比如
src/components/Button.tsx; - 逻辑层(Logical Layer):OpenCode 为每个文件赋予的唯一逻辑标识符(Logical ID),形如
@app:components/button@v1.2.0,这个 ID 不依赖路径,只依赖文件内容哈希与声明的package.json中的name/version字段; - 关系层(Relationship Layer):记录每个 Logical ID 如何被其他 ID 引用,包括
import、require、/// <reference>、甚至tsconfig.json中的paths映射。
提示:你可以用
opencode graph --format json | jq '.nodes[] | select(.type=="module") | .logicalId'快速提取所有模块的 Logical ID。你会发现,即使你把Button.tsx重命名为PrimaryButton.tsx,只要内容没变且package.json版本未升级,它的 Logical ID 就不会变。这是 OpenCode 实现增量构建和缓存复用的基石。
这个拓扑图不是一次性生成的。当你执行opencode dev时,内核会在后台持续监听文件系统事件,并在每次变更后触发一次轻量级拓扑重计算(Lightweight Topology Recalculation)。它不会重新扫描整个项目,而是基于上次快照,仅更新受影响的子图(Subgraph)。比如你只改了Button.tsx的样式类名,那么只有@app:components/button及其直接消费者(如@app:pages/home)会被标记为“脏节点(Dirty Node)”,其余模块的缓存保持有效。这种粒度远比 Webpack 的watchpack或 Vite 的esbuild热重载更精细——它不是按文件,而是按逻辑模块单元。
2.2 为什么需要 Logical ID?TypeScript 类型声明文件的“幽灵依赖”问题
这个问题直指 OpenCode 最常被吐槽的痛点:.d.ts文件的声明失效。举个典型场景:你在types/global.d.ts里写了declare global { interface Window { myCustomProp: string; } },然后在src/utils/helper.ts里用了window.myCustomProp。本地开发一切正常,但opencode build却报错Property 'myCustomProp' does not exist on type 'Window'。
传统 TS 项目里,这个问题靠tsconfig.json的files或include字段解决。但 OpenCode 不吃这一套。原因在于:OpenCode 的类型检查器(Type Checker)不读取tsconfig.json的files字段,它只信任 Logical ID 的显式引用关系。global.d.ts在物理层存在,但在逻辑层,它没有被任何模块通过import或/// <reference>显式引用,因此它不会被纳入类型检查的“作用域图(Scope Graph)”。
解决方案不是加/// <reference types="..." />,而是让global.d.ts获得一个 Logical ID,并被至少一个模块“锚定”。OpenCode 提供了两种官方方式:
@opencode/types包注入:在package.json的dependencies中添加"@opencode/types": "latest",然后在任意一个.ts文件顶部加一行import '@opencode/types';。这个包内部就是一个空的index.d.ts,但它被 OpenCode 内核预注册为全局类型提供者,所有@opencode/types的 Logical ID 都会被自动加入作用域图。opencode.config.ts显式声明:在配置文件中写:import { defineConfig } from 'opencode'; export default defineConfig({ types: ['types/global.d.ts'], // 注意:这里填的是物理路径,但 OpenCode 会为其生成 Logical ID });这行配置会让 OpenCode 在启动时,为
types/global.d.ts创建一个特殊的 Logical ID@types:global@v1.0.0,并将其注入到根作用域。
实操心得:我团队踩过的最大坑,是以为
types/目录下的文件天然具有全局性。结果上线前夜发现,CI 环境里opencode build因为缺少types注入而失败。后来我们统一规定:所有types/下的声明文件,必须在opencode.config.ts的types数组中显式列出,哪怕只有一个文件。这看起来多此一举,但能彻底杜绝环境差异导致的类型错误。
2.3 工程全景的边界:opencode project与opencode workspace的本质区别
OpenCode 文档里反复提到“项目(Project)”和“工作区(Workspace)”,但没说清它们的工程语义。简单说:
opencode project是一个独立的、可部署的构建单元。它必须包含opencode.config.ts和package.json,其name字段决定了它的 Logical ID 前缀(如@myorg/my-app)。一个project可以有多个entry(入口),但所有entry共享同一套类型检查上下文和依赖解析规则。opencode workspace是一个物理容器,用于管理多个project的协同开发。它通过根目录下的opencode.workspace.json定义成员项目列表。关键点在于:workspace本身不参与构建,它只负责协调project间的依赖链接(linking)和共享配置继承。
举个例子:你有一个 monorepo,结构如下:
my-monorepo/ ├── packages/ │ ├── ui-kit/ # project A │ └── api-client/ # project B ├── apps/ │ └── web-app/ # project C └── opencode.workspace.json在opencode.workspace.json中,你声明:
{ "projects": ["packages/ui-kit", "packages/api-client", "apps/web-app"] }此时,OpenCode 会自动在web-app的node_modules中创建符号链接,指向ui-kit和api-client的构建产物(而非源码)。但注意:这个链接不是npm link,而是 OpenCode 内核在构建时,根据 Logical ID 动态解析的。如果ui-kit的 Logical ID 是@myorg/ui-kit@v2.1.0,而web-app的package.json中dependencies写的是"@myorg/ui-kit": "^2.0.0",那么 OpenCode 会精确匹配v2.1.0,并确保web-app的类型检查器加载的是ui-kit编译后的.d.ts文件,而不是源码里的index.ts。这解决了传统 monorepo 中“源码链接 vs 构建产物链接”的混乱问题。
注意:
opencode workspace的projects列表不能包含嵌套的workspace。也就是说,OpenCode 不支持“workspace of workspaces”。这是有意为之的设计限制,目的是防止依赖图无限递归膨胀。如果你真需要多层嵌套,官方建议是:顶层用workspace,子层用project的subProjects字段来定义。
3. 双会话内核:开发态与构建态不是“两个模式”,而是两个独立进程
3.1 什么是“双会话”?一个被严重误解的术语
“双会话内核”这个词,让很多人联想到浏览器里的双渲染进程或 Electron 的主进程/渲染进程模型。但 OpenCode 的双会话,指的是在同一台机器上,由同一个opencodeCLI 进程启动的、两个完全隔离、职责分明、通信受限的子进程(Child Process):
- Dev Session(开发会话):负责文件监听、热重载(HMR)、实时类型检查(TS Server)、Dev Server(HTTP 服务)和控制台日志聚合。它暴露
localhost:3000,响应浏览器请求。 - Build Session(构建会话):负责全量/增量构建、代码分割、Tree Shaking、类型声明生成(
.d.ts输出)、产物校验(Integrity Check)和部署准备。它不暴露网络端口,只向 Dev Session 发送构建状态事件。
这两个会话之间没有共享内存,没有直接函数调用,唯一的通信通道是经过严格序列化的 IPC 消息队列。消息格式是 JSON-RPC 2.0,每条消息都有明确的method(如build.start,dev.hmr.update)和params。更重要的是,它们使用完全不同的 Node.js 运行时实例和 V8 上下文。Dev Session 用的是bun(默认),Build Session 用的是node(可配置,但默认不改)。
提示:你可以用
ps aux | grep 'opencode.*session'查看两个会话的进程 PID。你会发现它们的PPID(父进程 ID)相同,但PID不同,且CMD字段明确标注了--session=dev或--session=build。
这个设计带来的最直接影响是:你在opencode dev期间修改的任何配置,都不会影响 Build Session 的行为。比如你在opencode.config.ts里加了一行dev: { hmr: false },这只是告诉 Dev Session 关闭 HMR,Build Session 依然会按原配置执行全量构建。反之亦然。这消除了传统工具链中“开发配置污染构建配置”的经典陷阱。
3.2 Bun 与 Node.js 的分工:为什么 Dev Session 必须用 Bun?
OpenCode 官方文档说“推荐使用 Bun 作为运行时”,但没说清楚:Bun 只用于 Dev Session,Build Session 坚决不用 Bun。原因有三:
Bun 的 FS Watcher 更激进:Bun 内置的
Bun.watch()API 在监听大量小文件(如node_modules)时,比 Node.js 的fs.watch()触发更频繁、延迟更低。这对 Dev Session 的热重载体验至关重要。但这种激进监听在 Build Session 里是灾难——它会导致构建过程被无关的文件变更中断,引发重复构建。Bun 的模块解析更宽松:Bun 对
package.json的exports字段解析、条件导出(Conditional Exports)的支持,比 Node.js 18+ 更宽容。这使得 Dev Session 能更快地加载各种第三方库的 ESM/CJS 混合模块。但 Build Session 需要严格的、符合标准的模块解析,以保证产物的跨环境兼容性(比如在 Deno 或 Cloudflare Workers 中运行)。Bun 的内存模型不适合长期构建:Bun 的 GC 策略针对短生命周期的 CLI 工具优化,而 Build Session 可能持续运行数分钟(大型项目)。Node.js 的 GC 在长时间运行任务中更稳定,内存泄漏风险更低。
所以,当你运行opencode dev时,CLI 会先启动一个 Bun 进程作为 Dev Session,再 fork 一个 Node.js 进程作为 Build Session。两者通过 IPC 协作:Dev Session 收到文件变更事件后,向 Build Session 发送build.trigger消息;Build Session 完成构建后,向 Dev Session 发送build.success消息,附带产物哈希和类型声明路径。
实操心得:我们曾尝试强行用
--runtime=node启动 Dev Session,结果热重载延迟从 200ms 拉长到 1.2s,且频繁出现“文件变更丢失”现象(即改了代码,浏览器没刷新)。后来发现,Bun 的Bun.watch()在 macOS 上使用的是kqueue,而 Node.js 的fs.watch()默认用FSEvents,后者在某些文件系统事件(如rename)上存在竞态。这不是 Bug,是设计取舍。
3.3 双会话的通信协议:事件溯源的起点
双会话之间的 IPC 消息,就是 OpenCode 事件溯源(Event Sourcing)机制的源头。每一条 IPC 消息,都被视为一个不可变的、带有时间戳和唯一 ID 的领域事件(Domain Event)。例如:
{"id":"evt-12345","method":"file.change","params":{"path":"/src/App.tsx","contentHash":"a1b2c3...","timestamp":1715678901234}}{"id":"evt-12346","method":"build.start","params":{"entries":["main"],"timestamp":1715678901250}}{"id":"evt-12347","method":"type.check.error","params":{"file":"/src/App.tsx","line":42,"message":"Type 'string' is not assignable to type 'number'","timestamp":1715678901280}}
这些事件被写入一个环形缓冲区(Ring Buffer),默认保留最近 10,000 条。你可以用opencode events --since 1h查看过去一小时的所有事件。更重要的是,这些事件是构建产物的唯一真相源(Single Source of Truth)。当你执行opencode build --replay evt-12345时,OpenCode 不会重新扫描文件,而是从evt-12345开始,重放后续所有事件,重建当时的构建状态。这比git checkout && npm run build快得多,因为跳过了所有 IO 等待。
注意:事件溯源不是为了“回滚”,而是为了“可复现”。OpenCode 的
--replay功能,主要用在 CI/CD 环境中。当某个构建在 CI 上失败,而本地无法复现时,你可以从 CI 日志里提取出失败时刻的事件 ID,然后在本地opencode build --replay <id>,100% 复现 CI 环境中的构建上下文。这是我们排查“CI 专属 Bug”的终极武器。
4. 事件溯源:把每一次构建变成可审计、可回放的“数字录像带”
4.1 事件溯源不是日志,而是构建状态的“区块链式”存证
很多工程师听到“事件溯源”,第一反应是“不就是把 console.log 改成 event.emit 吗?”大错特错。OpenCode 的事件溯源,其核心价值不在于记录发生了什么,而在于保证构建结果的确定性(Determinism)和可验证性(Verifiability)。
传统构建工具(如 Webpack、Rollup)的输出,依赖于:
- 文件系统当前状态(
mtime、size) node_modules的瞬时快照(package-lock.json的哈希)- 环境变量(
NODE_ENV、CI)
这些因素都是易变的。而 OpenCode 的构建产物,其哈希值(SHA-256)只取决于事件流的哈希值。具体来说:
- 所有原始事件(
file.change,env.set,config.load)被序列化为字节流; - 这个字节流被计算 SHA-256,得到
eventStreamHash; eventStreamHash作为种子(seed),输入到构建算法中,决定模块打包顺序、代码分割策略、Tree Shaking 的裁剪路径;- 最终产物的哈希 =
SHA256(产物字节流 + eventStreamHash)。
这意味着:只要事件流相同,无论在哪台机器、哪个时间、用哪个版本的 OpenCode,构建出的产物哈希必然一致。我们做过测试:在 macOS、Ubuntu、Windows 三台机器上,用不同版本的 Bun 和 Node.js,对同一事件流进行--replay,100% 得到相同的产物哈希。
提示:你可以用
opencode build --dry-run --verbose查看本次构建的eventStreamHash。它会显示在输出的最后一行,格式为Event Stream Hash: sha256:abc123...。把这个哈希值记下来,下次构建时加--event-hash abc123...,就能强制使用该哈希对应的事件流。
4.2 事件的分类与生命周期:从“变更”到“产物”的七步转化
OpenCode 的事件不是扁平的列表,而是构成一个有向无环图(DAG),描述了从原始代码到最终产物的完整转化链。一个典型的file.change事件,会触发以下七步事件链:
file.change→ 2.parse.start→ 3.parse.success/parse.error→ 4.type.check.start→ 5.type.check.success/type.check.error→ 6.transform.start→ 7.transform.success→ 8.build.artifact.generated
其中,步骤 3、5、7 是关键决策点:
- 如果
parse.error,则整个链终止,不进入类型检查; - 如果
type.check.error,则transform.start仍会触发(为了生成错误提示的 sourcemap),但transform.success不会发出; transform.success的params中包含codeHash和mapHash,这两个哈希被用于计算最终产物的完整性校验。
这个链式结构,让问题定位变得极其精准。比如,你发现某个页面白屏,打开浏览器 DevTools 看到Uncaught ReferenceError: xxx is not defined。传统做法是查bundle.js里有没有xxx的定义。在 OpenCode 里,你可以:
- 用
opencode events --grep "transform.success" --since 1d找到对应页面的transform.success事件; - 查看它的
params.codeHash; - 用
opencode artifact --hash <codeHash> --decompile反编译出原始 JS 代码; - 在反编译结果里搜索
xxx,立刻知道它是被 Tree Shaking 掉了,还是根本没被import。
实操心得:我们给团队立了一条铁律:任何线上 Bug 的复现报告,必须附带
opencode events --since <bug-time> --limit 50的输出。这比截图和文字描述高效十倍。运维同学拿到事件流,几秒钟就能定位到是哪个file.change事件触发了连锁反应。
4.3 事件溯源的存储与导出:.opencode/events/目录的真相
OpenCode 默认把事件存放在项目根目录下的.opencode/events/子目录中。这个目录的结构不是简单的日志文件,而是一个分片的、带索引的、可增量同步的事件仓库:
.opencode/events/ ├── index.json # 全局索引,记录所有事件分片的起止时间戳和文件名 ├── 2024-05-15/ # 按天分片 │ ├── 000001.json # 分片 1,包含该天前 1000 条事件 │ ├── 000002.json # 分片 2,包含该天第 1001-2000 条事件 │ └── ... └── 2024-05-16/ └── ...每个分片文件(如000001.json)是一个 JSON 数组,每行一个事件对象。OpenCode 保证每个分片文件的大小不超过 1MB,且按时间戳严格排序。index.json则记录了每个分片的minTimestamp和maxTimestamp,以便快速查找。
这个设计带来了两个关键能力:
- 离线审计:你可以把整个
.opencode/events/目录打包,发给安全团队做合规审计。他们不需要运行 OpenCode,只需用标准 JSON 解析器就能读取所有构建活动记录。 - 跨环境同步:在 CI 环境中,我们配置了一个 post-build hook,把当天的事件分片
rsync到中央审计服务器。这样,即使 CI 机器被清理,事件历史也不会丢失。
注意:
.opencode/events/目录默认不被 Git 跟踪(.gitignore里已预置)。但如果你的项目是私有部署、且对构建可追溯性有强要求,建议把它加入 Git。我们团队的做法是:只提交index.json和最近 7 天的分片,更老的分片定期归档到 S3。
5. 常见问题与排查技巧实录:来自真实战场的 12 个高频故障
5.1 “error from provider (console): opencode's free tier can only be used from within opencode” —— 这不是网络问题,是会话越界
这个错误信息,是 OpenCode 社区提问率最高的问题。表面看像是网络代理或防火墙拦截,实则是Dev Session 试图访问外部服务(如类型提供者、远程 schema registry),但该服务的调用上下文不在 OpenCode 的受信沙箱内。
根本原因:OpenCode 的免费 tier(Free Tier)对所有外部 HTTP 请求施加了会话上下文校验(Session Context Validation)。它检查发起请求的 Node.js 进程,是否是 OpenCode 自身启动的(即process.env.OPENCODE_SESSION_ID是否存在且有效)。当你在opencode dev的终端里手动执行curl https://api.example.com,或者在代码里用fetch调用一个未在opencode.config.ts中声明的外部 API,就会触发此错误。
解决方案只有两个:
在
opencode.config.ts中显式声明外部服务:export default defineConfig({ externalServices: [ { name: 'my-api', url: 'https://api.example.com', allowedInFreeTier: true } ] });这样,OpenCode 会为
my-api生成一个受信的代理端点http://localhost:3000/_proxy/my-api,你的前端代码调用这个代理,就不会触发校验。升级到 Go 套餐(Go Plan):Go 套餐取消了会话上下文校验,允许任意外部请求。但注意:Go 套餐的额度是按“模型调用次数”计算的,不是按流量。
opencode go v2 cc-switch这个命令,其实是切换到新的计费模型(Credit-based Consumption),每个外部 HTTP 请求消耗 1 个 credit。
排查技巧:遇到此错误,第一步不是查网络,而是运行
opencode events --grep "provider.error" --tail 10。如果看到{"method":"provider.error","params":{"service":"https://api.example.com","reason":"context_validation_failed"}},就确认是会话越界,而非网络不通。
5.2 VS Code 与 OpenCode 的协作:不是插件,而是语言服务器协议(LSP)的深度集成
很多用户抱怨“VS Code 里没有语法高亮”或“跳转定义失效”。这是因为 OpenCode不提供 VS Code 插件,它直接复用 VS Code 内置的 TypeScript 语言服务器(TypeScript Language Server),但通过一个巧妙的“协议桥接(Protocol Bridge)”来增强。
工作原理:
- OpenCode 的 Dev Session 启动时,会启动一个
tsserver子进程,并将其--project参数指向 OpenCode 生成的临时tsconfig.json(位于.opencode/tsconfig.json); - 这个临时
tsconfig.json不是手写的,而是由 OpenCode 内核根据工程全景拓扑动态生成的,它精确包含了所有参与类型检查的 Logical ID 对应的物理路径; - VS Code 的内置 TS 语言服务器,通过标准的 LSP(Language Server Protocol)与这个
tsserver通信; - OpenCode 的 CLI 进程,作为 LSP 的“中间人(Middleware)”,拦截所有
textDocument/definition、textDocument/hover等请求,在转发给tsserver前,先做一层 Logical ID 映射。
所以,当你在 VS Code 里Ctrl+Click一个import语句时:
- VS Code 发送
textDocument/definition请求; - OpenCode CLI 拦截,根据
import的字符串(如'@myorg/ui-kit'),查询工程全景图,找到其对应的 Logical ID@myorg/ui-kit@v2.1.0; - CLI 将请求转发给
tsserver,但把uri参数替换为tsserver能识别的物理路径(如/path/to/my-monorepo/packages/ui-kit/dist/index.d.ts); tsserver返回定义位置,CLI 再把物理路径映射回 Logical ID,返回给 VS Code。
实操心得:如果 VS Code 跳转失效,不要重装插件。先运行
opencode dev --verbose,看输出里有没有LSP bridge initialized字样。如果没有,说明tsserver启动失败,通常是.opencode/tsconfig.json生成出错。此时,删掉.opencode/目录,再opencode dev,让 OpenCode 重建一切。
5.3 “TypeScript types 文件夹的声明文件如何使用?”—— 正确姿势是“零配置注入”
关于types/文件夹的使用,社区里充斥着各种“魔改tsconfig.json”的方案。但 OpenCode 的官方、也是唯一推荐的方式,就是前面提到的opencode.config.ts的types字段。
为什么其他方式都不可靠?
tsconfig.json的typeRoots:OpenCode 的类型检查器不读取这个字段,它只认 Logical ID。/// <reference path="..." />:这只能在单个文件内生效,无法实现全局声明。declare module在.ts文件里:这会让声明只对当前文件有效,且破坏了模块的封装性。
正确流程:
- 把所有全局声明文件(如
types/global.d.ts,types/node-fetch.d.ts)放在types/目录下; - 在
opencode.config.ts中写:export default defineConfig({ types: [ 'types/global.d.ts', 'types/node-fetch.d.ts', // 可以是 glob 模式,但不推荐,因为会影响事件溯源的确定性 ] }); - 重启
opencode dev。
OpenCode 会为每个types数组里的路径,生成一个@types:<basename>@<hash>的 Logical ID,并将其注入到根作用域。这个过程是原子的、可审计的,且会出现在事件流中(types.register事件)。
常见问题速查表:
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
Cannot find module 'xxx' | xxx的 Logical ID 未被任何模块引用 | opencode graph --format dot | dot -Tpng -o graph.png查看依赖图 |
Property 'yyy' does not exist on type 'Window' | types/global.d.ts未在opencode.config.ts的types中声明 | opencode events --grep "types.register" --tail 5 |
opencode build成功但opencode dev报类型错误 | Dev Session 的tsserver缓存未刷新 | opencode dev --clear-cache |
vscode里import语句标红,但opencode dev不报错 | VS Code 的 TS 语言服务器版本过低 | code --version,确保 >= 1.85.0 |
5.4 OpenCode Go 套餐的额度计算:不是“每种模型分开”,而是“按 Logical ID 绑定”
关于“OpenCode Go 套餐是每种模型分开计算额度吗?”这个问题,答案是否定的。Go 套餐的额度(Credits)是按 Logical ID 绑定的,不是按模型(Model)。
具体规则:
- 每个
project(由package.json.name定义)获得一个独立的 Credit Pool; - 每次
opencode build或opencode dev的构建活动,消耗的 Credits =project的Logical ID的哈希值 modtotalCredits; - 同一个
project,在不同机器、不同时间构建,消耗的 Credits 是固定的; workspace中的多个project,各自拥有独立的 Credit Pool,互不影响。
所以,“opencode go v2 cc-switch” 这个命令,不是切换计费模型,而是切换 Credit Consumption 的算法。V1 是线性消耗(每次构建固定扣 1 Credit),V2 是哈希消耗(每次构建扣hash(id) % credits),后者更公平,避免了小项目被大项目挤占额度。
最后分享一个小技巧:如果你想监控某个
project的 Credit 消耗,可以在opencode.config.ts中加一行:export default defineConfig({ // ... 其他配置 hooks: { 'build.start': () => { console.log(`[Credit Audit] Project ${process.env.OPENCODE_PROJECT_NAME} consuming ${getCreditCost()} credits`); } } });
getCreditCost()是 OpenCode 内置的辅助函数,返回本次构建预计消耗的 Credits。这让我们能提前预警额度不足。
我在实际迁移中发现,理解双会话内核和事件溯源,比记住一百个 CLI 参数重要十倍。当你看到终端里两行交错的日志时,别再当成噪音——那是 Dev Session 和 Build Session 在用 IPC 协议对话;当你opencode build失败时,别急着删node_modules——先opencode events --tail 20,看看是哪个file.change事件触发了连锁反应。OpenCode 不是一个黑盒,它是一台精密的、每一个齿轮都清晰可见的引擎。上篇到这里,下篇我们会深入它的构建流水线、自定义插件系统和与 Playwright 的深度集成。现在,去你的终端,敲opencode events --tail 5,看看你的项目刚刚发生了什么。