前言
入门篇中笔者已经和大家一起运行起来DeepSeek Harness,并学习了如何配置模型、接入MCP和Skill,还初步了解了 DeepSeek Harness 独特的插件机制。如果大家只是想把它当作 Codex、Claude Code 等智能体工具的日常平替,那么目前掌握的内容已经基本够用。
但如果大家是对 DeepSeek Harness 底层实现机制感兴趣的开发者,或者具备一定前端、 Node.js开发经验、希望进一步深入探索 DeepSeek Harness的大模型爱好者,那么进阶篇的内容就是为大家准备的。笔者会尽量清晰、系统地讲清DeepSeek Harness的核心特性和插件机制。
本期笔者将用通俗易懂的语言拆解DeepSeek Harness的插件机制,厘清插件相关的基本概念,最后实现一个简单插件,并将其挂载到 DeepSeek Harness系统中。大家一起来看看吧~
一、DeepSeek Harness的底座:Cordis框架
首先笔者需要明确,DeepSeek Harness的插件能力并非从零自研,而是建立在Cordis框架之上。Cordis是一个TypeScript编写的插件框架,最初为Koishi聊天机器人服务。
DeepSeek Harness对Cordis的使用非常纯粹:Cordis只负责插件的加载、卸载、依赖管理和生命周期管理,并不关心智能体具体如何工作。而智能体的所有功能包括会话管理、工具使用、智能体循环,甚至UI界面都由插件来实现。
换句话说,Cordis提供的是插件运行底座,而DeepSeek Harness的智能体能力则构建在这个底座之上并通过插件来实现。
二、 从DeepSeek Harness源码目录理解插件机制
要想理解DeepSeek Harness的插件机制在工程上是如何实现的,笔者有必要先简单分析一下它的源码结构。D它的仓库根目录结构如下:
deepseek-harness/ ├── apps/ # 最终可运行的应用 ├── packages/ # dsh 的插件包(主体) ├── vendor/ # Vendored Cordis 源码 ├── python/ # Python SDK 和运行时 ├── native/ # 原生扩展 ├── examples/ # 用户可运行的 cordis.yml 示例 ├── docs/ # 架构、子系统和开发文档 ├── website/ # VitePress 文档站 ├── scripts/ # 构建、检查、生成器 └── .agents/ # Agent 工作流和 Agent Notes在上述目录中,主体是packages/。该目录下的每个子目录通常是一个能力分组,内部包含一个或多个独立的npm workspace包,包名遵循@deepseek-ai/dsh-<name>规范。更直观地说,这些workspace包就是DeepSeek Harness的一个个插件;它们挂载在Cordis之上,为DeepSeek Harness提供各种能力。
在packages/的插件集合中,最核心的一组位于packages/core,主要包含六个核心npm包(插件):
session:事件溯源的会话日志,对应ctx.sessions服务system-prompt:系统提示词组装tools:工具注册表,对应ctx.tools服务agent:Agent 类型与接口定义agent-loop:驱动循环scope:作用域隔离
这六个包可以看作挂载在DeepSeek Harness上的六个核心插件。它们会把自己提供的服务注册到DeepSeek Harness插件体系的共享上下文(context)中,其他插件便可以借助这些服务实现更丰富的能力。packages/core之外的目录通常按能力系列分组,例如llm/、shell/、client/等。这些目录本身通常不包含代码,而是把相关的插件包聚合在一起。
这里再解释一下DeepSeek Harness“一切皆插件”的设计理念到底指什么。除了底层的Cordis框架负责插件的安装与管理之外,其余能力都由插件提供:插件挂载到上下文中并对外提供服务,其他插件再依赖这些服务扩展出更多能力。例如tool-skill相关插件依赖tools、session和skill插件,实现了对Skill技能目录下工具函数的注册与执行。看到这里,相信大家都已经理解DeepSeek Harness插件体系的核心设计理念啦~
三、插件作用的生命周期
通过源码大家已经理解了插件是如何组织的。但还有一个关键问题:大家写好的插件代码,究竟是如何在 DeepSeek Harness上运转起来的? 这就需要理解 DSH 插件体系中最容易混淆的三个概念:插件、Bundle和Profile。
3.1 三者的定位
插件是开发者编写的,通过apply(ctx)函数暴露能力的代码模块。
Bundle是将一个或多个插件源码,连同配置文件一起打包成的npm包,通过package.json中的dsh.bundle字段声明。
Profile是位于$DSH_HOME/profiles/<name>下的组合方案目录,通过package.json中的dsh.profile字段声明的,用来描述该组合方案是由哪些bundle按什么顺序组合的。
3.2 Bundle:将插件代码变为可分发单元
当大家写好一个DeepSeek Harness的插件后,它本身只是一个代码文件,其他用户无法直接安装使用。这时就需要把它打包成 Bundle。
打包的核心是创建一个package.json,并用dsh.bundle字段指向一个配置文件。示例如下:
{"name":"dsh-hello-plugin","version":"0.1.0","type":"module","main":"index.js","files":["index.js","cordis.patch.yml"],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}}}注意配置文件中的cordis.patch.yml,它可以说是Bundle的灵魂:描述该 Bundle 要向系统配置树中插入哪些插件,通常需要指定插件的id和名称。
# hello-plugin/cordis.patch.yml-insert:-id:helloname:dsh-hello-plugin3.3 Profile: 运行时的组合方案
Profile 是用户实际启动时的组合方案。大家常用的dsh web命令,本质上就是启动一个名为web的 Profile,其本质是一个目录。
每个Profile目录中都有一个package.json配置文件,其dsh.profile字段列出了该Profile要加载的Bundle列表(按顺序)。当用户启动DeepSeek Harness时,系统会按照这个列表的顺序,依次应用每个 Bundle 的cordis.patch.yml,最终构建出一棵完整的 Cordis 插件树,然后启动运行时。
以下是webProfile 相关的配置文件:
{"name":"dsh-profile-web","private":true,"dsh":{"profile":{"bundles":["@deepseek-ai/dsh-base","@deepseek-ai/dsh-web-app",],"patchReload":"live"}}}可以看到webprofile组合了两个Bundle。其中一个名为@deepseek-ai/dsh-base,它是多个插件的集合,提供Agent运行所需的核心能力,覆盖大模型、会话、持久化、工具、权限、Skill等基础功能。另一个Bundle名为@deepseek-ai/dsh-web-app,它在基础层之上叠加 Web 相关插件,如web-startup、webserver、web-runtime、api-gateway等,以及浏览器侧所需的前端静态资源。
3.4 插件的完整开发流程
在讲清插件完整开发流程之前,笔者先为大家理清一个容易混淆的点:把插件挂载到配置树中有两种方法。
第一种是patch,用于本地调试,专门快速验证插件源码。
另一种是add,用于打包分发,先把插件打包为 Bundle,然后通过命令将 Bundle 加入目标 Profile,插件才会正式生效。
现在,一个完整的插件开发流程就非常清晰了:
- 编写插件。
- 使用
--patch在本地快速验证插件逻辑。 - 将插件打包为 Bundle(包含
package.json和cordis.patch.yml)。 - 通过
dsh plugin --profile <name> add <你的-bundle>命令,将 Bundle 安装到某个 Profile 中。 - 启动 Profile,加载插件。
四、实战:写一个带Tool注册的插件
理论部分已经讲完。笔者向来推崇通过实战代码练习来巩固知识。下面就来写一个真正能用的插件:一个可以统计一段文字字符数和单词数的text_stats工具插件。
4.1 环境准备
首先大家先根据 DeepSeek Harness 入门很简单(一)——认识DeepSeek Harness并安装文章,通过源码方式安装好Deepseek Harness。
在仓库根目录下创建一个scratch-plugin/目录,其结构如下:
scratch-plugin/ ├── cordis.yml └── src/ └── my-plugin.ts4.2text_stats核心代码编写
在my-plugin.ts中编写核心代码。因为需要使用 DeepSeek Harness 的工具功能,所以要引入tools插件提供的能力作为服务。完整代码如下:
importtype{Context}from'@deepseek-ai/cordis'import{defineTool}from'@deepseek-ai/dsh-tools'exportconstname='text-stats'exportconstinject=['tools']exportfunctionapply(ctx:any){console.log('[text-stats] plugin loaded!')ctx.tools.register(defineTool({name:'text_stats',description:'统计一段文字的字符数和单词数',parameters:{text:{type:'string',required:true,description:'要统计的文字',},},output:{schema:{type:'string'},render:(_args:any,value:any)=>[{type:'text',text:valueasstring}],},asyncexecute(args:any){consttext=args.textconstcharCount=text.lengthconstwordCount=text.trim().split(/\s+/).filter(Boolean).lengthreturn`字符数:${charCount},单词数:${wordCount}`},}asany),)}以上代码有几个需要注意的地方:
inject = ['tools']声明该插件依赖ctx.tools服务。如果tools服务尚未就绪,apply不会执行,插件会一直等待。ctx.tools.register()把工具注册进ToolsService。该注册通过ctx.effect()管理,插件卸载时工具会自动消失。有了ctx.effect(),开发者就不需要在代码层面过多关心插件卸载和副作用撤销。defineTool来自@deepseek-ai/dsh-tools,它帮大家把工具定义转换成ToolsService能识别的 schema 格式,并定义输入参数、输出格式和执行函数。
完成了源码编写之后,下面来编写cordis.yml配置文件,内容如下,包含插件的id和名称,注意这里的name必须是绝对路径:
-insert:-id:test-stats name:'/home/sandui/deepseek-harness/scratch-plugin/src/my-plugin.ts'4.3patch插件调试
下面使用patch方法启动并验证。在deepseek-harness仓库根目录下执行以下命令:
pnpmdsh web--patch./scratch-plugin/cordis.yml执行结果如下,可以看到插件已被正确加载:
接下来测试插件功能,输入以下提示词:
请统计如下内容的字数: 本文从零搭建完整加工流水线:MinerU 统一解析为 Markdown,多模态模型为图片生成描述,再经去重、质量过滤、隐私脱敏三道清洗工序,带大家手把手把“原料”炼成可直接用于大模型训练的干净数据集。输出结果如下图所示,DeepSeek Harness会在可用工具列表中发现text_stats,传入原文,再按照规定的格式返回结果。可以看到 Tool call 日志里出现text_stats,说明“插件加载 → 工具注册 → 模型发现 → 调用执行”的整个链路已经走通。
到这里,插件的基本代码开发已经完成,并通过--patch完成了初步调试。但这种方式只适合开发阶段。如果想分享插件或在正式Profile中长期使用,就需要把它打包成Bundle,走add路径。
4.4 插件打包为bundle
Cordis 插件机制加载 Bundle 时,默认会加载编译后文件夹下的index.js。所以首先将my-plugin.ts改名为index.ts,同时按规范将cordis.yml改名为cordis.patch.yml。
修改 Bundle 的cordis.patch.yml中的内容。这里的name不再是绝对路径,而是包名dsh-text-stats。打包分发后,Node 的模块解析器会从相关链接中查找该包,而不是直接从文件系统路径查找:
-insert:-id:text-statsname:dsh-text-stats然后在scratch-plugin/目录下安装依赖:
pnpmadd@deepseek-ai/cordis @deepseek-ai/dsh-tools typescript在scratch-plugin/目录下新建tsconfig.json文件,用于把TypeScript代码编译为可执行的 JavaScript 代码。如果学习过TS开发,这部分会非常熟悉:
{"compilerOptions":{"target":"ESNext","module":"ESNext","moduleResolution":"Bundler","outDir":"./dist","rootDir":"src","strict":true,"esModuleInterop":true,// 关键!补齐内置API类型 WeakRef、Symbol.dispose、ErrorOptions"lib":["ES2021","DOM"],// 跳过外部库的类型检查(兜底,解决cosmokit Volatile 类型缺失)"skipLibCheck":true},"include":["src/**/*.ts"],"exclude":["dist"]}在scratch-plugin/目录下执行命令pnpm tsc编译代码。执行完成后,该目录下会生成dist/index.js文件:
在scratch-plugin/目录下新建package.json,配置如下,其中的关键字段是dsh.bundle.patch,它告诉DeepSeek Harness:“这个包贡献一个配置层,层的内容在cordis.patch.yml里。”files字段用于控制打包内容:
{"name":"dsh-text-stats","version":"0.1.0","type":"module","main":"dist/index.js","files":["dist","cordis.patch.yml"],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}}}在scratch-plugin/目录下执行如下命令:
dsh plugin--profilewebadd.这条命令做了三件事:
- 把包作为依赖安装到
webProfile 的目录中; - 把
dsh-text-stats追加到该 Profile 的dsh.profile.bundles列表; - 下次启动该Profile时,该插件的
cordis.patch.yml会被应用,插件正式生效。
下面验证该Bundle是否已经挂载成功。首先执行以下命令,查看deepseek harness web的配置树里有没有这个插件行:
dsh--profileweb --dump-config|greptext-statsdsh-text-stats出现在最终配置树里,说明add成功。
然后打开Web再测一次:请帮我统计下面这句话有多少个字符和多少个单词:Everything is a Plugin。结果依然正确。
pnpmdsh web到这里一个完整的插件就完成了从开发、调试、打包到安装验证的全过程。如果大家希望把这个插件分享给其他人使用,只需将dsh-text-stats这个包发布到 npm 平台(例如在scratch-plugin/目录下执行npm publish)。发布成功后,其他用户就可以通过dsh plugin --profile <name> add dsh-text-stats命令,将该插件安装到自己的Profile中。这样插件就从本地开发走向了真正的公开分发。
五、小结
本篇文章笔者先从Cordis的插件机制讲起,认识了DeepSeek Harness的插件底座Cordis;接着解析了源码目录,理解了packages/core/六个核心服务包和packages/按能力分组的组织方式;然后厘清了插件、Bundle、Profile 三者的定位,以及--patch本地调试和add打包分发两条挂载路径,也明白了dsh web本质上是组合dsh-base与dsh-web-app两个 Bundle 的内置 Profile;最后通过一个带 Tool 注册的插件,走通了“插件加载 → 服务注入 → 工具注册 → 模型调用”的完整链路。至此,从底层机制到工程实现,再到实战验证,DeepSeek Harness插件体系的核心脉络就全部串起来了。
如果大家觉得有收获,欢迎点赞、收藏,关注我的专栏《DeepSeek Harness 从入门到进阶》,后续更新不错过。下篇见!更多精彩内容,如果大家感兴趣,欢迎关注笔者的CSDN账号与专栏。