# 路线图 / 待办 这里放**已经确认要做、但排在下一个版本**的条目。写进仓库而不是留在聊天里, 是为了让下一个接手的人(包括未来的作者)能直接看到上下文和依据。 ## 0.2.0 计划 ### 1. 提示词注入做成可选开关 - **现状**:Host 半部**无条件**注入推理语言规则(`systemPrompt.section(10300)` + `context(130)`), 所以中文提问会跟随中文思考。 - **要改**:设置里加开关,**默认打开**(保持现有行为);关掉后完全不注入,回到出厂的英文思考。 - **为什么**:有人就是要默认的英文思考(部分任务上英文推理更稳),不该被插件单方面改掉。 ### 2. 设置里加「自动更新」开关 - **现状**:`auto-update` 路由已经在 Host 侧实现(`index.js`),但客户端没有入口; `holdVersion` 防回退循环只服务于手动路径。 - **要改**:设置行加开关,打开后插件在 DSH 启动时静默检查并安装新版本(沿用现有的 预检 + 备份 + 原子替换 + 回滚)。 ### 3. 图形化的一键回退入口 - **现状**:`rollback` 路由和按钮都已存在,但它和「有新版本可装」挤在同一行里,语义不显眼。 - **要改**:做成独立、醒目的入口,并显示「当前版本 → 将回到的版本」。 ### 4. ~~把插件设置独立成设置左侧导航的一项~~ → 已做,但 0.2.3 撤销了(方向反了) - **0.2.0 做过**:挂在 `settings.section`,在设置左侧导航里独立成一栏(order 62)。 - **0.2.3 撤销**:作者反馈 —— **多插件用户会把设置左侧栏挤满**,而本插件的设置并不需要常调。 改成 DSH 自带「插件」页里的配置位 `plugins.bundle.config`(keyed slot,填 bundle 包名)。 对照实现:`dsh-bottom-info-bar`、`dsh-plugin-wallpaper-engine` 都用这个槽位。 - **教训**:槽位本身选对了(两者都是正规挂载点),但**放哪儿是产品决策** —— 应该先问「这个插件的设置有多常改」,再决定它该不该占一个一级导航位。 ### 5. 自定义 HTTP 端点补「两端一致」的行为测试 - **为什么**:0.1.1 里「检查更新」报 **HTTP 405** —— 客户端按「有没有传 body」决定 GET/POST, 把 `check`(变更类、但不需要参数)发成了 GET,而 Host 侧把 `check` 列为变更类、一律要求 POST。 两端各写各的策略集合,**没有任何测试覆盖「客户端真实发出的 method 能否被 Host 接受」**, 所以只在用户点击时才炸。 - **要改**:mock `fetch`,对每个按钮断言它真实发出的 method + body 能通过 Host 的策略表; 或者更进一步,把策略收敛成**单一事实来源**(Host 导出一份方法表,客户端只读它)。 ## 已完成 - **0.2.1** —— 四个新功能 + 实测与审查挖出来的修复 - **思考中显示已用时间**:用出厂的 `data.time` (`settled?.time ?? state.firstVisibleTime ?? …`,流式期就有),所以**不自己计时** —— 只在 running 时开一个每秒的定时器算差值。文案是「深度思考中(已用时 N 秒)」 - **设置页「自检」**:官方组件可用性、渲染/降级计数、自动展开成功与放弃次数、 兼容性提示数 —— 样式哪天悄悄变回出厂,一眼就能看出是哪一环掉了 - **「回到底部」按钮**:只在没贴底时浮在面板右下角(长推理翻上去后不用一路拖回来) - **思考预览行数可配**:自绘滑动条,1–20 行、步长 1、拖动即时生效(走 `:root` 的 CSS 变量) - **设置页整块空白**:`if (state === null) return null;` 之后又写了 3 个 hook → React #310 (hooks 数量变化)。已把 hook 全部移到提前返回之前,并加了 `SectionBoundary` 把错误显示到页面上 - **思考内容只剩一行**:排版容器用了 `flex + gap`,flex item 的 `min-height:auto` 把 5 行滚动预览压成 1 行 → 改回 block + 相邻兄弟 margin - **插话后界面停在上一段**:真因是**面板长高**越过了 DSH 的 25px 跟随阈值 (`useScrollFollow(state.followingTail, 25)`),此后连 steering 都不再滚 (出厂源码 4979 行:`… && reading.followingTail`)。(该方案后来被整段撤销 —— 见文末「待定位:插话后界面不自动跟随」。) 前两轮修的都是「展开过程组」,方向错了 - ~~**设置导航图标**:DSH 的导航图标是 shell 里 `navIcon(id)` 的硬编码映射,第三方一律兜底齿轮 → 用 DOM 补丁克隆插件自己的 `IconThink` SVG 替换原图标(卸载时还原)~~ **(0.2.3 随设置入口迁走而整段删除 —— 设置栏里不再有本插件,也就没有图标可换)** - **`useTurnData` 从 `useMemo` 内提到组件顶层**:它内部是 `useSyncExternalStore`, 放在提前 return 之后同样会触发 #310(与设置页同一个病根) - **自动更新遇到开发目录只提示不自动装**:本机是 `link:` 安装,`packageDir` 就是工作区源码目录, 否则一发版就会覆盖未提交的改动 - 其余:mode 落盘改原子写且失败可见、`rollback` 补 semver 校验、 `Switch` 写入中禁用、复制按钮的 Enter 不再被标题行抢走、 `alive` 在 StrictMode 下复位、导航补丁死代码清理…… - **0.2.0** —— 设置页独立成栏 + 四个开关 + 审查报告里的剩余修复 - ~~**设置左侧导航独立一栏**(`settings.section`,order 62):不再挤在「通用设置」里~~ **(0.2.3 已迁到「插件」页的 `plugins.bundle.config`,见上方待办 4 的说明)** - **语言注入开关**:Host 侧改成可运行时切换(先注销再注册 prompt section),状态存插件数据目录, `cordis.patch.yml` 只提供默认值 - **自动更新开关 + 启动后自动检查安装**(默认打开;写入前预检、备份,失败回滚) - **一键回退**:记 `previousVersion`,回退到「上一个装过的版本」而不是当前版本 - 审查报告里的其余修复:排版容器、`pathImages` 走鉴权路由、`fileMentions` 补 tail 守卫、 `other` 块传真实载荷、错误边界可复位、条件调用 hook、effect 依赖、ResizeObserver 只建一次、 焦点还原判断、图片失败日志 - 新功能:思考正文**复制按钮**、兼容性提示显示到设置页、无障碍朗读 - **0.1.1** - 自动展开过程组(默认打开)+ 设置行第二个开关 - README 修正「工作步骤展示」相关论断(改设置项确实会决定你能不能看到面板) - README 图片改用绝对地址(npm 页面不解析仓库相对路径) - **0.1.2**(并入 0.2.0 一起发布,没有单独发版) - **HTTP 405**:客户端按「有没有传 body」决定 GET/POST,把变更类的 `check` 发成了 GET - **耗时改为读出厂的 `finalNode.timing`**:原先用模块级 Map 按 node key 记账,而 key 只在会话内唯一, 两个会话的 `1:1` 会互相污染;顺带干掉了「在 render 里写外部状态」和 LRU 淘汰 - **`interrupted` 状态**:思考被中断时显示「已停止」+ 出厂同款徽标,而不是「已思考(用时 N 秒)」 - **自动展开失败后会重试**:effect 依赖只有 `[autoReveal, running]`,不重试就等于这一轮彻底放弃 - **`mode: off` 不再吃掉自更新入口**:端点注册从 off 短路之后挪到了前面 - **自更新加了并发互斥**:桌面端与 Web GUI 同时点「更新」不会再交错写文件 - **体积上限改成流式判定**:原先读完整个响应才判大小,上限形同虚设 - **自更新面板的重入守卫与卸载保护** - **流式尾部判定**改用「后面还有没有正文块」(末块是图片时不再把「思考中」显示成「已思考」) ## 下一批(0.2.0) 上面 0.2.0 计划里的那五条,外加审查报告里的: 排版容器(字号/行高/16px 块间距)、`pathImages` 走鉴权文件路由、 `fileMentions` 补 `tail` 守卫、无障碍朗读、条件调用 hook、几个 effect 依赖与 `ResizeObserver` 反复重建、耗时与展开状态的持久化。 ## 未复现的观察 - **跟随失效(2026-10-13,作者报告)**:触发时「几乎一瞬间就有一堆内容出现」,且**发生在思考链里**(不是最终回答)。第五轮把判定改成「跟踪用户意图」之后没能复现。若以后再出现,先看设置页的**自检**数据,再确认 ollowingRef 是不是被某个 scroll 事件误置成了 false。 --- ## 待定位:插话后界面不自动跟随 **现象**(作者报告,可复现但不稳定):插件流式输出时插一句话,新一轮开始后**主界面不跟到底部**、 停在上一段,要手动拉一次。触发时「几乎一瞬间就有一堆内容出现」,且**发生在思考链里**。 **已经排除的方向**(都试过,相关代码已撤销,不要重走): 1. ~~展开过程组导致高度突变~~ —— 展开只发生一次,而问题在持续输出时才出现; 2. ~~沿 `parentElement` 找可滚动祖先~~ —— 那**确实是**个 bug(先撞上过程组自己的 `[data-step-process-body]`,把组体滚到底、外层纹丝不动),但与现象无关; 3. ~~面板长高越过 DSH 的 25px 跟随阈值~~(`useScrollFollow(state.followingTail, 25)`)—— 机制上说得通,但见下:真正的麻烦是「插件自己去滚」这件事本身; 4. ~~插件侧主动写外层 `scrollTop`~~ —— **这条路是错的,已整段撤销。** **为什么「插件自己滚」走不通**(代码审查对照出厂源码的结论): - 出厂有完整的滚动策略层:`useChatReading` / `ChatViewport` / `ScrollFollow` / `useProcessScroll`; - `ChatViewport.readScroll()` 判定 `movedByReader` **只看位置差是否 > 0.5px,不区分程序化写入** —— 插件写 `scrollTop` 会被记成「用户自己滚的」; - `ScrollFollow.sample()` 只在 `movedByReader && !animating` 时更新跟随意图; - DSH 有在飞的平滑目标时,插件这一写会让它在 `scrollend` 判出「偏离目标 > 25px」→ **主动释放跟随**,并通过 `stopPreserving()` 打掉分页锚点。 **下一版该走的方向**:不要直接改滚动位置,而是**发出语义意图** (复用出厂的 `followTail` / `ScrollFollow.owners` 路径);或者干脆向 DSH 提 issue —— 「贴底时内容长高导致失跟」看起来像**出厂的判定阈值过严**。 **方法学教训**:这一条修了五轮,前四轮都在「猜哪里长高了」,而答案一直在出厂的 `ScrollFollow` 里。 **先读源码,再动手。** --- ## 待定位:插件在 desktop profile 上「静默不激活」 **现象**(2026-10-14 实测):设置页看不到「深度思考面板」栏、自更新端点返回 **HTTP 404**。 ⚠️ 这个 404 与「国内网络能否直连」**无关** —— 它是插件**自己**在 DSH 里注册的本地端点。 **已经查清的(全部有实证)**: - 配置树里**有**这个插件:`dsh --profile dst-test --dump-config` → `- id: thinking-panel / name: dsh-thinking-panel / config: {mode: auto}`; - 模块本身**能加载**:用 DSH 自带的 Electron node(v24.18.1,`ELECTRON_RUN_AS_NODE=1`) import `index.js` / `self-update.js` **都成功**; - 但 **`apply` 从来没被调用过** —— 在 apply 开头写 trace 到 `$DSH_HOME/plugin-data/dsh-thinking-panel/apply-trace.log`,重启后**文件不存在**; - DSH 只留一句 `web boot: 1 entry did not activate`,浏览器控制台里**没有**更详细的原因; - 对照:npm 装的插件(`dsh-bottom-info-bar` 等)端点返回 **401**(= 路由存在,完全正常); 另一个 **symlink 装的 `dsh-cot-translator` 同样报 `import failed`**。 **排除过的假设(不要再试)**: 1. ~~`ctx.inject(['connection','webServer'])` 多要了可选服务~~ → 改成只 `['webServer']`,无效; 2. ~~`package.json` 缺 `main`~~ → 补了 `"main": "index.js"`,无效(但该字段本身该有); 3. ~~`dsh.client.inject` 里的 `@deepseek-ai/dsh-client-ui-settings` 不存在~~ → 摘掉后仍不激活; 4. ~~`export const inject = ['systemPrompt']` 静态硬门禁~~ → 删掉后仍不激活。 **下一步从这里开始**: - 嫌疑集中在 **本地链接安装(`link:` / symlink)的插件在 desktop profile 上不被激活** —— 四个本地装的插件里,两个 symlink 的都有问题,npm 装的全部正常; - 验证方法:把插件用 **`file:`(pnpm 的硬链接,仍与工作区同步)** 或打成 tarball 用 `dsh plugin add` 装进副本 profile,看是否恢复激活; - 注意 `dsh-infinite-gen-5` 是 **Junction 且没有报错**,所以未必是链接类型本身, 也可能是**依赖解析**:`link:` 的包不进 profile 的依赖树,而 DSH 可能按依赖树决定加载。 --- ## 已解决:「插件静默不激活」(2026-10-14) 排查用了 6 轮,**四个真因叠在一起**,全在插件自己这边: 1. **`apply` 里直接读 `ctx.systemPrompt`** —— cordis 不允许访问未声明 inject 的服务,抛 `cannot get property "systemPrompt" without inject`,整个插件激活失败。 修:改成运行时 `ctx.inject(['systemPrompt'], promptCtx => …)` 可选获取。 2. **静态 `export const inject = ['systemPrompt']` 也不能当硬门禁** —— 服务不满足时插件会 `pending` 而不激活(日志里那句 `1 entry did not activate` 就是它)。 3. **路由注册包了 `webCtx.effect`** —— 回调执行了、`register` 也是函数,但路由始终 404。 修:直接 `webCtx.webServer.register(...)` 并把返回值交给 cordis(与 dsh-bottom-info-bar 一致)。 4. **`connection` 在 `ctx.inject` 回调的 scope 里是 inactive context** —— 读属性就抛 `cannot get required service "connection" in inactive context`(端点从 404 变 500)。 修:不列进 inject,鉴权在 try/catch 里可选获取。 另外补了两处包元数据:`main` 入口、`exports` 里的 `./cordis.patch.yml` (Node 的 `exports` 一旦存在,就封闭所有未列出的子路径)。 **实测**(副本 profile `dst-test`):端点 `404 → 500 → 200`。 **教训**:这些失败在外部看起来**完全一样**(设置页无插件栏 + 端点 404),而 DSH 只在 stderr 里打一句被截断的 warning。三个突破口: ① `dsh --profile

--dump-config` 确认"配置树里有它"; ② 在 `apply` / `ctx.inject` 回调里写 trace 落地文件,区分「没调用 / 回调没跑 / 注册失败」; ③ 读 `resources/app.asar` 里的 `inactiveDiagnostic` —— 它会列出缺失的服务名。