ARTICLE · 1338035
两万人写 dsh 插件,我看了源码发现只需要三行

DSH 深度玩法第 2 篇 · 共 5 篇
两万人写 dsh 插件,我看了源码发现只需要三行
一个真实插件的完整诞生——从 1100 行代码里提炼出你只需要懂的四行
在写第一个 dsh 插件之前,我以为插件开发是一件复杂的事——要理解框架、要读文档、要搭脚手架。
写完之后我发现:DSH 不是「一个应用加一堆插件」,它本身就是一摞插件叠起来的。你看到的界面是插件,模型选择器是插件,连 Agent Loop 本身都是插件。写插件不是「给系统加东西」——是第一次真正看懂这个系统是怎么搭起来的。
这个认知转变,来自我花一晚上写了一个真实的插件:一个技能管理器,用来统一管理散落在七八个目录里的 Agent 技能。这篇文章把它的诞生过程完整走一遍——所有代码都是真实运行的,包括那个让我 debug 到凌晨两点的冷启动 bug。
你的问题可能跟我一样
用 dsh 一段时间后,技能(Skills)散落得到处都是:项目里有 .dsh/skills 和 .agents/skills,用户目录有 ~/.dsh/skills,其他 agent 工具还有 ~/.claude、~/.codex、~/.workbuddy 六七个目录。想统一看一眼、想互相同步?只能开文件夹逐个翻。
于是我写了 @local/skill-manager:在 dsh 侧栏里管理所有本机技能,支持查看、启停、跨 agent 一键同步。
插件的最小组成:三行代码,三个文件
先破除神秘感。一个 dsh 插件 = 一个 npm 包 + 一份声明 + 一个入口函数。
声明只需要三行——一个 YAML 文件:
▎ YAML
- insert: - id: skill-manager name: '@local/skill-manager'
意思是:在 profile 的行列表里插入一行,id 是身份,name 指向你的 npm 包。profile 启动时,把每个插件的 patch 依次叠起来,就组成了整个应用。
入口函数也只需要四行:
▎ JS
export const inject = ['skills', 'tools']; export function apply(ctx) { const manager = createSkillManager(ctx); ctx.provide('skillManager', manager); ctx.effect(() => registerTool(ctx, manager)); ctx.inject(['connection'], (innerCtx) => registerRoutes(innerCtx, manager)); }
四个概念,一行一个:
•inject 声明依赖——我的插件需要 skills 和 tools 服务,缺了就不加载
•ctx.provide 把我的 manager 注册成服务,其他插件也能用
•ctx.effect 注册副作用——这里是给 agent 加工具
•ctx.inject(['connection']) 延迟注册 HTTP 路由
最后一行是踩了坑才写对的。下面讲这个坑。
凌晨两点的 bug:一个静默失败的教训
写完前四行代码,工具能用了、服务能查了,但侧栏面板死活不出现。刷新、重启、清缓存——页面 404。
没有报错。没有日志。什么都没有。
折腾了两个小时,翻源码才发现原因:connection 服务在冷启动时还没就绪。如果在 apply 顶层直接取 ctx.get('connection'),会拿到 undefined——然后静默失败,路由注册被跳过,页面 404。没有任何错误提示。
解法就是那行 ctx.inject(['connection'], fn):把路由注册放进回调,等 connection 就绪了再执行。一行代码,修一个两小时的 bug。
教训:当你的代码「安静地不工作」时,第一件事不是检查逻辑,是检查执行时序。依赖还没就绪的代码不报错——它只是不执行。这个模式在异步初始化的系统里到处都是。
工具注册:description 就是模型的说明书
插件最常见的用途是给 agent 加工具。工具注册有一个容易被低估的设计点:
▎ JS
ctx.tools.register({ name: 'skill_manage', description: [ 'Manage the local skills installed on this machine.', '- list: every skill across project, user, other agent tools', '- get / create / update / set-invocation / remove', '- sync: copy skills into other agents directories', ].join('\n'), parameters: TOOL_PARAMETERS, output: { schema: TOOL_OUTPUT_SCHEMA, render: (_args, value) => renderToolResult(value), }, async execute(args, exec) { return { ok: true, result: toJsonSafe(await dispatch(manager, args, exec)) }; }, });
description 不是注释,它是 prompt 的一部分。这段文字会进入模型的上下文——agent 靠它判断什么时候调用这个工具、怎么传参数。写得越具体,调用越准;写得含糊,agent 就猜。
output.render 是给人看的。工具返回 JSON 给模型,render 函数把它折成人话。原则:一屏之内,别让用户滚动。
不用启动 App 就能测:两百行冒烟测试
写 Host 侧最烦的事:每次改一行代码,都要重启 dsh 验证。一个启动周期 30 秒,一天改 50 次,25 分钟全花在等重启上。
解法:给 ctx 做一个假替身,让插件以为自己运行在真实环境里:
▎ JS
const captures = { provided: new Map(), tools: [] }; const fakeCtx = { provide: (name, value) => captures.provided.set(name, value), effect: (fn) => fn(), inject: (deps, fn) => fn(fakeCtx), tools: { register: (def) => captures.tools.push(def) }, }; apply(fakeCtx);
冒烟测试用这个假 ctx 跑真的apply 函数和真的文件系统:断言服务已注册、工具恰好一个、扫描能找到 oil-ui、tai-maas 这些真实技能、同步的 dry-run 计划正确。
两秒钟跑完全部验证,不用启动任何东西。
这个技巧的适用范围远不止 dsh 插件——任何需要「在复杂环境里测试一个小组件」的场景,都可以用假环境替身 + 真逻辑的方式快速验证。
从三行到一千行:中间发生了什么
三行 YAML + 四行 JS 是最小骨架。真实插件最终写了一千一百行——多出来的部分:
•Web 客户端(client.js,六百行):用官方 sidebar 的插槽注册一个「技能」面板,左边列清单、右边显详情
•Host 路由(两百行):/api/skill-manager 提供 REST API
•数据扫描(一百行):遍历七个目录、解析 SKILL.md frontmatter
•同步引擎(两百行):跨 agent 复制技能、内容感知跳过
但每一层都是在那三行骨架上长出来的。先让 patch 挂载、让 apply 被调用、让工具能注册——然后才开始加肉。
装进 profile
开发期用 link: 协议直连本地目录:
▎ SH
dsh plugin --profile desktop add link:~/skill-manager
验证不用重启:看 package.json 的 dsh.profile.bundles 是否出现你的包名,或者跑 --dump-config 看 patch 是否挂载。
正式分发时 npm publish,再向 awesome-dsh-plugin 提个 PR——收录后市场里也能搜到你。
五个避坑点(都是我踩过的)
•声明 engines.dsh:版本检查自动拦截不兼容安装
•peerDependencies 指向官方包:运行时提供,别自己捆绑
•写操作加审批:工具审批管不到插件自身代码,删除/覆盖要主动设计确认
•冷启动顺序:依赖 connection 这类服务的逻辑一律放 ctx.inject 回调,别在 apply 顶层取
•id 一致性:patch 里的 id 和入口导出的 name 必须相同,否则行找不到实现
把话说透
写完这个插件,最大的收获不是技能管理器本身——是第一次看懂了 dsh 的构造方式。
你以为你在「给系统加功能」,实际上你在往一摞 patch 里插入自己的一层。模型选择器是别人写的 patch,侧栏是别人写的 patch,Agent Loop 也是别人写的 patch——而你写的 patch 和它们的地位完全平等。这就是「万物皆插件」不是口号而是架构描述的意思。
三行 YAML + 四行 JS 就是入场券。剩下的一切——工具的 description、output 的 render、冒烟测试的假 ctx、冷启动的时序坑——都是在入场之后才需要面对的细节。
从最小的那一行 patch 开始。能跑起来再加东西。生态里两万个插件,绝大多数就是这么长出来的。
下一篇:跨 agent 同步的 dry-run 设计——怎么让破坏性操作做到让人放心。
「DSH 深度玩法」系列 · 第 2 篇 / 共 5 篇
○你的 Agent 每天都在失忆、失明、失联
●两万人写 dsh 插件,我看了源码发现只需要三行(本篇)
○插件、Skill、MCP 到底差在哪?
○一句话到草稿箱:公众号全自动流水线实录
○给 Agent 装记忆:一个设计空间,四条路线
下一篇:插件、Skill、MCP 到底差在哪?

— DSH 深度玩法 · 第 2 篇 / 共 5 篇 —















































































