# 关键踩坑记录(勿重蹈) > 本项目开发过程中踩过的坑,按“现象 → 原因 → 正确做法”整理。新坑持续补充。 ## 配置与 dsh 集成 1. **settings 命名空间白名单** - dsh api-proxy 的 `settings.describe` 只返回硬编码 `WEB_SETTINGS_NAMESPACES` + model providers + product 名单。 - 第三方插件命名空间 RPC 永远看不到(官方注释 "deferred work")。 - 正确做法:第三方配置 UI 一律走插件自有 HTTP 路由。 2. **route 唯一性** - `ctx.webServer.register` 的 exact route 同 path 不能注册两次。 - 正确做法:GET/POST 合并进一个 handler,按 method 分发。 3. **client 插件机制** - patch 行 name 用包名。 - client row 由 modules node half 扫描 `dsh.client` 自动编入 `__DSH_BOOT__`,无需特殊 client 行。 4. **client 使用 `ctx.workspaces` 必须注入 `workspaces`** - `inject` 只写 `slots` 时 `ctx.workspaces` 为 undefined,托盘“新建任务”会静默失败。 - 正确做法:`inject: ['slots', ..., 'workspaces', 'sessions']`。 5. **事件类型合并** - `session/event` / `agent/created` 等 Events 由 `@deepseek-ai/dsh-session` / `@deepseek-ai/dsh-agent` 声明合并。 - 使用方需显式 `import type {} from` 这两个包。 6. **顶层会话 delegationDepth 是 undefined** - dsh 顶层会话 `delegationDepth` 为 absent(undefined),不是 0。 - 判断顶层会话时不要写 `!== 0`,应写 `(session.header?.delegationDepth ?? 0) !== 0`。 ## 窗口与 WebView2 7. **窗口退化 bug** - 状态记忆必须校验最小尺寸/坐标,防 boot 期 0 尺寸持久化。 - `??` 不会跳过 0,要显式判断。 8. **webviewjs 无关闭拦截**(WebView2 时代,已过时) - `window-close-requested` 只是通知,没有 preventDefault。 - 关闭到托盘用“不 exit + 隐藏保活窗口重建”方案。 9. **最小化到托盘恢复闪烁** - hide 前先 `setMinimized(false)` 清标志,showWindow 前同样清理。 10. **splash 期间主题探测** - splash 页无主题标记,探测必须三态(`na` / `-1`),否则标题栏启动时闪浅色。 11. **主动退出必须写 quit.marker + `process.exit(0)`**(WebView2 时代,已过时) - webviewjs 的 `app.exit()` 在 Windows 可能以 `0xC0000005` 崩溃。 - launcher 会把非 0 退出误判为异常并自动重启;主动退出走 `quit.marker`。 - 当前(dev-v2):退出语义归 Rust 壳 `src-tauri/src/helpers/quit.rs`(写 `quit.marker` + `process::exit(0)`,supervisor 区分主动退出/崩溃重启);webviewjs `app.exit()` 与 npm launcher 机制均已删除。 ## 主题与系统调用 12. **PowerShell 慢** - 冷启动约 1.3s/次,常驻 stdin pipeline 每行约 1s。 - 标题栏 Dwm 调用必须用 FFI(koffi,~1ms),PowerShell 仅兜底。 13. **`explorer.exe` 不要加 `windowsHide: true`** - 会导致 Explorer 窗口隐藏启动,“打开工作区”看起来无反应。 - 需要前置时用后台 PowerShell 按窗口标题查找并 `AppActivate` + 短暂 TOPMOST。 14. **evaluateScriptWithCallback 返回值带引号** - 字符串结果被带引号序列化(`'dark'` → `"dark"`),`.trim()` 无法匹配。 - 探测一律用数字(1/0/-1)避免歧义。 15. **托盘桥返回值用数字** - 字符串返回值同样带引号,host 判断永远失败并重复派发。 - `dispatchScript` 成功/未就绪返回 `1/0`。 ## 右侧栏 / 前端 16. **shell.overlay 方案不可用** - 官方 overlay 层在 AppFrame 内部,fixed 子元素定位/裁剪行为异常,侧边栏会跑到左侧。 - 正确做法:参考 DSH-better-sidebar,用 **body portal** 挂载右侧栏。 17. **新对话无法展开** - 官方 details 列对 blank session 强制 width 0(`detailsSession` 仅在 `blank === false` 时传入)。 - 不要依赖 details slot 承载主侧边栏;用 body portal 自管宽度。 18. **body portal 需要 react-dom** - tsdown 配置已 externalize `react-dom/client`,但构建需要本地安装 `react-dom` 与 `@types/react-dom`。 - 运行时由 dsh 平台模块表提供。 19. **npm install 会清掉 SDK junction** - `build-client` 建立的 `@deepseek-ai/dsh-*` junction 会被 `npm install` 移除。 - 装完依赖后必须重新运行 `npm run build:client`。 20. **postinstall 可能改动 bin/launcher.vbs**(WebView2 时代,已过时) - `npm install` 后 `git status` 常出现 `bin/launcher.vbs` 被修改。 - 提交前确认是否为无关改动;不需要时 `git checkout -- bin/launcher.vbs`。 ## 构建 / 发布 / 合并 21. **合并 PR 时生成文件冲突** - `lib/client.js.map`、`package-lock.json` 等生成文件极易冲突。 - 正确做法:保留源码合并结果后,重新 `npm install` + `npm run build` + `npm run build:client` 再提交。 22. **默认 npm registry 可能是华为云镜像** - `npm view` / `npm install` 默认走 `https://repo.huaweicloud.com/repository/npm/`,新版本可能未同步。 - 发布和验证请显式使用 `--registry=https://registry.npmjs.org/`。 23. **发布候选版用 `--tag rc`** - 直接 `npm publish` 会覆盖/影响 `latest`。 - 候选版应 `npm publish --tag rc`,用户安装用 `npm install @marecgents/dsh-hub@rc`。 ## 多实例 / 会话数据 24. **多个 dsh 实例并存会损坏会话数据(已实际发生)** - 现象:同一会话在两个 dsh 实例(不同端口)中同时打开 → 该会话读取报 corrupt、GUI 不可用。 - 根因:两个实例共享同一 `$DSH_HOME/sessions` 存储;在同一个 turn 处交错 append 时产生两组相同 seq 的冲突事件(一组「interrupted 死分支」+ 一组「真实活分支」),`SessionLogScanner` 检测到 seq 回退 → 判定会话损坏。 - 修复(2026-08-16):帧级字节拼接剔除死分支 4 行(checksum 无需重算),保留活分支后续 2 万+ 事件;备份 `.corrupt.bak` 可回滚。详见 `docs/会话损坏修复记录-2026-08-16-1023.md`(已归档外部档案仓库 `../docs/archived/会话损坏修复记录-2026-08-16-1023.md`,未随仓库分发)。 - **复发(2026-08-21)**:`session-7dbdbc85`(dsh-hub开发会话)再次因双实例双写损坏(seq 653843 回退,死分支帧 29329/29330),同法修复(剔除死分支 4 行,保留活分支后续 6 万+ 事件)。详见 `../docs/archived/会话损坏修复记录-2026-08-21-2322.md`(已归档外部档案仓库,未随仓库分发)。 - 预防: - dsh-hub 启动时检测已有 dsh 实例(任意端口),默认**拒绝**共存(设置 → DSH HUB 设置 → 「允许同时运行多个实例」未勾选时直接拦截)。 - 确需共存须勾选该选项,并确认严重警告(共享 `$DSH_HOME`、同会话双写会损坏日志)。 - 任何情况下都不要在两个实例中同时操作同一个会话。 ## 设置保存 / 皮肤 25. **设置保存回环不可信任服务端响应(勾选"保存后又被取消")** - 现象:设置卡片勾选 → 保存 → 勾选被取消(或重开卡片发现没保存)。 - 根因(两类): - a) 运行中的 dsh 实例内存里是**旧代码**(Node 启动时加载 lib/,之后改的 src/lib 不生效)——旧 host 的 POST 白名单没有新字段 → 响应缺字段 → 客户端 `setDraft(saved)` 静默回退。本机实测:实例 10:43 启动,早于皮肤/声音/新白名单,重启即恢复。 - b) 双实例并存时,旧 host 的 `writeShellConfig` 用**旧 DEFAULT 合并回写**,会抹掉新 host 写入的新字段(skin/soundEnabled)→ 皮肤选择/勾选被"取消"。 - 正确做法: - 客户端保存成功路径 `setDraft({ ...saved, ...patch })` **回放提交的 patch**——勾选永不静默回退;`config` 保持服务端真相,真丢字段时显示"未保存"而非消失(dsh-hub 已修复)。 - 测试新构建前先**重启**实例(会话持久化在 `$DSH_HOME/sessions`,重启不丢);不要在旧实例上判断新功能行为。 - 新增配置字段必须三处一致(ShellConfig 接口 / DEFAULT_SHELL_CONFIG / POST 白名单),漏白名单 = 本坑重现。 26. **皮肤只覆盖 alias 背景会导致左右侧边栏不跟随(浅色停留官方白)** - 现象:浅色模式下应用皮肤,只有中栏背景变了,左导航/右侧栏还是官方白。 - 根因:dsh 左导航背景用 `--dsw-specific-sidebar-fill`(specific token),卡片徽章用 `--dsw-alias-bg-module-platform`,浮层用 `--dsw-specific-menu`——都不在皮肤原本只覆盖的 alias bg 集合里。 - 正确做法:皮肤必须同时覆盖 alias 全量 + specific 组(sidebar-fill / sidebar-nav-item-active-accent / sidebar-nav-item-active / sidebar-nav-item-hover / menu)+ bg-module-platform,且 `skins.ts` 的 `buildCss` 按 `specific` 字段输出 `--dsw-specific-` 前缀(alias 与 specific 放错位置 = 前缀错误 = 覆盖无效)。详见 `docs/skins/AGENTS.md`。 ## 进程身份 / 音源 27. **Windows 进程身份 = exe 内嵌资源(不是运行时改名)**(WebView2 时代,已过时) - 现象:任务管理器把 dsh-hub 显示为 "Node.js JavaScript Runtime" + node 图标。 - 根因:进程显示名来自 exe 的 VERSIONINFO(ProductName/FileDescription)+ 内嵌图标 + 文件名;dsh 运行时就是 node.exe,资源是微软的。 - 正确做法(dsh-hub 已实现):`bin/hub-exe.mjs` 复制当前 node.exe → `dsh-hub.exe` / `dsh-hub-guard.exe`,用 `@electron/rcedit` 替换图标与版本信息(ProductName=DeepSeek Harness Hub / Launcher),缓存 `$DSH_HOME/dsh-hub/bin/` + stamp(node 升级自动重建);launcher re-exec 为 guard、spawn 应用用 patched exe。**补丁 exe 不可提交进 git**(跨机器/node 版本会失效);生成失败回退 cmd shim。 - 教训:别想用运行时 API 改名进程——任务管理器读的是 PE 资源。 - 当前(dev-v2):`hub-exe.mjs` / rcedit / cmd shim 已随 launcher 家族删除;Tauri 原生 exe 自带图标/版本信息(任务管理器显示 DeepSeek Harness Hub)。 28. **提示音素材不要原封复用他人项目的文件** - 现象:曾把 DeepSeek-Reasonix 的 4 个 mixkit WAV 复制进 assets/sounds(用户要求改为自有资源)。 - 正确做法:原创合成(`scripts/synthesize-sounds.mjs`,44.1kHz/16bit WAV,正弦+谐波+指数衰减包络)——零版权、体积 ~30-80KB、播放链路不变(winmm PlaySoundW 只认 WAV)。可脚本化下载的免费库(Pixabay/Mixkit)只提供 MP3,无 API key 拿不到 freesound 的 WAV,故合成是 WAV 原生播放的最优解。 ## 前端 DOM 增强(置顶会话) 29. **ui-workspace 的 CSS-module hash 不可作为插件定位锚点** - 现象/风险:`ui-workspace` 的行/标题/操作区类名是构建期 hash(`.YDXeBa_*`),dsh 升级重编译即变——把功能定位写死在 hash 上 = 升级即失效(PR #4 原实现即如此)。 - 正确做法(dsh-hub 已实现):定位只用 **framework 稳定契约**——`div[data-slot="sidebar.workspaces"]`(slot 渲染器固定输出,web-react/scoped-slots)、`role="tree"`、`div[role="treeitem"]`(排除 `[aria-expanded]` 分组行、`:not(button)` 搜索行);**行→会话映射用内容匹配**(行内文本 == displayTitle,同名整组跳过,读错=跳过不误标),插入位置 = 匹配到的标题元素之后。 - 单点豁免(仅 2 处,dsh 升级回归清单必查):① 自绘 pin 图标(官方 `Icon*Outline16` 无 pin,见根 AGENTS §3 豁免条款,官方提供后切换);② 无(标题提取已用内容匹配替代,零 hash)。 - 数据正确性纪律:pins 写路径必须等 `phase==='ready'` 基线落地(空 byId 时收缩会清空 pins.json,真实事故风险);boot 结果与用户交互增量合并(dirtyDelta),剪枝连续 2 次 ready 快照缺失才删;disposer 必须移除注入 DOM(HMR 重装防僵尸闭包写)。 30. **官方组件内联 token 不被皮肤覆盖(升级检查项,勿用 body.style 竞争)** - 现象/风险:部分官方表面**直接硬编码颜色**而不走 `--dsw-*` token——例如 ui-primitives Tooltip 文字固定 `bluish-00` 白、部分按钮变体。皮肤注入的 `