DeepSeek Harness 插件开发新手教程
写给"特别新"的新手:不需要你会写多少代码,只要会用命令行、会复制粘贴。
这篇教程会带你从零理解并复刻一个真实可用的插件:dsh-command-balance(顶栏余额胶囊 +/balance命令)。
所有代码都在本机D:\deepseek\dsh-command-balance\,你可以边看教程边对照。
1. 先搞懂三个词
| 词 | 是什么 | 打个比方 |
|---|---|---|
| DeepSeek Harness(dsh) | DeepSeek 的编程智能体框架,你在用的这个界面就是它 | 一台空舞台 |
| 插件(plugin) | 给 dsh 加功能的小程序,每个功能都是一个插件 | 舞台上的演员 |
| cordis | dsh 用来管理插件的"插座系统",负责插上、断电、互相配合 | 接线板 |
| dsh 的设计哲学是“Everything is a Plugin(一切皆插件)”:你看到的会话、文件编辑、 | ||
| 斜杠命令、甚至顶栏的每个按钮,全都是插件。所以"给 dsh 加功能" = “写一个插件”。 |
2. 你的电脑上有什么(文件地图)
| 位置 | 是什么 |
|---|---|
C:\Users\guanxi\.dsh\ | dsh 的"家目录"(DSH_HOME),所有全局数据都在这 |
.dsh\settings.yaml | 全局设置(界面语言、模型列表等) |
.dsh\.credentials.yaml | API key 保管库(DEEPSEEK_API_KEY就存在这,注意别把这个文件发给别人) |
.dsh\profiles\web\ | web这个 profile(界面档案)的配置间 |
.dsh\profiles\web\package.json | 这个 profile 用了哪些插件(不要手改,用命令改) |
.dsh\profiles\web\cordis.patch.yml | 你自己的"覆盖层",想改插件默认配置就写在这 |
.dsh\profiles\node_modules\ | 所有已安装插件的本体 |
D:\deepseek\dsh-command-balance\ | 我们写的插件,今天的主角 |
profile 是什么?一套"启动套餐"。dsh web= 用web这套套餐启动(带完整界面)。 | |
还有headless(无界面)、sdk等套餐。插件装进哪个套餐,哪个套餐就有这个功能。 |
3. 我们插件长什么样(每个文件干什么)
D:\deepseek\dsh-command-balance\ ├── package.json ← 插件的"身份证":叫什么、入口在哪、声明自己是 bundle + 客户端插件 ├── cordis.patch.yml ← "安装说明书":告诉 dsh 把我挂到系统树的哪个位置 ├── lib\ │ ├── index.js ← 服务端(跑在 Node 里):查余额、注册 /balance 命令、提供远程接口 │ └── client.js ← 客户端(跑在浏览器里):顶栏那颗胶囊 + 悬停气泡 ├── test\ │ ├── smoke.mjs ← 离线测试(不花一分钱,不打真接口) │ └── live.mjs ← 在线测试(用真实 key 打一次余额接口) └── README.md ← 说明文档一个插件可以只有服务端(比如只加个斜杠命令),也可以两头都有(要往界面上画东西就必须有客户端)。
4. 服务端插件的最小骨架(三件套)
任何 dsh 插件的 JS 都是同一个套路,导出三个东西:
constname="command-balance";// ① 名字:插件的编号,别和别人重名constinject=["commands"];// ② 依赖:我需要哪些系统服务才能干活functionapply(ctx,config){// ③ 干活:ctx 是"插线板",把功能接上去ctx.commands.register({name:"balance",// 用户输入 /balance 里的 balancedescription:"查询 DeepSeek 平台账户余额",handler:async()=>({kind:"success",text:"余额是……"})});}export{apply,inject,name};handler返回{kind: "success"|"error", text},界面会把 text 直接显示给用户,不经过模型,不花 token。config是安装时可以传的配置(比如 API 地址、超时时间),不传就用默认值。
API key 从哪来?
绝不把 key 写死在代码里。插件按这个顺序找:
- 问 dsh 的凭据库:
ctx.get("credentials").resolve("DEEPSEEK_API_KEY")
—— 就是你C:\Users\guanxi\.dsh\.credentials.yaml里那个,和模型页共用; - 凭据库没有 → 回退到系统环境变量
DEEPSEEK_API_KEY。
5. 让 dsh 认识你的插件(两个声明)
① package.json:身份证 + 两份声明
{"name":"dsh-command-balance","type":"module","main":"lib/index.js","exports":{".":"./lib/index.js","./client":"./lib/client.js"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"platform":"web"}}}"bundle": { "patch": ... }声明"我是一个功能包,装上就自动生效";"client": { "platform": "web" }声明"我还有一段跑在浏览器里的代码"(只有要画界面才需要)。
② cordis.patch.yml:把自己挂到系统树上
-insert:-id:command-balancename:dsh-command-balance意思:“往系统树里插入一行,编号command-balance,代码就是dsh-command-balance这个包”。
每个插件一行,像乐高说明书一样逐层叠加。
6. 动手:从零装一个插件(完整流程)
第 0 步:一次性准备
dsh 的插件命令需要pnpm(一个包管理器)在 PATH 里,没有就装一个(只需一次):
npmi-gpnpm第 1 步:建插件目录
照着第 4、5 节把三个文件写好(或直接复制D:\deepseek\dsh-command-balance\改名)。
第 2 步:安装进 profile
dsh plugin--profilewebaddD:\deepseek\dsh-command-balance⚠️必须用绝对路径。相对路径会被 dsh 锚定到它的 profile 目录,装错地方。
这条命令做的事:把你的目录以链接方式装进 profile,并且因为声明了dsh.bundle,
自动把它追加进dsh.profile.bundles。
第 3 步:重启验证
dsh--profileweb --dump-config|findstr balance# 确认组合树里有这一层dsh web# 正常启动重启后进任意会话:顶栏看到胶囊、输入/能看到 balance 命令,就成功了。
第 4 步(进阶):顶栏胶囊是怎么画的
浏览器里那段代码(lib/client.js)有三件套:
- 外壳格式:必须套一层
window.__ModuleLoader__.load({ id, factory }),
文件名必须是lib/client.js(和 package.json 的 exports 对应); - 画在哪:dsh 界面上有很多预定义的"插槽"(slot),比如
conversation.session.header.utilities就是"会话顶栏工具区"。
用ctx.slots.inject("插槽名", ...)把一个 React 组件放进去; - 数据怎么来:浏览器不能直接拿 API key(不安全),要反过来问服务端:
- 服务端:
ctx.provide("balanceController", 服务对象)挂一个服务; - 客户端:
ctx.remote.$mount(描述符)声明"我知道这个服务",
然后就能调用ctx.get("remote.balance").get(); - 返回值是信封
{ok, value},真正的业务结果在.value里(新手最容易栽的坑)。
- 服务端:
7. 测试与调试
| 想做什么 | 怎么做 |
|---|---|
| 语法检查(不运行) | node --check lib/index.js |
| 不花钱的功能测试 | node test/smoke.mjs(伪造接口响应,测各种分支) |
| 真接口连通测试 | node test/live.mjs(打真实 API,不回显 key) |
| 确认插件被装进套餐 | dsh --profile web --dump-config,搜你的插件 id |
| 页面行为不对 | 先 Ctrl+F5 强制刷新(旧页面常缓存旧代码) |
页面顶部出现"Failed to load plugins: 你的插件名"?说明客户端代码在激活时抛错了。
给自己代码里的apply包一层 try/catch、把错误写到window.__XXX__上,刷新后用
控制台读取,是最快的定位办法(我们就是这么找到问题的)。
8. 我们真实踩过的坑(提前帮你踩了)
pnpm 不在 PATH→
dsh plugin add报错。npm i -g pnpm解决。add 用了相对路径→ 被锚定到 profile 目录,装错位置。永远用绝对路径。
端口被占→ 起了两个
dsh web会撞端口。netstat -ano | findstr :端口找到 PID,taskkill /F /PID 数字杀掉。欢迎页看不到胶囊→ 顶栏工具区是"会话级"插槽,要先进入一个会话。
改了客户端代码页面没变化→ 客户端 bundle 在启动时编排,重启 dsh + 浏览器强刷。
客户端 codec 缺
create()工厂→ 插件激活直接失败,页面顶部有横幅提示。
每个远程接口描述符的 result/参数 codec 都要create: () => 校验器对象。客户端拿不到自己的服务→ 服务是插件自己挂的,别把它写进模块级
inject(自己等自己,永远卡死),
用ctx.get("remote.balance")在调用时取。以为拿到的是余额,其实是信封→ 远程调用返回
{ok, value}两层,解包再判断。客户端拿不到自己的服务→ 服务是插件自己挂的,别把它写进模块级
inject(自己等自己,永远卡死),
用ctx.get("remote.balance")在调用时取。以为拿到的是余额,其实是信封→ 远程调用返回
{ok, value}两层,解包再判断。往
ctx上挂自己的属性→ cordis 直接拒绝(cannot set property ... without provide),
整个插件激活失败。用自己的闭包对象共享状态(详见第 9 节坑 1)。远程服务调用传了"参数对象"→ 服务按位置取参数,传一个字典会被当成第一个参数。
按描述符的参数顺序展开再调(详见第 9 节坑 2)。工具
parameters用了 JSON-Schema 写法→ 官方是"属性表"方言:required: true写在
字段里,根上没有type: "object"(详见第 9 节坑 3)。
9. 实战复盘:番茄钟插件(dsh-pomodoro)——“测试全绿,真机全挂”
D:\deepseek\dsh-pomodoro\是第三个实例,也是最完整的一个:宿主端权威计时器(带持久化)、
顶栏胶囊、浮窗、设置页行、/pomodoro命令、还有给模型用的pomodoro工具。它写完时
自带测试 50 个用例全绿,真机却三个功能全坏——因为坑都藏在"测试替身没有模拟的真实契约"里。
这三个坑每个都值得背下来。
坑 1:别往ctx上挂自己的属性(激活直接失败)
ctx.togglePomodoroCard=()=>{...};// ✗ 抛 "cannot set property ... without provide"cordis 的ctx是代理:读未声明的属性会查 inject,写未声明的属性直接拒绝。
这行写在$mount之前,于是整个客户端插件激活失败,页面顶上出现
“Failed to load plugins”——所有界面一起消失。
正确姿势:插件内部的小状态用普通闭包对象,在apply里创建、传给需要的组件:
constchrome={togglePomodoroCard:()=>{...}};createPomodoroCard(ctx,store,chrome);// 组件闭包持有,不经过 ctx一句话:ctx 上的每个名字都该是正经服务;自己的小状态走闭包。
坑 2:远程服务调用是"位置参数",不是"参数对象"
service.pause({});// ✗ "expected 0 argument(s), got 1"service.start({durationMs,phase});// ✗ 字典被当成第一个参数 durationMsnamespace 服务按描述符里参数的声明顺序逐位取值。get()因为没有参数所以一直正常,
特别有欺骗性。正确写法是先查出"方法 → 参数名顺序",再展开:
constPARAM_ORDER=newMap(CONTRIBUTION.descriptors.map((d)=>[d.method,d.parameters.map((p)=>p.wire)]));constpositional=(PARAM_ORDER.get(method)??[]).map((name)=>args?.[name]);constenvelope=awaitservice[method](...positional);// service.start(25*60000, "focus")配套细节:每个参数 codec 要同时带schema(调用时读它校验)和create: () => schema(激活时 registry 查它),缺一个分别在两个阶段炸。
坑 3:工具parameters要用官方 spec 方言
// ✗ JSON-Schema 写法:根级 type/required,校验器不认parameters:{type:"object",required:["action"],properties:{action:{...}}}// ✓ 官方方言:根上就是"属性表",required: true 写在字段里(参考 dsh-tool-web)parameters:{action:{type:"string",required:true,enum:[...],description:"..."},durationMinutes:{type:"number",description:"..."}}教训:写工具之前,先抄一个官方工具的parameters形状(dsh-tool-web、dsh-tool-todo 都很短)。
测试的两个教训(为什么 50 个用例没拦住)
- 替身必须模拟真实契约:测试里的假 remote 服务若接受参数对象,就永远测不出位置参数
bug。写完插件至少装机实测一圈(点一遍每个按钮),替身再好也是替身。 - 测试要隔离
DSH_HOME:番茄钟的状态文件写在$DSH_HOME/pomodoro.json,不隔离的测试
会把"60 秒测试状态"写进你真实的~/.dsh/。一行搞定:process.env.DSH_HOME = await mkdtemp(join(tmpdir(), "test-"))(要在插件读它之前设置)。
顺便值得抄的设计(这份插件的亮点)
- 时钟推导而非计数推导:状态里只存
endsAt,倒计时 =endsAt - now。进程重启、电脑
睡眠、页面关掉,任何一次读取都能算出正确剩余——不用怕定时器丢失; - 持久化用临时文件 + rename(原子写),并发写串成队列;
- 阶段边界只响一次:宿主给里程碑盖章,客户端用 localStorage 认领,两个窗口不重复响铃;
- 一个轮询 store 供所有界面共用:不管页面上有几个番茄钟组件,只有一条远程轮询。
10. 卸载与回滚
dsh plugin--profileweb remove dsh-command-balance删除依赖、自动从套餐里摘除。你的插件源码目录还在,想再装随时 add 回来。
11. 名词小抄
- profile:启动套餐(web / headless / sdk……),插件装进套餐才生效
- bundle:声明了
dsh.bundle.patch的插件,装上即自动生效 - patch / insert:往系统树里"插入一行"的说明书,后写的覆盖先写的
- slot(插槽):界面上预留的扩展位,如"会话顶栏工具区";分全局级和会话级
- 凭据 seam:dsh 的钥匙保管库,代码里只写钥匙名字,不写钥匙内容
- Remote 服务 / RPC:浏览器里的插件代码想拿数据,就通过它问 Node 里的服务端
- SRC 模式:Gateway 的一种"免注册表"发现方式,服务对象带上约定的标记字段即可被发现
- 位置参数:远程调用按描述符里参数的声明顺序逐个传,不是传一个参数对象
- spec 方言:工具
parameters的官方写法——根上是属性表,required: true写在字段里
12. 学完可以玩什么
📘进阶读物:同目录的 DeepSeek-Harness-插件开发-循序渐进实战.md
把上面第 2、3 条那两个插件也拆开讲了一遍,并且按"每章只加一样新东西"重排:
心智模型 → 宿主注册服务 → 客户端取数画界面 → 纯客户端读会话投影 → 状态机/持久化/工具/多插槽
→ 三套离线测试。第 8.6.1 节专门复盘了"测试全绿、真机全挂"的三个替身陷阱。
- 让 AI 自己查余额:给插件再加一个"工具(tool)"声明,模型就能在对话中自己调用查余额;
- 任务列表面板:
D:\deepseek\dsh-panel-tasks\是第二个实例——右侧边栏"任务"标签页,
实时镜像模型的任务清单(dsh 的todos会话投影),完成一项自动打勾。
它是纯客户端插件(服务端是空壳),读数据用的正是 dsh 自带的会话投影机制; - 番茄时钟:
D:\deepseek\dsh-pomodoro\是第三个实例,也是目前最完整的——宿主权威计时、
状态持久化、顶栏胶囊、浮窗、设置行、/pomodoro命令、模型工具六件套,
踩坑复盘见第 9 节; - 做别的胶囊:换成"今日用量"“最近会话数”,套路完全一样,换个 slot 和数据源而已;
- 多 profile:同一插件
dsh plugin --profile headless add ...也装进无界面模式; - 读真源码:所有官方插件都在
C:\Users\guanxi\.dsh\profiles\node_modules\@deepseek-ai\下,
每个都带中文 README,是最好的教科书;官方仓库文档:github.com/deepseek-ai/deepseek-harness(docs/ 目录)。
祝玩得开心!遇到报错先看第 8、9 节,九成是老坑。