一、最大的认知转变:插件不是 VSCode 扩展
一开始我把 dsh 插件理解成 VSCode 扩展:缺什么功能,把功能写好拼上去就能用;缺 git 就加 git 插件,装进一个全局编辑器,装完就有界面、有命令。
这个类比方向对,但机制差得很远。dsh 的插件不是装进「编辑器」这个全局单体,而是装进某个 profile(配置档)的插件树里,作为 Cordis 服务被依赖注入连接起来。差就差在两点:装在哪,以及怎么被连接。
一句话的差别:VSCode 扩展是「装进一个编辑器」,dsh 插件是「装进某个 profile 的插件树」。所以三个 vue 项目不需要装三次,插件跟着 profile 走,装一次就够。
二、一个 web profile 里都装了些什么
profile 在磁盘上就是一个目录,比如 $DSH_HOME/profiles/web/。它的 package.json 里有个 dsh.profile.bundles 字段,是一个有序字符串数组,按顺序叠加,后叠覆盖先叠。
默认 web 先叠两层:
@deepseek-ai/dsh-base | |
@deepseek-ai/dsh-web-app |
拆开底座,至少三类能力都来自插件:模型侧是 llm、agent-default-model;工具侧是 tool-fs(读改文件)、tool-bash / tool-pwsh(跑命令)、approval(审批);会话与界面侧是 session、session-persistence-jsonl。读改文件走 tool-fs,跑命令走 bash / pwsh 加沙箱和审批插件——这就是 Web 形态 dsh web(默认 http://127.0.0.1:3080)背后的组装逻辑。
后来我又往 web 上挂了社区侧边栏 better-sidebar,bundles 就变成三层:底座、web-app、再加 dsh-better-sidebar。命令是往 profile 里加,不是改 harness 源码。
三、怎么给 profile 加插件(装卸笔记)
会拆之后,下一步往往不是立刻写插件,而是先学会往树上挂别人已经写好的包。这条路径我后来用 better-sidebar 走通了一次,笔记如下。
1. 装在哪
插件装进某个 profile,常见是 web。磁盘上就是 $DSH_HOME/profiles/web/(Windows 多半是用户目录下的 .dsh/profiles/web)。dsh plugin --profile web add … 本质上是:在这个目录里调 pnpm 装依赖,再根据包声明的 dsh.bundle 把包名写进 dsh.profile.bundles。
所以:加插件 = 改 profile,不是 fork deepseek-harness 仓库。
2. 一条命令怎么写
在已经装好 @deepseek-ai/dsh 的目录里(我这边是旁支 labs/deepseek-harness):
dsh plugin --profile web add <包名或路径>几种常见规格:
dsh-better-sidebar@latest | |
file:D:/…/playground/my-plugins/lab-shout | |
github:某组织/某仓库 |
本机没有全局 dsh 时,可以用本地入口,例如:
node node_modules/@deepseek-ai/dsh/lib/bin.js plugin --profile web add dsh-better-sidebar@latest3. 装完怎么确认
三件事,缺一不可:
1. 看 profiles/web/package.json:dependencies 里有包名,dsh.profile.bundles 里也有包名。有时 pnpm 装依赖成功了,但命令中途失败,bundles 没写上——需要再跑一次 add,或对照文档手动核对。
2. 跑 dsh --profile web --dump-config,输出里能搜到该插件的挂载行(例如 better-sidebar 的 id)。
3. 重启 dsh web,浏览器再硬刷新。只刷新页面往往不够,尤其是带宿主路由的 UI 插件。
4. 我踩到的装卸坑
- 构建脚本被拦:装 better-sidebar 时 pnpm 报 Ignored build scripts: node-pty。终端依赖要本机编译或放行预构建。处理:在 profile 目录执行 pnpm approve-builds(或按提示放行),再 pnpm rebuild node-pty,然后重新 add 一次,让 bundles 对齐。
- 话题榜 ≠ 能装:GitHub 上打了 dsh-plugin 标签的仓库很多,星多不等于能 dsh plugin add。要认两样:README 里有没有官方装卸命令;package.json 里有没有 dsh.bundle(或明确的挂载说明)。
- 入口可能藏很深:有的 UI 能力不是设置页一级菜单,而是挂在别人提供的槽位上。只装「能力包」、不装「宿主设置页」,界面上会像没装成功。装卸前先读 README 的依赖说明。
- 本地 file: 常是拷贝:改 playground 源码,profile 的 node_modules 不一定跟着变。开发期要么做成目录联接(junction),要么改完再装一次 / 同步 lib/。
- 安全边界:插件是本机第三方代码,工具审批拦不住插件自身。陌生仓先看源码,别在有密钥的环境乱装。
5. 卸掉同样简单
dsh plugin --profile web remove <包名>然后重启 web。bundles 和 dependencies 应对齐消失;再用 --dump-config 扫一眼确认。
一句话:找插件用话题或精选列表,装插件用 dsh plugin --profile … add,验插件看 bundles + dump-config,用插件要重启。
四、动手:一个把文字转大写的小工具
会拆之后,我写了第一个真正能用的工具插件,叫 lab-shout,功能是把手里的文字转成大写,再在前面加一个喇叭符号,适合让输出醒目。
插件外壳只有三样东西:name(插件 id,patch 里用它定位)、inject(声明要用哪些服务,这里只要 tools)、apply(启动时执行,把工具注册进 ctx.tools)。工具本身用 defineTool 定义,核心结构长这样:
import{ defineTool }from&quot;@deepseek-ai/dsh-tools&quot;;/** * 插件外壳(Cordis 四要素里需要的三样:name / inject / apply) * - name: 插件 id,patch 里用 `- id: lab-shout` 定位它 * - inject: 声明要用的服务——这里只需要 tools(注册工具的地方) * - apply: 启动时执行;把工具注册进 ctx.tools */const name =&quot;lab-shout&quot;;const inject =[&quot;tools&quot;];functionapply(ctx){// defineTool:给工具下定义,返回一个注册就绪的工具对象 ctx.tools.register(defineTool({name:&quot;shout&quot;,description:&quot;把传入的文字转成大写,并加上 🔊 前缀返回。&quot;,// 模型调用时传来的参数,按这个 schema 校验parameters:{text:{type:&quot;string&quot;,required:true,description:&quot;要转成大写的文字。&quot;}},output:{schema:{type:&quot;string&quot;},// 必须返回内容块数组;直接返回字符串会让下一轮序列化炸成假 TRANSPORTrender:(_args, value)=&gt;[{type:&quot;text&quot;,text: value }]},// 真正干活的地方:把文字转大写,前面加 🔊asyncexecute(args){return`🔊 ${args.text.toUpperCase()}`;}}));}export{ apply, inject, name };后来我又写了第二个插件 lab-read-goal,读工作区里的 tasks.json 并返回它的 goal 字段。它比 shout 多注入一个 fs 文件服务,用 ctx.fs.resolve 和 ctx.fs.readText 读文件——和官方 tool-fs 同一套来源。
五、三个坑:从「网络错误」到「双份依赖」
写代码只花了几分钟,把它跑通花了一晚上。按出现顺序,我踩了三个坑。
坑一:render 返回了裸字符串
一调 shout,程序就报 DeepSeek API stream failed,看起来像网络故障;可纯问答正常,Web 也能正常聊。我一度以为是 key 或代理的问题,测连通性、开代理,方向全错。
真正的原因很小:render 回调必须返回内容块数组 [{ type: "text", text: value }],我最初写成了直接返回裸字符串。下一轮序列化时 content.some 崩掉,被外层包装成了一条假「网络错误」。把 render 改成内容块数组就通了。
坑二:双份依赖把调度弄断
render 修好之后,命令行 lab profile 已经通了,但 Web 新会话里调 shout 仍然炸,报 reading 'prepare'。
根因藏在依赖图里,不是代码逻辑里。我的插件把 @deepseek-ai/dsh-tools 同时写进了 package.json 的 peerDependencies 和 devDependencies,这是双份声明。pnpm 于是物化出两份物理拷贝,一份是 registry 版,一份是 file 链接版。
问题在于 dsh-tools 内部有模块身份敏感的东西:调度器入口 TOOL_RUNTIME_SCHEDULER 是一个模块私有的 Symbol,错误处理里还有 instanceof HarnessError 这类身份判断。插件用 B 拷贝的 defineTool 造工具,宿主用 A 拷贝的 ToolRuntime 去派发,两个拷贝的 Symbol 对不上,ctx.tools 上的调度器就是 undefined,于是报 reading 'prepare'。
修法有三步:插件里只用 peerDependencies 声明这份依赖;profile 不再独立装一份 dsh-tools;再把链接接到宿主同一份。这是我学到的关键一课:报错先怀疑自己刚写的代码,包括它的依赖声明,而不只是逻辑本身。
坑三:脏会话里的 INVALID_REQUEST
前两个坑修完之后,某次又出现 INVALID_REQUEST。这次不是插件问题,是历史记录里已经发了 tool_calls 却没有对应的工具回执,会话已经脏了。处理办法很简单:新建会话再试,不要在烂掉的对话里让它继续往下写。
六、和 Cursor 对照:它是实验室,不是替代品
我还用同一个任务把 dsh 和 Cursor 放在一起比了一次:让两边都写一个脚本,读 tasks.json 里的名字并打印问候语。
结果是 dsh 能建文件、能搭出骨架,但这次需求贴合度偏弱——它先交出的是命令行参数版,没严格读 json;Cursor 按验收一步改到位,还能点开 diff 看改动、决定用不用。
结论很明确:现阶段 dsh 适合当 harness 实验室,用来拆插件、看轨迹、做无头试跑;Cursor 适合审代码、看目录树、管 Git。dsh 目前是开发者预览版,会破兼容,不当 Cursor 的日常替代品。
七、现在能做什么,不能做什么
这一轮走完,我能做的事:起一个 web profile,看懂它的插件树;用官方命令给 profile 加 / 卸社区插件,并用 dump-config 验收;写出一个最小工具插件并挂进 profile;出问题时知道从 render 返回值、依赖声明、以及「装上了但 UI 没入口」三类方向排错。
还不该做的事:拿它去重写主线工程,或者把插件练习冒充成主线验收成果;也不该在话题榜上按星扫射乱装。它现在的位置很清楚——一个旁支实验室,练的是 harness 的组装与运行时,不是日常生产力。
回过头看,写插件时最贵的坑是把一条「网络错误」当真;装插件时最贵的坑是以为「命令跑过了」就等于「树上挂上了、界面能看见了」。装卸看 bundles,排错看声明,比多记几条 npm 包名更有用。
参考链接
- DeepSeek Harness 官方仓库:https://github.com/deepseek-ai/deepseek-harness
- 社区插件话题 dsh-plugin:https://github.com/topics/dsh-plugin
- better-sidebar(侧边工作台示例):https://github.com/omdsh-dev/DSH-better-sidebar
夜雨聆风