云计算百科
云计算领域专业知识百科平台

DeepSeek Harness 架构解剖:「一切皆插件」是怎么实现的?Cordis 微内核与双端沙箱全景

CFzjy AI Engineer

  ▍深耕领域  AI Agent 架构设计 Harness 插件开发 Context Engineering 多智能体协同 & RAG   ▍技术底座  Python TypeScript · Cordis 插件 MCP & Skills · FastAPI Docker   ▍工程能力  专注 Agent 运行时与 Harness 插件开发、长上下文与流式交互优化、LLM 全链路工程化落地

   

「 让 AI 交互更智能,让技术落地更高效 」   |   欢迎技术探讨 / 项目合作,关注解锁大模型与智能交互的无限可能!

DeepSeek Harness 架构解剖:「一切皆插件」是怎么实现的?Cordis 微内核与双端沙箱全景

DeepSeek Harness 插件架构系列封面

📚 《DeepSeek Harness 插件架构》连载专栏 · 第 01 篇

2026 年如果你去翻企业 Agent 项目的技术选型文档,会发现一个越来越常见的名字:DeepSeek Harness(DSH)。它是 DeepSeek 官方开源的 Agent 运行框架,而它身上最响亮的标签只有五个字:「一切皆插件」。工具是插件、模型接入是插件、系统提示词是插件、连会话管理和 UI 面板都是插件——框架本体薄到只剩一个内核,其余全是可插拔的积木。

这句话听起来像口号,但它是 DSH 全部设计的出发点。本文是这个专栏的第一篇,任务是把这个口号拆回工程事实:DSH 底层用了一个叫 Cordis 的插件框架(以 vendor 方式引入),它用五个核心概念撑起了「一切皆插件」;上层是 Host/Client 双端沙箱和六大于系统。读完你会拿到:最小插件的逐行解剖、五核心概念的完整拆解、子系统地图、五种事件分发模式的可运行模拟器,和一张避坑速查表。

痛点一:Agent 框架越用越像「修改地狱」。 用封装式框架扩展能力,要么继承别人的类,要么 fork 源码打补丁——框架一升级,你的定制全崩。

痛点二:插件化框架的文档全是"是什么",没人讲"为什么"。 你照着教程能写出第一个插件,但不知道为什么 apply(ctx) 长这样、为什么卸载不会泄漏、为什么插件之间能互相调服务——离开教程还是不会设计。

痛点三:DSH 的生态在爆发,学习窗口在关闭。 官方文档体系刚成型、插件市场刚开始收 PR,现在是入场写第一批深度内容的时候;半年后满街都是"保姆级教程",先发者通吃。

网上已有的 DSH 文章大多是"部署 + 第一个插件"的入门流水账。本文的差异化很明确:从官方文档与源码视角讲架构底座——五个核心概念逐个拆、五种事件分发模式给可运行模拟器、双端沙箱和子系统地图一次画全。本文事实全部来自 DeepSeek Harness 官方文档(Cordis 入门、服务与依赖、第一个插件教程),代码均为官方示例或可运行的等价演示。想追这个 14 篇的完整系列(下一篇:插件生命周期与 ctx.effect 清理机制),可以先关注我的 CSDN 主页。

文章目录

    • DeepSeek Harness 架构解剖:「一切皆插件」是怎么实现的?Cordis 微内核与双端沙箱全景
      • 一、为什么 Agent 框架需要「插件化」?(原理拆解篇)
        • 1.1 从一个直觉误区说起:框架是"工具箱"还是"运行时"?
        • 1.2 主流 Agent 框架的扩展机制全景对比
      • 二、基础篇:Cordis 的五个核心概念——「一切皆插件」的地基
        • 2.1 概念一:插件——实现服务的最小单位
        • 2.2 概念二:上下文——服务的容器
        • 2.3 概念三:依赖注入——加载顺序靠声明,不靠编排
        • 2.4 概念四:类型化事件——插件通信的公共语言
        • 2.5 概念五:可逆副作用——注册的一切都能撤销
        • 2.6 五概念串起来:一张最小系统的完整画像
      • 三、进阶篇:双端沙箱与六大于系统——地基之上盖了什么
        • 3.1 Host/Client 双端架构:一个内核,两个世界
        • 3.2 六大于系统地图:能力边界一目了然
        • 3.3 四类扩展点:宿主怎么被插件"改造"
      • 四、高阶:五种事件分发模式——插件通信的完整语法
        • 4.1 一张表看懂五种模式
        • 4.2 一段可运行的模拟器:把五种模式跑给你看
        • 4.3 Waterfall 的短路语义:为什么"拦截"天然安全
        • 4.4 服务隔离:同一份服务,两个互不干扰的实例
      • 五、实战避坑指南:高频问题与一键解决速查表
      • 结语

一、为什么 Agent 框架需要「插件化」?(原理拆解篇)

1.1 从一个直觉误区说起:框架是"工具箱"还是"运行时"?

大多数人选 Agent 框架时的心智模型是工具箱:框架 = 一堆现成组件(记忆、检索、工具调用)装在一个箱子里,我打开箱子拿组件用,不够就自己往箱子里塞一个。LangChain 就是这个模型的极致——几百个 integration 包,箱子大得惊人。

但这个模型有一个被掩盖的问题:箱子本身没有结构。组件之间怎么通信、状态放哪、生命周期谁管、两个第三方组件怎么协作——这些"运行时问题"全靠使用方自己组织。于是你看到的现象是:Agent 项目做着做着,业务代码和框架代码长在了一起;想升级框架版本,先准备好改一周代码;想把同事写的"记忆模块"复用到自己的项目,得先读懂他的 import 链。

说一个具体到能闻到味道的场景——"修改地狱"三部曲:第一步,你要给 Agent 加一个"工具调用前审计",发现框架的 AgentExecutor 没留钩子,只能继承它重写 _call 方法;第二步,框架升了个小版本,AgentExecutor 内部改了三行,你的子类静默失效,生产环境的审计日志断了一周没人发现;第三步,审计逻辑修好后你想把这套东西给另一个项目用,发现它已经和公司内部的会话管理、模型网关耦合在一起,抽出来要改二十个文件。三个步骤走完,你从一个"框架使用者"变成了"框架维护者"——而且维护的是一份没有上游支持的私改版。这就是没有运行时结构的框架给企业项目的真实账单。

DSH 换了一个心智模型:框架是运行时(runtime),不是工具箱。用一个类比说透:传统框架像菜市场——食材(组件)很丰富,但洗菜、切配、火候、摆盘(运行时逻辑)全是你的活;DSH 像连锁餐厅的中央厨房——每个厨师(插件)签一份标准契约(apply(ctx)),进场(加载)排队(依赖就绪)有调度,离场(卸载)清理灶台(ctx.effect)有保障,新菜式(新插件)上菜单不用改后厨结构。菜市场能做出更野的菜,但中央厨房才能规模化——企业 Agent 项目要的是后者。

那你的项目到底需不需要一个插件化运行时?给三个自查信号:信号一,你的 Agent 能力是否由多个团队贡献——如果只有一个人写代码,插件化的收益是负的,直接写死更快;信号二,你是否已经在 fork 框架或继承内部类打补丁——出现这个动作,说明"扩展"已经开始变成"修改",插件化能止血;信号三,你的能力是否需要按客户/环境开关——A 客户要飞书集成、B 客户要钉钉,插件化让你按 profile 装配而不是改代码发布。三个信号中两个以上,就值得认真看 DSH。这个判据不是纸上谈兵:本文开头提到的企业选型趋势背后,几乎都能对号入座到信号二和信号三——它们在用真金白银的迁移成本,为"修改地狱"投票。

「一切皆插件」翻译成工程语言就是三句话:框架本体只有内核;能力全部由插件贡献;插件之间的协作规则由内核统一保证。先看这张对比图:

#mermaid-svg-PBStgwsHRZyEMWM1{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PBStgwsHRZyEMWM1 .error-icon{fill:#552222;}#mermaid-svg-PBStgwsHRZyEMWM1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PBStgwsHRZyEMWM1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PBStgwsHRZyEMWM1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PBStgwsHRZyEMWM1 .marker.cross{stroke:#333333;}#mermaid-svg-PBStgwsHRZyEMWM1 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PBStgwsHRZyEMWM1 p{margin:0;}#mermaid-svg-PBStgwsHRZyEMWM1 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-PBStgwsHRZyEMWM1 .cluster-label text{fill:#333;}#mermaid-svg-PBStgwsHRZyEMWM1 .cluster-label span{color:#333;}#mermaid-svg-PBStgwsHRZyEMWM1 .cluster-label span p{background-color:transparent;}#mermaid-svg-PBStgwsHRZyEMWM1 .label text,#mermaid-svg-PBStgwsHRZyEMWM1 span{fill:#333;color:#333;}#mermaid-svg-PBStgwsHRZyEMWM1 .node rect,#mermaid-svg-PBStgwsHRZyEMWM1 .node circle,#mermaid-svg-PBStgwsHRZyEMWM1 .node ellipse,#mermaid-svg-PBStgwsHRZyEMWM1 .node polygon,#mermaid-svg-PBStgwsHRZyEMWM1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PBStgwsHRZyEMWM1 .rough-node .label text,#mermaid-svg-PBStgwsHRZyEMWM1 .node .label text,#mermaid-svg-PBStgwsHRZyEMWM1 .image-shape .label,#mermaid-svg-PBStgwsHRZyEMWM1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PBStgwsHRZyEMWM1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PBStgwsHRZyEMWM1 .rough-node .label,#mermaid-svg-PBStgwsHRZyEMWM1 .node .label,#mermaid-svg-PBStgwsHRZyEMWM1 .image-shape .label,#mermaid-svg-PBStgwsHRZyEMWM1 .icon-shape .label{text-align:center;}#mermaid-svg-PBStgwsHRZyEMWM1 .node.clickable{cursor:pointer;}#mermaid-svg-PBStgwsHRZyEMWM1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PBStgwsHRZyEMWM1 .arrowheadPath{fill:#333333;}#mermaid-svg-PBStgwsHRZyEMWM1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PBStgwsHRZyEMWM1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PBStgwsHRZyEMWM1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PBStgwsHRZyEMWM1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PBStgwsHRZyEMWM1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PBStgwsHRZyEMWM1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PBStgwsHRZyEMWM1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PBStgwsHRZyEMWM1 .cluster text{fill:#333;}#mermaid-svg-PBStgwsHRZyEMWM1 .cluster span{color:#333;}#mermaid-svg-PBStgwsHRZyEMWM1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PBStgwsHRZyEMWM1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PBStgwsHRZyEMWM1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PBStgwsHRZyEMWM1 .icon-shape,#mermaid-svg-PBStgwsHRZyEMWM1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PBStgwsHRZyEMWM1 .icon-shape p,#mermaid-svg-PBStgwsHRZyEMWM1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PBStgwsHRZyEMWM1 .icon-shape .label rect,#mermaid-svg-PBStgwsHRZyEMWM1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PBStgwsHRZyEMWM1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PBStgwsHRZyEMWM1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PBStgwsHRZyEMWM1 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

运行时模型(DSH 插件式)

工具箱模型(封装式框架)

❌ 扩展即修改

✅ 扩展即添加

框架 = 一堆组件包

组件间协作靠使用方自己组织

业务代码与框架代码纠缠升级 = 改一周代码

框架 = 薄内核 + 插件契约

协作规则由内核保证依赖/事件/生命周期

新能力 = 新插件卸载不残留、升级不塌方

修改地狱

可插拔演进

1.2 主流 Agent 框架的扩展机制全景对比

把四类主流框架的"扩展方式"放在一张表里,你就明白 DSH 的位置在哪:

框架/生态扩展机制扩展时的代价适用场景
LangChain / LlamaIndex 封装 integration 包 + 继承类 组件间协作自己组织,升级成本高 快速原型、一次性脚本
CrewAI 角色/任务声明式配置 控制流黑盒,深定制要 fork 演示级多角色协作
Claude Code / MCP 生态 工具协议(MCP server)+ Skills 工具层可插拔,宿主本身难定制 编码助手、工具接入
DeepSeek Harness Cordis 插件化:一切皆插件 插件契约统一,宿主本身可被插件改造 企业级 Agent 运行时

注意最后一行和前两行的本质区别:LangChain/CrewAI 里,框架是成品,你只能按它的路走;DSH 里,框架是半成品,宿主的行为本身也是插件贡献的——系统提示词怎么拼、LLM 怎么调、工具怎么执行、连 UI 面板长什么样,都是插件说了算。这就是"一切皆插件"的字面含义,也是企业选择它的理由:把框架当底座,把业务能力全部插件化沉淀。

当然要说清楚 DSH 不是万能药,它有明确的"不适合"场景:一次性脚本、两天生命的原型——插件化的契约成本(name/inject/effect 这些样板)对它们纯属负担,LangChain 一把梭更合适;强绑定其他生态的存量项目——如果团队已经在 LangGraph 上建了大量图状态和评测资产,迁移要算总账(本专栏第 12 篇会专门给迁移判据)。选型的诚实答案是:DSH 的主场是"要长期演进、要多团队协作、要按环境装配"的企业 Agent 底座,其他场景用工具箱模型就好。

二、基础篇:Cordis 的五个核心概念——「一切皆插件」的地基

DSH 底层的 Cordis 框架,官方文档开篇就给了五个核心概念。这五条就是全部地基,每一条都对应一个你写插件时天天要面对的问题。先看全景图,再逐条拆。

在逐条拆之前,先用一句话把五条串成一句"人话版地图":插件(我是谁)带着依赖注入(我需要谁)进入上下文(我在哪、能碰到谁),用类型化事件(我怎么跟别人说话)协作,所有行为都是可逆副作用(我走了要收拾干净)。后面每一节都会回到这句话上。

#mermaid-svg-2ckUJ0xEY5bVBNW8{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .error-icon{fill:#552222;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .marker.cross{stroke:#333333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 p{margin:0;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .cluster-label text{fill:#333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .cluster-label span{color:#333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .cluster-label span p{background-color:transparent;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .label text,#mermaid-svg-2ckUJ0xEY5bVBNW8 span{fill:#333;color:#333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .node rect,#mermaid-svg-2ckUJ0xEY5bVBNW8 .node circle,#mermaid-svg-2ckUJ0xEY5bVBNW8 .node ellipse,#mermaid-svg-2ckUJ0xEY5bVBNW8 .node polygon,#mermaid-svg-2ckUJ0xEY5bVBNW8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .rough-node .label text,#mermaid-svg-2ckUJ0xEY5bVBNW8 .node .label text,#mermaid-svg-2ckUJ0xEY5bVBNW8 .image-shape .label,#mermaid-svg-2ckUJ0xEY5bVBNW8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .rough-node .label,#mermaid-svg-2ckUJ0xEY5bVBNW8 .node .label,#mermaid-svg-2ckUJ0xEY5bVBNW8 .image-shape .label,#mermaid-svg-2ckUJ0xEY5bVBNW8 .icon-shape .label{text-align:center;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .node.clickable{cursor:pointer;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .arrowheadPath{fill:#333333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2ckUJ0xEY5bVBNW8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ckUJ0xEY5bVBNW8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2ckUJ0xEY5bVBNW8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .cluster text{fill:#333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .cluster span{color:#333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2ckUJ0xEY5bVBNW8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .icon-shape,#mermaid-svg-2ckUJ0xEY5bVBNW8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .icon-shape p,#mermaid-svg-2ckUJ0xEY5bVBNW8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .icon-shape .label rect,#mermaid-svg-2ckUJ0xEY5bVBNW8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ckUJ0xEY5bVBNW8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2ckUJ0xEY5bVBNW8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2ckUJ0xEY5bVBNW8 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

插件 Plugin函数/对象/类 三形态

上下文 Context服务的容器 ctx.

依赖注入 inject声明依赖,就绪才启动

类型化事件 Eventsemit/waterfall/parallel/serial/bail

可逆副作用 ctx.effect卸载自动清理

四件套 + 插件 = 「一切皆插件」的地基

2.1 概念一:插件——实现服务的最小单位

在 Harness 中,插件是一个导出 apply 函数的 TypeScript 模块。框架在加载时调用 apply,把一个 ctx(上下文对象)交给你,你通过 ctx 注册能力。这是官方教程里最小插件的完整形态:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
// Required dependencies are ready before apply runs.
console.log('[hello-plugin] plugin loaded!')
}

逐行拆解这三行关键代码:name 是插件在注册表里的唯一标识——调试日志靠它定位、依赖声明靠它引用、服务隔离靠它分组,起名要认真,它是插件在框架里的"身份证";apply(ctx) 是插件唯一必须实现的生命周期入口——没有 main 函数、没有启动脚本,apply 就是全部;ctx 是你和整个框架对话的唯一窗口,注册工具、监听事件、声明清理,全走它。官方原文强调:“这就是完整配置”——没有隐藏的样板代码,这是插件化框架"薄内核"的第一观感。

ctx 这个窗口上最常用的四个"把手",提前认识一下,后面各章都会反复用到:ctx.get(key) 查可选服务、ctx.on(event, fn) 注册事件监听、ctx.effect(fn) 声明带清理的副作用、ctx.<service> 访问已注入的服务实例。四个把手对应"查、听、做、用"四类动作——整个 DSH 插件开发,不外乎这四个动作的组合。

插件支持三种形态,能力完全相同,选哪个看使用场景。官方文档给了完整对照,我把三种形态并排放:

函数形式(最常用):导出 name + apply,适合绝大多数"我要加点能力"的场景——工具插件、提示词插件、监听器插件基本都是函数形式。

对象形式:把依赖声明和入口收进一个对象,适合带配置、希望结构更紧凑的插件:

import type { Context } from '@deepseek-ai/cordis'

export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// …
},
}

类形式:继承 Service,在 constructor(ctx) 里 super(ctx, 'myService')——当你的插件要向其他插件提供服务时用它(下一节细讲):

import { Service, type Context } from '@deepseek-ai/cordis'

export default class MyService extends Service {
static inject = ['tools']

constructor(ctx: Context) {
super(ctx, 'myService')
// Perform synchronous initialization in the constructor.
}
}

官方原话值得划重点:“大多数情况下,函数形式足够了。当插件需要向其他插件提供服务时,可使用类形式。” 三种形态可以混用、可以互相依赖——形态只是入口语法糖,内核对待它们一视同仁。这又印证了"薄内核":框架不关心你的插件长什么样,只关心你实现没实现契约。

2.2 概念二:上下文——服务的容器

官方原文对 Context 的定义一句话:“上下文是服务的容器。一个服务占据一个稳定的 ctx.<key>(如 ctx.tools、ctx.llm、ctx.sessions);其他插件通过 key 查找服务,而非导入具体实现。”

这句话里有三个字值千金:“而非导入”。传统 JS 项目里要用一个模块就是 import——编译期就把两个模块焊死了。Cordis 里你永远不 import 别的插件的实现,只声明"我需要 key 为 tools 的服务",运行时内核把就绪的实例放到 ctx.tools 上给你。查找服务靠 key,不靠 import——这是插件之间互不感知、可独立装卸的前提。

内置服务一览(全部挂在 ctx 上):

ctx key服务管什么
ctx.tools ToolRuntime 工具注册与执行流水线
ctx.llm LLM 服务 模型流式输出、token 计量
ctx.agents Agent 服务 实时智能体协调
ctx.sessions 会话服务 会话与持久化

用类比收住这一条:ctx 像写字楼的前台——每个服务是一间固定门牌号的办公室,你找人(服务)报门牌号(key),不用认识办公室里的人(实现)。办公室换人(服务实现被替换),门牌号不变,你的代码一行不用改。

"稳定 key + 可替换实现"还有一个被低估的价值:可测试性。单元测试里想 mock 掉 LLM 服务?不用 mock 模块系统,注册一个 key 相同、实现不同(固定返回)的假服务就行——被测插件的代码一行不动。在企业 Agent 项目里,这个特性直接决定你的插件能不能进 CI 流水线。

顺带立两条命名纪律:key 要语义化(tools、llm、sessions 都是能力名而非实现名),因为 key 是跨插件、跨团队的公共契约,改 key 等于改协议;自己的服务起名前先查官方子系统页面,官方文档明确说了——服务名、公开方法、源码位置都会生成到每个子系统的页面里,“不要维护第二份静态列表”。按官方生成区开发,是避免 key 冲突和过时信息的第一纪律。

2.3 概念三:依赖注入——加载顺序靠声明,不靠编排

插件声明依赖的方式是导出 inject 数组:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-tool-plugin'
export const inject = ['tools']

export function apply(ctx: Context) {
// ctx.tools is ready here.
ctx.tools.register(/* … */)
}

inject = ['tools'] 告诉内核三件事:我的 apply 必须等 tools 服务就绪后才执行;执行时 ctx.tools 一定可用;如果 tools 服务在运行中消失(比如提供它的插件被卸载),我自动被销毁,等服务回来再自动重启。官方原文把最后一点讲得很重:“This prevents a plugin from calling a service that no longer exists.”——依赖声明同时是生命周期保险,杜绝了传统框架里最经典的"空指针式"运行时崩溃。

依赖还分必选和可选两种:inject 里的必选,服务不在插件就不加载;可选依赖不写 inject,用的时候 ctx.get('metrics') 查一下,查不到拿 undefined 自己兜底。什么时候用哪个?给一张决策表:

判断选必选 inject选可选 ctx.get
服务不在时插件还有意义吗 没有 → 必选 有(降级运行)→ 可选
服务出现时机 启动时一定在 可能晚到、可能消失
典型例子 工具插件需要 tools 指标插件需要 metrics
失败表现 插件不加载(干净) 拿 undefined 自己兜底

一句话决策:"没有它插件没意义"用必选,"有它更好没有也行"用可选——比如一个"给每次工具调用记录指标"的插件,metrics 服务就是可选的,没有它工具照常用、指标只是不记了。

这套机制解决了插件化框架最大的工程难题:启动顺序。传统插件系统(比如 WordPress)靠"优先级数字"排序,两个插件谁先谁后靠拍脑袋,数字冲突就翻车。Cordis 的答案是:顺序由依赖图自然导出——你不需要编排,声明"我需要什么",拓扑排序由内核完成。用类比:排课表。传统方式是教务主任给每门课手填优先级;Cordis 是每个学生(插件)只填"我要先修什么课"(inject),课表(加载顺序)自动生成。

看一个具体依赖图的例子:

#mermaid-svg-CmoEGTXGMZkgfWdu{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CmoEGTXGMZkgfWdu .error-icon{fill:#552222;}#mermaid-svg-CmoEGTXGMZkgfWdu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CmoEGTXGMZkgfWdu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CmoEGTXGMZkgfWdu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CmoEGTXGMZkgfWdu .marker.cross{stroke:#333333;}#mermaid-svg-CmoEGTXGMZkgfWdu svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CmoEGTXGMZkgfWdu p{margin:0;}#mermaid-svg-CmoEGTXGMZkgfWdu .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-CmoEGTXGMZkgfWdu .cluster-label text{fill:#333;}#mermaid-svg-CmoEGTXGMZkgfWdu .cluster-label span{color:#333;}#mermaid-svg-CmoEGTXGMZkgfWdu .cluster-label span p{background-color:transparent;}#mermaid-svg-CmoEGTXGMZkgfWdu .label text,#mermaid-svg-CmoEGTXGMZkgfWdu span{fill:#333;color:#333;}#mermaid-svg-CmoEGTXGMZkgfWdu .node rect,#mermaid-svg-CmoEGTXGMZkgfWdu .node circle,#mermaid-svg-CmoEGTXGMZkgfWdu .node ellipse,#mermaid-svg-CmoEGTXGMZkgfWdu .node polygon,#mermaid-svg-CmoEGTXGMZkgfWdu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CmoEGTXGMZkgfWdu .rough-node .label text,#mermaid-svg-CmoEGTXGMZkgfWdu .node .label text,#mermaid-svg-CmoEGTXGMZkgfWdu .image-shape .label,#mermaid-svg-CmoEGTXGMZkgfWdu .icon-shape .label{text-anchor:middle;}#mermaid-svg-CmoEGTXGMZkgfWdu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CmoEGTXGMZkgfWdu .rough-node .label,#mermaid-svg-CmoEGTXGMZkgfWdu .node .label,#mermaid-svg-CmoEGTXGMZkgfWdu .image-shape .label,#mermaid-svg-CmoEGTXGMZkgfWdu .icon-shape .label{text-align:center;}#mermaid-svg-CmoEGTXGMZkgfWdu .node.clickable{cursor:pointer;}#mermaid-svg-CmoEGTXGMZkgfWdu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CmoEGTXGMZkgfWdu .arrowheadPath{fill:#333333;}#mermaid-svg-CmoEGTXGMZkgfWdu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CmoEGTXGMZkgfWdu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CmoEGTXGMZkgfWdu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CmoEGTXGMZkgfWdu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CmoEGTXGMZkgfWdu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CmoEGTXGMZkgfWdu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CmoEGTXGMZkgfWdu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CmoEGTXGMZkgfWdu .cluster text{fill:#333;}#mermaid-svg-CmoEGTXGMZkgfWdu .cluster span{color:#333;}#mermaid-svg-CmoEGTXGMZkgfWdu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CmoEGTXGMZkgfWdu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CmoEGTXGMZkgfWdu rect.text{fill:none;stroke-width:0;}#mermaid-svg-CmoEGTXGMZkgfWdu .icon-shape,#mermaid-svg-CmoEGTXGMZkgfWdu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CmoEGTXGMZkgfWdu .icon-shape p,#mermaid-svg-CmoEGTXGMZkgfWdu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CmoEGTXGMZkgfWdu .icon-shape .label rect,#mermaid-svg-CmoEGTXGMZkgfWdu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CmoEGTXGMZkgfWdu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CmoEGTXGMZkgfWdu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CmoEGTXGMZkgfWdu :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

llm 服务(模型接入插件)

审计插件inject: [llm, tools]

tools 服务(工具运行时)

业务插件inject: [audit]

这张图里没有任何一个插件写了"我先启动"——四个插件只各自声明"我依赖谁",加载顺序(llm/tools 先 → audit 后 → business 最后)是内核推出来的。更妙的是运行时动态性:如果 llm 插件被卸载,依赖它的 audit 和 business 自动销毁;llm 重新装上,链条自动恢复。传统框架里这叫"惊群恢复",要自己写一套心跳和重连;Cordis 里这是依赖声明的默认语义。

2.4 概念四:类型化事件——插件通信的公共语言

插件之间除了调用服务,还有第二种通信方式:事件。官方原文:"服务通过 TypeScript 声明合并注册事件名,然后以 emit、waterfall、parallel、serial 或 bail 方式分发。"翻译成人话:事件名是类型安全的公共契约,五种分发方式覆盖五种协作形态——观察、包装、并行扇出、按序执行、投票表决。这一条信息量太大,第四章会用整整一章加可运行模拟器拆透,这里先记住它的存在和定位:

实践规则(官方原文):“拦截和策略优先使用事件;直接能力调用优先使用服务方法。” 想改别人的行为(比如给每个工具调用前加个审计日志)→ 监听事件;想直接用别人的能力(比如注册一个工具)→ 调服务方法。这条分工规则是整个框架的设计品味所在。

事件名从哪来?官方文档的答案是TypeScript 声明合并:提供服务的插件在自己的类型声明里"合并"出事件名,所有监听方共享同一份类型定义——事件名拼错、参数类型不符,编译期直接报错,而不是上线后在日志里大海捞针。对习惯了"字符串事件名 + 手写文档"的开发者来说,这是体验上的代差:在 DSH 里,通信契约的文档就是代码本身。

五种分发模式各自最典型的真实场景,提前在这里给一张"什么时候用哪个"的速查,第四章再逐个跑模拟器:

模式DSH 里的典型场景选它的理由
emit 会话创建通知、指标打点 纯观察,不需要回应
waterfall 系统提示词组装、工具执行前后拦截 需要"包装/替换"下游结果
parallel 多个数据源并行查询 互不依赖,并行省时
serial 多步加工流水线(后一步依赖前一步) 顺序强依赖
bail 审批裁决、模型选择投票 第一个决策者说了算
2.5 概念五:可逆副作用——注册的一切都能撤销

插件化框架的噩梦是卸载残留:插件注册了一个定时器、加了几个事件监听,卸载时忘了清,内存泄漏、幽灵回调、双份执行全来了。Cordis 的解法是把它变成默认行为——官方原文:“任何通过 ctx 注册的东西——事件监听、工具、定时器——在插件卸载时都会被自动清理。你不需要手动 removeListener 或 clearInterval。”

对于框架不认识的自有资源(网络连接、子进程、文件句柄),用 ctx.effect() 声明清理函数:

import type { Context } from '@deepseek-ai/cordis'

export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)

// The returned function runs when the plugin unloads.
return () => clearInterval(timer)
})
}

这段官方示例值得记住一辈子:ctx.effect 的回调返回一个清理函数,插件卸载时框架替你调用它。注册是可逆的副作用——这条原则让"热重载"成为可能:改代码 → 卸载插件(所有副作用自动撤销)→ 重新加载,干净利落,这就是 DSH 开发体验丝滑的秘密。

再往深一层,官方还规定了一条清理纪律:“如果 teardown 顺序有要求,请将相关工作放在同一个 effect 中,以确保资源按预期顺序释放。” 翻译成场景就是:你开了一个数据库连接池,又注册了一个依赖这个连接池的定时刷新任务——这俩必须放进同一个 effect,让框架"先停任务、再关连接"的顺序由嵌套关系自然保证。如果拆成两个 effect,框架按注册逆序清理,顺序碰巧对了是运气,不对就是"卸载后还在用已关闭的连接"这类幽灵 bug。清理逻辑的嵌套结构,应该镜像资源的依赖结构——这条纪律在后续「生命周期」一篇会展开成完整方法论。

2.6 五概念串起来:一张最小系统的完整画像

把五条放在一起,一个最小插件系统的运行画像就出来了:插件(概念一)声明依赖(概念三)后被内核放进上下文(概念二)里运行,通过事件(概念四)与其他插件协作,所有注册都是可逆的(概念五)。五条规则,没有第六条——"一切皆插件"的全部地基就这么多。下一章往上走,看这套地基之上 DSH 盖出了什么。

新手对五概念最常见的三个误解,顺手纠偏。误解一:“ctx 是全局变量。” 错——每个插件有自己独立的 ctx,服务挂在 ctx 上但实例由内核统一管理;往 ctx 上乱挂私有数据是反模式,ctx 是"服务容器"不是"共享储物柜"。误解二:“事件就是发布订阅。” 只对了一半——emit 确实是发布订阅,但 waterfall/serial/bail 是"带决策权的协作协议",把它们当普通 pub/sub 用会丢掉框架最值钱的能力。误解三:“inject 只是自动 import。” 错——import 是编译期静态绑定,inject 是运行时动态装配,后者带来的是"服务消失自动卸载、服务回来自动重启"的生命周期语义,静态 import 永远做不到。三个误解清掉,五概念才算真正入了门。

三、进阶篇:双端沙箱与六大于系统——地基之上盖了什么

3.1 Host/Client 双端架构:一个内核,两个世界

Cordis 是地基,DSH 在这上面盖的第一层是双端架构:Host 端(宿主)跑在 Node 环境,负责模型调用、工具执行、文件系统、会话持久化这些"重活";Client 端(客户端)跑在浏览器/WebView 里,负责 UI 渲染、面板交互这些"轻活"。两端各自是一个独立的 Cordis 上下文,插件可以选择只挂一端,也可以两端各挂一份——UI 插件和逻辑插件天然分离。这张图画出全貌:

#mermaid-svg-0AKjZgeGbaDo0Mtm{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0AKjZgeGbaDo0Mtm .error-icon{fill:#552222;}#mermaid-svg-0AKjZgeGbaDo0Mtm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0AKjZgeGbaDo0Mtm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .marker.cross{stroke:#333333;}#mermaid-svg-0AKjZgeGbaDo0Mtm svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0AKjZgeGbaDo0Mtm p{margin:0;}#mermaid-svg-0AKjZgeGbaDo0Mtm .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .cluster-label text{fill:#333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .cluster-label span{color:#333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .cluster-label span p{background-color:transparent;}#mermaid-svg-0AKjZgeGbaDo0Mtm .label text,#mermaid-svg-0AKjZgeGbaDo0Mtm span{fill:#333;color:#333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .node rect,#mermaid-svg-0AKjZgeGbaDo0Mtm .node circle,#mermaid-svg-0AKjZgeGbaDo0Mtm .node ellipse,#mermaid-svg-0AKjZgeGbaDo0Mtm .node polygon,#mermaid-svg-0AKjZgeGbaDo0Mtm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0AKjZgeGbaDo0Mtm .rough-node .label text,#mermaid-svg-0AKjZgeGbaDo0Mtm .node .label text,#mermaid-svg-0AKjZgeGbaDo0Mtm .image-shape .label,#mermaid-svg-0AKjZgeGbaDo0Mtm .icon-shape .label{text-anchor:middle;}#mermaid-svg-0AKjZgeGbaDo0Mtm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0AKjZgeGbaDo0Mtm .rough-node .label,#mermaid-svg-0AKjZgeGbaDo0Mtm .node .label,#mermaid-svg-0AKjZgeGbaDo0Mtm .image-shape .label,#mermaid-svg-0AKjZgeGbaDo0Mtm .icon-shape .label{text-align:center;}#mermaid-svg-0AKjZgeGbaDo0Mtm .node.clickable{cursor:pointer;}#mermaid-svg-0AKjZgeGbaDo0Mtm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .arrowheadPath{fill:#333333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0AKjZgeGbaDo0Mtm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0AKjZgeGbaDo0Mtm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0AKjZgeGbaDo0Mtm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0AKjZgeGbaDo0Mtm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0AKjZgeGbaDo0Mtm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0AKjZgeGbaDo0Mtm .cluster text{fill:#333;}#mermaid-svg-0AKjZgeGbaDo0Mtm .cluster span{color:#333;}#mermaid-svg-0AKjZgeGbaDo0Mtm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0AKjZgeGbaDo0Mtm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0AKjZgeGbaDo0Mtm rect.text{fill:none;stroke-width:0;}#mermaid-svg-0AKjZgeGbaDo0Mtm .icon-shape,#mermaid-svg-0AKjZgeGbaDo0Mtm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0AKjZgeGbaDo0Mtm .icon-shape p,#mermaid-svg-0AKjZgeGbaDo0Mtm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0AKjZgeGbaDo0Mtm .icon-shape .label rect,#mermaid-svg-0AKjZgeGbaDo0Mtm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0AKjZgeGbaDo0Mtm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0AKjZgeGbaDo0Mtm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0AKjZgeGbaDo0Mtm :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

RPC / 流式通道

Client 端(浏览器/WebView)

Cordis 内核

Web Client 架构客户端模块/Slots/资源

UI 面板与设置页

Host 端(Node 运行时)

Cordis 内核

模型与上下文LLM 流式/系统提示词/压缩

执行与工具Bash/文件/LSP/技能

会话与持久化会话投影/Spill 存储

双端设计为什么这么分?三个工程理由:第一,权限边界——文件系统、网络、进程这些危险操作被物理隔离在 Host 端,Client 端的 UI 插件再自由也碰不到磁盘,沙箱不是事后补的补丁,是架构长出来的;第二,更新节奏解耦——UI 面板的迭代频率远高于模型/工具内核,两端各自独立装卸,改 UI 不用重启模型会话;第三,算力分工——浏览器负责渲染、Node 负责推理,长上下文流式输出不会把 UI 线程卡死。这是企业敢把 DSH 装进生产环境的底气之一。

3.2 六大于系统地图:能力边界一目了然

官方文档把 Harness 的全部能力组织成六个子系统,每个子系统内部又是一批"服务 + 事件"。这张地图是插件作者的选点指南——你想加的能力,一定落在某个子系统里:

子系统包含什么(官方文档目录)典型插件落点
内核与作用域 核心作用域、插件注册表、Context/Events/Fiber 所有插件的地基
会话与持久化 会话查询/引用/标题/投影/持久化、Spill 存储 会话管理插件、记忆插件
模型与上下文 LLM 流式响应、Token 计量、系统提示词、上下文压缩 模型接入适配器、提示词策略插件
执行与工具 Bash 执行、子进程、PTY 会话、后台任务、文件系统、LSP 导航、Web 访问、技能工作流 工具插件的主战场
策略与交互 审批权限、预设沙箱、计划模式、用户交互、定时提醒 安全策略、审批流插件
平台与接入 HTTP 服务器、Web Client 架构、客户端模块/Slots/资源、工作区、用户设置/凭据 集成插件、UI 插件

读这张表有个心法:不要背服务名,要背"能力住哪层"。写插件前先问自己:我要加的东西是"能力"(执行与工具)还是"策略"(审批)还是"表现"(平台与接入)?落点对了,剩下就是找那个子系统里现成的服务和方法。

给每个子系统配一个"插件作者视角"的落点示例,帮你建立直觉:想在会话与持久化里做文章?做一个"会话自动归档到知识库"的插件,监听会话结束事件、调 Spill 存储;想在模型与上下文里动手?做一个"按项目动态注入提示词"的插件,挂进系统提示词组装链;想在执行与工具里发力?做一个内部 API 工具包插件(这是最主流的插件类型,本专栏第 08 篇整篇拆它);想在策略与交互里立规矩?做一个"危险命令审批"插件,在工具执行前拦截(waterfall 短路,第四章讲过的语义在这里变成安全能力);想在平台与接入里做集成?做一个设置页插件,让用户在 UI 上配置你的工具。六个子系统 = 六条产品线,你未来的插件几乎都落在其中一条上。

3.3 四类扩展点:宿主怎么被插件"改造"

第二章说过"宿主的系统提示词、模型、工具执行都是插件贡献的"。落到 API 上,是四类最常用的扩展点(官方实践规则原文):工具流水线事件属于 ctx.tools,模型流式输出属于 ctx.llm,实时 agent 协调属于 ctx.agents,系统提示词组装走提示词相关事件。四类扩展点 = 四个"宿主让渡给插件的决策点":

扩展点插件能做什么典型场景
tools 工具流水线 注册工具 schema、拦截工具调用前后、改写执行结果 加一个飞书工具、给工具加审计日志
systemPrompt 系统提示词 贡献提示词片段、按会话组装、拦截修改 注入公司规范、按项目动态拼提示词
llm 模型流式 接入新模型适配器、改写流式输出、token 计量 接 Ollama 本地模型、输出脱敏
agent 智能体协调 监听 agent 决策、注入策略、改写动作 审批拦截、计划模式

关键理解:扩展点不是"框架留给你的口子",而是"宿主把自己也做成了插件"。DSH 内置的系统提示词组装逻辑,和你的第三方提示词插件走的是同一个事件——官方能力与你的扩展在架构上完全平等。这是"一切皆插件"最深的一层含义,也是专栏后续「事件与扩展点」一篇要展开的主线。

给两个扩展点的官方风格示例(等价演示,正式 API 见后续篇目)。贡献系统提示词片段:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'company-policy-prompt'
export const inject = ['systemPrompt']

export function apply(ctx: Context) {
// 向提示词组装流水线贡献一个片段(等价演示)
ctx.systemPrompt.add('回答必须遵守公司安全编码规范,禁止输出密钥与内部路径')
}

注册一个工具(最常用的扩展点,下下篇「工具插件开发」会把它拆到每一行):

import type { Context } from '@deepseek-ai/cordis'

export const name = 'time-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
ctx.tools.register({
name: 'get_current_time',
description: '获取当前服务器时间',
parameters: { type: 'object', properties: {}, required: [] },
execute: async () => ({ now: new Date().toISOString() }),
})
}

注意两个示例的共性:都是"注册"而不是"覆盖"——你没有改宿主的任何一行代码,宿主的行为却多了你的贡献。这就是插件化扩展和"修改地狱"的分水岭。

选扩展点的决策流程一句话版本:加能力 → tools;改提示 → systemPrompt;换模型 → llm;管决策 → agent。如果拿不准,就回到 3.2 的子系统地图问"我要影响的环节住在哪个子系统",扩展点几乎总是子系统里那条最宽的流水线事件。

四、高阶:五种事件分发模式——插件通信的完整语法

4.1 一张表看懂五种模式

官方文档对事件通信给了一张定义表(本文翻译并加注释),这张表值得打印贴在工位上:

模式是否 await分发顺序是否有返回值用一句话理解
emit 否 按注册顺序观察 否 广播通知,说完就走
waterfall 否 按注册顺序观察 是 洋葱圈:每个监听器可包装/替换下游结果
parallel 是 所有监听器并行 否 并行扇出,各干各的
serial 是 按注册顺序执行 是 流水线:按序加工
bail 否 按注册顺序,到首个 bail 值停 是 投票表决:第一个拍板的说了算

五个模式覆盖五种协作形态,官方文档强调:“分发模式是事件公开约定的一部分”——事件声明时就要定模式,分发时只能按约定模式分发,声明与调用点还能交叉校验。这就是"类型化事件"的含义:不只是事件名有类型,连协作语义都是契约。

选模式时除了语义,还要考虑一个工程因素:await 的代价。表里能看到,只有 parallel 和 serial 会 await 监听器——意味着这两种模式会阻塞调用方直到全部完成,延迟敏感的路径(比如每次工具调用都要经过的事件)要慎用重型监听器;而 emit/waterfall/bail 不 await,监听器里开异步任务要自己管生命周期。语义决定用哪个模式,性能决定监听器写多轻——两条同时想,事件系统的用法才完整。

"声明与调用点交叉校验"值得多解释一句,它是插件化框架质量保障的隐性设计:事件声明处(服务接口上)标注了 @mode 标签,生成的事件目录会自动比对"声明说的模式"和"实际分发用的方法"是否一致——声明是 waterfall 却用 emit 分发,会直接暴露在校验里。在多人协作的企业插件生态里,这个机制的价值不亚于单元测试:它把"协作契约"从文档约定变成了机器可查的事实。传统框架里两个团队各写各的事件、口径对不上线上才炸;DSH 里契约偏差在生成目录那一步就现形。

4.2 一段可运行的模拟器:把五种模式跑给你看

五种模式的语义光看表还是抽象,下面这段 Python 用最朴素的结构完整模拟了 Cordis 的五种分发方式——跑一遍,五种模式的差别就刻在脑子里了(真实 Cordis 是 TypeScript 事件系统,这里用等价语义做教学演示):

"""Cordis 五种事件分发模式模拟器(教学等价实现)。
对照官方文档:emit=广播 / waterfall=洋葱包装 / parallel=并行扇出 /
serial=按序流水线 / bail=首个裁决。真实实现见 @deepseek-ai/cordis 事件系统。"""

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable

@dataclass
class EventBus:
"""极简事件总线:每种分发模式一个方法,监听器按注册顺序存放。"""
listeners: dict[str, list[Callable]] = field(default_factory=dict)

def on(self, event: str, fn: Callable) –> None:
"""注册监听器(等价 ctx.on / ctx.effect 注册)。"""
self.listeners.setdefault(event, []).append(fn)

def emit(self, event: str, *args) –> None:
"""广播:所有监听器按序观察,无返回值。"""
for fn in self.listeners.get(event, []):
fn(*args)

def waterfall(self, event: str, *args):
"""洋葱圈:监听器收 (…args, next),可包装或替换下游结果。"""
fns = self.listeners.get(event, [])
def dispatch(i: int, *a):
if i >= len(fns):
return a[0] if len(a) == 1 else a
return fns[i](*a, next=lambda *na: dispatch(i + 1, *na))
return dispatch(0, *args)

def parallel(self, event: str, *args) –> None:
"""并行扇出:真实实现是异步并发,这里演示"各自独立执行"。"""
for fn in self.listeners.get(event, []):
fn(*args)

def serial(self, event: str, *args):
"""流水线:返回值作为下一个监听器的输入(等价 async serial)。"""
result = args
for fn in self.listeners.get(event, []):
result = fn(*result)
return result

def bail(self, event: str, *args):
"""表决:监听器返回真值即中断,返回值裁决全局。"""
for fn in self.listeners.get(event, []):
verdict = fn(*args)
if verdict:
return verdict
return None

if __name__ == "__main__":
bus = EventBus()

# emit:两个观察者都收到,互不影响
bus.on("tick", lambda t: print(f" [observer-1] tick {t}"))
bus.on("tick", lambda t: print(f" [observer-2] tick {t}"))
print("emit:")
bus.emit("tick", 1)

# waterfall:第二个监听器包装第一个的结果(洋葱圈)
bus.on("prompt", lambda p, next: f"<policy>{next(p)}</policy>")
bus.on("prompt", lambda p, next: next(p) + " 记得安全编码")
print("waterfall:", bus.waterfall("prompt", "写一个登录接口"))

# serial:按序加工,每步返回值喂给下一步
bus.on("transform", lambda s: s.strip())
bus.on("transform", lambda s: s.upper())
print("serial:", bus.serial("transform", " hello dsh "))

# bail:第一个返回真值的监听器裁决,后续不再执行
bus.on("approve", lambda tool: "允许" if tool == "ls" else None)
bus.on("approve", lambda tool: "拒绝" if tool != "ls" else None)
print("bail:", bus.bail("approve", "ls"))
print("bail(未裁决):", bus.bail("approve", "rm"))

跑起来你会看到四组输出,逐个解读:

emit 组:两个观察者各自打了一行 tick 1——广播式通知,互不影响,谁也不阻塞谁。

waterfall 组:输出 <policy>写一个登录接口 记得安全编码</policy>——注意执行轨迹是"外→内→外":先进入 policy 监听器,它调 next() 触发内层(拼接"记得安全编码"),内层结果返回后再被 policy 包上标签。先注册的监听器在外层,后注册的在内层——洋葱结构。

serial 组: hello dsh 先被 strip 成 hello dsh,再被 upper 成 HELLO DSH——每一步的返回值是下一步的输入,像流水线。

bail 组:ls 被第一个监听器裁决"允许",第二个监听器根本没执行;rm 两个监听器都没裁决,返回 None——第一个拍板的说了算,没拍板就落空。

五种模式 = 五种协作拓扑。选错模式是插件通信 bug 的第一来源(避坑表第 3 条):想通知所有人却用了 bail,后面插件全被短路;想拦截却注册进了 emit,拦截器形同虚设。

4.3 Waterfall 的短路语义:为什么"拦截"天然安全

五种模式里最值得单独讲的是 waterfall,因为它有一个反直觉的规则——官方原文:“不调用 next() 直接返回则短路。” 监听器收到 (…args, next),调 next() 会继续执行下游监听器;直接 return 则下游全部不执行。

这个规则让"策略拦截"变得天然安全:一个审批插件想拦截危险工具调用,只需要在 waterfall 监听器里判断"该不该放行"——放行就 return next(…),拦截就 return 拒绝结果。没有任何机制能绕过拦截器,因为不调 next 就短路是框架语义,不是插件自觉。官方还划了一条纪律线:“仅做标注或观察的监听器必须委托”(必须调 next)——观察者不许挡路,决策者才有权短路,两种角色用 next 的调不调区分得一清二楚。

用一张洋葱图把执行轨迹画出来(三个监听器 L1/L2/L3 按注册顺序):

#mermaid-svg-pkAqWqiN9d5OtSKw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pkAqWqiN9d5OtSKw .error-icon{fill:#552222;}#mermaid-svg-pkAqWqiN9d5OtSKw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pkAqWqiN9d5OtSKw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pkAqWqiN9d5OtSKw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pkAqWqiN9d5OtSKw .marker.cross{stroke:#333333;}#mermaid-svg-pkAqWqiN9d5OtSKw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pkAqWqiN9d5OtSKw p{margin:0;}#mermaid-svg-pkAqWqiN9d5OtSKw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-pkAqWqiN9d5OtSKw .cluster-label text{fill:#333;}#mermaid-svg-pkAqWqiN9d5OtSKw .cluster-label span{color:#333;}#mermaid-svg-pkAqWqiN9d5OtSKw .cluster-label span p{background-color:transparent;}#mermaid-svg-pkAqWqiN9d5OtSKw .label text,#mermaid-svg-pkAqWqiN9d5OtSKw span{fill:#333;color:#333;}#mermaid-svg-pkAqWqiN9d5OtSKw .node rect,#mermaid-svg-pkAqWqiN9d5OtSKw .node circle,#mermaid-svg-pkAqWqiN9d5OtSKw .node ellipse,#mermaid-svg-pkAqWqiN9d5OtSKw .node polygon,#mermaid-svg-pkAqWqiN9d5OtSKw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pkAqWqiN9d5OtSKw .rough-node .label text,#mermaid-svg-pkAqWqiN9d5OtSKw .node .label text,#mermaid-svg-pkAqWqiN9d5OtSKw .image-shape .label,#mermaid-svg-pkAqWqiN9d5OtSKw .icon-shape .label{text-anchor:middle;}#mermaid-svg-pkAqWqiN9d5OtSKw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pkAqWqiN9d5OtSKw .rough-node .label,#mermaid-svg-pkAqWqiN9d5OtSKw .node .label,#mermaid-svg-pkAqWqiN9d5OtSKw .image-shape .label,#mermaid-svg-pkAqWqiN9d5OtSKw .icon-shape .label{text-align:center;}#mermaid-svg-pkAqWqiN9d5OtSKw .node.clickable{cursor:pointer;}#mermaid-svg-pkAqWqiN9d5OtSKw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pkAqWqiN9d5OtSKw .arrowheadPath{fill:#333333;}#mermaid-svg-pkAqWqiN9d5OtSKw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pkAqWqiN9d5OtSKw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pkAqWqiN9d5OtSKw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pkAqWqiN9d5OtSKw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pkAqWqiN9d5OtSKw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pkAqWqiN9d5OtSKw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pkAqWqiN9d5OtSKw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pkAqWqiN9d5OtSKw .cluster text{fill:#333;}#mermaid-svg-pkAqWqiN9d5OtSKw .cluster span{color:#333;}#mermaid-svg-pkAqWqiN9d5OtSKw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pkAqWqiN9d5OtSKw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pkAqWqiN9d5OtSKw rect.text{fill:none;stroke-width:0;}#mermaid-svg-pkAqWqiN9d5OtSKw .icon-shape,#mermaid-svg-pkAqWqiN9d5OtSKw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pkAqWqiN9d5OtSKw .icon-shape p,#mermaid-svg-pkAqWqiN9d5OtSKw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pkAqWqiN9d5OtSKw .icon-shape .label rect,#mermaid-svg-pkAqWqiN9d5OtSKw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pkAqWqiN9d5OtSKw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pkAqWqiN9d5OtSKw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pkAqWqiN9d5OtSKw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

return next()

return next()

结果返回

包装后返回

不调 next 直接 return

事件进入

L1 外层拦截器(决策权)

L2 中间层改写参数

L3 内层实际执行

最终结果

短路:下游全部跳过

图中 L1 有两条路:调 next 走完整链路并拿到可包装的结果;不调 next 直接短路,L2/L3 完全不知道事件发生过。"能拦截"和"能绕过"是一枚硬币的两面——当你既是拦截器作者又是被拦截者时,别忘了硬币的另一面永远存在。

4.4 服务隔离:同一份服务,两个互不干扰的实例

插件化框架还有一个进阶问题:同一个服务(比如 Bash 执行)要给两组插件不同的配置怎么办?官方给的能力叫服务隔离(Service isolation)——在 cordis.yml 里用 isolate 声明:

– id: group–a
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true
config:
– name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 5000

– id: group–b
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true
config:
– name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 60000

官方示例一目了然:group-a 和 group-b 各自拿到一个独立的 Bash 服务实例,超时分别 5 秒和 60 秒,跨组零影响。这是多租户、多环境(比如"测试环境工具组"和"生产环境工具组")场景的官方解法。第一章说 DSH 是"中央厨房",这一节就是中央厨房的分区灶台——同样的菜谱,两个灶台各调各的火候。

一个选型提醒:隔离不是免费的。每个隔离实例都有自己的资源开销(独立的子进程、连接、内存),隔离的组越多,运行时越重。所以判断标准是"配置分歧度":两组插件的服务配置真的不同(超时、权限、沙箱级别)才隔离;如果只是逻辑上分组、配置相同,让它们共享同一个服务实例即可。多租户 SaaS 场景几乎必用隔离;单项目内部分模块分组,先问一句"配置真的不同吗"。

五、实战避坑指南:高频问题与一键解决速查表

初学 DSH 架构最常撞的八个坑,按「现象 – 原因 – 解决办法」整理成表,建议收藏:

序号遇到的报错或现象背后的真正原因一句话解决办法
1 apply 里调 ctx.tools 报 undefined 没在 inject 里声明依赖,服务还没就绪 导出 inject = ['tools'],框架会等就绪再调 apply
2 插件卸载后定时器还在打日志 自有资源没走 ctx.effect 声明清理 用 ctx.effect(() => { …; return 清理函数 }) 包裹
3 事件监听"时灵时不灵",有的插件收不到 分发模式选错(比如把 bail 当 emit 用),或观察者忘调 next 先查事件的声明模式;waterfall 观察者必须 return next(…)
4 两个插件都想拦截同一个行为,顺序错乱 依赖了注册顺序,没有用 prepend: true 显式声明优先级 必须在普通注册前运行时才用 prepend: true
5 服务提供者被卸载后,消费者报错崩溃 消费者把服务当"永远存在"用,没做可选依赖兜底 可选依赖用 ctx.get('xxx') 查,拿不到走降级逻辑
6 两个插件组共用 Bash 服务,配置互相覆盖 没开服务隔离,同一个服务实例被共享 cordis.yml 里用 isolate 给每组独立实例
7 类型检查报错:ctx 上没有我新加的服务 提供服务的插件没做 TypeScript 声明合并 declare module '@deepseek-ai/cordis' { interface Context {…} }
8 本地插件热重载后行为不对 patch 文件里用了相对路径,loader 解析不到 cordis.yml 里插件路径必须写绝对路径

这张表的用法照旧是"对号入座":现象列找到你的情况,直接跳最后一列执行。八条里前三条是五核心概念的直接应用,后五条是双端/事件/隔离的工程细节——全部能在本系列后续篇目找到展开讲解。

挑两条讲讲背后的"事故现场",加深印象。**第 1 条(apply 里 ctx.tools 为 undefined)**是初学者的头号坑:函数插件里直接写 ctx.tools.register(…),跑起来就是 undefined——因为没声明 inject,框架不知道你要等 tools。它背后的教训是:在 Cordis 里,"我需要"三个字必须显式说出口,框架的体贴(自动等待)以你的坦诚(声明依赖)为前提。**第 4 条(拦截顺序错乱)**是进阶者的坑:两个插件都监听同一个 waterfall 事件做拦截,你以为后安装的会先拦截,结果顺序跟你想的相反——因为默认按注册顺序,除非用 prepend: true 显式要求前置。它背后的教训是:涉及顺序的行为,永远不要依赖"我以为",要依赖"我声明"。这两条加起来,就是这篇文章想交给你的全部心法。

结语

本文把「一切皆插件」这句口号拆回了工程事实:Cordis 五个核心概念是地基——插件三形态(函数/对象/类)、上下文即服务容器(按 key 找服务不按 import)、inject 声明式依赖(顺序靠依赖图、生命周期有保险)、类型化事件(五种分发模式是协作契约)、可逆副作用(ctx.effect 让热重载无残留);双端沙箱与六大于系统是楼层——Host 管重活、Client 管 UI,能力落点按子系统选;四种扩展点让宿主本身也被插件化——官方能力和你的插件在架构上完全平等。这就是 DSH 敢说"一切皆插件"的全部底气。

一张图总结全文:

DeepSeek Harness 架构全景·思维导图总结

知识要点文字回顾:① 框架是运行时不是工具箱:插件契约统一,扩展即添加、卸载不残留;② 五核心概念:插件/上下文/依赖注入/类型化事件/可逆副作用,是全部地基;③ ctx 按 key 查服务,绝不 import 实现——插件互不感知的前提;④ 五种事件分发 = 五种协作拓扑,waterfall 的"不调 next 即短路"让拦截天然安全;⑤ 双端沙箱把权限边界长进架构,六大于系统是插件选点地图。

最后给准备入场的架构学习者一句实话:DSH 的学习曲线前陡后平。五个核心概念 + 五种分发模式看起来陌生,但它们就是全部——学完这一篇的地基,后面 13 篇的每个主题(生命周期、依赖注入、事件系统、工具开发、发布链路)都只是这些概念在具体场景里的展开。地基一次打牢,后面的路会越走越快。

附上本专栏 14 篇的完整地图,方便你规划学习路径:原理篇(02 插件生命周期与 ctx.effect 清理 / 03 依赖注入与服务发现 / 04 事件总线与扩展点 / 05 bundle 与 patch 底层实现);学习篇(06 学习路线图与概念百科);开发篇(07 插件工程骨架 / 08 工具插件 defineTool 契约 / 09 客户端插件与调试);实战篇(10 官方插件解剖 / 11 改造官方插件);迁移发布篇(12 从 LangChain 迁移 / 13 发布全链路 npm 与市场 PR);延伸篇(14 插件化是不是 Agent 应用层的未来)。每篇独立成文、顺序推进、文末互相预告,关注 CSDN 主页 一次收齐。

本篇读完的行动清单(建议今天就做):① 打开官方文档的 Cordis 入门页,把五个核心概念对照本文再读一遍,重点标注每条的英文原文;② 把五种分发模式表抄进自己的笔记,每行后面写一个你项目里的真实场景;③ 按官方「第一个插件」教程跑通 hello-plugin,观察终端打印 [hello-plugin] plugin loaded!;④ 把本文第 4.2 节的 Python 模拟器跑一遍,亲手改一下 waterfall 监听器的 next 调用,感受短路语义。四条做完,你就拿到了进入这个专栏后续 13 篇的门票。

下一篇连载预告:《DSH 插件为什么不会内存泄漏?拆 Cordis 生命周期 define/run/stop 与 ctx.effect 资源清理》——插件的一生、卸载顺序、热重载的完整机制,关注 CSDN 主页 不错过更新。

你在公司项目里用上 DSH 了吗?你的 Agent 框架现在卡在哪个扩展问题上?评论区聊聊,我挨个回。 本文事实基于 DeepSeek Harness 官方文档(2026-10 版本),框架迭代快,有出入评论区提醒我。

参考资料(正文数据来源):DeepSeek Harness 官方文档《Cordis 入门》《服务与依赖》《第一个插件》《子系统》各页、@deepseek-ai/cordis 事件系统源码说明。

赞(0)
未经允许不得转载:网硕互联帮助中心 » DeepSeek Harness 架构解剖:「一切皆插件」是怎么实现的?Cordis 微内核与双端沙箱全景
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!