写第一个 dsh 插件踩的六个坑(0.1.0-rc.6 本机复核) #380
Replies: 3 comments 1 reply
|
这六个坑写得非常实在,尤其是第 1 条——@deepseek-ai/* 的 import 取决于插件装在哪(link 开发 vs npm 安装),这个差别确实最隐蔽。我们写手册时也踩过,补充两点给后来人:
我们把这些 rc 版本的坑(link 依赖断裂、版本线选择、插件挂载 2 步)整理进了 dsh-handbook 的 FAQ 和插件开发章节,欢迎来补充或纠正: |
|
同步进展:六坑已收录进手册 FAQ(含致谢),dsh-installers(免装 Node 安装包)已收录进第 2 章安装方式对比 + 生态章节:https://github.com/Electricitysheep/dsh-handbook/blob/main/docs/faq.md |
|
这六个坑对 DSH-native 插件都很有价值。再补一条目前已经跑通的替代分发路径:如果能力本来就是 Pi 扩展,或者作者愿意只依赖 Pi 公共扩展 API,可以通过 pi2dsh 原包进入 DSH,不需要每个包再写一层 DSH bundle。 用户侧: dsh plugin --profile web add pi2dsh@0.11.0
dsh plugin --profile web add <Pi-package>作者侧仍发布普通 Pi npm 包,不需要 这不是纸面兼容:视觉、search/fetch、 |
Uh oh!
There was an error while loading. Please reload this page.
开源两天,插件仓库冒出来一大批,估计不少人和我一样在写第一个。把我卡住过的地方记下来,省得别人再卡一遍。
环境:macOS,dsh
0.1.0-rc.6。下面每条报错和源码位置都在本机复核过。1.
@deepseek-ai/*能不能 import,取决于你的插件装在哪开发时把插件 link 进 profile,一启动就炸:
同一份代码发到 npm 再装回来,import 却是好的。这个差别卡了我很久。
dsh 维护了一个扁平兜底目录。
dsh-app-boot里的healProfilesModuleFallback()会在$DSH_HOME/profiles/node_modules(我这儿是~/.dsh/profiles/node_modules)下面,给 dsh 自身依赖闭包里的每个包建一条软链接。源码注释写得很直白:从 registry 装的插件,真实目录在
~/.dsh/profiles/<名字>/node_modules/里,Node 往上走能撞到兜底目录。本机验证:本地目录装进来的开发版是另一回事。我那条
~/.dsh/profiles/plugintest/node_modules/dsh-plugin-superpowers是指向~/IdeaProjects/.../dsh-plugin-superpowers的软链接,而 Node 解析模块走的是软链接指向的真实路径。源码在~/IdeaProjects底下,往上走一辈子也走不到~/.dsh/profiles/node_modules。同一个createRequire从真实路径跑,三个包全是MODULE_NOT_FOUND。我最后让插件不 import 任何
@deepseek-ai/*,只留 node 内置模块,要什么都从ctx上拿:dev 链接和 registry 安装两种形态都不会出事,版本也不用对。
要用 schemastery 写 config schema 的话,peer dependency 那条路在 registry 安装下是通的。兜底目录抓的是整个依赖闭包,注释里说这就是为 out-of-tree 插件的 peer deps 准备的。dev 链接时你得自己想办法。
2.
inject只能是字符串数组写成这样,启动就停在那儿不动了:
required和optional被当成两个服务名了。@deepseek-ai/cordis的Inject.resolve:数组走第一个分支,每项就是服务名。对象走最后一个分支,
Object.keys()才是服务名,值是 intercept 配置。{ required, optional }这个形状于是变成「我要 required 和 optional 这两个服务」,等到天荒地老。官方包都是数组:
3. 想成为一层 profile,package.json 要声明
dsh.bundledsh plugin add装完不报错,插件毫无动静。往回翻输出能找到这么一行:这条警告写得很好,可惜夹在 pnpm 一大堆输出里,很容易划过去。
dsh plugin是个 pnpm 转发器,装完会对着已安装状态核对dsh.profile.bundles:解析得到的包声明了dsh.bundle就进层列表,没有就当普通依赖躺着。package.json:
{ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }cordis.patch.yml:
name是 loader 拿去 import 的模块名。改包名的时候它得跟着改,我改名忘了同步一次,换来:从 git 地址装插件还有一条:pnpm 会拦住 prepare 脚本,这时 dsh 会提示你把 pnpm 打印的那个 key 加到 profile 目录
pnpm-workspace.yaml的allowBuilds下面再重跑。4. prompt section 压缩之后还在
这条是好消息,也是 dsh 让我比较舒服的一点。
ctx.systemPrompt.section()注册的内容,上下文压缩之后依然在。不用监听会话开始事件反复注入,不用写去重守卫。dsh-system-prompt的 README 开头:每一步都从注册表重新组装。压缩动的是消息历史,system prompt 不在被收进 summary checkpoint 的那个范围里。
order 有约定好的分段,README 写了:
-100是 harness identity,0是 deployment persona,100-199留给工具指导。同一个 order 上的多个 section 按注册顺序排,而注册顺序是插件加载的副产物(README 原话 "a plugin-load artifact"),别指望它稳定,自己挑个不撞的数。
想理解这套装配线为什么这么设计,#110 那篇源码拆解讲得比我细。
5. Web 面不用改 preset,但 persona 是例外
Web bundle 里这几行是 disabled 的:
agent-instructions也是。我看到这个的第一反应是全局插件在 Web 上不会生效,得往每个 preset 里塞东西。不对。旁边的注释就解释了:skill 注册表和 prompt section 都是「全局层 + scope 链」合并着读的,全局层注册的东西每个 agent 都拿得到。挪到 preset 后面的只是 per-agent 的那几行。
我栽的地方是 persona。同名遮蔽按 scope 层算,而 preset 自己挂了一个:
这一行注册的 section 就叫
deployment:persona,和全局那个撞名,agent 层压过全局层。我改 profile patch 里的 persona,Web 默认会话纹丝不动,得去改 preset。反过来看,插件注册的 section 只要名字带自己的前缀,没人会遮它。
另外
@deepseek-ai/dsh-persona这一行别挂在 host 层。它的 README 说了,会和注册表自己的deployment:persona撞名然后 fail loud,这一行就是给 preset 用的。6. 发 npm 的两个坑
和 dsh 无关,但同样会挡住你把插件发出去。
registry 指着国内镜像的话,login 和 publish 都得显式带官方源,镜像是只读的:
npm config get registry # https://registry.npmmirror.com npm login --registry=https://registry.npmjs.org npm publish --registry=https://registry.npmjs.org第二个更磨人。
npm publish只认--otp:没有
--auth-type这个选项,--auth-type=web只对npm login生效。我本机npm config get auth-type就是web,publish 照样开口要 OTP。2FA 只绑了 passkey / Touch ID 的话,这里拿不出那串数字,得用 TOTP 验证码或者恢复码。npm 10.9.8。以上都在 macOS + dsh
0.1.0-rc.6上复核过。rc 迭代得快,过阵子可能就不准了,看到失效的欢迎直接指出来。这些坑来自我做的 dsh-superpowers(把 Superpowers 那套方法论搬进 dsh),仓库在 https://github.com/codeAnqiang-ma/dsh-superpowers ,说错的地方欢迎指正。
本文在 AI 辅助下整理,所有结论均在本机 dsh 0.1.0-rc.6 上实测复核。
All reactions