# 开发规范与事故复盘 > 本项目在 DSH 0.1.0-rc.6 / rc.7 / 0.1.2-rc.1 上真实踩过的五类事故,以及由此固化的开发检查清单与发布流程。 > 事故时间:2026-08-15 ~ 2026-08-18。代码级细节见 [架构与机制](架构与机制.md)。 ## 1. 事故复盘 ### 1.1 ESM 重导出不产生局部绑定 - **现象**:启动 `dsh web` 报 `plugin tree failed to load … SEARCH_POOL_PROVIDER_ID is not defined`。 - **根因**:`export { X } from './provider.js'` 只转发名字,**不会**在当前模块建立局部绑定,而入口内部直接使用了 `X`。 - **修复**:改为 `import { SearchPoolProvider, SEARCH_POOL_PROVIDER_ID } from './provider.js'` 后再 `export`。 - **教训**:入口内部要用的常量/函数必须显式 `import`;改完至少 `node --check` 并实际重启验证 `apply`。 ### 1.2 向会话日志写未注册事件导致历史加载失败 - **现象**:会话重启后 `SessionFormatUnsupportedError: … event type "web/search-pool-attempt" … not marked ignorable; refusing to interpret the log`,整个会话历史打不开。 - **根因**:为可观测性调用了 `session.append('web/search-pool-attempt', …)`,该事件不在核心 `KNOWN_SESSION_EVENT_TYPES` 中;读取端遇到未知且未标 `ignorable: true` 的事件会拒绝整份日志,且 rc.6 的 API 不开放 `ignorable`。 - **修复**:受影响日志补 `"ignorable": true` 恢复;插件改走 `ctx.logger?.info(...)`,不再写会话事件。 - **教训**:**禁止写核心不认识的会话事件**;观测优先 `ctx.logger`。 ### 1.3 额度总览不更新 / 「立即刷新」静默无反馈 - **现象**:设置页额度一直是默认值;点「立即刷新」后 `usageRefreshTick` 递增但 `usage.updatedAt` 不变,也没有任何提示。 - **根因(三层)**: 1. Host 用 `ctx.get('settings')` 取服务,插件上下文里可能拿不到 → 发布函数静默 return;Client 用带 `expectedRevision` 的写入 → 撞 `SettingsConflictError`,tick 写不进去。 2. 真正根因:`installSettingsSection` 的 `scope.watch` 在 `include:` 装配下**已注册但不触发**,插件 `onChange` 不执行。 3. `writeUsage` 分两次写 `usage` / `usageDiagnostic`,失败路径连诊断都丢;Client 用空 `.catch` 吞掉 mutate 拒绝。 - **修复**:Host 改 `ctx.inject(['settings'])` 取服务、未就绪存 `pendingUsage` 补发;入口同时监听 `settings/updated` 事件用 `next` 快照触发刷新;`refreshUsage` 加 15s 总超时并聚合每 key 失败进 `usageDiagnostic`;`writeUsage(snapshot, diagnostic)` 单次 `settings.update`;Client 不再吞错并显示「已请求刷新 / 刷新请求失败」。 - **教训**:Host 取共享服务用 `ctx.inject` 而非 `ctx.get`;settings 驱动的动作要有事件兜底;用户可点的「刷新」必须有可见结果;网络刷新必须带超时;诊断与业务数据尽量同一次写入。 ### 1.4 rc.7 keyed slot 用旧版 id 注册 - **现象**:升级 rc.7 后设置卡片消失,控制台 `settings.plugin.item slot requires options.key`。 - **根因**:rc.7 把 `settings.plugin.item` 从 list slot 改为 **keyed slot**,必须用 `key`,且 key 要与 Host 的 settings namespace 严格一致(本项目 `web-search-pool`)。 - **修复**:`ctx.slots.register({ name: 'settings.plugin.item', key: 'web-search-pool', order: 21 }, Card)`。 - **教训**:升级宿主前先核对 slot 契约的破坏性变更;为此建立了 bundle / client / install 三类契约测试防回归。 ### 1.5 rc.7 仍依赖 `WEB_SETTINGS_NAMESPACES` 白名单脚本 - **现象**:rc.7 下仍跑 `scripts/patch-api-proxy-namespace.mjs`,找不到目标数组并影响启动。 - **根因**:rc.7 的 `dsh-host-apiproxy` 不再用该白名单,已注册的 settings namespace 由设置代理直接暴露;该脚本是 rc.6 时代的安装目录 patch 手段。 - **修复**:脚本降级为诊断模式(检测不到白名单即提示并成功退出);安装链路改由 Bundle patch 原生接管。 - **教训**:避免依赖 DSH 安装目录 patch,优先官方 Bundle 机制;所有机制结论标注调研日期与源码来源。 ## 2. DSH 插件开发检查清单 1. **ESM 绑定**:入口内部使用的符号必须 `import`;`export { x } from …` 不产生局部绑定。 2. **会话事件**:禁止 `session.append` 写未注册且未标 `ignorable` 的自定义事件;观测用 `ctx.logger`。 3. **机制先查源码**:涉及 seam / settings / api-proxy / session 的结论,先读已安装源码确认,不凭印象。 4. **改动后必验证**:`node --check` + 实际重启验证 `apply`;改 Host/client 代码必须重启进程,刷新浏览器无效。 5. **不直接改 npm 安装目录**:必须 patch 时做成可重复脚本(如 `scripts/patch-api-proxy-namespace.mjs`),升级后重跑。 6. **凭据**:只走 `ctx.credentials`(`credentialRef` 为环境变量名),每次操作 resolve,不缓存明文、不写配置文件。 7. **客户端密钥写入顺序**:先 `await api.credentials.set` 成功再提交 settings,保存后重新 `credentials.describe` 刷新徽标。 8. **运行时状态发布**:Host 用 `ctx.inject(['settings'])`;发布失败写 `usageDiagnostic`;Client 显式触发用不带 `expectedRevision` 的 mutate。 9. **刷新类 UI**:必须有请求/失败反馈与超时,不能只写日志。 10. **参数 `off` 要覆盖同维度所有派生参数**(如 `timeRange: off` 必须同时禁用 `days`),测试覆盖。 11. **settings 驱动动作要事件兜底**:`settings/updated` 监听 + tick 比较防重,不单靠 `scope.watch`。 12. **契约测试**:bundle / client / install / 版本兼容各有测试守住格式,升级宿主先跑 `npm run test:local`。 ## 3. 发布流程 1. 改 `package.json` 的 `version`,并在 `CHANGELOG.md` 顶部加条目(保持「新增 / 变更 / 修复」三段)。 2. 全量测试与语法检查:`node scripts/run-tests.mjs`、`node --check` 关键文件。 3. 打包校验:`npm pack --dry-run` 确认包内含 `src/`、`scripts/`、`docs/`、`cordis.patch.yml`、`README.md`、`LICENSE`,不含测试与凭据。 4. 提交并推送:`git push origin main`;需要发版时打 tag(如 `v0.2.1`)并在 GitHub Releases 附 tgz。 5. 发布前安全检查:无硬编码密钥、`.credentials.yaml` / `.env` 不在跟踪列表、文档无个人绝对路径、`CHANGELOG` 无敏感信息。 6. 本机安装验证:`dsh plugin --profile web add ` + `dsh --profile web --dump-config`。 ## 4. 文档规范 - **一份信息一个出处**:对外门面在 `README.md`,安装/升级/排障在 `docs/安装与升级.md`,机制与设计在 `docs/架构与机制.md`,开发规范在本文;同一事实不写第二遍,只放链接。 - **机制结论标注来源**:写明调研日期 + 本地源码路径或官方链接(见 `docs/架构与机制.md` 开头的「事实边界」)。 - **路径占位符**:文档示例统一用 `$DSH_HOME` 与 `<...>`,不出现个人盘符与用户名。 - **过程稿不长期保留**:方案/计划定稿后并入 `docs/`,旧稿由 git 历史保留。