关于 plugin-runtime-debug
plugin-runtime-debug 是用于诊断 DSH Web 插件运行时问题的技能,面向插件开发者。它要求先查阅宿主源码中的 API 契约,并结合症状排查浏览器行为,再修改插件代码。
主要功能
- 01修改前查阅宿主 API 契约
- 02追踪偏移量与数据表示差异
- 03诊断浏览器特有的 URL 行为
- 04从可见症状定位运行时原因
- 05通过重复交互验证修复
适用场景
- 01调查插件调用静默无效的问题
- 02诊断过期或虚假的插件界面
- 03追踪仅在浏览器出现的资源查找故障
- 04排查客户端包或插件激活失败
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ oh-my-dsh/ dsh-plugin-upgrade-skill --skill plugin-runtime-debug安装后在 Claude Code 中输入/plugin-runtime-debug确认已加载。
附带文件README.mdreferences/browser-forensics.md
详细介绍
调试 DSH Web 插件的运行时行为
外部 Web 插件会调用宿主客户端 API,这些 API 的契约定义在 DSH 源代码树中,而不在插件自身的类型中。运行时行为偏离预期时,几乎总是因为误读了契约——诊断必须依据宿主源代码,而不能只看 API 名称。
一贯原则:先在宿主源代码中阅读动词对应的契约
在更改任何对宿主 API 的调用之前,先打开 DSH 源代码检出目录(~/.dsh/source/current 或 vendored 副本)中的实现包,阅读实际方法——包括它的文档注释、保护条件,以及用于比较的类型。对插件传入的每个值都重复此操作。以下四个问题涵盖了大多数问题:
- 偏移量对应的是哪段文本? 当某个动词接收范围或偏移量时,弄清这些数字索引的是哪个字符串。已发布的快照字段与内部编辑器投影不一定是同一个字符串;如果插件把一种表示中的偏移量传给保护条件针对另一种表示进行比较的动词,调用就会静默失败——返回
false或不执行任何操作,不会抛出异常。 - 每种表示中的一个“单位”占多宽? 如果文档包含不透明的内联单元(标签、令牌、附件),检查该单元在已发布字段中占据的宽度,是否与在动词保护条件所针对的投影中相同。宽度不同时,只有在不存在任何单元时偏移量才正确——请确认第一次调用成功而之后每次调用都失败说明了什么。
- 动词拒绝执行时,谁会注意到? 返回布尔值的动词若静默失败,会引发下游状态错误:调用方仍会删除自己的状态记录,而 UI 会在一个始终存在的对象旁显示“缺失/不可用”占位符。审查每个调用点,查找“发起调用、忽略结果、仍然清理状态”这种模式。
- 这行代码由哪个引擎执行? Node 下运行的测试无法覆盖的 Web 平台行为,在用户的浏览器中可能有所不同,因为较旧的引擎早于现行标准。已知的一类问题是:解析非特殊方案的 URL——WHATWG URL Standard 要求
new URL('dsh-resource://file/…').hostname为"file",Node 和当前版 Chromium 都会返回该值,但在 Chromium 实现符合标准的非特殊 URL 解析之前,较旧版本返回的是"",因此较旧的 Chromium 内核 Edge 版本也会静默返回""。发现这种情况时,记录确切的浏览器版本。任何通过自定义方案的URL.hostname/.pathname进行路由的宿主或插件代码,在所有基于 Node 的测试中都能正常运行,却只会在受影响的真实浏览器中失败。信任自定义方案上的 URL 属性之前,请在实际浏览器引擎中断言其值(参见 references/browser-forensics.md),或手动解析字符串。
症状类别及其指向
- 首次交互正常,之后每次都报错 — 较早的调用写入的状态改变了插件计算出的映射与动词预期映射之间的关系。比较插入前后的两种表示;根据宿主源码中的单位宽度推导出修正方法,然后将同样的推导应用到每一个传递偏移量的调用点,而不只是崩溃的那个。
- 移除按钮留下带占位标签的行 — 移除动词拒绝了操作(见问题 3),但账目记录已经被删除。通过动词的返回值确认这一点,并且只有在实际移除成功后才注销账目记录。
- 派生 UI 显示过时或虚构的条目 — 找到事实的权威来源,并据此派生视图。如果插件侧缓存的订阅会在任何瞬时快照(协调/重新挂载期间的空状态)出现时注销条目,就会误删仍然有效的条目;应优先在作出决定时读取实时发布的状态,并且只把缓存当作加速手段。
- 更新/版本标记显示错误的“最新”版本 — 远程标签和原始文件端点会被 CDN 缓存,实际推送后可能延迟数分钟。若获取到的远程值可能早于当前运行的构建版本,就绝不要将其当作事实依据;应根据运行版本判断“当前版本还是有更新”,并显示两者中较新的版本。
- 内置界面在用户的浏览器中显示通用的“服务不可用”回退文案,但注册、组合和模块激活均已确认无误 — 停止审查注册流程,转而审查出错代码从其平台读取的内容。真实案例(0.1.6-alpha.2,2026-09):所有文件的右侧边栏文档预览都显示「文件资源服务不可用。」;
file资源提供方确已注册,__DSH_BOOT__也完整无缺,且没有任何激活失败——提供方路由器对dsh-resource://地址调用new URL(address).hostname来获取协议,但在用户的 Edge 上得到的是"",因此查找总是失败,也从未发送过请求。占位文案说明的是功能,而非故障层;只有检测路由器才能得到关键日志行(parsedHost="")。当用户只能报告一条回退文案时,应先在受控浏览器中使用带检测代码的构建复现(见 references/browser-forensics.md),再对注册状态作出任何结论——这起完全相同的事件曾被错误诊断为“提供方悄然取消注册”。 - 发布后,整个插槽的 UI 悄然消失 — 插槽组件中的表达式抛出异常(典型情况是悬空标识符:引用了另一个组件的状态变量,但该变量不在当前作用域内),会被框架的插槽级错误边界捕获,导致整个条目卸载;错误只会出现在控制台中,因此用户只会报告“标记/面板不见了”。两种延迟机制会让作者难以及时发现:
||短路会让表达式一直不求值,直到左操作数为假;而空状态下提前返回的组件,会一直等到真实数据渲染时才对该表达式求值。不要默认归咎于最新的差异——通过回滚或在有数据时进行最小化渲染挂载来二分排查,检查抛出异常的代码行是否早已发布,并通过移除引用来修复(将此类状态限定在本地作用域内)。对插槽组件进行低成本加固:采用防御式读取(x?.items ?? [])和可选链 DOM 访问(target.closest?.())——在错误边界内,任何异常都会导致整个插槽失效。 - 仓库修改始终不会反映到 GUI / 配置目录的 node_modules 下出现 EBUSY — 首先确定安装模式:
Get-Item <profile>/node_modules/<pkg> | Select LinkType, Target(或 cordis.patch.yml 中的link:<path>标记)。如果通过 Junction/链接安装,仓库工作树本身就是已安装的副本——不存在也不需要复制步骤,向 node_modules 执行 Copy-Item 至多只是无效操作。持有 EBUSY 的是正在运行的 dsh 宿主进程(关闭浏览器不会释放它),而且宿主重启后,浏览器仍可能提供缓存的客户端 bundle。对于通过链接安装、仅包含库代码的插件,激活步骤为:彻底停止宿主,重启dsh web,强制刷新,然后验证已加载的版本标记。路径尚未确认时,绝不要将文件改名移走:通过 Junction,“两个”目录其实是同一个,改名会移走唯一的副本。通过源码启动宿主(在 harness 检出目录中运行pnpm dsh web)还会带来两项限制:客户端 bundle 组合只会在启动时组装一次(不会通过 HMR 重建——每次修改插件都需要完整重启宿主);在 Windows 上,监听端口会继续被即将退出的进程树占用,除非停止整个进程树(taskkill /PID <pid> /T /F),否则下次启动会因 EADDRINUSE 而失败。 - 一个插件的客户端 bundle 中含有原始 ESM,导致所有插件都无法运行,且错误指向无辜的条目 — 宿主会将所有客户端 bundle 拼接成一个经典
<script>组合包;任何位置只要出现一个顶层import,整个数 MB 的组合包就会编译失败,所有插件都无法注册,而浏览器会显示failed to import loader entry <first-entry>——这里指的是第一个等待的条目(通常是dsh-typert-registry,它本身完全正常),而不是罪魁祸首。不要追查错误指向的条目:对配置目录的insert行进行二分排查(或使用node --check检查可疑 bundle),直到组合包能够再次加载。客户端部分不是纯 ESM 模块——它必须通过window.__ModuleLoader__.load({ id, factory })注册,在工厂函数中通过require("react")加载 react,并导出inject/apply。 - 终端精灵图右侧边缘出现杂点(轮廓、Z 符号、爱心),切换帧后仍有残影像素——两种半块 ANSI 渲染缺陷,在帧数据中都不可见:(a) 半填充单元格(仅一半着色的上半块)会设置前景色,却保留前一个单元格的 SGR 背景色——SGR 会跨单元格持续生效,因此过期的背景色会在空白的半边绘出杂点;应显式重置(对每个半填充单元格使用 ESC[49m)。(b) 修剪掉末尾透明单元格的行,会让较窄的帧在修剪位置右侧留下前一帧的像素——将每行绘制到精灵图的完整宽度(透明单元格绘制为普通空格),并以擦除至行尾(ESC[K)收尾。还要固定帧数据本身:手动移植的精灵图帧会与源图逐渐偏差几个单元格(只截取部分内容做回归测试会漏掉)——对照源图计算每一帧的摘要,并断言这些摘要。
- 头部/UI 进程在工作完成后仍挂起数分钟(或直到 CI 超时)——重调度的计时器链(已挂载时会无限重新设置 setTimeout 的动画规划器)会在宿主挂载组件但始终不卸载时让事件循环保持运行(探测器和测试宿主;GitHub 作业的默认超时时间为 6h)。交互式 TUI 无论如何都会通过 TTY/stdin 句柄保持运行——因此对这条链调用 unref(timer.unref()):探测器可以正常耗尽并退出,实际会话则不受影响。每当启用某项功能后,原本能正常结束的作业开始超时时,都应怀疑这一问题。
工作流
- 重现一次,并记录用户实际看到的精确字符串(提示文本、 标签文字、控制台输出)——它们是 Bug 报告的依据。
- 将每个字符串映射到输出它的代码路径;识别边界处的宿主动词。
- 打开该动词对应的宿主源代码;回答四个常见问题。
- 编写任何修复前,准确说明不匹配之处(涉及哪种表示形式、哪个守卫条件、哪些 调用点);如果说不清楚,说明你还没有充分阅读源代码。
- 如果任何控制台输出都看不出这种不匹配——例如通用 回退文案、静默无操作,或只有用户的浏览器会显示差异—— 则使用临时插桩的 bundle,在受控浏览器中重现(备份、 打上包含一条关键日志的补丁、验证、还原), 并遵循 references/browser-forensics.md。 用户粘贴的 DevTools 截图只能证明症状,不能证明 原因。
- 修复所有传入依赖表示形式的值的调用点,而不只是报告中的症状;相同的不匹配通常会通过两个不同的动词破坏两个功能。
- 使用失败的交互序列证明修复有效:连续重复操作两次,并断言两次行为一致;同时断言移除路径会清除对象在所有视图中的内容。
- 对于仅含 lib 的插件 bundle(没有构建步骤):确保手动内联的版本常量与
package.json保持同步,通过语法检查 bundle (node --check),并在硬刷新后于浏览器中验证——提供的产物就是你编辑的文件。
评论
支持 Markdown