news 2026/8/19 9:32:39

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

作者头像

张小明

前端开发工程师

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

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

dsh 说"一切皆插件",但插件到底怎么挂上去的?靠的是一个叫 Cordis 的框架。这篇拆解 Cordis 的五个核心思想——你不需要精通它才能用 dsh,但读懂这些机制后,翻源码和写插件会顺畅很多。

文章目录

  • 理解 Cordis:让插件像乐高一样拼装的底层引擎——《DSH 从入门到精通》系列
    • Cordis 是什么
    • 五个核心思想
      • 思想一:插件是实现了 Service 的对象
      • 思想二:上下文是服务仓库
      • 思想三:通过 inject 声明依赖
      • 思想四:类型化事件用于通信
      • 思想五:注册是可逆 effect
    • Fiber 状态机
    • 加载器与 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}from'@deepseek-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.toolsctx.llmctx.sessions),其他插件通过这个 key 查找服务,而不是 import 具体实现。

Context (ctx) 服务仓库

register

register

ctx.tools

ctx.llm

ctx.tools

ctx.llm

ctx.sessions

ctx.agents

Provider 插件 A
注册 ctx.tools

Provider 插件 B
注册 ctx.llm

Consumer 插件
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 一定准备好了constllm=ctx.llmconsttools=ctx.tools// ...}

这意味着cordis.yml中的行顺序不影响加载顺序——依赖关系决定激活时机。你把 Consumer 放在 Provider 前面,Cordis 会等 Provider 就绪后才激活 Consumer。

更关键的是,inject不是一次性的启动检查。如果运行中某个服务消失了(Provider 被卸载或热替换),所有依赖它的插件也会被卸载,等新 Provider 出现后再重新加载。这保证了运行中的 Consumer 永远不会持有一个不可用的服务引用。

思想四:类型化事件用于通信

服务通过 TypeScript declaration merging 声明事件名,然后以四种模式之一派发:

模式是否 await派发顺序有返回值适用场景
emit注册顺序观察通知(日志、遥测)
waterfall注册顺序拦截/包装(around-middleware)
parallel并行扇出(多监听器独立处理)
serial注册顺序有序决策(如 turn-stopping)

waterfall 是最特殊的模式——它是 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(()=>{consttimer=setInterval(()=>console.log('tick'),1000)return()=>clearInterval(timer)// 卸载时执行})}

如果卸载顺序重要——比如先关闭连接再清理缓存——把相关工作放在同一个ctx.effect()中,disposer 按声明顺序的逆序执行。

Fiber 状态机

每个加载的插件实例拥有一个 fiber,经历以下状态:

声明但依赖未就绪

依赖全部就绪

apply 执行完成

apply 或配置校验抛异常

配置变更/热替换/依赖消失

所有 disposer 执行完毕

PENDING

LOADING

ACTIVE

FAILED

UNLOADING

DISPOSED

理解 fiber 状态对于诊断"插件为什么没加载"很关键。一个 PENDING 状态的 fiber 不会保持 Node 事件循环活跃——如果整个应用只有 PENDING 的 fiber,进程会以 exit code 0 退出,没有任何报错。这种情况通常意味着某个inject声明的服务没有被任何 Provider 提供。

加载器与 cordis.yml

cordis.yml是一个有序的插件行列表。每行声明一个插件的idname(模块标识符或 npm 包名)、可选的configinject

-id:llm-deepseekname:'@deepseek-ai/dsh-llm-deepseek'config:thinking:enabledreasoningEffort:maxinject:[llm]

加载器并发挂载所有行——行顺序不决定加载顺序,服务依赖才决定。配置中支持!!js表达式插值,让环境变量选择插件成为可能:

-id:shellname:'@deepseek-ai/dsh-bash-local'disabled:!!jsprocess.env.DSH_SANDBOX === 'true'# 当 DSH_SANDBOX=true 时,这行被禁用,换用 dsh-bash-sandbox

!!js在两处被插值:条目的config(在声明的 inject 激活后,针对该插件的ctx.serviceName)和disabled字段(每次挂载决策时)。其他元数据保持字面量。

服务定义的两种形态

Cordis 的 Service Definition 可以是抽象类或具体注册表:

抽象类形态(如ShellExecutor):声明接口契约,Provider 继承它并实现方法。dsh-shell声明了执行器契约,dsh-bash-localdsh-bash-sandbox分别实现它。

具体注册表形态(如WebRuntime):Service 本身就是一个注册表,Provider 往里面注册实例。dsh-web声明了 Provider 注册和选择服务,web-search-exaweb-search-perplexity各自往注册表里注册。

两种形态的选择标准:如果能力是"找一个实现来执行"用抽象类;如果能力是"从多个候选中选一个"用注册表。

declaration merging 与类型安全

Cordis 通过 TypeScript 的 declaration merging 机制让ctx.<key>在编译期类型安全:

// 在 Service Definition 包中declaremodule'@deepseek-ai/cordis'{interfaceContext{greeter:GreeterService}}exportclassGreeterServiceextendsService{constructor(ctx:Context){super(ctx,'greeter')// 运行时注册}greet(who:string){return`Hello,${who}!`}}

两段代码协作:super(ctx, 'greeter')在运行时把实例注册到ctx.greeterdeclare module块在编译期把greeter加到Context接口。没有 declaration merging,服务在运行时仍然工作,但 Consumer 失去类型安全——ctx.greeter会被标红。这正是 dsh 中ctx.toolsctx.llmctx.sessions等所有服务 key 的来源。

从 Cordis 到 dsh

dsh 在 Cordis 之上构建的每一样东西都遵循这五个思想:

dsh 概念Cordis 机制
能力接缝三角色Service Definition(Service 子类)+ Provider(注册到 ctx.key)+ Consumer(inject 声明依赖)
Profile/Bundle/Patchcordis.yml 加载器 +!!js插值 + patch 按 id 覆盖
HMR 热替换fiber 状态机 + 可逆 effect + inject 依赖追踪
事件扩展点四种派发模式 + declaration merging 声明事件
工具注册表ctx.tools服务 +ctx.effect()可逆注册

当你理解了 Cordis,dsh 的源码就不再是一个黑箱——你能在packages/目录下找到每一个 Service Definition、Provider 和 Consumer,理解它们如何通过ctx连接在一起。

小结

说实话,Cordis 的五个思想单独拎出来都不新鲜——Service Locator、依赖注入、事件总线、可逆注册,每个都是经典模式。但把它们捏在一起做成一个"一切皆插件"的运行时,效果就不一样了:热替换 Provider 不用重启、HMR 重组插件树、运行时保证依赖一致性,这些能力是自然而然流出来的,不是事后补的。

下一篇看 Agent 怎么用事件溯源来"记住"对话。

相关文件

  • Cordis 入门:docs/cordis-primer.md
  • Cordis 教程:docs/cordis-tutorial/index.md
  • Cordis API 参考:docs/cordis-api/
  • 事件语义:docs/event-producer-consumer.md
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 9:31:58

进玉电极外挂V6到V6.1升级指南:安全迁移与功能验证

在实际的模具设计与制造流程中&#xff0c;电极设计是连接CAD模型与CNC加工的关键环节。许多工程师会使用特定的外挂工具来提升电极设计的效率与规范性&#xff0c;例如在UG/NX环境中&#xff0c;进玉电极外挂就是一款广为人知的辅助工具。当这类工具发布新版本时&#xff0c;如…

作者头像 李华
网站建设 2026/8/19 9:30:11

ubuntu24.04 启动故障 处理

一 故障现象服务器在启动过程中无法进入操作系统&#xff0c;卡在 (initramfs) BusyBox 紧急救援模式。屏幕持续报错&#xff1a;ALERT! /dev/mapper/ubuntu--vg-lv--0 does not exist. Dropping to a shell!同时系统不断提示 mdadm: No arrays found...。此现象导致 Kubernete…

作者头像 李华
网站建设 2026/8/19 9:30:01

LibreTranslate 离线部署实战:搭建一台完全自主的机器翻译 API

LibreTranslate 离线部署实战&#xff1a;搭建一台完全自主的机器翻译 API 【免费下载链接】LibreTranslate Free and Open Source Machine Translation API. Self-hosted, offline capable and easy to setup. 项目地址: https://gitcode.com/GitHub_Trending/li/LibreTrans…

作者头像 李华
网站建设 2026/8/19 9:29:51

嵌入式开发中版本控制实践:Keil与RT-Thread Studio集成Git/SVN指南

1. 为什么嵌入式开发也需要版本控制&#xff1f; 如果你还在用“项目备份_最终版”、“项目备份_最终版2”、“项目备份_真最终版”这样的文件夹来管理你的Keil或RT-Thread Studio工程&#xff0c;那么是时候停下来&#xff0c;认真考虑引入一套正经的版本控制系统了。这不仅仅…

作者头像 李华
网站建设 2026/8/19 9:29:49

时间敏感网络(TSN)是什么?

设想一条繁忙的高速公路。传统以太网上的数据包像普通车辆&#xff0c;它们排队、等待、在拥堵中挣扎&#xff0c;无法保证何时到达。但对于工业自动化、自动驾驶汽车或专业音视频等应用&#xff0c;有些数据就像救护车或消防车&#xff0c;必须在精确的时间窗口内抵达&#xf…

作者头像 李华
网站建设 2026/8/19 9:28:38

基于ESP32与L298N的WiFi遥控四驱格斗机器人全栈开发指南

1. 项目概述&#xff1a;从零打造一台WiFi遥控的四驱格斗机器人 几年前&#xff0c;我在一个创客展上看到一群爱好者围着一块场地&#xff0c;用自己制作的机器人互相“搏斗”&#xff0c;那种机械碰撞的激情和亲手创造的成就感&#xff0c;让我瞬间着了迷。从那时起&#xff0…

作者头像 李华