@nimoxie/dsh-ui-tweaks 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DEVELOPMENT.md CHANGED
@@ -5,16 +5,17 @@
5
5
  **导读**:维护者笔记;每条「坑」都给成因与对策。
6
6
  - **骨架 / 本地开发**:文件构成、四处须同步的标识、`file:` vs `link:`、装卸与刷新。
7
7
  - **已知坑**:注入、类名、data 标记、Shadow DOM 浮层。
8
- - **更新检查(模块 3)**:成熟期门禁与豁免表、cooling、强制更新、兼容门禁、Auto review。
8
+ - **更新检查(模块 3)**:成熟期门禁与豁免表、cooling、强制更新、豁免到期清理、兼容门禁、Auto review。
9
9
  - **动画修复(模块 4)**:删 `prefers-reduced-motion` 规则 + `matchMedia` 门控。
10
- - **回合改动补位卡(模块 5)**:用会话日志里的 `meta.diffs` 重建回合尾卡、探测式补位、`tool/ptc-dispatch` 的 turn 归属。
10
+ - **回合改动补位卡(模块 5)**:用会话日志里的 `meta.diffs` 重建回合尾卡、**必须实现 `buildLocationData` 才能被卡片读到**、补位条件是「官方那侧不会画」而非「探测到 404」、`tool/ptc-dispatch` 的 turn 归属。
11
+ - **改动卡数据/动作适配层(模块 5b)**:包 `window.fetch` 把日志重建的 summary/diff 喂给官方卡(坐标在渲染期登记);会话释放后 `/api/changes.open` 的「打开 / 显示位置 / 打开方式」由 host 复用官方 `dsh-native-command` 补位。
11
12
  - **窗口记忆(模块 6,仅 host 半区)**:helper + 预编译 DLL、宿主识别、编码坑、360 误报。
12
13
  - **立即重启(模块 7)**:三模式判定、会话运行态、蒙皮、弹窗定位。
13
14
  - **其余**:键盘与焦点、界面验证手法、迁移、待办、发布。
14
15
 
15
16
  ## 骨架(与 dsh-model-organizer 同构)
16
17
 
17
- - `lib/index.js`:host 半区五条回环路由:`GET /api/ui-tweaks/local-plugins`(本地来源/DSH 版本,含 `dsh` 字段)、`POST /api/ui-tweaks/normalize-excludes`(豁免表归并)、`POST precheck-exempt` / `POST force-exempt`(预检/写豁免)、`GET|POST /api/ui-tweaks/restart`(立即重启);加载时静默归并一次。
18
+ - `lib/index.js`:host 半区 8 条回环路由(统一前缀 `/api/ui-tweaks/`):`GET local-plugins`(本地来源清单 + DSH 版本)、`POST normalize-excludes`(豁免表归并)、`POST precheck-exempt` / `POST force-exempt`(预检 / 写豁免)、`POST schedule-exempt-cleanup`(豁免到期自动删)、`POST open-path`(打开 / 显示位置 / 用指定应用打开;`action:"probe"` 只诊断不弹窗)、`POST open-apps`(系统关联应用列表)、`GET|POST restart`(立即重启);加载时静默归并一次豁免表。
18
19
  - `lib/client.js`:浏览器半区,`module.exports = { name, apply, inject }`;`apply(ctx)` 由客户端 cordis 调用。
19
20
  - `cordis.patch.yml`:`- insert: [{id: ui-tweaks, name: '@nimoxie/dsh-ui-tweaks'}]`。**profile 必须声明 `dsh.bundle.patch`,否则启动报 declares no dsh.bundle**;**`insert.name` 必须写全 scope**。
20
21
  - `package.json`:`exports["./client"]` 指 client bundle;`dsh.client.platform: "web"`;inject 只需 `@deepseek-ai/dsh-api-remotes`(提供 `remote.*`);`react`/`react-dom` 是 shell 内置(PLATFORM_MODULES)。
@@ -55,11 +56,13 @@
55
56
  1. **精确版本 spec 装未成熟版本 → 成功**:pnpm 打印 `Added 1 entry to minimumReleaseAgeExclude in pnpm-workspace.yaml` 并自行写豁免表 → 0.1.5 的「写精确版本也救不了」只对**无污染 lockfile** 成立。
56
57
  2. **豁免表为空 + lockfile 含未成熟条目**:跑 `install`(无事可做)→ 开头校验、整体失败、**磁盘零改动**;**若安装有活要干,安装照常落地、门禁在结尾才报 exit 1**(T2b/T3c)→ 必须**先预检、拒绝在动手之前**;「先试一下看报什么错」会把 profile 留在「装上了但命令报错」的混乱态。
57
58
  3. **遮蔽语义确认**:`[陈旧条, 未成熟条]` 失败、`[未成熟条, 陈旧条]` 通过 → 写入必须把目标版本放**块内第一条**。
58
- - 实现:host 两条回环路由 `POST /api/ui-tweaks/precheck-exempt`(只读预检)与 `POST /api/ui-tweaks/force-exempt`;`forceExemptVersion` 只动豁免段落(目标插块首、其余字节逐字节保留、备份+renameSync+写后重读校验;**有意突破**归并器「结果恒为子集」);`precheckExempt` 只扫 **profile 直连依赖**(lockfile importers)+发布时刻 10 分钟缓存,列出「未生效豁免且仍在窗口内」的包(传递依赖不扫,漏判靠安装失败时的反应式报错兜底)。客户端三段式:入口 → 预检 → 拦路者清单/二次确认 → 写豁免 → 安装;`enqueue(name, { force: true })` 是唯一放行冷却行的入口。
59
+ - 实现:host 侧新增 `POST /api/ui-tweaks/precheck-exempt`(只读预检)与 `POST /api/ui-tweaks/force-exempt`;`forceExemptVersion` 只动豁免段落(目标插块首、其余字节逐字节保留、备份+renameSync+写后重读校验;**有意突破**归并器「结果恒为子集」);`precheckExempt` 只扫 **profile 直连依赖**(lockfile importers)+发布时刻 10 分钟缓存,列出「未生效豁免且仍在窗口内」的包(传递依赖不扫,漏判靠安装失败时的反应式报错兜底)。客户端三段式:入口 → 预检 → 拦路者清单/二次确认 → 写豁免 → 安装;`enqueue(name, { force: true })` 是唯一放行冷却行的入口。
59
60
  - 验证:host 隔离 15/15(写入位置、字节保留、去重、预检筛选、目标跳过);UI 渲染 9/9;真机路由 `precheck-exempt` 返回 `{ok:true,blocked:[]}`,`local-plugins` 正常。
60
61
  - **cooling 文案(0.1.6 定稿)**:列表默认只有**一句话**——`0.70.0 还需等待 10 小时 40 分才能更新(10-15 07:54 到期,点标签看详情)`。完整解释走 `coolingReasonText(detail, windowMs)`,**只在点「冷静期中」标签展开时渲染**(`.dtl`):为什么等(dsh 的 24 小时策略讲成人话)→ 硬装的后果(连坐整个 profile,故不提供直接安装)→ 到点自动恢复 → 末尾附原始诊断(预检=registry 记录,反应式=pnpm 报错)。等待期已过 / 结束时间未知各有文案。换行由 `.why` 的 `white-space:pre-wrap` 呈现。
61
62
  - **到期自动重查(0.1.6)**:`scheduleCoolingRefresh(name, matureAt)` 给 cooling 行排到点(+3s 缓冲)的 `checkOne(name)` 定时器(按包名去重);触发点=`applyCoolingGuess` 落 cooling 后、`judge` 冷却分支落 cooling 后;上限(约 24.8 天)之外不排。
62
63
  - **兼容门禁(0.1.7,借鉴 plugin-manager)**:新版 registry 清单可能声明最低 DSH 版本(`dsh.engines.dsh` → 顶层 `engines.dsh` → `peerDependencies["@deepseek-ai/dsh"]`);本机版本由 host 半区读 `profileContext.installAnchor` 的清单,兜底读 profile 里的 `@deepseek-ai/dsh`。低于要求 → 标 **「DSH 不符」**、不给点、不计入「全部更新」。**只认 `>=x.y.z`**;未声明、写法不认识(`^`/`~`)、读不到版本 → 一律不拦。
64
+ - **更新=卸载 + 安装(0.1.8)**:官方 `installBundle` 在**开着 HMR 的运行时**里会直接拒绝已装过的包 —— `@deepseek-ai/dsh-plugin-manager/lib/index.js` 的 `installBundle`:`if (this.ownerContext.get("hmr") !== void 0 && known !== void 0 && Object.hasOwn(before, known)) throw new ManagementFailure("already-installed")`;判据是「包名已在 profile 依赖里」,**与版本无关**(所以 0.1.7 前直接 `installBundle("name@新版本")` 的更新必然以 `already-installed` 收场)。官方工具说明本身就写着:换版本要 **先 `removeBundle` 再 `installBundle`**,官方 UI 也只有「装 / 卸 / 开关」、没有更新。对策 `installBundleEx()`:① 先直接装(没有 HMR 的运行时一步到位、不碰卸载);② 只有被 `already-installed` 拒绝时才 `removeBundle(包名)` → 再装;③ 卸载也被拒就**把原始拒绝原样报出**(不掩盖原因,也别白拆一次)。**失败回滚**:卸载成功过而结果落 `failed`/`cancelled` 时用原版本 `name@busyBefore` 装回去;`unknown` 不回滚(安装可能仍在飞,硬回滚会打架)改为在文案里提醒确认。错误码统一翻人话(`installCodeText`:already-installed / incompatible-version / not-removable / bundle-in-use / operation-error / stale-approval …),带错误码的拒绝不再走「回包丢了」的恢复逻辑。**两种形状都要认**(`isAlreadyInstalledResult`):① 远程 API 的拒绝信封 `{ok:false, error:{code}}`;② **桌面端进程内管理器把它报成「应用层失败」** —— `{ok:true, value:{application:"failed", error:{code:"already-installed"}}}`(`failureText` 取的就是 `st.error.code`,真机上报的正是这条,0.2.1 第一版只认①所以没生效)。测试:`installBundleEx` 四条路径(直接成功 / 拒绝信封转两步 / 别的码不卸载 / 卸载被拒)+ **失败信封形状触发两步** + 判定谓词六种输入 + 错误码表 + 回滚与「不再单步安装」的静态断言(client 60/60)。
65
+ - **0.2.1 补丁二:失败信封也会骗人 + 等卸载落地 + 退避重试**:① `resultFailure()` 统一判定"这次调用的结果到底成没成" —— **不能只看 `ok`**:桌面端把失败包成 `{ok:true, value:{application:"failed", error:{code}}}`,0.2.1 第一版用 `rm.ok !== false` 判"卸载成功",于是**卸载被拒被当成成功**、随后重装又失败,最终还假报"已回滚到 0.4.6"、把真原因(`not-removable` 之类)吞掉。现在失败信息会把"卸载被拒(X)"如实带出。② `removeBundle` 会触发一次 profile 重载;真机上"卸载刚返回就重装"仍被打回 `already-installed`(新请求可能还在上一代管理器里跑、读的是卸载前的清单 —— 而紧接着的回滚安装却成功,差别只在"中间隔了一次重载")。对策:`waitBundlesGone()` 轮询 `listBundles()` 直到清单里没有该包(10s 上限、400ms 间隔),再 `installRetrying()` 退避重试(3 次 × 1.5s);拿不到清单就当"没有",绝不把更新卡死。测试:新增"等清单消失 + 重试到成功"的调用序列断言、"卸载被拒不会再被当成功"、"一直没消失就到点返回 false"、`resultFailure` 六种输入(client 64/64)。
63
66
  - **「Auto review」提醒条(0.1.7,只读不改)**:DSH 0.2.0-rc.2 的已知缺陷 —— `auto` 模式的会话会让 `permissionPresets` 在热重载(装/更新插件就会触发)时构造失败(`pinInitialPermission` 构造期遍历已驻留会话,`autoAdmit` 还没注册就抛错),表现为装插件报一长串错、连带 `sessionProjections` 也起不来,要重启客户端才恢复。面板顶部加琥珀色提醒,**只读检测**「是否有驻留会话的 permissions 投影 currentValue === 'auto'」(读法同官方权限面板:`ctx.get('sessions')` → `list.getSnapshot().ids` → `binding(id).session.projections.faceOf('permissions').getSnapshot()`);**不写设置、不拦操作**。
64
67
  - **0.2.1-rc.2 复核(2026-10-18)**:预设标识仍是 `AUTO_PRESET = "auto"`,界面文案改叫「自动审查」,判定串 `currentValue === 'auto'` **不受影响**;旧缺陷的 throw 仍在,但 auto-review 集成新增卸载时的收尾迁移(先把所有 auto 会话改成 `danger-full-access` 再退出),触发窗口收窄、日志无该错误痕迹。该模式还有**调用前审查**:服务不可用或限流时工具调用**直接被拒、工具体不执行**(`503` 与 `429 请求过于频繁(limit 30)`)。
65
68
  - **本机环境**:dsh 用**自带的 pnpm 11.7.0**,**不是** PATH 上那个(PATH 是 11.21.0);排查必须用前者复现,否则结论会反过来。自带运行时在 **`<客户端安装目录>/resources/runtime/`**:`pnpm/`(11.7.0 包)、`bin/node`(shim,exec `$DSH_DESKTOP_NODE_EXECUTABLE`=客户端 Electron + `ELECTRON_RUN_AS_NODE=1`)、`versions.json`(node 24.18.1 / pnpm 11.7.0)。复现:`ELECTRON_RUN_AS_NODE=1 npm_config_minimum_release_age=1440 "<客户端>/DeepSeek Harness.exe" --expose-internals "<客户端>/resources/runtime/pnpm/bin/pnpm.cjs" add <pkg>@<ver> --registry=https://registry.npmjs.org/`。
@@ -77,11 +80,19 @@
77
80
  - **`waitForInstall` 不是进度轮询器**(0.1.3 的理解是错的,正是「更新永远超时」的成因):它只在**响应丢失后**恢复同一请求,请求不在活动中返回 `null` 且**已完成结果不保留**。装完再轮询只会空转到 240s 看门狗、丢掉真实结果。0.1.4 起只读 `res.value`,仅信封失败(`res.ok === false`)才恢复一次,仍 `null` 就报「未能确认」。
78
81
  - **终态判定读 `application`**:`applied` / `restart-required`(都算成功,后者提示重启)→ 回读版本;`cancelled` → 退回「有新版本」;`failed` → 标「更新失败」,把 `error.code`(译中文)+ `error.diagnostic` + `packageResult.kind/exitCode/output/logPath` + `pendingBuilds` + `failedAt` 写进行内 `why`(带 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` 时判 `cooling`,反推窗口记进会话复用);`overridden` → 可重试;未知值按「结果未知」。
79
82
  - **回读版本是唯一成功判据**:`listBundles()` 里读到该行版本且**与安装前不同**才判「已是最新」。只用 `pluginManager.listBundles()`(`pluginInventory` 只有 `list()` 没有 `listBundles`,会造成假阳性);清单短暂不可用时重试 3 次(1.5s)。基准 `busyBefore` 在 `startOne` 抓取。
80
- - **registry 比对在浏览器直连**:`registry.npmjs.org` 带 CORS *,逐包**串行** fetch `/latest`。代价:安装走 `.npmrc` 的 npmmirror,镜像未同步的新版本会「面板说有、装时报 no-matching-version」。
83
+ - **registry 比对在浏览器直连**:`registry.npmjs.org` 带 CORS *,逐包**串行** fetch `/latest`(0.2.2 起 12 秒超时,见下条)。代价:安装走 `.npmrc` 的 npmmirror,镜像未同步的新版本会「面板说有、装时报 no-matching-version」。
81
84
  - **取消要分清对象**(0.1.4 修):排队项取消只摘出自己(`cancelQueued`);在装项取消才 `cancelInstall(requestId)`,且**保留队列**,落定后 `done() → pump()`。旧实现 `requestCancel` 一句 `queue = []` 清空全队列,导致「等待更新」的项全退回「有新版本」——这是「取消一项后其余项不更新」的成因。回值 `{status: cancelled|not-running|too-late}`。
82
85
  - **重新检查必须回填在飞状态**(0.1.4 修):`checkNow` 重建 `rows` 会把「更新中/等待更新」抹成「有新版本」而 `busyName`/`queue` 还在,点那行撞上 `enqueue` 的 `name === busyName` 保护而**静默无反应**(「重新检查后无法更新」的成因)。修法:重建后立刻 `reconcileInstallStates()` 盖回 busy/queuing,这两种行不参与比对。
83
- - **总看门狗只是兜底**(0.1.4 起):正常路径一次 `installBundle` 定案,不必等 240s;只覆盖 fetchLatest(fetch 无超时)这类异常分支。
86
+ - **看门狗按「进度」而不是「总时长」判活**(0.2.2 修):旧实现固定 240 秒、从调用开始计时;真机一次重链 44 个包的卸载跑了 **5 分 08 秒**就被误判超时。更糟的是判超时后立刻 `pump()` 下一项、旧请求还在跑,两条 pnpm 叠在 profile 锁上——表现就是「全都卡住」。现在:`ctx.remote.$on("plugin-manager/install-log|install-state|changed")` 当心跳(拿不到就退回纯计时);无输出 90s 只在行内提示「仍在运行」;单项 15 分钟才判超时,判超时后仍等底层链收尾(最多再 5 分钟)才放行队列。放行闸是 `releaseBusy()`:`busyChainDone`(底层链收尾)+ `busyFinished`(终态已判定)同时成立,才清 busy 并 `pump()`。
87
+ - **unknown 终态必须复查并回滚**(0.2.2 加):判超时/结果未知且本次**中途卸载过**时,旧实现只写一句「请确认」,而后续「装回」那一步的结果会因为 `settled` 被丢弃 —— 真机上 `@linxin666/dsh-session-archive` 就是这样被卸掉后没装回来。现在 `recoverAfterUnknown()` 主动 `listBundles()` 复查:不在清单里就用 `busyBefore` 自动装回,并把结果写进行内文案。
88
+ - **registry / host 请求必须有超时**(0.2.2 加):`fetchLatest`(`/latest`)12 秒、host 来源名单 8 秒。比对是串行的,一个挂起的请求会让后面所有包永远停在「比对中 …」;`fetchLocalNames` 更靠前 —— 它不回调,整条比对链根本不会开始。
89
+ - **清单里「装了但读不到」的行要带原因**(0.2.2 加):`listBundles` 对读不到 `package.json` 的包仍会返回一行(带 `error`、没有 `version`),面板原来只显示「无法比对」。现在把 `error.code` / `availability: "missing"` 写进 `why`,悬停就能看出是文件缺失还是清单坏了。
90
+ - **总看门狗只是兜底**(0.1.4 起,0.2.2 重写):正常路径一次 `installBundle` 定案,不必等它;它只兜 fetch/网络这类异常分支。
84
91
  - **`enabled` 用清单真实状态**:传 `!(row.enabled === false)`,保留用户手动禁用(旧实现一律 `enabled: true`,会重新启用)。
92
+ - **更新完仍是关闭的插件给一键启用**(0.2.2 加):官方安装默认不启用(`installBundle` 的 `enabled` 默认 false),面板又刻意保留原状态,所以「原本关着的插件」更新完仍然关着。`finishOne` 回读清单时记下这次更新过、且 `enabled === false` 的名字(`updatedOff`),重启提示栏里长出一枚橙色按钮(`.fbtn.fgo`,与冷静期详情里的「强制更新」同款),点它**串行**调 `pluginManager.setBundleEnabled(name, true)`(官方插件页总开关同款 API);成功即清标记,失败写红字并留按钮可重试。只动本次更新过的包,不碰用户没更新的插件。
93
+ - **豁免表写 YAML 必须能过 pnpm 的解析器**(0.2.2 修):`minimumReleaseAgeExclude` 里的 scoped 包名不能做 plain scalar —— `@scope/pkg@1.2.3` 裸写会让整个 `pnpm-workspace.yaml` 解析失败(`bad indentation of a sequence entry`),后果是 profile 里**所有** pnpm 操作(安装、卸载)全部 `operation-error`,面板只会显示「卸载被拒(operation-error)」,看不出真因。现在新条目统一经 `yamlScalar()` 输出(仅必要时加单引号、内部单引号翻倍),归并器还会顺手修复历史上写坏的行;客户端在卸载被拒时把 `error.diagnostic` / `packageResult.output` 首行 + 日志路径带进失败文案。
94
+ - **强制更新的豁免要到期才删**(0.2.2 加):pnpm 每次安装都拿**整个 lockfile** 校验供应链策略,被强制装上的版本还没满 24h 时删掉豁免,之后所有 pnpm 操作都会被整体拒绝(比留着更糟)。所以客户端在写豁免成功后调 `POST /api/ui-tweaks/schedule-exempt-cleanup` 登记「成熟时刻 +60s」;host 侧 `dropExemptVersion()` 到点删表项,清单落盘在 `profile/ui-tweaks/exempt-pending.json`,客户端重启后按记录重排(已到期的立刻删);删失败保留记录、10 分钟后再试。
95
+ - **面板滚动条参照对话区那条自绘滚动条**(0.2.2 加):原生滚动条在 Windows 上会占列表 17px 宽,把行内容整体挤窄。面板列表隐藏原生条(`scrollbar-width:none` / `::-webkit-scrollbar{width:0}`),在 `.rowswrap` 右缘绝对定位 12px 轨道 + `.sbthumb`;视觉走 `.sbthumb::after`:`color-mix(in srgb,currentColor 36%,transparent)`,悬停/拖动加宽到 9px、60%(与模块 2 同一配方,颜色跟随主题文字色,不再靠容器 opacity)。**可拖动**:PointerCapture + 按下点偏移(不是拇指中心跳过来),点轨道先把拇指中心挪到点击处再顺势进入拖动;pointermove 同步写 `scrollTop`,`scroll` 事件回纠几何;轨道上滚轮转发给真容器。`paint()` 每次重画后 `bindSb()+syncSb()`(innerHTML 换过就重绑),resize/重排后补一次 `syncSb`。
85
96
  - **失败原因要可见**:`fail`/`unchanged`/`unconfirmed`/`error`/`cooling` 行下多渲染 `.why`(只放 `title` 悬停等于没显示)。0.1.5 起先 `stripAnsi`(CSI/OSC/控制字符/CRLF,否则首屏是 `鈺?` 残渣),`diagnostic` 与 `packageResult.output` 互相包含只留更完整的一份。
86
97
 
87
98
  ## 动画修复(模块 4)
@@ -100,12 +111,39 @@
100
111
  - `ctx.uiConversation.events.register({ kind, match, start, update })`:`match(event) → {id, role:"start"|"update"}`,`id` 只是分组键(官方用 `String(turn)`);`update(context, match)` 第一个参数带 `.state`,**`match.event` 才是事件**(把 match 当事件读会全空);返回值经 `requireState` 校验,**不能 undefined**。
101
112
  - 槽位:`ctx.slots.register({ name:"conversation.chat.turnTail", id, order, inject }, Component)` —— 契约 `kind:"list"`,现有 deliverables / plan(order 20) / schedule 三家,我们取 **order 30**。
102
113
  - 卡片 props:`{ turn, seq, openFile, sessionId, t, …hooks }`;**Definition 的状态用 `props.turn.data.get(kind)` 读**(deliverables 的 `changesForClosing` 就是这么取)。
114
+ - ⚠️ **那份状态必须由本 Definition 自己发布**:装配器只为实现了 **`buildLocationData(context, scope, previous)`** 的 Definition 发布 Turn 作用域数据(`dsh-client-ui-conversation` 的 `buildLocationData` → `replaceLocationData` / `applyDirtyLocationData` → `locationIndex` 的 `MutableLocationDataStore`),而 `turn.data.get(key)` 返回的正是发布项里的 `value`。发布项还必须满足 `data.kind === scope`、`data.key === 本 Definition 的 kind`(不符装配器**直接抛错**)、`data.turn` 为安全整数 —— 所以取不到 `turn` 时返回 `null` 而不是硬发。**0.2.0 漏了这一步:`state` 恒为 `null`,卡片一次都没画出来**。离线 harness 当时把 `turn.data.get` 直接桩掉了,所以 31/31 是假通过;0.2.1 起注册前显式校验 `changesDefinition().buildLocationData` 存在,否则 guard 报错。
115
+ - 顺带一个性能约定:`buildLocationData` 在状态未变时**返回入参 `previous` 本身**,装配器据此判定「未变化」而跳过重发布(否则每回合重绘一次卡片子树)。
103
116
  - **嵌套调用的 turn 归属**:`tool/ptc-dispatch`(run_code 里的 edit/write)**没有 turn 字段**(只有 `rootCallId/parentCallId/subCallId` + `meta.diffs`),用 `tool/call`(有 `turn`/`callId`)建 `rootCallId → turn` 映射挂回去。前端**收得到** ptc 事件(`dsh-client-ui-chat`/`tool`/`plan` 都在匹配它),否则本模块会漏掉绝大多数改动。
104
- - **探测式补位**:`GET api/changes.summary?sessionId=…&seq=<workspace/changes 的 seq>`(**文档相对**,无前导斜杠;官方 `changesSummaryUrl` 同形)。**200 → 不画**(让位官方卡);**只有 404 才画**;401/其它状态/网络异常一律不画 —— 宁可漏补,不可误补出双份。
117
+ - **补位条件(0.2.1 修)**:先判断「官方那侧会不会画」= `configForms.developerTools.enabled`(设置项**「显示编码工具视图」**,正是官方卡注入的 `showCodeDiff`)**且**该回合有 `workspace/changes` 公告(`seq` 非 null)。缺任一个 → 官方一定不画,**直接补位、不发探测**;两者都满足 → 才探测 `GET api/changes.summary?sessionId=…&seq=<公告 seq>`(**文档相对**,无前导斜杠;官方 `changesSummaryUrl` 同形),**200 → 不画**(让位官方卡),**只有 404 才画**,401/其它状态/网络异常一律不画 —— 宁可漏补,不可误补出双份。0.2.0 只看 404,于是「开关关着」时两边都不画(全空白)。开关 store 用 `ctx.inject(["configForms"], …)` 取,拿不到就按「可能开着」走探测(与 0.2.0 同行为,不会多出卡)。
105
118
  - **缓存策略**:探测结果只缓存在**本次连接**内,`connection/reset` 清空。官方自己永久缓存 missing(`{failed:"missing", retryable:()=>false}`),那会在宿主恢复或探测期误判后留下双份卡且**无法自愈**。
106
119
  - **同文件多次改动**:按 old→new 链折成一份净变化(后段 `oldText` 应等于前段 `newText`);链断则分段显示并分别计数,不报错。
107
120
  - **边界**:只覆盖文件工具;pwsh / npm 等造成的改动进不来(只有官方卡含)。已释放的旧会话**不需要回填** —— 日志在磁盘上就能立刻重建(这是选 A 而非自己持久化的最大理由)。
108
- - **验证**:离线 harness(`%TEMP%\dshuiw-changes-test\test-changes.cjs`,vm + React/DOM 桩)**31/31** —— Definition 聚合与 turn 归属、链式合并、折叠/展开渲染与计数、200 让位、401 不画、fetch 抛异常不崩、链断降级、服务缺失只 warn 不拖垮其它模块、React 缺失不注册槽位。**真机待验证**。
121
+ - **验证**:离线 harness(`%TEMP%\dshuiw-changes-test\test-changes.cjs`,vm + React/DOM 桩)**31/31** —— Definition 聚合与 turn 归属、链式合并、折叠/展开渲染与计数、200 让位、401 不画、fetch 抛异常不崩、链断降级、服务缺失只 warn 不拖垮其它模块、React 缺失不注册槽位。但该 harness **把 `turn.data.get` 桩掉了**,所以没暴露 0.2.0「卡片一次都没画出来」的缺陷;0.2.1 补上 `buildLocationData` 后另有 21/21(见文末 0.2.1 条),并已真机验证。
122
+
123
+ ## 官方改动卡数据适配层(模块 5b,0.2.1 起)
124
+
125
+ - **为什么**:官方那张卡的 summary/diff 只活在 Host 的 Session 内存里,会话一释放就 404,卡直接不画。而它只要 `response.ok` + 形状合法,**不问数据来源**(`HostReadStore.read` 就是一句 `fetch(url, { signal })`,deliverables client.js:208)。所以这一版不再画卡,改成**喂数据**:官方组件原生渲染,卡头 / hover 预览 / 右侧 Review 全是官方的。
126
+ - **怎么喂**:幂等包装 `window.fetch`,只认 `/api/changes.summary` 与 `/api/changes.diff`,且只对索引里存在的 `(sessionId, seq)` 回合成 200;其它请求一律 `original.apply(this, arguments)` 透传;`ctx.effect` 里 yield 卸载函数还原原 fetch。
127
+ - **时序(最容易踩)**:坐标必须在官方组件发请求**之前**进索引。官方在 `useEffect` 里发请求,而 React 先跑完所有 slot 的 render 再跑 effect —— 所以登记放在卡片**渲染期**(`registerChangesCoordinates`),放 effect 里就是竞态。
128
+ - **turn 从哪来**:Definition 发布的状态只有 `{seq, files}`,轮号在 slot props 的 `owner.turn.turn`(TurnLocation)。官方 validator 要求 `turn` 是 ≥1 整数,取不到就**不拦**——宁可不补,也不能发非法形状。
129
+ - **形状**:`summary = {turn, files:[{path,display,added,deleted}], total, added, deleted}`;`diff = {kind:"text", path, display, before, after, coarse, hunks:[{oldStart,oldLines,newStart,newLines,lines}]}`,hunk 按 3 行上下文切、行号 1 基、每行带 `+`/`-`/空格 前缀、单文件上限 2000 行。
130
+ - **口径差异(有意为之)**:官方是回合首末**净变化**,旧自画卡是**分段累加**(链断时按段计),同一轮数字会不同(实测 +16 −13 vs +2 −12)。要官方外观就走官方口径。
131
+ - **display**:按官方规范取相对 cwd 的路径;**家目录 `~` 归一化暂缺**(客户端拿不到 home),仓库外文件退化为绝对路径,只影响观感、不影响校验。
132
+ - **验证**:离线 harness `.dsh-tmp/check-adapter.mjs`(临时脚本,未入库)—— 真 `client.js` + 假 ModuleLoader/React/slots/sessions + **官方 4 个 validator**,16 项(summary/diff 形状、净变化计数、hunk 行号与上下文、index 越界透传、跨会话/跨 seq/无关路由不拦、卸载复原);真机已在旧会话 `session-11fed76a` turn 57(seq 3156)验证:官方卡与右侧 Review 均正常,旧卡自动让位。
133
+
134
+ ### 自画卡的定位(切片 2 之后)
135
+
136
+ - **只在两种情形画**:编码视图关、无公告。其余情形一律让官方卡渲染,本卡不抢。
137
+ - **骨架照官方**:60px 卡头 + 40×40 图标块(自绘 chevron-code SVG)+ 标题(单文件用 basename,多文件「已编辑 N 个文件」)+ 千分位统计;**多于 4 个文件**才出「全部 N 个」,点了只展开**列表**,绝不展开 diff(官方也只展开列表)。
138
+ - **类名不硬编码**:运行时从官方注入的 `<style data-plugin-css$="ChangedFiles.module.css">` 里正则取 hash 前缀(`\.([A-Za-z0-9_-]{4,12})_card`),再拼 `_card/_header/_list/_row/...`;读不到就退回内置近似样式 `CHANGES_FALLBACK_STYLES`(只影响观感,不影响结构)。
139
+ - **数据复用 5b 入口**:卡片直接读 `changesEntryFor(sid, seq)` 的 files(同一份净变化与 hunk),保证卡上的数字与喂给官方卡的那份永远一致,不另算一套。
140
+ - **点击 = 打开官方 Review**:`sidebarRight.openResource("dsh-resource://changes-review/session/<sid>/<seq>/<turn>", { params: { index } })`。**无公告的回合用合成坐标 `seq = 0`**,同时把 `sid:0` 登记进 5b 索引,Review 面板的取数就有着落。
141
+ - **动作补位(0.2.2)**:数据补上后 Review 能画 diff,但「显示文件位置 / 打开方式」走 `/api/changes.open`(GET 列应用 / POST 打开),它要先 `workspaceChanges.summary(sessionId, seq)` 才能把 `file.path` 解析成绝对路径 —— 会话释放即 404。适配层因此也拦这条:先走官方,只有 404 才补位(GET 交给 host 的 `open-apps`,POST 交给 `open-path`)。
142
+ - **补位实现**:host 从 `resources/app.asar/dsh/...` 动态 import 官方 `@deepseek-ai/dsh-native-command`,应用列表用 `nativeFileApplications()`,动作走 `openNativePath` / `revealNativePath` / `openNativeFileApplication`;取不到模块才退回自带 `nativeOpenCommand()`(`explorer` / `explorer /select,<file-uri>`,URI 编码照官方 `explorerTarget`)。`action:"probe"` 只让子进程 echo,用来分清「子进程起不来」和「窗口被隐藏」。
143
+ - **两条真机坑**:① 首次失败是 host 没重启(新路由未注册,回 401/404);② GUI 启动器带 `windowsHide:true` 时窗口会继承隐藏、看起来像「点了没反应」——官方对 GUI 用 `windowsHide:false`,照抄。应用列表按官方口径**只列真实应用**(主按钮=列表第一个应用),自造条目已删;枚举失败回空数组,控件退化成只有「显示文件位置」。
144
+ - **hover 迷你 diff**:500ms 延迟;浮层用 `position: fixed`(卡片带 `overflow:hidden`,绝对定位会被裁),只画 `+`/`-`/上下文行(官方预览里同样隐藏 hunk 头)。
145
+ - **小字规则**:无公告 → 卡内底部一行 11px 可见小字 + `title`;编码视图关 → 只走 `title`,不破坏与官方的一致性。
146
+ - **hover 预览与官方对齐**:行号单列(删除行给旧行号、新增行给新行号、上下文行给新行号)、改动行淡底色(绿/红 16%)、左侧 2px 色条、`white-space: pre` 不换行(长行横向滚动)、hunk 之间留 6px 分隔。
109
147
 
110
148
  ## 窗口记忆(模块 6,仅 host 半区)
111
149
 
@@ -184,6 +222,18 @@
184
222
  - [x] 0.1.7(2026-10-16,tag `v0.1.7`):立即重启(模块 7)、兼容门禁(`DSH 不符`)、按钮迁到「已安装」标题行、脚注。
185
223
  - [x] 0.1.8(2026-10-17 00:34,npm latest):弹窗定位实测外框(26px)、`room` 下限、重排判据;面板显示权归还「打开/收起」;蒙皮补 `inert` 与键盘滚动锁;控件补 `role/tabindex` + `Enter/Space` + 焦点环;README 补自带 CLI 例外。tag `v0.1.8`(`8aed2bc`)。
186
224
  - [x] 0.2.0(2026-10-18):回合改动补位卡(模块 5)—— 用会话日志里的 `meta.diffs` 重建回合尾卡、探测式补位(只在官方 404 时画)、`tool/ptc-dispatch` 的 turn 归属、缓存随连接重置。离线 31/31;**真机待验证**(刷新客户端 → 跑一轮改文件的对话 → 切走再切回 / 重启后重开,看卡是否仍在且不重复)。
225
+ - [x] 0.2.1(2026-10-20):① 修 0.2.0 补位卡「一次都没画出来」—— 补 `buildLocationData`(缺它则 `turn.data.get(kind)` 恒空)、注册前显式校验、状态未变时复用 `previous`。② 补位条件从「探测到 404」改为「官方那侧不会画」:`developerTools.enabled` 关 / 该回合无公告 → 直接补位且不探测;两者都满足才探测,非 404 不画。③「自动审查」提醒条文案 150+ 字压到 59 字,README 同段一并压缩。④ 新增**模块 5b 数据适配层**:包装 `window.fetch` 把日志重建的 summary/diff 顶给官方卡,已释放会话改由官方组件原生渲染(hover 预览、右侧 Review 都是官方的),自画卡自动让位。⑤ 自画卡只服务两种兜底(编码视图关 / 无公告):骨架照官方(60px 卡头、图标块、千分位统计、4 行折叠、hover 迷你 diff、点击开官方 Review),删掉内联展开与「第 N 段改动」文案,无公告用合成 `seq=0` 走官方 Review。⑥ hover 迷你 diff 补官方观感(行号列 / 改动行底色 / 左侧色条 / 不换行横向滚动);清掉切片后失效的 4 个死函数(`shortPath` / `foldParts` / `countParts` / `DIFF_ROW_LIMIT`)。⑦ 验证:离线 harness **21/21**(`check-ui-tweaks-changes.mjs`)+ **16/16**(`check-adapter.mjs`,两个脚本都在 `.dsh-tmp/`,未入库);真机通过(旧会话 `session-11fed76a` turn 57:官方卡 + 右侧 Review 正常,无双份)。
226
+ - [x] 0.2.2(2026-10-22):修「全部更新全都卡住 / genui 无法比对」,顺带收掉更新与改动卡的边角。
227
+ - 看门狗改按「进度」判活:`install-log` / `install-state` / `changed` 当心跳,无输出 90s 只提示,单项 15 分钟才判超时;判超时也等底层链收尾(`releaseBusy()` 双闸)才放行队列。
228
+ - `unknown` 且中途卸载过 → `recoverAfterUnknown()` 复查清单并用原版本装回。
229
+ - `fetchLatest` 12s / `fetchLocalNames` 8s 超时,杜绝「比对中 …」永久挂起;`listBundles` 无版本的行显示 `error.code` / `availability: missing`。
230
+ - 真机现场:profile 曾被中断的 pnpm 操作掏空 44 个包,一次重链跑了 5 分 08 秒;`@linxin666/dsh-session-archive` 被卸后未装回,已手动恢复 0.4.8。
231
+ - 更新成功的关闭插件:重启提示栏出现「启用已更新的 N 个插件」,串行 `setBundleEnabled(name, true)`。
232
+ - `hostPost` / `hostGet` 全部带超时(force-exempt 超时自动重试一次,写豁免是幂等的),不再永久停在「正在写入豁免表…」。
233
+ - `yamlScalar()`:scoped 包名写豁免表必须加引号,否则 `pnpm-workspace.yaml` 解析失败、profile 里所有 pnpm 操作被拒;归并器顺手修历史坏行,卸载失败文案带上 pnpm 报错首行与日志路径。
234
+ - 强制更新的豁免改为**到期自动清理**(`schedule-exempt-cleanup` + `dropExemptVersion` + `profile/ui-tweaks/exempt-pending.json` 落盘重排)。
235
+ - 更新面板列表滚动条改**浮层拇指**(隐藏原生条、可拖动、不占行内宽)。
236
+ - 改动卡动作补位:`/api/changes.open` 404 时由 host 复用官方 `dsh-native-command` 完成打开 / 显示位置 / 用指定应用打开;应用列表只列真实应用(主按钮=第一个应用)。
187
237
  - [ ] `shell` 模式重启不留日志(`ui-tweaks-restart.log` 只有 `relaunch` 分支写),要硬证据就在 `performRestart` 退出前追加一行;「点按钮 → 宿主退出」目前只有 `crash-*-host.log` 作间接证据。
188
238
  - [ ] 窗口记忆「零闪现」只能靠常驻 watcher 进程(代价:常驻 + 卸载残留),**待与用户确认**。
189
239
  - [ ] cooling 预检更准:现在只扫 importers 直连依赖,传递依赖靠反应式报错兜底;要覆盖全量 lockfile 得有更狠的缓存/批量策略。
package/README.md CHANGED
@@ -2,37 +2,37 @@
2
2
 
3
3
  给 DSH(DeepSeek Harness)的 Web UI 的一批界面微调:装上即生效,无需配置。
4
4
 
5
- 官方界面上有几处用得难受的地方:宽表格会撑出对话区右沿、滚动条贴在屏幕最右端要横跨半屏去够、Windows 关闭「动画效果」时前端动画被系统设置一并关掉、插件版本更新只能去插件管理页挨个看。这个插件把它们一次性修掉,另附一项桌面客户端的窗口记忆。
5
+ 官方界面有几处用得难受的地方:宽表格会撑出对话区右沿、滚动条贴在屏幕最右端要横跨半屏去够、Windows 关闭「动画效果」时前端动画被系统设置一并关掉、会话释放后回合尾的改动卡会消失、插件版本更新只能去插件管理页挨个看。这个插件把它们一次性修掉,另附桌面客户端的窗口记忆。
6
6
 
7
7
  ## 功能
8
8
 
9
9
  **宽表格越界修复**
10
- - 宽表格(≥4 列)不再撑出对话区,收敛回正文列并支持横向滚动,观察沿裁掉的问题消失。
10
+ - ≥4 列的宽表格不再撑出对话区,收敛回正文列并支持横向滚动。
11
11
 
12
12
  **对话区左侧滚动条**
13
- - 在右侧宽度调整条的左边放一条自绘滚动条:拖拽、点轨道翻页、滚轮转动手感与原生一致,主题跟随明暗。
14
- - 只在会话页出现,切到插件页等非会话页自动移除。
13
+ - 在右侧宽度调整条的左边放一条自绘滚动条:拖拽、点轨道翻页、滚轮手感与原生一致,主题跟随明暗,只在会话页出现。
15
14
 
16
- **回合尾「本回合改动」补位**
17
- - 官方那张「本回合改动」卡的数据只活在会话内存里,会话一被释放(切走、重启客户端、重开旧会话)就变成「这轮改动的内容已不可用」。本插件在同一位置补一张**从会话日志重建**的同名卡,所以**重启客户端、重开旧会话后它依然在**,逐行 diff 也点得开。
18
- - **只在官方那张已经失效时才出现**:官方数据还在时(会话活着)完全不画,避免出现两张重复的卡。
19
- - **口径**:只覆盖**文件工具**(写/改文件)造成的改动,同一文件在一个回合里改多次会按前后文链成一份净变化;用脚本、`npm install` 之类改出来的内容不在此列(那只有官方卡含)。卡片底部有一行小字标明口径。
15
+ **回合尾改动卡**
16
+ - 官方「本回合改动」卡在会话释放后(切走会话、重启客户端、重开旧会话)原本会消失;本插件用会话日志里的文件改动数据补上,卡片仍由**官方组件原样渲染**——卡头、统计、4 行折叠、hover 预览、右侧「改动审查」都与活会话里一致。
17
+ - 官方本来就不会画的两种情形(关着「显示编码工具视图」、该回合没有官方改动记录)才由插件画一张同款,两种情形都不会与官方卡重复。
18
+ - 口径:只含**文件工具**(写/改文件)造成的改动;数字按回合首末的**净变化**计。
19
+ - 会话释放后卡片仍能打开官方 Review;Review 里的「打开 / 显示文件位置 / 打开方式」由插件交给 Host 桌面执行(会话活着时仍走官方原路)。
20
+ - 打开方式列表照官方:只列系统关联里的真实应用,主按钮=列表第一个应用,菜单底部固定「显示文件位置」;枚举不到时只剩「显示文件位置」。
20
21
 
21
22
  **动画修复**
22
- - Windows 关闭「动画效果」时 Chromium 会向网页报告 `prefers-reduced-motion: reduce`,本插件清除因此失效的前端动画规则,让动画恢复;web 与桌面客户端都生效。
23
+ - Windows 关闭「动画效果」时,清除因此失效的界面动画;web 与桌面客户端都生效。
23
24
 
24
25
  **插件更新检查**
25
- - 插件管理页「已安装」标题那一行的右侧多两枚按钮:「检查更新」比对已装插件与 npm registry 的版本,有新版本可点上就地更新,支持全部更新与中途取消(不检查 dsh 本体与官方 `@deepseek-ai` 插件——它们的更新走 dsh 本体流程)。
26
- - 新版本还没过 pnpm 的发布时长窗口(默认 24 小时)时,该行标 **「冷静期中」** 并给出可更新时间,到点自动变回可更新;确需立刻安装,可在详情里走「强制更新」,它会先检查这次安装会不会被别的包连累拦下。
27
- - 新版声明了更高的 DSH 最低版本时,该行标 **「DSH 不符」**(鼠标悬停显示「需要 DSH ≥ x」),不给点——省得装上去起不来。
28
- - 更新成功后,面板顶部常驻「重启客户端后生效」。旁边的「**立即重启**」会先看有没有会话在跑:**有**就列出来、说明会被中断,**没有**则说明不会打断任何任务;**任何情况都要你点一下「重启 dsh」才会执行**。桌面端要两步 —— dsh 先退出,再由客户端弹出官方的「应用无法启动或已意外停止」对话框,**在那里点「重启」才真正拉起来**(也可手动退出后重开)。点下去后**整页会被蒙住**(此刻客户端已不可用);20 秒没退成就自动撤掉、提示你刷新页面或手动重启。两个面板都**不可拖动**,位置只有一个来源:贴在触发它的那枚按钮旁,下方放不下就翻到上方。
29
- - 两个弹窗都能**纯键盘操作**:Tab 逐枚走过「重新检查 / 全部更新 / ×」和每一行的标签,**回车或空格**即触发(与鼠标走同一套逻辑,不存在两套行为),**Esc** 收起;焦点环用官方主题令牌,与原生控件同色同宽;收起后焦点回到触发它的那枚按钮。
30
- - 安装落地后会自动清理 profile 里会让后续所有安装都失败的豁免表冗余(后台静默完成,无需你操作)。
26
+ - 插件管理页「已安装」标题行右侧多一组按钮:检查更新、逐个更新、全部更新与取消(不检查 dsh 本体与官方 `@deepseek-ai` 插件)。
27
+ - 未过 pnpm 发布冷静期的新版本标「冷静期中」并给出可更新时间,到点自动恢复;确需立刻安装可在详情里走「强制更新」。声明了更高 DSH 版本的标「DSH 不符」。两者默认都不给装。
28
+ - 「强制更新」写进豁免表的那条会在该版本满 24 小时后自动删掉;不能装完立刻删——pnpm 每次安装都校验整个 lockfile,未成熟时删豁免会让之后所有 pnpm 操作被整体拒绝。
29
+ - 更新成功的插件会在顶部提示「重启客户端后生效」;「立即重启」会先告诉你有没有会话在跑,**任何情况都要你点确认**才会执行。
30
+ - 面板与弹窗支持纯键盘操作(Tab / 回车 / 空格 / Esc),焦点环与官方控件一致。
31
+ - 列表滚动条是**浮层拇指**(与对话区那条自绘滚动条同款):不占行内宽度(Windows 原生滚动条会吃掉 17px),颜色跟随主题文字色;可以直接用鼠标拖动它,点轨道会把拇指跳过去。
31
32
 
32
33
  **窗口记忆(仅桌面客户端)**
33
- - 官方客户端每次启动都是固定的 1280×820 居中小窗,本插件让它记住上次的窗口大小、位置和最大化状态,下次启动自动还原。
34
- - 不修改官方客户端任何文件,卸载即完全还原官方行为;浏览器版不动作。
35
- - 状态存在 profile 的 `ui-tweaks/window-state.json`,删掉即重置记忆。
34
+ - 记住上次的窗口大小、位置与最大化状态,下次启动自动还原;不修改官方任何文件,卸载即恢复。
35
+ - 状态存在 profile 的 `ui-tweaks/window-state.json`,删掉即重置。
36
36
 
37
37
  ## 安装
38
38
 
@@ -68,85 +68,55 @@ dsh plugin --profile web add link:/absolute/path/to/dsh-ui-tweaks
68
68
 
69
69
  > **要求**:dsh `>= 0.2.0-rc.1`(需要 `remote.pluginManager` / `remote.pluginInventory` 接口)。
70
70
  >
71
- > 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(浏览器可能还在用缓存的旧版本)。
71
+ > 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(浏览器可能还在用缓存的旧版本)。
72
+ >
73
+ > 刷新只换前端代码;插件的 host 半区(更新检查、改动卡打开文件等走的本机路由)要**完全退出客户端再打开**才会换。
72
74
 
73
75
  ## 已知限制
74
76
 
75
77
  - 非 npm 来源的插件没有可比的 registry 版本,直接跳过比对:`link:` / `file:` 本地路径标「本地链接」,`git:` / `github:` / `http` 直装标「Git 直装」(例:`github:Fisfzy/dsh-ego-browser` 装的是仓库源码,与 npm 上的同名包不是一回事)。
76
- - 更新失败的行会直接给出原因(pnpm 输出与日志位置),点它可重试;取消只作用于你点的那一项,后续排队的会接着装。
77
- - 面板比对走 npm 官方 registry,安装走你本机 pnpm 配置的源(如 npmmirror 镜像);镜像尚未同步的新版本会出现「面板说有新版、装时报找不到版本」,稍后重试即可。
78
- - 会话正在用「**自动审查**」(Auto review)访问模式时,更新面板顶部会出现一条琥珀色提醒:该模式下装/更新插件有时会失败、要重启客户端才能恢复;而且它对每次工具调用会先审查一遍,**审查服务不可用或限流时这次调用会被直接拒绝、工具体不执行**(`503` / `429 请求过于频繁`),表现为助手突然什么都做不了。按提醒把该会话切成「工作区内修改」、装完再切回即可。本插件**只提示,不改动你的权限设置**。
79
- - 「本回合改动」补位卡的口径见上文(只含文件工具的改动);它每个回合会向官方接口探一次「官方数据还在吗」,探测结果**只在本次连接内有效**(刷新页面或重连后重新判断)——这是为了防止宿主恢复数据后出现双份卡。
80
- - 「DSH 不符」只在能确认版本时生效:本机 DSH 版本读不到、或新版写的是 `^`/`~` 这类无法判定的范围,就不拦(宁可漏拦,不误伤正常更新)。
78
+ - 更新一个**已经装过**的插件时会先卸载再装回:DSH 安装接口在有热重载时不允许原地覆盖已装的包。中途失败会自动回滚到原版本,失败原因(pnpm 输出与日志位置)写在该行,点它可重试。一次要重链几十个包时 pnpm 可能**安静地跑好几分钟**:行内会显示「pnpm 仍在运行 …」,不会提前判超时;只有超过 15 分钟才判超时,且如果这次中途卸载过,会自动复查清单并在必要时把旧版本装回去。
79
+ - 安装/更新默认**不会自动启用**插件(DSH 的安装与启用是两份状态)。如果这次更新过的插件装完仍是关闭的,顶部绿色「已更新 N 个插件」提示栏里会多一枚橙色按钮「**启用已更新的 N 个插件**」(和冷静期详情里的「强制更新」同款样式):点一下就会逐个把这次更新过的插件打开;有失败会在栏内标红并可再点一次。它只动这次更新过的插件,不会碰你没更新的。
80
+ - 桌面端点「重启 dsh」后 dsh 会先退出,再由客户端弹出官方的「应用无法启动或已意外停止」对话框,**在那里点「重启」才真正拉起来**(也可手动退出后重开)。
81
+ - 取消只作用于你点的那一项,后续排队项会继续装;面板比对走 npm 官方 registry,安装走你本机 pnpm 配置的源(如 npmmirror 镜像),镜像尚未同步的新版本会出现「面板说有新版、装时报找不到版本」,稍后重试即可。
82
+ - 「本回合改动」卡只含文件工具的改动,且按官方净变化口径计数;「官方那侧会不会画」的判断结果**只在本次连接内有效**(刷新页面或重连后重新判断),用于避免宿主恢复数据后出现双份卡。
83
+ - 「DSH 不符」只在能确认版本时生效:本机 DSH 版本读不到、或新版写的是 `^` / `~` 这类无法判定的范围,就不拦(宁可漏拦,不误伤正常更新)。
84
+ - 会话在用「自动审查」时,更新面板顶部会有琥珀色提醒:该模式下装/更新插件可能失败,工具调用也可能被审查拦掉(`503` / `429`)。按提醒切成「工作区内修改」,装完再切回即可。本插件**只提示,不改动你的权限设置**。
81
85
  - 与启动器 dsh-web-shell 的分工:界面增强已由本插件接管;dsh-web-shell 的「检查更新」按钮只负责 **dsh 本体**的比对与升级提示。
82
- - 窗口记忆的归位发生在窗口显示后约 0.2~1 秒内,会有一帧跳变(官方客户端在插件加载之前就把窗口显示出来了,没法再早);窗口最小化不会中断记忆。
86
+ - 窗口记忆的归位发生在窗口显示后约 0.2~1 秒内,会有一帧跳变(官方客户端在插件加载之前就把窗口显示出来了);窗口最小化不会中断记忆。
83
87
  - 个别插件(如社区插件管理页扩展)自带单独的「检查更新」入口,与本插件的检查互不关联,结果可能不同。
84
- - **360 提示「PowerShell 执行攻击命令」是误报**:桌面端的窗口记忆需要在 dsh 进程外跑一个隐藏的 PowerShell 小助手,360 对「非系统程序创建 powershell.exe」一律按高危拦截,选「允许」即可,功能不受影响。这不是本插件独有的问题 —— dsh 官方桌面版每次启动自己也会创建隐藏 PowerShell(采集系统信息、提取图标),所以关掉窗口记忆也消不掉这个弹窗。
88
+ - **360 提示「PowerShell 执行攻击命令」是误报**:桌面端的窗口记忆需要在 dsh 进程外跑一个隐藏的 PowerShell 小助手,360 对「非系统程序创建 powershell.exe」一律按高危拦截,选「允许」即可。dsh 官方桌面版每次启动自己也会创建隐藏 PowerShell,所以关掉窗口记忆也消不掉这个弹窗。
85
89
 
86
90
  ## 本人常用插件(可选)
87
91
 
88
92
  > 本插件不会自动安装任何其他插件,插件也不属于必装项——不装同样能正常使用上述全部功能。
89
93
 
90
- 浏览器端操作:
91
-
92
94
  ```sh
93
- # 安装
95
+ # 浏览器版
94
96
  dsh plugin --profile web add <PluginName>
95
-
96
- # 卸载
97
97
  dsh plugin --profile web remove <PluginName>
98
+ dsh --profile web --dump-config | findstr /i <PluginName子串> # 查看
98
99
 
99
- # 查看
100
- dsh --profile web --dump-config | findstr /i <PluginName子串>
101
- ```
102
-
103
- 桌面客户端:PATH 上的 `dsh` 装不了(桌面 profile 被保留给客户端),但客户端自带的那份 CLI 可以 —— 把上面的 `dsh plugin --profile web` 换成:
104
-
105
- ```sh
100
+ # 桌面客户端(PATH 上的 dsh 装不了,用客户端自带的那份)
106
101
  "<安装目录>\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add <PluginName>
107
102
  ```
108
103
 
109
- 也可以直接在客户端「插件管理」中进行安装、卸载和查看。
110
-
111
- 常用插件(web 与桌面端都能用):
112
-
113
- ```sh
114
- # @changfenhuang/dsh-genui:生成式消息回复
115
- dsh plugin --profile web add @changfenhuang/dsh-genui
116
-
117
- # @linxin666/dsh-session-archive:会话归档与批量清理
118
- dsh plugin --profile web add @linxin666/dsh-session-archive
119
-
120
- # @liustack/modsearch:联网搜索
121
- dsh plugin --profile web add @liustack/modsearch
122
-
123
- # @michengai/dsh-skills-manager:技能加载与管理
124
- dsh plugin --profile web add @michengai/dsh-skills-manager
125
-
126
- # @nimoxie/dsh-model-organizer:模型显示管理
127
- dsh plugin --profile web add @nimoxie/dsh-model-organizer
128
-
129
- # @nimoxie/dsh-ui-tweaks:界面优化调整
130
- dsh plugin --profile web add @nimoxie/dsh-ui-tweaks
131
-
132
- # cc-safety-net:危险命令与密钥防护
133
- dsh plugin --profile web add cc-safety-net
134
-
135
- # dsh-context:上下文管理
136
- dsh plugin --profile web add dsh-context
137
-
138
- # dsh-cost-meter:会话费用统计
139
- dsh plugin --profile web add dsh-cost-meter
140
-
141
- # dsh-ego-browser:模拟浏览器点击
142
- dsh plugin --profile web add dsh-ego-browser
143
-
144
- # dsh-ops:bash / PowerShell 7 与 Rust 高性能工具
145
- dsh plugin --profile web add dsh-ops
146
-
147
- # dsh-whale-widget:右下角小鲸鱼挂件
148
- dsh plugin --profile web add dsh-whale-widget
149
- ```
104
+ 也可以直接在客户端「插件管理」中安装、卸载和查看。常用插件(web 与桌面端都能用):
105
+
106
+ | 插件 | 用途 |
107
+ | --- | --- |
108
+ | `@changfenhuang/dsh-genui` | 生成式消息回复 |
109
+ | `@linxin666/dsh-session-archive` | 会话归档与批量清理 |
110
+ | `@liustack/modsearch` | 联网搜索 |
111
+ | `@michengai/dsh-skills-manager` | 技能加载与管理 |
112
+ | `@nimoxie/dsh-model-organizer` | 模型显示管理 |
113
+ | `@nimoxie/dsh-ui-tweaks` | 界面优化调整(本插件) |
114
+ | `cc-safety-net` | 危险命令与密钥防护 |
115
+ | `dsh-context` | 上下文管理 |
116
+ | `dsh-cost-meter` | 会话费用统计 |
117
+ | `dsh-ego-browser` | 模拟浏览器点击 |
118
+ | `dsh-ops` | bash / PowerShell 7 与 Rust 高性能工具 |
119
+ | `dsh-whale-widget` | 右下角小鲸鱼挂件 |
150
120
 
151
121
  ## 许可
152
122