夜雨聆风学习资料网

ARTICLE · 1015581

插件加载了却什么都没干:DSH 插件的 4 个静默失败

插件加载了却什么都没干:DSH 插件的 4 个静默失败

我是大飞象,只写自己跑通的东西。DSH 在 9 月 10 日发了 v0.1.5,这次更新里有一条对写插件的人特别重要:官方给插件作者开放了更多标准化的 UI 扩展入口。插件这条线正在变热,社区收录的公开插件已经过 900 个,仓库创建不到一个月 Star 就过了 21 万。但真正动手写第一个插件的人会先撞上一类很特别的错误,它不报错、不弹窗、日志干净,插件看上去加载成功了,实际什么都没干。这篇把我踩过的四个静默失败和排查方法一次写完。

一、先看这次更新里跟我有关的几条

v0.1.5 是 9 月 10 日发布的,跟 V4.1 Flash 做了训练层面的结合,模型在 DSH 的不同配置里都做过专项优化。功能层面有好几条跟写插件的人直接相关。

变化具体内容对插件作者的影响
UI 扩展入口为插件作者提供更多标准化的 UI 扩展入口以前要绕路做的界面,现在有官方口子
文件上传新增文件上传能力插件可以处理用户上传的文件,不用自己造传输
侧边栏文件预览新增侧边栏文件预览产物文件能直接给用户看,省掉一层展示
长会话优化优化加载、恢复、持续对话性能,降低内存占用,优化存储格式历史会话相关插件要注意存储格式变了
内容折叠与导航会话内容折叠、完整历史轮次导航、正文宽度与字号、运行统计UI 类插件要适配新的展示结构
AgentTeams实验性功能,主 Agent 可创建多个团队成员,共享任务列表拆分与跟踪默认关闭,要手动把实验性插件加进 Profile,且会产生额外 token 消耗

这篇不讲怎么用这些新功能,讲的是我写插件过程中反复撞上的四个静默失败。它们的共同点是错误信息不指向根因,你看着日志是干净的,实际上插件根本没跑起来。

说明:为什么值得单独写一篇。插件加载失败通常有两种表现,一种是直接报错退出,这种最好办,照着报错改就行。另一种是静默失效,进程活着、界面正常、模型照常回话,只有你那个插件不工作。第二种能耗掉一整晚。

二、第一个坑:export default 会让插件静默失效

这是我撞过最难受的一个。插件文件写完了,挂进配置,重启,界面一切正常,对话也正常,但我的工具怎么都不出现。翻日志没有报错,回到插件文件里逐行看也没看出问题。

根因在导出方式上。DSH 的插件目录约定是具名导出,要导出 name、inject、apply 这几项。如果用了 export default 把整个对象吐出去,框架拿到的是一个默认导出对象,它会认为这个插件没有声明注入项,于是静默跳过依赖注入,apply 里的注册逻辑就没有按预期执行。

错误写法(看着能跑,实际不生效)

// my-plugin.ts export default { name: 'my-plugin', inject: ['tools'], apply(ctx) { /* ... */ } }

正确写法(具名导出)

// my-plugin.ts import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'my-plugin' export const inject = ['tools'] export function apply(ctx) { // 注册逻辑 }

这个坑恶劣的地方在于它不告诉你哪里错了。你写的代码语法完全合法,TypeScript 不报错,打包也过,框架也不拒绝加载。唯一的现象是你的插件没起作用。排查的时候如果没有一个探针工具,你会先怀疑配置、再怀疑路径、再怀疑版本,最后才怀疑到导出方式上。

能抄:所有插件一律用具名导出,不要用 export default。这条没有例外,而且要在写完第一个插件时就固定成肌肉记忆,不要等踩了坑再改。

三、第二个坑:object 输出 schema 必须显式写 additionalProperties

第二个坑会直接让注册失败,比第一个好一点,至少它拦住了你。但只要你的输出是对象类型,就一定会撞上。

DSH 在注册工具时会校验工具的输出 schema。如果输出的 type 是 object,框架要求你在 schema 里显式声明 additionalProperties 这个字段,写 true 或者 false 都可以,但不能省略。省了就直接注册失败。

这里有一个我一开始被绕晕的地方。网上能搜到的教程对这件事的说法是打架的,有的写必须写成 additionalProperties: false,有的写必须写成 true,两边语气都很肯定。我自己试下来的结论是两边都对了一半,真正强制的是必须显式写明,值是 true 还是 false 由你的返回结构决定。

说法实际含义什么时候用
必须显式写明这是框架的硬约束,不能省略所有 object 类型输出,无例外
写成 false表示返回值里不允许出现 schema 没定义的额外字段返回结构完全可控时用,校验更严
写成 true表示允许出现 schema 之外的额外字段返回值可能带可选字段、或者模型会往里塞东西时用

我自己的习惯是一律先写 false,跑通之后如果发现模型那边有额外字段被拦掉,再改成 true。这样至少能知道自己的返回结构是不是真的干净。

避坑:不要抄教程里的值,要抄的是「必须显式声明」这条规则。别人的返回结构跟你的不一样,值照抄会出现两种结果,要么校验把合法返回拦掉,要么该拦的额外字段全放过。

四、第三个坑:output.schema 的结构不能照 parameters 的样子写

这个坑我当时花了很久,因为报错指向的是插件树加载失败,看起来像是配置问题,实际是我把 schema 的结构写错了。

写工具参数的时候,parameters 是一个属性映射,键是参数名,值是该参数的类型定义。我顺手把输出 schema 也写成了同样的形状,启动直接崩。

启动时的报错原文

Error: dsh: plugin tree failed to load: ... unsupported JSON schema: schema.type must be string/number/integer/boolean/null/array/object/json, or use oneOf

看清楚报错里的关键词,它说的是 schema.type 的取值不合法。原因是 output.schema 接收的不是属性映射,而是一个完整的 schema 描述对象,你要把 type: 'object' 和 properties 都包进去,属性映射是放在 properties 里面的。

错误写法:把属性映射直接写在 schema 下

output: { schema: { message: { type: 'string', required: true } }, render: (_args, value) => [{ type: 'text', text: value.message }] }

正确写法:包住 type 和 properties

output: { schema: { type: 'object', additionalProperties: false, properties: { message: { type: 'string', required: true } } }, render: (_args, value) => [{ type: 'text', text: value.message }] }

顺带说一句,这个报错的定位质量很好。它指到了具体的 loader 条目,还把 schema.type 的合法取值枚举了一遍。我在别的框架里踩 schema 错误,经常只得到一句 cannot read property,要自己一行行往回找。框架在 apply 阶段就把坏 schema 拦住,而不是等模型调用时才炸,这一点对调试时间的节省很实在。

说明:判断一个框架的成熟度,看它报错的质量是个靠谱的角度。理想的报错要做到三件事,指向具体位置、说清期望值、给出可选项。DSH 在这条上做得不错。

五、第四个坑:inject 的用法,以及哪些服务不该写进去

前三个坑都是硬错误,第四个是理解偏差,不会报错,但会让插件在某些情况下莫名不工作。

inject 数组声明的是这个插件依赖哪些服务,框架会等这些服务就绪之后再执行 apply。最常见的写法是 inject: ['tools'],意思是等工具注册表准备好了再注册我的工具。这个顺序不能乱,如果 apply 在注册表就绪之前跑,你的注册动作会落空。

容易搞错的是可选服务。有些服务是「有就用,没有也能跑」,这类服务不要写进 inject。写进去的结果是框架会强制等你依赖的服务出现,那个服务如果没挂载,你的插件就永远不执行。可选服务应该写在 apply 内部用 ctx.get 去取,取到就用,取不到就跳过。

可选服务用 ctx.get,不要写进 inject

export function apply(ctx) { // 正确:可选服务用 ctx.get const skills = ctx.get('skills') if (skills) { skills.register({ /* ... */ }) } // 错误做法:把 skills 写进 inject,服务没挂载时插件永不执行 }

我自己就吃过这个亏。有一个能力在某些运行模式下默认不挂载,我把它当硬依赖写进了 inject,结果在默认配置下插件一切正常,换到另一个 profile 跑就完全没反应,而且没有任何提示。改写成 ctx.get 之后,两种环境都能跑,功能在缺失时自动降级。

依赖类型写法表现
硬依赖写进 inject 数组服务缺失时插件不执行,等就绪后自动跑
可选依赖在 apply 内用 ctx.get 取服务缺失时自动降级,插件仍能运行
能抄:判断标准很简单,问自己这个服务没有的时候,插件还有没有意义。还有意义就是可选,用 ctx.get;完全没意义就是硬依赖,写进 inject。

六、五分钟确认插件到底加载了没有

上面四个坑之所以难受,是因为缺一个「插件到底活了没有」的确定信号。我的做法是固定一个探针工具,每次新插件都先让它跑通。

探针工具就一件事,接收一个名字,返回一句打招呼。它没有任何业务逻辑,唯一的用途是确认注册链路通了。

import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'my-plugin' export const inject = ['tools'] export function apply(ctx) { ctx.tools.register(defineTool({ name: 'hello_world', description: '向某人打招呼,用来验证插件是否加载成功', parameters: { who: { type: 'string', required: true, description: '要打招呼的人' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { message: { type: 'string', required: true } } }, render: (_args, value) => [{ type: 'text', text: value.message }] }, async execute(args) { return { message: `你好,${args.who},插件加载成功` } } })) }

装好之后重启,在对话框里让模型调用这个工具。工具出现并能返回,说明导出方式、schema 结构、注入顺序这三条全过了。这时候再把你的真实逻辑替换进去,出问题只会在业务代码里,不会在框架对接层。

我固定会看的四样东西

【1】工具名有没有出现在模型的可用工具列表里。不出现,问题在注册环节。

【2】出现但调用报错,问题在参数或输出 schema。

【3】能调用但返回内容不对,问题在 execute 的返回值跟 schema 对不上。

【4】以上都正常但模型不用它,问题在 description 的写法,见下一节。

七、插件加载成功不等于模型会用

这一条是我从别人的踩坑记录里学到的,也是我自己验证过的。工具注册成功、能正常调用,跟模型在真实对话里会不会主动用它,是两件不同的事。

模型选不选一个工具,取决于 description 写得够不够像一个动作说明。我的经验是 description 里要写清三件事,这个工具干什么、什么情况下该用它、用完会返回什么。只写「查询数据」这种四个字,模型基本不会主动调用。

description 写法模型行为
查询数据几乎不会被主动调用
读取指定 URL 的正文并返回标题、字数与正文片段,当用户给出链接需要了解内容时使用在相关场景下会被主动调用

还有个实际问题,手上没有 API Key 的时候怎么验证工具真能被调用。我第一次写下插件的时候没配 Key,只能确认它注册成功了,没法确认模型会用。后来用的办法是接一个兼容的本地模型端点,把工具的调用链路完整跑一遍,专门的验证场景再切回正式的 Key。

避坑:不要用「插件加载成功」当成验收通过。加载成功只说明框架接受了你的插件,不说明这件事做完了。真正的验收标准是模型在一个真实任务里自己选了这个工具并且拿到了正确结果。

八、把这四条收成一张检查表

检查项正确做法错误表现
导出方式用具名导出用 export default,插件静默不生效
object 输出 schema显式声明 additionalProperties省略该字段,注册直接失败
schema 结构把 type 和 properties 包进去直接写属性映射,报 unsupported JSON schema
依赖声明硬依赖进 inject,可选依赖用 ctx.get可选服务写进 inject,换环境就不执行
验证方式先用探针工具确认链路通跳过探针直接写业务,出问题分不清是哪层
验收标准模型在真实任务里主动调用只看插件是否加载成功
一句话总结

插件出问题先分两类,报错的是 schema 和依赖写错了,不报错的先查导出方式。确认链路一定用探针工具,不要拿业务逻辑当探针。

▶ 下一篇:下一篇写版本升级:一次 Destructive 变更之后,我的插件在新版本里全部失效,以及怎么提前判断一个插件会不会崩。

相关学习资料