创建一个最小插件并在运行中的 dsh 挂载,确认其注册的服务在上下文中可见。
上一课以最小 patch 收尾:我们手写了一条补丁,并看清它按行 id 覆盖的目标行。配置补丁装配的是 dsh 的启动层,而要让运行中的 dsh 长出真正的能力,同样的“最小”路径要在插件上再走一遍——目标很具体:写一个最小插件,把它挂进运行中的 dsh,然后在上下文里看到它注册的服务名。整个验证路径只有三步:声明、挂载、确认。
先立一条架构事实,后面所有步骤都建立在它之上:dsh 没有需要修补的特权内核。要扩展它,就在其他插件旁边再挂一个插件;插件向共享上下文贡献服务、类型化事件与可逆效果。挂载、卸载与依赖解析由 Cordis 完成,它还提供插件协作所需的共享服务与事件。
核心概念
Fiber 状态机:插件的生命周期
把插件想成手机里安装的应用。点开图标,应用进入运行状态;切到后台,它被挂起;从设置里停止它,状态回到已停止;卸载时,系统把它申请的通知权限与存储一并清掉。操作系统替每个应用记录当前处于哪个阶段。
技术定义:Cordis 为每个插件维护一个 Fiber——负责推进并记录插件生命周期阶段的状态机。插件在启动阶段向上下文做的注册(服务、事件、效果)都被当作可逆效果:挂载时生效,插件卸载时由框架自动撤销。所以挂载与卸载是对称操作——挂上时注册,卸下时清空,上下文不残留死服务。
对运行中的 dsh 来说,这层机制意味着每次挂载与卸载都是干净的事务:服务名可见,说明 Fiber 已完成启动;服务消失,说明 Fiber 已结束并撤销了它的注册。
每个包都在自己的 package.json 里声明 dsh 字段,声明内容决定它扮演什么角色:
- dsh.profile:列出某个 profile 叠放的 bundle;
- dsh.bundle:指向该 bundle 的补丁文件。
你要写的本地插件,就是一个最小 bundle:package.json、插件入口代码,以及它挂载用的配置行。profile 本身还持有它安装的树外插件,这是另一条挂载入口;本课走补丁行这条路。
先声明、后挂载,顺序不能反:dsh 靠 package.json 里的 dsh 字段知道去哪里找你的代码。
创建、挂载并确认最小插件
- 1
确认 dsh 在运行。若未运行,在项目目录启动本地 Web UI:
shnpx @deepseek-ai/dsh web - 2
新建插件目录 hello-service,在其中创建 package.json,用 dsh 字段声明它指向自己的 bundle 补丁文件:
json{ "name": "hello-service", "version": "0.1.0", "dsh": { "bundle": "./cordis.bundle.yml" } } - 3
编写插件入口:在 apply 中向上下文注册一个服务,并在日志中打印服务名。下面的代码是示意,具体 API 签名以 docs 的 Services 页为准:
tsimport { Context } from 'cordis' export const name = 'hello-service' export function apply(ctx: Context) { ctx.provide('hello', { greet: () => 'hi' }) ctx.logger('hello').info('service registered: hello') } - 4
把插件挂进配置树:在 profile 的 cordis.patch.yml 插入一行,或用 --patch 覆盖层。补丁按 id 定位一行并整体替换其配置,或插入新行;行 id 与字段名以生成的 config catalog 为准。
- 5
让配置树重新生效(按你的运行方式重载或重启)。层序固定:profile 的 cordis.patch.yml → home 级补丁 → --patch 覆盖层。
- 6
确认结果:日志出现服务注册信息,运行中的上下文/注册表里能查到 hello 服务名。随后移除补丁行,再确认该服务随插件卸载自动消失。
你应该看到
插件挂载成功:日志打印服务注册信息,hello 服务名出现在运行中的上下文/注册表中;移除挂载后,该服务随插件卸载自动消失。
补丁套叠顺序固定:profile 的 cordis.patch.yml → home 级补丁 → --patch 覆盖层。同一条行 id 在更上层出现,会整体替换下层的整行配置。插件行没生效时,先查它是否被更高层同 id 的行覆盖,再看日志里该插件行的状态。
dsh 仍是 developer preview,兼容性破坏性变更随时可能发生。仓库要求 Node.js ^22.19.0 或 >=24.0.0;版本不满足时,即使 npx 本身能跑,也可能因 engine 校验失败而无法启动。动手前固定一个已知可用的包版本,并对照当前文档验证。
遇到问题:插件行已写入补丁,但日志没有服务注册信息,上下文/注册表中也查不到服务名。
可能原因
- 行 id 与更高层(home 级或 --patch)冲突,整行配置被替换。
- 插件入口代码在启动阶段抛错,Fiber 未能推进到已启动,注册未执行。
- 插件挂载后又很快被卸载,其效果被自动撤销,服务随之消失。
排查步骤
- 核对层序与行 id 唯一性,确认插件行未被更高层覆盖。
- 修正插件入口代码并重新应用配置,观察 Fiber 状态与日志错误。
- 确认不是主动或自动卸载导致:注册是效果,卸载即撤销,这是预期行为。
自检:为什么插件卸载后,它注册的服务会自动从上下文中消失?
插件向上下文做出的注册都是可逆效果:挂载时生效,卸载时由框架自动撤销。dsh 没有需要手工清理的特权内核——每个插件由 Fiber 管理生命周期阶段,插件卸载时,其注册的服务、事件与效果一并解除,上下文不残留死服务。