ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

deepseek-harness之理解 Cordis:让插件像乐高一样拼装的底层引擎——《DSH 从入门到精通》系列

deepseek-harness之理解 Cordis:让插件像乐高一样拼装的底层引擎——《DSH 从入门到精通》系列 理解 Cordis让插件像乐高一样拼装的底层引擎——《DSH 从入门到精通》系列dsh 说一切皆插件但插件到底怎么挂上去的靠的是一个叫 Cordis 的框架。这篇拆解 Cordis 的五个核心思想——你不需要精通它才能用 dsh但读懂这些机制后翻源码和写插件会顺畅很多。文章目录理解 Cordis让插件像乐高一样拼装的底层引擎——《DSH 从入门到精通》系列Cordis 是什么五个核心思想思想一插件是实现了 Service 的对象思想二上下文是服务仓库思想三通过 inject 声明依赖思想四类型化事件用于通信思想五注册是可逆 effectFiber 状态机加载器与 cordis.yml服务定义的两种形态declaration merging 与类型安全从 Cordis 到 dsh小结Cordis 是什么Cordis 是 dsh 内置的插件框架vendored 在vendor/目录下。它的设计理念在一篇论文《A Programming Paradigm for Spatiotemporal Composability》中有完整阐述。对于 dsh 的使用者来说Cordis 提供了三个关键能力把功能封装为插件、通过服务 key 而非 import 来发现依赖、让所有注册可逆。你不需要先精通 Cordis 才能用 dsh但理解它的核心思想会让你在阅读源码和编写插件时事半功倍。五个核心思想Cordis 的设计可以用五句话概括。我们逐一展开。思想一插件是实现了 Service 的对象Cordis 接受三种插件形态import{Service,typeContext}fromdeepseek-ai/cordis// 形态 1函数插件最常见exportfunctionapply(ctx:Context){ctx.effect((){console.log(插件加载了)return()console.log(插件卸载了)})}// 形态 2对象插件exportconstplugin{name:my-plugin,apply(ctx:Context){// ...},}// 形态 3类插件Service 子类需要暴露服务时使用exportclassMyServiceextendsService{constructor(ctx:Context){super(ctx,myService)}}函数形态适合只需要注册副作用的场景类形态在需要暴露一个ctx.key服务时才使用。一个插件模块只需要导出一个apply函数或apply方法的对象/类Cordis 加载时调用它传入上下文对象ctx。思想二上下文是服务仓库ctx是一个服务仓库。每个服务从上下文中认领一个稳定的ctx.key如ctx.tools、ctx.llm、ctx.sessions其他插件通过这个 key 查找服务而不是 import 具体实现。Context (ctx) 服务仓库registerregisterctx.toolsctx.llmctx.toolsctx.llmctx.sessionsctx.agentsProvider 插件 A注册 ctx.toolsProvider 插件 B注册 ctx.llmConsumer 插件inject: [tools]Consumer 插件inject: [llm]这种设计的核心价值Consumer 不知道也不关心 Provider 是谁。你把ctx.tools上注册的 Provider 从本地实现换成沙箱实现所有注入了tools的插件会自动重启并绑定到新实现Consumer 代码不需要任何修改。思想三通过 inject 声明依赖一个插件通过inject字段声明它需要的服务。Cordis 会把这个插件保持在 PENDING 状态直到所有声明的服务都存在才激活它。exportconstinject[llm,tools]exportfunctionapply(ctx:Context){// 到这里时 ctx.llm 和 ctx.tools 一定准备好了constllmctx.llmconsttoolsctx.tools// ...}这意味着cordis.yml中的行顺序不影响加载顺序——依赖关系决定激活时机。你把 Consumer 放在 Provider 前面Cordis 会等 Provider 就绪后才激活 Consumer。更关键的是inject不是一次性的启动检查。如果运行中某个服务消失了Provider 被卸载或热替换所有依赖它的插件也会被卸载等新 Provider 出现后再重新加载。这保证了运行中的 Consumer 永远不会持有一个不可用的服务引用。思想四类型化事件用于通信服务通过 TypeScript declaration merging 声明事件名然后以四种模式之一派发模式是否 await派发顺序有返回值适用场景emit否注册顺序否观察通知日志、遥测waterfall否注册顺序是拦截/包装around-middlewareparallel是并行否扇出多监听器独立处理serial是注册顺序是有序决策如 turn-stoppingwaterfall 是最特殊的模式——它是 around-middleware 语义。监听器收到(...args, next)调用next()把可能修改后的结果委派给下一个监听器不调next()则短路整个链// 一个 waterfall 监听器拦截工具执行请求ctx.on(tools/pre-execute,(exec,next){if(isDangerous(exec.name)){return{kind:deny,reason:危险操作被拒绝}// 不调 next()短路链}returnnext(exec)// 委派给下一个监听器})对于单决策事件短路就是设计意图。策略监听器拥有决策权时可以不调next()只做标注或观察的监听器必须委派。思想五注册是可逆 effect所有通过 Cordis API 注册的东西——prompt 段、工具 schema、适配器、provider、监听器——都是 effect。它们在插件加载时安装在插件卸载时按序回退。exportfunctionapply(ctx:Context){// 注册一个事件监听器返回 disposerctx.on(session/event,(event){console.log(事件:,event.type)})// 用 ctx.effect() 包装非 Cordis 管理的资源ctx.effect((){consttimersetInterval(()console.log(tick),1000)return()clearInterval(timer)// 卸载时执行})}如果卸载顺序重要——比如先关闭连接再清理缓存——把相关工作放在同一个ctx.effect()中disposer 按声明顺序的逆序执行。Fiber 状态机每个加载的插件实例拥有一个 fiber经历以下状态声明但依赖未就绪依赖全部就绪apply 执行完成apply 或配置校验抛异常配置变更/热替换/依赖消失所有 disposer 执行完毕PENDINGLOADINGACTIVEFAILEDUNLOADINGDISPOSED理解 fiber 状态对于诊断插件为什么没加载很关键。一个 PENDING 状态的 fiber 不会保持 Node 事件循环活跃——如果整个应用只有 PENDING 的 fiber进程会以 exit code 0 退出没有任何报错。这种情况通常意味着某个inject声明的服务没有被任何 Provider 提供。加载器与 cordis.ymlcordis.yml是一个有序的插件行列表。每行声明一个插件的id、name模块标识符或 npm 包名、可选的config和inject-id:llm-deepseekname:deepseek-ai/dsh-llm-deepseekconfig:thinking:enabledreasoningEffort:maxinject:[llm]加载器并发挂载所有行——行顺序不决定加载顺序服务依赖才决定。配置中支持!!js表达式插值让环境变量选择插件成为可能-id:shellname:deepseek-ai/dsh-bash-localdisabled:!!jsprocess.env.DSH_SANDBOX true# 当 DSH_SANDBOXtrue 时这行被禁用换用 dsh-bash-sandbox!!js在两处被插值条目的config在声明的 inject 激活后针对该插件的ctx.serviceName和disabled字段每次挂载决策时。其他元数据保持字面量。服务定义的两种形态Cordis 的 Service Definition 可以是抽象类或具体注册表抽象类形态如ShellExecutor声明接口契约Provider 继承它并实现方法。dsh-shell声明了执行器契约dsh-bash-local和dsh-bash-sandbox分别实现它。具体注册表形态如WebRuntimeService 本身就是一个注册表Provider 往里面注册实例。dsh-web声明了 Provider 注册和选择服务web-search-exa和web-search-perplexity各自往注册表里注册。两种形态的选择标准如果能力是找一个实现来执行用抽象类如果能力是从多个候选中选一个用注册表。declaration merging 与类型安全Cordis 通过 TypeScript 的 declaration merging 机制让ctx.key在编译期类型安全// 在 Service Definition 包中declaremoduledeepseek-ai/cordis{interfaceContext{greeter:GreeterService}}exportclassGreeterServiceextendsService{constructor(ctx:Context){super(ctx,greeter)// 运行时注册}greet(who:string){returnHello,${who}!}}两段代码协作super(ctx, greeter)在运行时把实例注册到ctx.greeterdeclare module块在编译期把greeter加到Context接口。没有 declaration merging服务在运行时仍然工作但 Consumer 失去类型安全——ctx.greeter会被标红。这正是 dsh 中ctx.tools、ctx.llm、ctx.sessions等所有服务 key 的来源。从 Cordis 到 dshdsh 在 Cordis 之上构建的每一样东西都遵循这五个思想dsh 概念Cordis 机制能力接缝三角色Service DefinitionService 子类 Provider注册到 ctx.key Consumerinject 声明依赖Profile/Bundle/Patchcordis.yml 加载器 !!js插值 patch 按 id 覆盖HMR 热替换fiber 状态机 可逆 effect inject 依赖追踪事件扩展点四种派发模式 declaration merging 声明事件工具注册表ctx.tools服务 ctx.effect()可逆注册当你理解了 Cordisdsh 的源码就不再是一个黑箱——你能在packages/目录下找到每一个 Service Definition、Provider 和 Consumer理解它们如何通过ctx连接在一起。小结说实话Cordis 的五个思想单独拎出来都不新鲜——Service Locator、依赖注入、事件总线、可逆注册每个都是经典模式。但把它们捏在一起做成一个一切皆插件的运行时效果就不一样了热替换 Provider 不用重启、HMR 重组插件树、运行时保证依赖一致性这些能力是自然而然流出来的不是事后补的。下一篇看 Agent 怎么用事件溯源来记住对话。相关文件Cordis 入门docs/cordis-primer.mdCordis 教程docs/cordis-tutorial/index.mdCordis API 参考docs/cordis-api/事件语义docs/event-producer-consumer.md
返回列表