夜雨聆风学习资料网

ARTICLE · 1338035

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

两万人写 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 篇 —

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料