资讯动态

OpenCode V2 核心架构指南:用“薄容器 + 插件钩子“重构 packages/core

发布时间:2026/9/7 19:35:35 来源:尧图企业网站定制
OpenCode V2 核心架构指南用薄容器 插件钩子重构 packages/core【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode本文基于 specs/v2/instructions.md 完整讲解 OpenCode v2 移植期间packages/core的开发规范核心服务应当长什么样、插件钩子Plugin Hooks的约定与适用边界、内置插件的启动Plugin Boot组装方式以及 schema、状态、事件与代码风格的统一约束。读完本文你能按官方规范写出符合 v2 架构的领域服务service、为服务添加扩展钩子并理解packages/opencode与packages/core之间的依赖边界。一、方向把行为从大服务里移出来文档开宗明义给出了 v2 移植的总方向Direction把行为从大型应用服务中移出交给插件实现核心服务变成小型的、带类型typed的容器——它们拥有状态own state、暴露简单操作expose simple operations并在策略policy或集成integration逻辑真正属于它们的地方触发钩子。目标形态可以概括为四条packages/core承载领域 schema、带类型的错误typed errors、状态容器、事件以及插件钩子契约plugin hook contracts插件plugins实现provider 特定的、配置特定的、鉴权特定的、模型发现的以及生成generation类行为服务按设计支持热重载hot-reloadable更新是细粒度的、可观测的不需要拆掉整个进程packages/opencode会越来越薄UI、服务端路由、CLI、存储胶水与遗留兼容代码应调用核心服务而不是自己持有领域逻辑。这条方向与仓库中另一个规格 specs/v2/catalog-config-plugin-lifecycle.md 呼应catalog 与 config 的划分本身就是状态归容器、行为归插件的落地。二、服务形态Service Shape七个要素与哑容器 API文档指定了核心服务的标准外形要求向Catalog、AccountV2、AgentV2看齐。一个合格的核心服务模块应包含七个要素在模块顶部定义 schema 与 branded id带品牌标识的 id 类型为预期内的失败定义带类型的Schema.TaggedErrorClass错误定义一个只包含小操作的Interface暴露一个Context.ServiceEffect 的服务令牌实现layer内部使用私有内存状态暴露显式声明依赖的defaultLayer以自导出形式收口模块export * as Name from ./file。源码印证Catalog 就是范本packages/core/src/catalog.ts 完整体现了上述形态。文件首行即自导出export * as Catalog from ./catalog随后定义领域类型ProviderRecord、DefaultModel、复用 schema 包中的事件定义export const Event Catalog.Event并给出Interfacecatalog.ts#L47-L60export interface Interface extends State.TransformableDraft { readonly provider: { readonly get: (providerID: ProviderV2.ID) Effect.EffectProviderV2.Info | undefined readonly all: () Effect.EffectProviderV2.Info[] readonly available: () Effect.EffectProviderV2.Info[] } readonly model: { readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) Effect.EffectModelV2.Info | undefined readonly all: () Effect.EffectModelV2.Info[] readonly available: () Effect.EffectModelV2.Info[] readonly default: () Effect.EffectModelV2.Info | undefined readonly small: (providerID: ProviderV2.ID) Effect.EffectModelV2.Info | undefined } }这正是文档所说的优先哑容器 APIdumb container API操作只由get、all、available、default、update、remove、activate这类小领域动词组成注册与变更统一走update(id, draft ...)。Service则是标准的Context.Service声明catalog.ts#L62export class Service extends Context.ServiceService, Interface()(opencode/v2/Catalog) {}AgentV2 同样遵循Interface Draft Service的三段式结构。钩子在前事件在后文档还给出了容器内部两条时序规则提交变更之前调用钩子当插件需要增强enrich、取消cancel或校验变更时提交变更之后发布事件当其他服务或前端需要对该变更作出反应时。并且明确警告除非是领域不变量domain invariant否则不要把应用策略直接写进核心服务。文档举了一个例子——模型 endpoint 继承的解析属于 catalog 的职责决定注册哪些 provider 则属于插件的职责。在源码里Catalog的projectModel()catalog.ts#L78-L97正是在做前者把 provider 级的api/request默认值合并进 model 记录这是纯粹的领域归一化逻辑而注册哪些 provider则由packages/core/src/plugin/provider/目录下的 30 余个 provider 插件文件完成两者分得泾渭分明。三、插件钩子Plugin Hooksv2 的扩展边界文档将插件定义为 v2 的扩展边界当某段逻辑应当由集成方而非容器本身提供时就向PluginV2.HookSpec增加钩子。钩子的六条约定约定说明不可变输入 可变输出钩子接收 immutable input 与 mutable output 两类数据Immer draft可变对象输出以 Immer draft 形式暴露插件可直接写属性cancel: boolean当插件可以阻止某次变更时输出中必须包含取消标记顺序触发钩子按注册顺序串行执行保证顺序确定性领域导向命名钩子名要像provider.update、model.update、account.activate、agent.generate这样围绕领域动词命名小载荷、强类型钩子载荷保持精简并用核心 schema 做类型约束适合用钩子的场景注册 provider 与 model应用由环境变量/账户/配置推导出来的启用enablement状态转换 SDK/provider 选项实现 agent 生成等生成式行为在选择默认值属于策略而非状态时做默认值决策。文档同时划了红线不要把钩子当作传输层细节、UI 行为或兼容 shims 的垃圾场。源码印证transform 与 runtime 两类钩子插件侧的 packages/plugin/src/v2/effect/README.md 展示了当前钩子体系的实际形态状态型领域通过transform钩子参与状态重建运行期操作通过 runtime 钩子拦截。例如 transform 钩子以 draft 风格直接修改领域对象yield* ctx.agent.transform((agent) { agent.update(reviewer, (item) { item.description Reviews code for regressions item.mode subagent }) })可用领域包括agent、catalog、command、integration、reference、skill六个命名空间对应ctx.catalog.transform等。runtime 钩子则拦截实时操作例如为特定包注入 AISDK 的 SDK 实例yield* ctx.aisdk.sdk(Effect.fn(function* (event) { if (event.package ! ai-sdk/xai) return const mod yield* Effect.promise(() import(ai-sdk/xai)) event.sdk mod.createXai(event.options) }))README 明确说明钩子按注册顺序串行执行后注册的钩子能看到先注册钩子做的修改Hooks run sequentially in registration order与规范中顺序触发、确定性排序的约定完全一致。此外transform 领域支持按域reload如ctx.catalog.reload()即重跑该域所有活跃 transform 并重新发布重建后的领域状态——这正是下一节细粒度重配置目标在插件侧的入口。四、插件启动Plugin Boot只做组合不做策略规范要求内置核心插件由 packages/core/src/plugin/boot.ts 统一注册文档写作时路径如此指定从当前源码结构看同一套启动组合现位于 packages/core/src/plugin/internal.ts其PluginInternal层即承担 boot 职责。当一个新的核心服务希望暴露给插件时boot 层需要四步配合把服务加入 boot 层依赖类型在 layer 内yield*取到该服务实例在add中为每个插件 effect 通过Effect.provideService提供该服务只有在不引入循环依赖时才把该服务的 default layer 加入 boot 的默认 layer。internal.ts的源码是这四步的直接体现。它先声明Requirements联合类型internal.ts#L37-L52列齐所有可用服务export type Requirements | AgentV2.Service | Catalog.Service | CommandV2.Service | Config.Service | EventV2.Service | ...layer 内部逐一yield*取服务并在add中为每个插件 effect 批量provideServiceinternal.ts#L81-L106。最后用State.batch批量注册全部内置插件——ConfigReferencePlugin、AgentPlugin、CommandPlugin、SkillPlugin、ModelsDevPlugin、各Config*Plugin、ProviderPlugins30 余个 provider 插件、ConfigExternalPlugin、ConfigProviderPlugin、VariantPlugininternal.ts#L108-L123yield* State.batch( Effect.gen(function* () { yield* add(ConfigReferencePlugin.Plugin) yield* add(AgentPlugin.Plugin) yield* add(CommandPlugin.Plugin) // ... for (const item of ProviderPlugins) yield* add(item) yield* add(ConfigProviderPlugin.Plugin) yield* add(VariantPlugin.Plugin) }), ).pipe(Effect.withSpan(PluginInternal.boot), Effect.forkScoped({ startImmediately: true }))文档对 boot 的定性只有一句但很关键保持 boot 为纯组合composition only它自身不应包含 provider、account、agent 或 model 的策略。上面的代码里看不到任何策略分支只有依赖装配与插件注册正是这句话的执行标准。插件生命周期底座真正负责加载/卸载/等待插件的是 packages/core/src/plugin.ts 中的PluginV2服务其Interface只有三个操作add、remove、wait。从实现看plugin.ts#L43-L126add通过KeyedMutex按插件 id 加锁检测并拒绝加载循环Plugin load cycle detected为每个插件 fork 一个子 Scope加载完成后发布Event.Added并唤醒wait的等待者remove关闭对应子 Scope 完成卸载wait借助Deferred挂起调用者直到指定插件加载完成或返回其失败Exit。这解释了为什么热重载不需要拆进程替换一个插件就是关旧 Scope 注册新 effect容器状态本身不动。五、边界Boundariescore 与 opencode 的单向依赖文档划定的边界规则packages/core不得 importpackages/opencode。如果核心需要某个类型或概念先把领域形状domain shape在 core 中移动或重新建模不要整包搬运遗留服务Avoid moving legacy services over wholesale。正确姿势是移植领域形状与容器 API把具体行为留在钩子后面交给插件实现。文档还给出了移植一个 opencode 服务的六步清单识别它拥有的状态识别调用方真正需要的操作识别哪些分支是策略或集成行为在packages/core中对状态与操作建模为策略/集成分支添加钩子在调用方完成渐进迁移之前让旧包代码继续可用。这条清单把最小正确移植smallest correct port落成了可执行的动作序列第 6 步尤其重要——它允许 v2 与旧代码并行演进而不是要求一次性切换。六、Schema 与类型Effect Schema 即公共契约文档要求以 Effect Schema 作为对外契约id 使用 branded schema带品牌类型杜绝裸字符串/数字 id 的误用领域数据用Schema.Class或Schema.Struct预期错误用Schema.TaggedErrorClass合理处复用 core 现有辅助工具如DeepMutable、statics与整数 schema。在数据结构上优先使用Info对象作为持久化的领域记录当 update API 需要在首次变更时创建记录时为其添加静态empty(...)构造器。Catalog 源码正是这样做的draft 更新中若 provider 尚不存在就现场用ProviderV2.Info.empty(providerID)创建catalog.ts#L112-L120update: (providerID, fn) { let current draft.providers.get(providerID) if (!current) { current { provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, models: new MapModelV2.ID, ModelV2.MutableInfo(), } draft.providers.set(providerID, current) } // ...文档最后强调保持 schema 稳定且显式除非配置形状本身就是领域模型否则不要把 opencode 的配置形状当成 core 的领域形状。这一条防止了配置对象即领域对象的常见腐化路径。七、状态与事件细粒度重配置的目标状态私有状态保持在对服务层的私有访问范围内当持久化或并发需要时使用不可变替换immutable replacement或 Effect refs。Catalog 即通过State.createData, Draft管理私有数据与 draft 视图catalog.ts#L105。事件只描述已提交的事实为已提交的领域变更发布事件而不是为尝试中的变更发布。事件命名应描述领域事实例如catalog.model.updated。v2 的目标是细粒度重配置granular reconfiguration一次模型更新应让依赖方只对这次模型更新作出反应而不需要触发全局重载。这正是第三节插件侧reload机制ctx.catalog.reload()只重建 catalog 域的动机——重载的最小单位是一个领域而不是整个进程。八、代码风格向最小正确移植收敛文档给出的风格清单逐条对应 Effect 生态的惯用法规则用途Effect.gen(function* () { ... })服务组合Effect.fn(Domain.method)公开服务方法自带可观测性 span 命名Effect.fnUntraced小型内部变更助手yield* new ErrorClass(...)带类型的失败配合 TaggedErrorClass最小化 helper除非它命名了一个真实概念否则不抽禁用any除非既有插件边界确实要求不做无依据的兼容代码没有具体的持久化或外部消费者需求就不写兼容层结尾一句值得单独记下来优先最小正确移植。目标是让服务更容易被替换、更容易推理而不是把旧架构搬进一个新包。这既是风格要求也是整个 v2 移植的验收标准。小结specs/v2/instructions.md 把packages/core的 v2 移植压缩为五个可检查的要点服务是schema TaggedErrorClass Interface Context.Service 私有状态 layer的薄容器行为外移为插件钩子钩子顺序确定、载荷小且强类型boot 层只做组合不做策略依赖方向严格单向core 不 import opencodeschema、状态与事件都服务于细粒度、可热重载、可推理的总目标。对照 packages/core/src/catalog.ts、packages/core/src/plugin.ts、packages/core/src/plugin/internal.ts 与 packages/plugin/src/v2/effect/README.md 的源码实现可以看到这套规范并非纸面约束而是当前代码库已经运行其上的实际架构。【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价