@nimoxie/dsh-ui-tweaks 0.1.0 → 0.1.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/DEVELOPING.md CHANGED
@@ -5,68 +5,84 @@
5
5
  ## 骨架(与 dsh-model-organizer 同构)
6
6
 
7
7
  - `lib/index.js`:host 半区,空 `apply()`,只为让插件出现在 cordis loader / bundle 列表。
8
- - `lib/client.js`:浏览器半区,`window.__ModuleLoader__.load({ id, factory })` 包裹,`module.exports = { name, apply, inject }`;`apply(ctx)` 由客户端 cordis 上下文调用。
9
- - `cordis.patch.yml`:`- insert: [{id: ui-tweaks, name: '@nimoxie/dsh-ui-tweaks'}]`。**profile bundle 必须声明 `dsh.bundle.patch`,否则 dsh 启动即报 declares no dsh.bundle**。
10
- - `package.json`:`exports["./client"]` 指向 client bundle;`dsh.client.platform: "web"`;`dsh.client.inject` 列出需要的官方包(本插件只要 `@deepseek-ai/dsh-api-remotes`,`remote.*` 服务由它提供)。
11
- - 客户端可用模块:`react` / `react-dom` / `@deepseek-ai/dsh-client-ui-primitives` 都是 shell 内置(PLATFORM_MODULES),不需要 `external` 声明。本插件 v1 没用 React。
8
+ - `lib/client.js`:浏览器半区,`module.exports = { name, apply, inject }`;`apply(ctx)` 由客户端 cordis 上下文调用。
9
+ - `cordis.patch.yml`:`- insert: [{id: ui-tweaks, name: '@nimoxie/dsh-ui-tweaks'}]`。**profile 必须声明 `dsh.bundle.patch`,否则 dsh 启动即报 declares no dsh.bundle**;**insert.name 必须写全 scope**(loader 按它在 node_modules 解析包)。
10
+ - `package.json`:`exports["./client"]` 指向 client bundle;`dsh.client.platform: "web"`;inject 只需 `@deepseek-ai/dsh-api-remotes`(`remote.*` 服务由它提供)。`react` / `react-dom` 等是 shell 内置(PLATFORM_MODULES),不用声明。
11
+ - loader `id` 与 `exports.name` 只是内部标识,可以不跟包名走(换名也不用改);真正要同步四处的是 package.json、cordis.patch.yml、两个 profile 的依赖键 + `dsh.profile.bundles`、(桌面 profile 还有 lockfile 与 node_modules 软链路径)。
12
12
 
13
- ### 命名(scoped 后的约定,2026-09-30 因重名加 @nimoxie scope)
13
+ ## 本地开发
14
14
 
15
- - npm 包名 `@nimoxie/dsh-ui-tweaks`;**patch 里的 `insert.name` 必须写全 scope** —— `@linxin666` 系插件全是这样(loader 按它在 node_modules 里解析包)。
16
- - loader `id` 与 `module.exports.name` 只是内部标识,换名不必跟着改(dsh-model-organizer 改 scoped 后这俩仍是旧值,工作正常);本插件 loader id 用了全名,`exports.name` 用 `ui-tweaks`。
17
- - 换名要同步四处:package.json、cordis.patch.yml、两个 profile 的依赖键 + `dsh.profile.bundles` 数组、(桌面 profile 还有 lockfile 与 node_modules 软链路径)。
18
-
19
- ## 本地开发(link: 才是软链!)
20
-
21
- - **`file:` 依赖是打包快照,`link:` 才是符号链接。** profile 里写 `file:` 时 pnpm 把目录复制进 store,改仓库不生效。
15
+ - **`file:` 是打包快照,`link:` 才是符号链接**:写 `file:` 时 pnpm 把目录复制进 store,改仓库不生效。
22
16
  - 安装/重装:
23
17
  ```sh
24
18
  dsh plugin --profile web remove @nimoxie/dsh-ui-tweaks
25
- dsh plugin --profile web add "link:D:/tools/DeepSeek Harness/dsh-ui-tweaks"
19
+ dsh plugin --profile web add "link:<本仓库绝对路径>"
26
20
  ```
27
- - 改完 client.js 后页面要 **Ctrl+Shift+R**:客户端 bundle 以 `Cache-Control: immutable, max-age=31536000` 下发且 URL 不随代码变化,普通刷新一直吃缓存。
28
- - 加载/卸载插件需要**重启 dsh web**(插件在启动时装载)。
29
- - **桌面 profile 由 Electron 应用独占管理**:npm 版 CLI 装/卸会被拒(`managed exclusively by the Electron application`)。两条路:① 客户端插件管理页安装,spec 填 `link:<路径>`;② 应用完全退出后手动改四件套(package.json 依赖 + `dsh.profile.bundles`、pnpm-lock importers、node_modules 软链)。实测客户端会监听 package.json 变化、自动用自带 pnpm 建好软链。
21
+ - 改完 client.js 后页面要 **Ctrl+Shift+R**(bundle 以 `Cache-Control: immutable` 下发且 URL 不随代码变化,普通刷新一直吃缓存)。加载/卸载插件需要**重启 dsh web**。
22
+ - **桌面 profile 由 Electron 独占管理**:npm 版 CLI 装/卸会被拒(对它的报错是 `managed exclusively by the Electron application`)。两条路:① 客户端插件管理页安装,spec 填 `link:<路径>`;② 应用完全退出后手动改四件套(package.json 依赖 + `dsh.profile.bundles`、pnpm-lock importers、node_modules 软链)。客户端会监听 package.json 变化、自动用自带 pnpm 建好软链。
30
23
 
31
- ## 已知坑(多半继承自 dsh-model-organizer 的踩坑记录)
24
+ ## 已知坑
32
25
 
33
26
  - **`ctx.inject([...])` 只要有一个服务名不存在,回调永远不执行且不报错**。升级 dsh 后功能静默消失,第一件事去新版 `dsh-client-ui-*` 包里 grep 注入的每个服务名。本插件在 8 秒拿不到服务时会给出明确的错误行,不会无限等。
34
27
  - 每个模块的注册各套 `guard()`:失败记 `console.warn`,不拖垮同文件其它模块。
35
- - 官方 CSS-module 类名是「哈希_原名」结构,锚定一律用**属性前缀**(`[class*="tableScroll"]`、`[class*="widthHandle"]`、`[class*="composerSeat"]`),绝不硬编码 hash。
28
+ - 官方 CSS-module 类名是「哈希_原名」结构,锚定一律用**属性前缀**(`[class*="tableScroll"]`、`[class*="widthHandle"]`、`[class*="composerSeat"]`),绝不硬编码 hash——官方 hash 版本间会变。
36
29
  - **0.2.0-rc.1 已把 0.1.x 的 `data-dsh-part` 标记全部移除**(滚动条/调宽条/输入框的选择器都因此改过一轮);`[data-slot="..."]` 是渲染器统一输出的,锚插槽容器用它。
37
- - 浮层挂在 `document.body` 的 Shadow DOM 里:座位在页面深子树里时 `position:fixed` 会被困在堆叠上下文,z-index 再高也没用。
30
+ - 浮层挂在 Shadow DOM 里时 `position: fixed` 会被困在深层的堆叠上下文,z-index 再高也没用——面板浮层挂在 body 的 shadow root 上规避。
31
+
32
+ ## 更新检查(模块 3)
38
33
 
39
- ## 更新检查的实现要点
34
+ ### 关键经验:注解必须回读,而且 pnpm 的 minimumReleaseAge 是主要的静止源
40
35
 
41
- - **全部走官方 remote API**,不自建 host 路由:`pluginManager.listBundles()`(清单+版本)、`installBundle("<名>@latest", { enabled: true, requestId })`(更新)、`waitForInstall(requestId)`(进度)、`cancelInstall(requestId)`(取消)。`installBundle` 的第二参里官方传了 `registry`(来自 `pluginManager.registries()`),本插件省略 —— 若某天报 registry 缺失,先补这个。
42
- - **registry 比对在浏览器直连**:`registry.npmjs.org` 带 `Access-Control-Allow-Origin: *`(2026-09 实测),逐包串行 fetch `/latest`,不给 registry 打并发。
43
- - **装完必须回读实际版本**:pnpm 11 的 `minimumReleaseAge` 门禁会静默暂缓当日发布,只看安装请求成功会把「没装上」报成「成功」。回读**只用 `pluginManager.listBundles()`** —— `pluginInventory` 只有 `list()` 没有 `listBundles`,先取它会掉进「回读版本失败」假阳性(踩过);装完瞬间清单可能短暂不可用,重试 3 次(1.5s 间隔)再下结论。重试后仍读不到就标「已发起·未确认」(可点重查),版本没变才标「暂缓更新」。
44
- - **本地包识别 = host 路由为主,404 签名兜底**:`listBundles()` 不透传安装来源(`installed` 不是 spec 字符串),而发布到 npm 的本地包(如 `@nimoxie/dsh-model-organizer`)连 404 签名都不命中 —— 所以 host 半区挂了 `GET /api/ui-tweaks/local-plugins`(读 profile `package.json` 的 dependencies,把 `link:/file:/git+/http` 声明的包名回给面板;仅回环放行)。client 每轮检查先取这份名单标「本地链接」,取不到(host 半区未重启)再退回 404 签名。**改 host 半区必须重启 dsh/客户端才生效**(client bundle 刷新即可)。
36
+ - **更新用 `name@latest` 的教训**:pnpm 11 的 `minimumReleaseAge`(profile 的 `pnpm-workspace.yaml`,本机 1440min=24h)会**静默**跳过发布不足 24h 的新版本——区级安装请求会成功、但它解析到「最新合法版本」而**不是**时候刚发布的版本,装上后版本没变,面板才改判「暂缓」。这不是失败,是 pnpm 的窗口保护,不报错。
37
+ - **绕法(已实测)**:把 spec 写成 **`name@精确版本号`**(`dsh-context@0.61.0` 而不是 `dsh-context@latest`),pnpm 会把该版本自动写入 workspace 的 `minimumReleaseAgeExclude`(实测输出 `Added 1 entry to minimumReleaseAgeExclude in pnpm-workspace.yaml`)并照常安装。所以本插件的 `startOne` 现在用面板解析出的 `latest` 拼 spec(`name + "@" + row.latest`),而不是 `name + "@latest"`。
38
+ - `finishOne` 的「版本没变 → 暂缓更新」判据仍然保留作为兜底(防 registry 解析与 pnpm 最终解析不一致),只是正常情况下它不会再被触发。
39
+ - 参考实现:社区分叉版 `@linxin666/dsh-client-ui-plugin-manager` 的 update handler 也是先 `fetchManifest` 拿 registry 的 `version`,再 `installBundle(name@version)`——即它「能更新」不是因为绕过了 pnpm,而是因为用的是精确版本。
40
+
41
+ ### 本地包识别 = host 路由为主,404 签名兜底
42
+
43
+ - `listBundles()` 不透传安装来源(`installed` 不是 spec 字符串),而发布到 npm 的本地包(如 `@nimoxie/dsh-model-organizer`)连 registry 404 签名都不命中 —— 所以 host 半区挂了 `GET /api/ui-tweaks/local-plugins`(读 profile `package.json` 的 dependencies,按 source 分两类回给面板:**local** = `link:`/`file:` 本地路径,标「本地链接」;**remote** = `git:`/`github:`/`http` 直装,标「Git 直装」;仅回环放行)。两者都**不参与 registry 比对**——即使 npm 上有同名包(如 `dsh-ego-browser` 有 `github:` 声明、registry 也存在 0.8.6),装的也是仓库源码、与 npm 版不是一回事。client 每轮检查先取这份名单,取不到(host 半区未重启)再退回 404 签名(404 时也按 `installed` 来源区分 local/remote)。**改 host 半区必须重启 dsh/客户端才生效**(client bundle 刷新即可)。
45
44
  profile 目录定位:先从 `import.meta.url` 向上找带 `dsh.profile.bundles` 且包含自身的 package.json(npm 直装必命中);软链安装时模块被解析到 profile 外的真实目录,退回 `DSH_HOME/profiles/*` 扫描,多 profile 命中时按 `ELECTRON_RUN_AS_NODE` 区分 desktop/web。
46
- - **更新严格串行**:pnpm 对 profile 的 package.json 有文件锁,并发发起不会真并行、只会在锁上排队。队列里的项标「等待更新」,正在装的标「更新中 …」,两者都可点取消。
45
+
46
+ ### 其它要点
47
+
48
+ - **更新主路径全部走官方 remote API**(host 半区只挂一条本地包识别路由):`pluginManager.listBundles()`(清单+版本)、`installBundle("<名>@版本", { enabled: true, requestId })`(更新)、`waitForInstall(requestId)`(进度)、`cancelInstall(requestId)`(取消)。`installBundle` 的第二参官方还传了 `registry`(来自 `pluginManager.registries()`),本插件省略——若某天报 registry 缺失,先补它。
49
+ - **registry 比对在浏览器直连**:`registry.npmjs.org` 带 CORS *(实测),逐包**串行** fetch `/latest`,不打并发。
50
+ - **装完必须回读实际版本**:回读**只用 `pluginManager.listBundles()`**——`pluginInventory` 只有 `list()` 没有 `listBundles`,先取它掉进「回读版本失败」假阳性(踩过);装完瞬间清单可能短暂不可用,重试 3 次(1.5s 间隔)再下结论。重试后仍读不到就直接 mark「已发起·未确认」(可点重查)。
51
+ - **更新严格串行**:pnpm 对 profile 的 package.json 有文件锁,并发发起不会真并行、只会排队。队列顶项标「等待更新」,正在装的标「更新中 …」,两者都可点取消。
47
52
  - `waitForInstall` 的返回形态官方页面之外没有文档,本插件对 `phase/state/status/ok` 多形状都认、240 秒兜底超时;真实跑通后按实际返回收紧。
48
- - **刻意不检查 dsh 本体**(用户决策):桌面端自带更新器,web 端本体升级走 npm。
49
53
 
50
- ## 动画修复的实现要点(模块 4)
54
+ ## 动画修复(模块 4)
51
55
 
52
- - **原理**:Windows「动画效果」关闭 → Chromium 报 `prefers-reduced-motion: reduce` → dsh 的 40 处(17 包)`@media` 规则关掉动画。页面内**删不掉媒体查询的求值**,但删得掉规则本身:dsh 的 CSS 全部经 `<style>` 标签注入(实测 0 处 adoptedStyleSheets),同源 sheet 的 `cssRules` 可遍历可 `deleteRule` —— 把 reduce 的 media 块整条递归删除,基础动画声明(非 media 规则)立即生效。效果与 dsh-web-shell 的 CDP `Emulation.setEmulatedMedia` 等价,但**不需要调试端口,桌面端也能用**。
53
- - **JS 门控**:chat 包 3 处 `matchMedia("(prefers-reduced-motion: reduce)")`(滚动平滑 vs 跳转)用 `window.matchMedia` 包装覆盖 —— 在返回的 MediaQueryList 上 `defineProperty("matches")` 影子化,按 no-preference 应答。包装必须在应用渲染前生效(本插件 apply 在合成期,早于挂载)。
54
- - **晚到样式**:分包懒加载/换肤重写都靠 `MutationObserver(head)` 防抖重扫 + 2s 轮询兜底(纯 CSSOM 枚举,无布局开销)。
55
- - **取舍**:与 dsh-web-shell 的 `forceAnimations` 相同 —— 覆盖系统无障碍设置。窗口级覆盖(启动器)换成了会话级覆盖(插件),想恢复系统行为就得停用本插件;若将来要做成可开关,参考 dsh-model-organizer 的设置卡座位。
56
+ - **原理**:Windows「动画效果」关闭 → Chromium 报 `prefers-reduced-motion: reduce`,dsh 的 40 处(17 包)`@media (prefers-reduced-motion: reduce)` 规则关掉动画。页面内**删不掉媒体查询的求值**,但删得掉规则本身:dsh 的 CSS 全部经 `<style>` 标签注入(实测 0 处 adoptedStyleSheets),同源 sheet 的 `cssRules` 可遍历可 `deleteRule`——把 reduce 的整条递归删除,基础动画声明(非媒体内)立即生效。效果与 dsh-web-shell 的 CDP `Emulation.setEmulatedMedia` 等价,但**不需要调试端口,桌面端也能用**。
57
+ - **JS 门控**:chat 包 3 处 `matchMedia("(prefers-reduced-motion: reduce)")`(滚动平滑 vs 跳转)用 `window.matchMedia` 包装覆盖——在返回的 MediaQueryList 上 `defineProperty("matches")` 影子化,按 no-preference 应答。包装必须在应用渲染前(apply 在合成期,早于 mount)。
58
+ - **晚到样式**:分包懒加载/换肤重写会在加载后重建样式,靠 `MutationObserver(head)` 防抖重扫 + 2s 轮询兜底(纯 CSSOM 枚举,无布局开销)。
59
+ - **取舍**:与 dsh-web-shell 的 `forceAnimations` 相同——覆盖系统无障碍设置。窗口级覆盖(启动器)换成会话级覆盖(插件),想恢复系统行为就得停用本插件;将来要做成可开关就着 dsh-model-organizer 的设置卡方案。
56
60
  - web 窗口里本模块与 dsh-web-shell 的 CDP 覆盖并存是**无害冗余**(都指向 no-preference)。
57
61
 
58
- ## 从 dsh-web-shell 迁移的功能(2026-09-30)
62
+ ## 从 dsh-web-shell 迁移的功能
59
63
 
60
- 对话区左侧滚动条与宽表格修复原是 dsh-web-shell 的 CDP 注入脚本,已整体迁入本插件(client bundle 自由 JS),dsh-web-shell 自身删除了对应代码与 `leftScrollbar` / `tableWidthFix` 配置。迁移时保留的全部实现要点都在 `lib/client.js` 各模块的注释里(宿主挂 body 的理由、composerSeat 裁剪 −4px、拖动三层防御、零位移 wheel 解阅读锚点、出血布局三件套等),此处不重复;下面只记两块迁移后才成立的知识:
64
+ 对话区左侧滚动条与宽表格修复原是 dsh-web-shell 的 CDP 注入脚本,已整体迁入本插件(client bundle 自由 JS),dsh-web-shell 自身删除了对应代码与 `leftScrollbar` / `tableWidthFix` 配置。此处不重复实现细节(`lib/client.js` 各模块注释里保留:宿主挂 body 的理由、composerSeat 裁剪 −4px、拖动三层防御、零位移 wheel 事件解阅读锚点、出血布局三件套),只记两条迁移后才知道的知识:
61
65
 
62
- - **「检查更新」按安装来源分流**:dsh-web-shell 时代来源由 PowerShell 读 profile 依赖判定;迁入插件后走 host 路由(见「更新检查的实现要点」)。
63
- - **判断 dsh 是否已修宽表格**(决定何时删除模块 1):打开含 ≥4 列表格的会话,控制台执行 `getComputedStyle(document.querySelector('[class*="tableScroll"]')).maxWidth` —— 若不再是 `100%`(或包裹层不再越界)说明官方已修,删除 `installTableWidthFix` 函数及其 `apply` 里的 guard 行即完全回滚。
66
+ - **「检查更新」按安装来源分流**:dsh-web-shell 时代来源由 PowerShell 读 profile 依赖判定;迁入插件后走 host 路由(见「更新检查」)。
67
+ - **判断 dsh 是否已修宽表格**(决定何时删掉模块 1):打开含 ≥4 列表格的会话,控制台执行 `getComputedStyle(document.querySelector('[class*="tableScroll"]')).maxWidth`——若不再是 `100%`(或包裹层不再越界)说明官方已修,删除 `installTableWidthFix` 的 guard 行即完全回滚。
64
68
 
65
69
  ## 待办 / 路线
66
70
 
67
- - [ ] 入口按钮从「会话头部 utilities」迁到 `sidebar.footer.action`(常驻可见,@linxin666/dsh-update 用同一个座位)—— 需先验证空座位容器是否也渲染 `data-slot` 锚点。
68
- - [ ] 有更新时按钮加角标(现在要点开面板才看到)。
69
- - [ ] i18n(`ctx.locale.register(NS, {zh, en})`,照 dsh-model-organizer 的写法)。
70
- - [ ] 第 3 优化点:会话内「重启 dsh」按钮(需要 host 侧配合或官方重启 API,未调研)。
71
- - [ ] `waitForInstall` 终态按实测收紧;`installBundle` 的 registry 参数补齐。
71
+ - [ ] 入口按钮支持 `sidebar.footer.action` 常驻入口(社区同款用此座位),当前只在插件管理页顶栏。
72
+ - [ ] 有更新时按钮加角标(现在要点开面板才看得到)。
73
+ - [ ] i18n(`ctx.locale.register(NS, {zh, en})`,照 dsh-model-organizer 写法)。
74
+ - [ ] 会话内重启 dsh 的入口(需要 host 配合或官方重启 API,未调研)。
75
+ - [ ] `waitForInstall` 终态按实测收紧;`installBundle` 的 `registry` 参数补齐。
72
76
  - [ ] 插件被禁用/卸载时的运行时清理(目前 DOM 残留到页面刷新为止)。
77
+
78
+ ## 发布
79
+
80
+ - npm 不允许覆盖已发布的版本号:**先升版本,再提交,最后发布**。
81
+ - 版本号同时在 `package.json` 和 `lib/client.js` 的 `BUILD` 常量里(改两处)。发版命令:
82
+ ```sh
83
+ npm version patch # 或手改 package.json 的 version;记得同步 BUILD
84
+ git add -A && git commit -m "..." && git push
85
+ npm publish
86
+ ```
87
+ - **Token**:npm 2025-11 起只支持 granular token,发布需要有 **Bypass 2FA** 的权限:npmjs.com → 头像 → Access Tokens → Generate New Token → Packages 选 **All Packages + Read and Write (publish and stage)**、Organizations 选 **No access**、勾 **Bypass two-factor authentication**;然后 `npm config set //registry.npmjs.org/:_authToken=npm_xxx`(写进用户级 `.npmrc`,不要放进仓库——`.gitignore` 已排除 `.npmrc` 与 `*.tgz`)。
88
+ - 包是 scoped(`@nimoxie/dsh-ui-tweaks`),`publishConfig.access: "public"` 已写在 package.json,`npm publish` 不需要额外参数。
package/README.md CHANGED
@@ -1,34 +1,68 @@
1
- # @nimoxie/dsh-ui-tweaks
1
+ # dsh-ui-tweaks
2
2
 
3
- [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) Web UI 的微调合集插件。装上即生效,无需配置。
3
+ 给 DSH(DeepSeek Harness)的 Web UI 的一批界面微调:装上即生效,无需配置。
4
+
5
+ ## 它解决什么问题
6
+
7
+ 官方界面上有几处用得难受的地方:宽表格会撑出对话区右沿、滚动条贴在屏幕最右端要横跨半屏去够、Windows 关闭「动画效果」时前端动画被系统设置一并关掉、插件版本更新只能去插件管理页挨个看。这个插件把它们一次性修掉。
4
8
 
5
9
  ## 功能
6
10
 
7
- - **宽表格越界修复** —— 修复 dsh 本体的一个 CSS 缺陷(≥4 列表格撑出对话区、右沿被裁掉还看不到滚动条)。放弃官方的「出血」布局,把宽表格收敛回正文列,横向可滚动。
8
- - **对话区左侧滚动条** —— 对话区的原生滚动条贴在最右缘,宽屏下要横跨半屏去够;本插件在右侧宽度调整条的左边放一条自绘的常显滚动条:拖拽、点轨道翻页、滚轮转动手感都与原生一致,主题自动跟随明暗,且**只在会话页出现**、切到插件页等非会话页自动移除。
9
- - **动画修复** —— Windows 关闭「动画效果」时 Chromium 会报告 `prefers-reduced-motion: reduce`,dsh 前端几十处媒体查询规则把动画全关(圈圈不转、流光消失)。本插件在页面内清除这些覆盖规则并让 `matchMedia` 按「有动画」应答,效果与 dsh-web-shell 的调试端口覆盖相同,**web 与桌面客户端都生效**。
10
- - **插件更新检查** —— 会话工具栏多一枚「插件更新」按钮:逐个比对**全部已装插件**与 npm registry 的版本,弹面板列出(名字 / 当前版本 → 远端 / 状态标签);**有新版本的插件点标签就地更新**,支持「全部更新」与中途取消。**不检查 dsh 本体**(桌面客户端自带更新器;web 端本体升级请走 `npm install -g @deepseek-ai/dsh@next`)。
11
+ **宽表格越界修复**
12
+ - 宽表格(≥4 列)不再撑出对话区,收敛回正文列并支持横向滚动,观察沿裁掉的问题消失
11
13
 
12
- ## 环境要求
14
+ **对话区左侧滚动条**
15
+ - 在右侧宽度调整条的左边放一条自绘滚动条:拖拽、点轨道翻页、滚轮转动手感与原生一致,主题跟随明暗
16
+ - **只在会话页出现**,切到插件页等非会话页自动移除
13
17
 
14
- - dsh `>= 0.2.0-rc.1`(依赖 0.2.x 的 `remote.pluginManager` / `remote.pluginInventory` 接口)。
15
- - web / 桌面客户端均可(桌面端同样适用,但桌面端有自己的本体更新,本插件的更新检查只覆盖插件)。
18
+ **动画修复**
19
+ - Windows 关闭「动画效果」时 Chromium 会向网页报告 `prefers-reduced-motion: reduce`,本插件清除前端几十处因此失效的动画规则,让动画恢复
20
+ - web 与桌面客户端都生效
21
+
22
+ **插件更新检查**
23
+ - 插件管理页顶部多一枚「检查更新」按钮(刷新按钮左侧,样式与「添加插件」按钮一致):比对已装插件(官方 `@deepseek-ai` 插件除外)与 npm registry 的版本,有新版本可点上就地更新,支持全部更新与中途取消
24
+ - 不检查 dsh 本体,也不检查 `@deepseek-ai` 官方插件——两者的更新都由 dsh 本体的更新流程负责(桌面端自带更新器;web 端本体升级走 `npm install -g @deepseek-ai/dsh@next`)
16
25
 
17
26
  ## 安装
18
27
 
28
+ dsh 目前有浏览器版与桌面客户端两种形态,命令里的 `<profile>` 就是安装目标环境:浏览器用 `web`,桌面客户端用 `desktop`。
29
+
19
30
  ```sh
20
- # 从 npm 安装
21
31
  dsh plugin --profile <profile> add @nimoxie/dsh-ui-tweaks
32
+ ```
33
+
34
+ - 浏览器版(`web`):装完重启服务(关掉再 `dsh web`)。
35
+ - 桌面客户端(`desktop`):桌面 profile 由客户端独占管理,请在客户端的「插件管理」页添加本包(包名 `@nimoxie/dsh-ui-tweaks`)。
22
36
 
23
- # 本地开发(软链,改完 Ctrl+Shift+R 即生效,见 DEVELOPING.md)
24
- dsh plugin --profile <profile> add link:<本仓库路径>
37
+ 卸载(CLI 可管理的环境):
38
+
39
+ ```sh
40
+ dsh plugin --profile <profile> remove @nimoxie/dsh-ui-tweaks
25
41
  ```
26
42
 
27
- ## 注意
43
+ 别人也可以从 GitHub 或本地目录装(开发用):
44
+
45
+ ```sh
46
+ dsh plugin --profile <profile> add github:NimoXie15/dsh-ui-tweaks
47
+ dsh plugin --profile <profile> add link:/absolute/path/to/dsh-ui-tweaks
48
+ ```
49
+
50
+ > **要求**:dsh `>= 0.2.0-rc.1`(需要 `remote.pluginManager` / `remote.pluginInventory` 接口)。
51
+ >
52
+ > 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(浏览器可能还在用缓存的旧版本)。
53
+
54
+ ## 已知限制
55
+
56
+ - 非 npm 来源的插件没有可比的 registry 版本,直接跳过比对:`link:` / `file:` 本地路径标「本地链接」;`git:` / `github:` / `http` 直装标「Git 直装」(例如 `github:Fisfzy/dsh-ego-browser` 会显示为 Git 直装,即使 npm 上有同名包,装的也是仓库源码、与 npm 版本不是一回事)
57
+ - `@deepseek-ai` 官方插件直接从检查列表里剔除:它们的更新由 dsh 本体流程负责
58
+ - 更新走 profile 内的插件安装接口,与官方插件管理页同源;安装按 registry 解析出的**精确版本号**发起(而非 `@latest`),不受 pnpm 发布时长策略(minimumReleaseAge)拦挡,刚发布的版本也能装
59
+ - 个别插件(如社区插件管理页扩展)自带单独的「检查更新」入口,与本插件的检查互不关联,结果可能不同
60
+ - 与启动器 dsh-web-shell 的分工:对话区滚动条、宽表格修复等界面增强已由本插件接管,dsh-web-shell 自身不再注入;其「检查更新」按钮仍负责 **dsh 本体**的比对与升级提示
61
+
62
+ ## 许可
28
63
 
29
- - 本插件是启动器 dsh-web-shell 的配套插件:对话区滚动条、宽表格修复等界面增强已由本插件提供,`dsh-web-shell` 自身不再注入(其「检查更新」按钮仍负责 **dsh 本体**的比对与升级提示)。
30
- - 更新走 profile 内的插件安装接口,与官方插件管理页同源;`link:` / `file:` / `git+` 直装的插件没有可比的 registry 版本,会标为「本地链接」跳过。
64
+ MIT
31
65
 
32
- ## 开源协议
66
+ ---
33
67
 
34
- [MIT](./LICENSE)
68
+ 想自己改这个插件、或者想了解它在内部怎么实现的,见 [DEVELOPING.md](./DEVELOPING.md)。
package/lib/client.js CHANGED
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
14
14
  const module = { exports: {} };
15
15
  const exports = module.exports;
16
16
 
17
- const BUILD = "0.1.0";
17
+ const BUILD = "0.1.1";
18
18
 
19
19
  /* 每个模块的注册各套一层 guard:失败只记 warn(座位/模块失败模式是「静默消失」,
20
20
  必须主动吭声;但不能让一个模块的异常打断同文件里其它模块的 apply)。 */
@@ -359,8 +359,11 @@ window.__ModuleLoader__.load({
359
359
  * registry 比对从浏览器直连(registry.npmjs.org 带 CORS *)。
360
360
  *
361
361
  * 更新严格串行(pnpm 对 profile 的 package.json 有文件锁,并发只会在锁上
362
- * 排队);更新完成后回读实际版本 —— pnpm 的 minimumReleaseAge 可能静默
363
- * 暂缓当日发布,只看退出状态会把「没装上」报成「成功」。
362
+ * 排队);安装 spec 用面板解析出的**精确版本**(`name@版本`,不是 @latest):
363
+ * pnpm 11 的 minimumReleaseAge 门禁只拦「最新版本」解析,显式版本会自动
364
+ * 写入 profile 的 pnpm-workspace.yaml 的 minimumReleaseAgeExclude 并放行。
365
+ * 装完仍回读实际版本兜底(registry 与 pnpm 解析可能不一致),版本没变
366
+ * 才是「暂缓更新」。
364
367
  * ==================================================================== */
365
368
  function installPluginUpdates(ctx) {
366
369
  if (window.__DSHUIW_UPDATES__) { return; }
@@ -447,23 +450,27 @@ window.__ModuleLoader__.load({
447
450
  .then(function (d) { return d && d.version ? String(d.version) : null; });
448
451
  }
449
452
 
450
- /* 本地直装(link:/file:/git+/http)没有可比的 registry 版本。
453
+ /* 非 registry 直装(link:/file:/git+/github:/http)没有可比的 registry 版本。
451
454
  * 来源判定主路径是 host 半区的 /api/ui-tweaks/local-plugins(读 profile
452
- * 依赖声明);host 未重启/路由不可用时退回 registry 404 签名。 */
455
+ * 依赖声明):本地路径(link:/file:)与仓库/URL 直装(git/github/http)分列,
456
+ * host 未重启/路由不可用时退回 registry 404 签名。 */
453
457
  function fetchLocalNames(cb) {
454
458
  fetch("/api/ui-tweaks/local-plugins", { cache: "no-store" })
455
459
  .then(function (r) { if (!r.ok) { throw new Error("HTTP " + r.status); } return r.json(); })
456
- .then(function (d) { cb(d && d.ok && Array.isArray(d.local) ? d.local : []); })
457
- .catch(function () { cb([]); });
458
- }
459
- function localSource(src) {
460
- var s = String(src || "");
461
- return /^(link:|file:|git[+:@]|github:|http)/.test(s);
460
+ .then(function (d) {
461
+ cb({
462
+ local: d && d.ok && Array.isArray(d.local) ? d.local : [],
463
+ remote: d && d.ok && Array.isArray(d.remote) ? d.remote : []
464
+ });
465
+ })
466
+ .catch(function () { cb({ local: [], remote: [] }); });
462
467
  }
468
+ function isLocalSrc(src) { return /^(link:|file:)/.test(String(src || "")); }
469
+ function isRemoteSrc(src) { return /^(git[+:@]|github:|git:|http)/.test(String(src || "")); }
463
470
 
464
471
  /* ---- 面板状态 ---- */
465
472
  var rows = []; // {name, version, latest, state, why}
466
- // state: pending | checking | current | update | local | error | queuing | busy | done | unchanged
473
+ // state: pending | checking | current | update | local | remote | error | queuing | busy | done | unchanged
467
474
  var queue = []; // 待更新名单(串行队列)
468
475
  var busyName = ""; // 正在更新的那个
469
476
  var busyReq = "";
@@ -480,6 +487,12 @@ window.__ModuleLoader__.load({
480
487
  emit();
481
488
  }
482
489
 
490
+ /* 404 签名兜底:npm 上没有此包 → 按来源分「本地链接」/「Git 直装」;
491
+ host 半区未重启时走到这里。 */
492
+ function notFoundState(row) {
493
+ return isLocalSrc(row.src) ? "local" : (isRemoteSrc(row.src) ? "remote" : "local");
494
+ }
495
+
483
496
  function checkNow() {
484
497
  if (checking) { return; }
485
498
  checking = true;
@@ -512,22 +525,29 @@ window.__ModuleLoader__.load({
512
525
  var src = typeof b.installed === "string" ? b.installed : (typeof b.source === "string" ? b.source : "");
513
526
  return {
514
527
  name: name, version: ver, latest: "",
515
- state: localSource(src) ? "local" : (ver ? "checking" : "unknown"),
528
+ state: isLocalSrc(src) ? "local" : (isRemoteSrc(src) ? "remote" : (ver ? "checking" : "unknown")),
516
529
  src: src, why: ""
517
530
  };
518
- }).filter(function (x) { return x.name; });
531
+ }).filter(function (x) { return x.name; })
532
+ /* 官方 @deepseek-ai 插件由 dsh 本体更新流程负责,直接从检查列表里剔除 */
533
+ .filter(function (x) { return !/^@deepseek-ai\//.test(x.name); });
519
534
  checking = false;
520
535
  emit();
521
- // 先向 host 半区要本地直装名单,命中的行直接标「本地链接」不进比对 ——
522
- // 已发布到 npm 的本地包(如 @nimoxie/dsh-model-organizer)靠它认出;
523
- // host 未重启(无路由)时名单为空,退回 registry 404 签名判定。
524
- fetchLocalNames(function (localNames) {
525
- var localSet = {};
526
- (localNames || []).forEach(function (n) { localSet[n] = true; });
536
+ // 先向 host 半区要非 registry 直装名单,命中的行直接标「本地链接」或「Git 直装」
537
+ // 不进比对 —— 已发布到 npm 的本地/仓库源(如 @nimoxie/dsh-model-organizer、
538
+ // dsh-ego-browser)靠它认出;host 未重启(无路由)时名单为空,退回
539
+ // registry 404 签名判定。
540
+ fetchLocalNames(function (srcs) {
541
+ var localSet = {}, remoteSet = {};
542
+ (srcs.local || []).forEach(function (n) { localSet[n] = true; });
543
+ (srcs.remote || []).forEach(function (n) { remoteSet[n] = true; });
527
544
  rows.forEach(function (row) {
528
545
  if (localSet[row.name] && row.state === "checking") {
529
546
  row.state = "local";
530
547
  row.why = "profile 依赖声明为本地路径,不参与 registry 比对";
548
+ } else if (remoteSet[row.name] && row.state === "checking") {
549
+ row.state = "remote";
550
+ row.why = "profile 依赖声明为 git/URL 直装,不参与 registry 比对";
531
551
  }
532
552
  });
533
553
  emit();
@@ -541,7 +561,7 @@ window.__ModuleLoader__.load({
541
561
  row.state = v && cmpSemver(v, row.version) > 0 ? "update" : "current";
542
562
  emit();
543
563
  }).catch(function (e) {
544
- row.state = e && e.notFound ? "local" : "error";
564
+ row.state = e && e.notFound ? notFoundState(row) : "error";
545
565
  row.why = String(e && e.message || e);
546
566
  emit();
547
567
  });
@@ -584,36 +604,54 @@ window.__ModuleLoader__.load({
584
604
  settled = true;
585
605
  finishOne(name, ok, why || "");
586
606
  }
587
- try {
588
- pm.installBundle(name + "@latest", { enabled: true, requestId: busyReq })
589
- .then(function (res) {
590
- if (!res || res.ok === false) {
591
- finish(false, (res && res.failure && (res.failure.message || res.failure.reason)) || (res && res.message) || "安装请求被拒");
592
- return;
593
- }
594
- /* 轮询进度;终态形态未知,多形状都认,240s 兜底 */
595
- var t0 = Date.now();
596
- function poll() {
597
- var done = false;
598
- try {
599
- var p = pm.waitForInstall ? pm.waitForInstall(busyReq) : null;
600
- Promise.resolve(p).then(function (st) {
601
- var ph = st && (st.phase || st.state || st.status);
602
- if (ph === "done" || ph === "completed" || ph === "finished" || (st && st.ok === true)) { done = true; finish(true, ""); }
603
- else if (ph === "failed" || ph === "error" || ph === "cancelled" || (st && st.ok === false)) {
604
- finish(false, (st && st.failure && (st.failure.message || st.failure.reason)) || "安装失败");
605
- }
606
- }).catch(function () { /* 轮询失败不作为终态 */ });
607
- } catch (e) {}
608
- if (!done && !settled && Date.now() - t0 < 240000) { setTimeout(poll, 900); }
609
- else if (!settled && Date.now() - t0 >= 240000) { finish(false, "更新超时(240 秒未结束)"); }
610
- }
611
- poll();
612
- })
613
- .catch(function (e) { finish(false, String(e && e.message || e)); });
614
- } catch (e) { finish(false, String(e && e.message || e)); }
607
+ /* 安装 spec 用面板已解析的精确版本(`name@版本`)而不是 @latest:
608
+ * pnpm 11 的 minimumReleaseAge 门禁只拦「最新版本」解析 —— 发布不足
609
+ * 24h 的新版会被静默跳过、回落到旧版;显式版本则会触发 pnpm 的
610
+ * minimumReleaseAgeExclude 自动豁免并照常安装(社区分叉版
611
+ * @linxin666/dsh-client-ui-plugin-manager 的 update 正是这样:先
612
+ * fetchManifest 拿 registry 的 version,再按 `id@版本` 安装)。
613
+ * 行内没解析过(没点过检查)则现场查 registry,仍失败退回 @latest。 */
614
+ var row = null;
615
+ for (var i = 0; i < rows.length; i++) { if (rows[i].name === name) { row = rows[i]; break; } }
616
+ var specPromise = (row && row.latest)
617
+ ? Promise.resolve(name + "@" + row.latest)
618
+ : fetchLatest(name)
619
+ .then(function (v) { return v ? name + "@" + v : name + "@latest"; })
620
+ .catch(function () { return name + "@latest"; });
621
+ specPromise.then(function (spec) {
622
+ try {
623
+ pm.installBundle(spec, { enabled: true, requestId: busyReq })
624
+ .then(function (res) {
625
+ if (!res || res.ok === false) {
626
+ finish(false, (res && res.failure && (res.failure.message || res.failure.reason)) || (res && res.message) || "安装请求被拒");
627
+ return;
628
+ }
629
+ /* 轮询进度;终态形态未知,多形状都认,240s 兜底 */
630
+ var t0 = Date.now();
631
+ function poll() {
632
+ var done = false;
633
+ try {
634
+ var p = pm.waitForInstall ? pm.waitForInstall(busyReq) : null;
635
+ Promise.resolve(p).then(function (st) {
636
+ var ph = st && (st.phase || st.state || st.status);
637
+ if (ph === "done" || ph === "completed" || ph === "finished" || (st && st.ok === true)) { done = true; finish(true, ""); }
638
+ else if (ph === "failed" || ph === "error" || ph === "cancelled" || (st && st.ok === false)) {
639
+ finish(false, (st && st.failure && (st.failure.message || st.failure.reason)) || "安装失败");
640
+ }
641
+ }).catch(function () { /* 轮询失败不作为终态 */ });
642
+ } catch (e) {}
643
+ if (!done && !settled && Date.now() - t0 < 240000) { setTimeout(poll, 900); }
644
+ else if (!settled && Date.now() - t0 >= 240000) { finish(false, "更新超时(240 秒未结束)"); }
645
+ }
646
+ poll();
647
+ })
648
+ .catch(function (e) { finish(false, String(e && e.message || e)); });
649
+ } catch (e) { finish(false, String(e && e.message || e)); }
650
+ });
615
651
  }
616
- /* 装完回读清单:版本变了才算真的装上(pnpm minimumReleaseAge 可能静默暂缓)。
652
+ /* 装完回读清单:版本变了才算真的装上——安装走精确版本后 pnpm 已不会因
653
+ * minimumReleaseAge 拦新版本,但如果 registry 与 pnpm 解析不一致(如
654
+ * profile 配了镜像源),仍可能装回旧版,所以回读判据保留作最后兜底。
617
655
  * 只用 pluginManager.listBundles —— pluginInventory 没有 listBundles 方法
618
656
  * (只有 list()),之前先取 inventory 导致 guard 落到 reject,面板上的
619
657
  * 「安装已发起,但回读版本失败」假阳性就是它。装完瞬间清单可能短暂
@@ -642,7 +680,7 @@ window.__ModuleLoader__.load({
642
680
  if (!bundles.length && n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
643
681
  if (ver) {
644
682
  if (before && ver === before) {
645
- setRow(name, { state: "unchanged", why: "版本未变化:可能被 pnpm 发布时长策略暂缓,也可能检查时远端版本信息有误" });
683
+ setRow(name, { state: "unchanged", why: "版本未变化:可能 registry 解析与 pnpm 实际安装不一致(如配置了镜像源),也可能检查时远端版本信息有误" });
646
684
  } else {
647
685
  setRow(name, { version: ver, state: "current", latest: ver, why: "" });
648
686
  }
@@ -701,12 +739,14 @@ window.__ModuleLoader__.load({
701
739
  ".nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
702
740
  ".ver{opacity:.6;font-size:12px;white-space:nowrap;font-variant-numeric:tabular-nums}" +
703
741
  ".tag{font-size:11px;padding:1px 7px;border-radius:999px;white-space:nowrap;" +
704
- "border:1px solid transparent;flex:0 0 auto}" +
705
- ".tag.update{background:rgba(255,138,0,.16);color:#f0a35e;border-color:rgba(255,138,0,.32);cursor:pointer}" +
742
+ "border:1px solid transparent;flex:0 0 auto;cursor:not-allowed}" +
743
+ ".tag.update{background:rgba(255,138,0,.16);color:#f0a35e;border-color:rgba(255,138,0,.32)}" +
706
744
  ".tag.current{background:rgba(62,207,142,.14);color:#4fbf8b;border-color:rgba(62,207,142,.28)}" +
707
745
  ".tag.other{background:rgba(128,128,128,.14);opacity:.85}" +
708
746
  ".tag.busy{opacity:.6}" +
709
- ".tag.error{background:rgba(255,80,80,.14);color:#e07070;border-color:rgba(255,80,80,.28);cursor:pointer}" +
747
+ ".tag.error{background:rgba(255,80,80,.14);color:#e07070;border-color:rgba(255,80,80,.28)}" +
748
+ ".tag.clickable{cursor:pointer}" +
749
+ ".tag.clickable:hover{filter:brightness(1.15)}" +
710
750
  ".allbtn{cursor:pointer;font-size:12px;padding:2px 9px;border-radius:6px;flex:0 0 auto;" +
711
751
  "border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4))}" +
712
752
  ".allbtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14))}" +
@@ -798,7 +838,8 @@ window.__ModuleLoader__.load({
798
838
  function checkOne(name) {
799
839
  for (var i = 0; i < rows.length; i++) {
800
840
  if (rows[i].name === name) {
801
- rows[i].state = "checking"; rows[i].why = "";
841
+ var cur = rows[i];
842
+ cur.state = "checking"; cur.why = "";
802
843
  emit();
803
844
  fetchLatest(name).then(function (v) {
804
845
  var r = null;
@@ -808,7 +849,7 @@ window.__ModuleLoader__.load({
808
849
  r.state = v && cmpSemver(v, r.version) > 0 ? "update" : "current";
809
850
  emit();
810
851
  }).catch(function (e) {
811
- setRow(name, e && e.notFound ? { state: "local", why: String(e.message) } : { state: "error", why: String(e && e.message || e) });
852
+ setRow(name, e && e.notFound ? { state: notFoundState(cur), why: String(e.message) } : { state: "error", why: String(e && e.message || e) });
812
853
  });
813
854
  return;
814
855
  }
@@ -817,18 +858,20 @@ window.__ModuleLoader__.load({
817
858
 
818
859
  var TAG_TEXT = {
819
860
  checking: "比对中 …", current: "已是最新", update: "有新版本",
820
- local: "本地链接", unknown: "未比对", error: "检查失败", queuing: "等待更新",
861
+ local: "本地链接", remote: "Git 直装", unknown: "无法比对", error: "检查失败", queuing: "等待更新",
821
862
  busy: "更新中 …", unchanged: "暂缓更新", unconfirmed: "已发起·未确认"
822
863
  };
823
864
  function tagHtml(r) {
824
865
  var cls = "tag other", act = "", title = "";
825
- if (r.state === "update") { cls = "tag update act"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"点击更新到最新版\""; }
866
+ if (r.state === "update") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"点击更新到最新版\""; }
826
867
  else if (r.state === "current") { cls = "tag current"; }
827
- else if (r.state === "error") { cls = "tag error retry"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "检查失败") + "(点击重查)\""; }
868
+ else if (r.state === "error") { cls = "tag error retry clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "检查失败") + "(点击重查)\""; }
828
869
  else if (r.state === "local") { title = " title=\"" + esc(r.src || r.why || "本地直装") + ";没有可比的 registry 版本\""; }
829
- else if (r.state === "queuing" || r.state === "busy") { cls = "tag other cancel"; title = " title=\"点击取消" + (r.state === "busy" ? "本次更新" : "排队") + "\""; }
830
- else if (r.state === "unchanged") { cls = "tag update act"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "") + "(点击重试)\""; }
831
- else if (r.state === "unconfirmed") { cls = "tag other reverify"; title = " title=\"" + esc(r.why || "") + "(点击重查)\""; }
870
+ else if (r.state === "remote") { title = " title=\"" + esc(r.src || r.why || "Git/URL 直装") + ";没有可比的 registry 版本\""; }
871
+ else if (r.state === "unknown") { title = " title=\"" + esc(r.why || "插件清单未提供已装版本") + ",因此无法与 registry 比对\""; }
872
+ else if (r.state === "queuing" || r.state === "busy") { cls = "tag other cancel clickable"; title = " title=\"点击取消" + (r.state === "busy" ? "本次更新" : "排队") + "\""; }
873
+ else if (r.state === "unchanged") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "") + "(点击重试)\""; }
874
+ else if (r.state === "unconfirmed") { cls = "tag other reverify clickable"; title = " title=\"" + esc(r.why || "") + "(点击重查)\""; }
832
875
  return "<span class=\"" + cls + "\"" + act + title + ">" + esc(TAG_TEXT[r.state] || "未比对") + "</span>";
833
876
  }
834
877
  function paint() {
@@ -861,35 +904,54 @@ window.__ModuleLoader__.load({
861
904
  wrap.hidden = false;
862
905
  }
863
906
 
864
- /* ---- 入口按钮:挂会话头部 utilities 槽(0.2.0-rc.1 实测容器带
865
- data-slot 属性;React 重渲染会抹掉手插节点,MutationObserver 补挂)---- */
907
+ /* ---- 入口按钮:挂官方插件页顶栏(刷新按钮左侧),样式克隆「添加
908
+ 插件」按钮。官方类名是 CSS-module hash(不硬编码),按钮在官方
909
+ 页面渲染后由 findToolbar 实时克隆出来,换版本也不破。刷新按钮
910
+ 与添加按钮同处一个 toolbar 容器,用文案「添加插件/Add plugin」
911
+ 定位该容器,再插到容器首位(刷新按钮之前),保持既有更新面板。
912
+ React 重渲染会抹掉手插节点,MO + 轮询兜底补挂。---- */
866
913
  function mountButton(openPanel) {
914
+ function findToolbar() {
915
+ var panel = document.querySelector('[data-plugin-panel]');
916
+ if (!panel) { return null; }
917
+ var header = panel.querySelector("header");
918
+ if (!header) { return null; }
919
+ var add = null;
920
+ var btns = header.querySelectorAll("button");
921
+ for (var i = 0; i < btns.length; i++) {
922
+ var t = String(btns[i].textContent || "").trim();
923
+ /* 「添加插件」在安装进行中会变成「查看安装任务」,两种都要认 */
924
+ if (t === "添加插件" || t === "Add plugin" || t === "查看安装任务" || t === "View installation") { add = btns[i]; break; }
925
+ }
926
+ if (!add || !add.parentElement) { return null; }
927
+ return { toolbar: add.parentElement, addBtn: add };
928
+ }
867
929
  function mount() {
868
- var slot = document.querySelector('[data-slot="conversation.session.header.utilities"]');
869
- if (!slot || !slot.parentElement) { return false; }
870
- if (document.getElementById(BTN_ID)) { return true; }
871
- var host = document.createElement("span");
872
- host.id = BTN_ID;
873
- var root = host.attachShadow({ mode: "open" });
874
- root.innerHTML = "<style>" +
875
- "button{font:inherit;font-size:13px;height:26px;padding:0 10px;border-radius:6px;" +
876
- "border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:transparent;" +
877
- "color:var(--dsw-alias-label-primary,inherit);cursor:pointer;white-space:nowrap;" +
878
- "transition:background-color .13s,border-color .13s}" +
879
- "button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12))}" +
880
- "</style><button type=\"button\" title=\"比对全部已装插件与 npm registry 的版本(不含 dsh 本体)\">插件更新</button>";
881
- root.querySelector("button").addEventListener("click", openPanel);
882
- slot.parentElement.insertBefore(host, slot);
930
+ var found = findToolbar();
931
+ var host = document.getElementById(BTN_ID);
932
+ if (host) {
933
+ /* 按钮已在 DOM:若工具栏在而按钮被挤到别处(React 重渲染),归位 */
934
+ if (found && host.parentElement !== found.toolbar) {
935
+ found.toolbar.insertBefore(host, found.toolbar.firstElementChild);
936
+ }
937
+ return true;
938
+ }
939
+ if (!found) { return false; }
940
+ var b = found.addBtn.cloneNode(false);
941
+ b.id = BTN_ID;
942
+ b.disabled = false;
943
+ b.textContent = "检查更新";
944
+ b.title = "比对全部已装插件与 npm registry 的版本(不含 dsh 本体)";
945
+ b.addEventListener("click", openPanel);
946
+ found.toolbar.insertBefore(b, found.toolbar.firstElementChild);
883
947
  return true;
884
948
  }
885
949
  mount();
886
950
  try {
887
- var mo = new MutationObserver(function () {
888
- if (!document.getElementById(BTN_ID)) { mount(); }
889
- });
951
+ var mo = new MutationObserver(function () { mount(); });
890
952
  mo.observe(document.documentElement, { childList: true, subtree: true });
891
953
  } catch (e) {}
892
- setInterval(function () { if (!document.getElementById(BTN_ID)) { mount(); } }, 2000);
954
+ setInterval(mount, 2000);
893
955
  }
894
956
 
895
957
  mountButton(function () { paint(); if (!rows.length && !checking) { checkNow(); } });
package/lib/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
  * dsh-ui-tweaks host half.
3
3
  *
4
4
  * 挂一条 GET /api/ui-tweaks/local-plugins:返回当前 profile package.json
5
- * dependencies 里声明为本地路径(link:/file:/git+/http)的包名清单。
6
- * 浏览器半区的更新面板据此把这些插件标成「本地链接」、跳过 registry 比对
7
- * —— listBundles 不透传安装来源,而「npm 上恰好也发布过」的本地包(如
8
- * @nimoxie/dsh-model-organizer)在客户端侧没有任何别的办法认出来。
5
+ * dependencies 里非 registry 直装(link:/file:/git+/github:/http)的包名清单。
6
+ * 浏览器半区的更新面板据此把这些插件标成「本地链接」或「Git 直装」、跳过
7
+ * registry 比对 —— listBundles 不透传安装来源,而「npm 上恰好也发布过」的
8
+ * 非 registry 源(如 @nimoxie/dsh-model-organizer)在客户端侧没有别的办法认。
9
9
  *
10
10
  * 纯读操作:只读 profile 的 package.json,不改任何东西;仅回环访问放行。
11
11
  */
@@ -18,7 +18,8 @@ export const name = "ui-tweaks";
18
18
  export const inject = ["webServer"];
19
19
 
20
20
  const SELF = "@nimoxie/dsh-ui-tweaks";
21
- const LOCAL_SPEC_RE = /^(link:|file:|git[+:@]|github:|http)/;
21
+ const LOCAL_SPEC_RE = /^(link:|file:)/; // 本地路径(link:→按包名查 node_modules)
22
+ const REMOTE_SPEC_RE = /^(git[+:@]|github:|git:|http)/; // Git 仓库 / URL 直装
22
23
  const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
23
24
 
24
25
  function readJson(p) {
@@ -78,8 +79,14 @@ export function apply(ctx) {
78
79
  const dir = findProfileDir();
79
80
  const manifest = dir ? readJson(join(dir, "package.json")) : null;
80
81
  const deps = (manifest && manifest.dependencies) || {};
81
- const local = Object.keys(deps).filter((n) => LOCAL_SPEC_RE.test(String(deps[n])));
82
- done(200, JSON.stringify({ ok: true, local, profile: (manifest && manifest.name) || "" }));
82
+ const local = []; // link:/file: —— 本地路径
83
+ const remote = []; // git/github/http —— 仓库或 URL 直装
84
+ for (const [n, spec] of Object.entries(deps)) {
85
+ const s = String(spec);
86
+ if (LOCAL_SPEC_RE.test(s)) { local.push(n); }
87
+ else if (REMOTE_SPEC_RE.test(s)) { remote.push(n); }
88
+ }
89
+ done(200, JSON.stringify({ ok: true, local, remote, profile: (manifest && manifest.name) || "" }));
83
90
  } catch (e) {
84
91
  done(500, JSON.stringify({ ok: false, code: String((e && e.message) || e) }));
85
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimoxie/dsh-ui-tweaks",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "dsh Web UI 微调合集:宽表格越界修复、对话区左侧自绘滚动条、插件更新检查面板(不含 dsh 本体)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",