写给"特别新"的新手:不需要你会写多少代码,只要会用命令行、会复制粘贴。 这篇教程会带你从零理解并复刻一个真实可用的插件:
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. 我们插件长什么样(每个文件干什么)
vbnet
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 都是同一个套路,导出三个东西:
js
const name = "command-balance"; // ① 名字:插件的编号,别和别人重名
const inject = ["commands"]; // ② 依赖:我需要哪些系统服务才能干活
function apply(ctx, config) { // ③ 干活:ctx 是"插线板",把功能接上去
ctx.commands.register({
name: "balance", // 用户输入 /balance 里的 balance
description: "查询 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:身份证 + 两份声明
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:把自己挂到系统树上
yaml
- insert:
- id: command-balance
name: dsh-command-balance
意思:"往系统树里插入一行,编号 command-balance,代码就是 dsh-command-balance 这个包"。 每个插件一行,像乐高说明书一样逐层叠加。
6. 动手:从零装一个插件(完整流程)
第 0 步:一次性准备
dsh 的插件命令需要 pnpm(一个包管理器)在 PATH 里,没有就装一个(只需一次):
sh
npm i -g pnpm
第 1 步:建插件目录
照着第 4、5 节把三个文件写好(或直接复制 D:\deepseek\dsh-command-balance\ 改名)。
第 2 步:安装进 profile
sh
dsh plugin --profile web add D:\deepseek\dsh-command-balance
⚠️ 必须用绝对路径 。相对路径会被 dsh 锚定到它的 profile 目录,装错地方。 这条命令做的事:把你的目录以链接方式装进 profile,并且因为声明了
dsh.bundle, 自动把它追加进dsh.profile.bundles。
第 3 步:重启验证
sh
dsh --profile web --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}两层,解包再判断。
9. 卸载与回滚
sh
dsh plugin --profile web remove dsh-command-balance
删除依赖、自动从套餐里摘除。你的插件源码目录还在,想再装随时 add 回来。
10. 名词小抄
- profile:启动套餐(web / headless / sdk......),插件装进套餐才生效
- bundle :声明了
dsh.bundle.patch的插件,装上即自动生效 - patch / insert:往系统树里"插入一行"的说明书,后写的覆盖先写的
- slot(插槽):界面上预留的扩展位,如"会话顶栏工具区";分全局级和会话级
- 凭据 seam:dsh 的钥匙保管库,代码里只写钥匙名字,不写钥匙内容
- Remote 服务 / RPC:浏览器里的插件代码想拿数据,就通过它问 Node 里的服务端
- SRC 模式:Gateway 的一种"免注册表"发现方式,服务对象带上约定的标记字段即可被发现
11. 学完可以玩什么
- 让 AI 自己查余额:给插件再加一个"工具(tool)"声明,模型就能在对话中自己调用查余额;
- 任务列表面板 :
D:\deepseek\dsh-panel-tasks\是第二个实例------右侧边栏"任务"标签页, 实时镜像模型的任务清单(dsh 的todos会话投影),完成一项自动打勾。 它是纯客户端插件(服务端是空壳),读数据用的正是 dsh 自带的会话投影机制; - 做别的胶囊:换成"今日用量""最近会话数",套路完全一样,换个 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 节,九成是老坑。