ARTICLE · 1015581
插件加载了却什么都没干: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 不报错,打包也过,框架也不拒绝加载。唯一的现象是你的插件没起作用。排查的时候如果没有一个探针工具,你会先怀疑配置、再怀疑路径、再怀疑版本,最后才怀疑到导出方式上。
三、第二个坑: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 拦住,而不是等模型调用时才炸,这一点对调试时间的节省很实在。
五、第四个坑: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 取 | 服务缺失时自动降级,插件仍能运行 |
六、五分钟确认插件到底加载了没有
上面四个坑之所以难受,是因为缺一个「插件到底活了没有」的确定信号。我的做法是固定一个探针工具,每次新插件都先让它跑通。
探针工具就一件事,接收一个名字,返回一句打招呼。它没有任何业务逻辑,唯一的用途是确认注册链路通了。
装好之后重启,在对话框里让模型调用这个工具。工具出现并能返回,说明导出方式、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 变更之后,我的插件在新版本里全部失效,以及怎么提前判断一个插件会不会崩。