@nimoxie/dsh-ui-tweaks 0.1.3 → 0.1.4

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.
@@ -45,12 +45,18 @@
45
45
 
46
46
  ### 其它要点
47
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 有文件锁,并发发起不会真并行、只会排队。队列顶项标「等待更新」,正在装的标「更新中 …」,两者都可点取消。
52
- - **总看门狗(0.1.3)**:`startOne` 入口起 240s 定时器,`finish` 里 `clearTimeout`——覆盖 fetchLatest(现场查 registry,fetch 本身无超时)与 specPromise(原先无 catch)这两段不设防的异步;超时先 `finish(false, "更新超时…")` 再 `cancelInstall(busyReq)`。
53
- - **`waitForInstall` 的返回形态已实测收紧(0.1.3,从 app.asar 的 zod schema 解出)**:终态读 `application`(`applied` / `restart-required` / `failed` / `cancelled` / `overridden`),进行中读 `stage`(`remove` / `install` / `enable`,行内 why 显示「阶段:…」),失败定位读 `failedAt`(`registry` / `spec-host`)。原先按 `phase/state/status/ok` 猜的形状全都不存在——那正是更新中无限挂起的原因:终态永远匹配不上,且更早的 fetchLatest/specPromise 两段异步在 240s 兜底之前就不设防。
48
+ - **更新主路径全部走官方 remote API**(host 半区只挂一条本地包识别路由):`pluginManager.listBundles()`(清单+版本)、`installBundle("<名>@版本", { enabled, requestId })`(更新)、`waitForInstall(requestId)`(**仅**响应丢失后的恢复)、`cancelInstall(requestId)`(取消,回 `{status}`)。
49
+ - **`installBundle` 是「等到装完才返回」的调用,它的返回值就是终态**(0.1.4 的关键纠正):remote 信封是 `{ok, value}`,`value` 即 `{changed, application, stage, target, enabled?, error?, warnings?, packageResult?, bundle?, pendingBuilds?, approvedBuilds?, registries?, failedAt?}`。官方桌面端插件页也是 `await installBundle(...)` 之后直接 `settleInstall(result.value)`。
50
+ - **`waitForInstall` 不是进度轮询器**(**0.1.3 的理解是错的,那正是「更新永远超时」的真因**):它只用于**响应丢失后**恢复同一请求的结果,请求不在活动中时返回 `null`,而且**已完成的结果不保留**。`installBundle` 已经 await 到装完,此时再轮询 `waitForInstall` 必然一路读到 `null` → 空转到 240s 看门狗 → 面板报「更新超时」,真实结果(成功或 pnpm 报错)全被丢掉。0.1.4 起:正常路径只读 `res.value`;**仅当信封本身失败(`res.ok === false`,结果未知)**才调一次 `waitForInstall` 恢复,仍为 `null` 就如实报「已发起·未确认」。
51
+ - **终态判定读 `application`**:`applied` / `restart-required`(都算成功,后者提示重启)→ 回读版本核对;`cancelled` → 该行退回「有新版本」(取消不是失败,仍可重试);`failed` → 行标「更新失败」,并把 `error.code`(译成中文)+ `error.diagnostic` + `packageResult.kind/exitCode/output/logPath` + `pendingBuilds` + `failedAt` 一起写进行内 `why`;`overridden` → 提示被覆盖、可重试;未知值一律按「结果未知」处理,不硬判成功。
52
+ - **回读版本是唯一成功判据**:只有在 `listBundles()` 里读到该行版本、且**与安装前不同**时才判「已是最新」。回读**只用 `pluginManager.listBundles()`**——`pluginInventory` 只有 `list()` 没有 `listBundles`(踩过,会造成「回读版本失败」假阳性);装完瞬间清单可能短暂不可用,重试 3 次(1.5s 间隔)。比对基准 `busyBefore` 在 `startOne` 抓取,**不受中途「重新检查」重建行的影响**。
53
+ - **registry 比对在浏览器直连**:`registry.npmjs.org` 带 CORS *(实测),逐包**串行** fetch `/latest`,不打并发。代价要知道:比对源是 npmjs,而安装走 pnpm 自己的 registry(本机 `.npmrc` 指向 npmmirror)——镜像尚未同步到的新版本会「面板说有、装时报 no-matching-version」(见「待办」)。
54
+ - **取消要分清对象**(0.1.4 修):排队项点取消只把它自己摘出队列(`cancelQueued`);在装项点取消才 `cancelInstall(requestId)`,且**保留队列**,等这次落定后 `done() → pump()` 接着装下一项。旧实现 `requestCancel` 里一句 `queue = []` 把整个队列清空,于是「等待更新」的项全部退回「有新版本」不再开始——这就是「取消一项后,等待的项不更新、反而变回有新版本」的真因。`cancelInstall` 的回值 `{status: cancelled|not-running|too-late}` 也要读:`too-late` 说明已进入应用阶段、取消不掉,得如实告诉用户。
55
+ - **重新检查必须回填在飞状态**(0.1.4 修):`checkNow` 重建 `rows` 会把「更新中/等待更新」抹成「有新版本」,而 `busyName`/`queue` 仍在——此时点那一行会撞上 `enqueue` 的 `name === busyName` 保护而**静默无反应**(这就是「重新检查后无法更新」的真因),真正在装的那一项也失去取消入口。修法是重建行后立刻 `reconcileInstallStates()` 把 busy/queuing 盖回去,且这两种行不参与 registry 比对。
56
+ - **总看门狗(0.1.4 起只是兜底)**:正常路径一次 `installBundle` 就定案,不必再等 240s;看门狗只覆盖 fetchLatest(现场查 registry,fetch 无超时)这类异常分支,触发时报「结果未知」并 `cancelInstall`。
57
+ - **`enabled` 用清单里的真实状态**:`installBundle` 的 `enabled` 传 `!(row.enabled === false)`,保留用户手动禁用的选择(旧实现一律 `enabled: true`,会把禁用的插件重新启用)。
58
+ - **失败原因要可见**:`fail` / `unchanged` / `unconfirmed` / `error` 行在行下多渲染一行 `.why`——只放在 `title` 悬停提示里,等于 pnpm 的报错没显示。
59
+
54
60
 
55
61
  ## 动画修复(模块 4)
56
62
 
@@ -73,7 +79,11 @@
73
79
  - [ ] 有更新时按钮加角标(现在要点开面板才看得到)。
74
80
  - [ ] i18n(`ctx.locale.register(NS, {zh, en})`,照 dsh-model-organizer 写法)。
75
81
  - [ ] 会话内重启 dsh 的入口(需要 host 配合或官方重启 API,未调研)。
76
- - [x] 0.1.3:`waitForInstall` 终态已按实测收紧(application/stage/failedAt)+ 全程总看门狗。
82
+ - [x] 0.1.3:`waitForInstall` 终态按 zod schema 收紧了字段名(`application`/`stage`/`failedAt`)+ 全程总看门狗。**注意:这一版的结论是错的**——字段名猜对了,但把 `waitForInstall` 当进度轮询器用,而它只做「响应丢失恢复」;见 0.1.4 条目。
83
+ - [x] 0.1.4:终态改读 `installBundle().value`(`waitForInstall` 只在信封失败时兜一次);取消不再清空队列、排队项可单独取消;`checkNow` 回填 busy/queuing;失败原因(`error`/`packageResult`/`pendingBuilds`)落到面板;`enabled` 沿用清单里的真实状态。
84
+ - [ ] 比对 registry 与安装 registry 对齐:面板查 `registry.npmjs.org`,pnpm 走 `.npmrc`(npmmirror)。镜像未同步的新版本会造成「面板说有新版、装时报 no-matching-version」。可用 `pluginManager.registries()` 取实际 registry 再比对(需先确认镜像的 CORS)。
85
+ - [ ] `pendingBuilds` 的「允许这些脚本并重试」入口(`installBundle(spec, { approvedBuilds })`)。现在只在失败行里列出待授权名单。
86
+ - [ ] `removable === false` / `optional` / 带 `readOnlyReason` 的 bundle 在面板隐藏或标注——否则点了会以 `management-required` 失败。
77
87
  - [ ] `installBundle` 的 `registry` 参数补齐(官方传了,本插件省略)。
78
88
  - [ ] 插件被禁用/卸载时的运行时清理(目前 DOM 残留到页面刷新为止)。
79
89
 
package/README.md CHANGED
@@ -56,6 +56,8 @@ dsh plugin --profile <profile> add link:/absolute/path/to/dsh-ui-tweaks
56
56
  - 非 npm 来源的插件没有可比的 registry 版本,直接跳过比对:`link:` / `file:` 本地路径标「本地链接」;`git:` / `github:` / `http` 直装标「Git 直装」(例如 `github:Fisfzy/dsh-ego-browser` 会显示为 Git 直装,即使 npm 上有同名包,装的也是仓库源码、与 npm 版本不是一回事)
57
57
  - `@deepseek-ai` 官方插件直接从检查列表里剔除:它们的更新由 dsh 本体流程负责
58
58
  - 更新走 profile 内的插件安装接口,与官方插件管理页同源;安装按 registry 解析出的**精确版本号**发起(而非 `@latest`),不受 pnpm 发布时长策略(minimumReleaseAge)拦挡,刚发布的版本也能装
59
+ - 更新失败时行内会直接给出原因(pnpm 的输出、退出码与日志路径),不再只说一句「失败」;失败行可点击重试。取消只作用于你点的那一项:取消排队的项不影响正在装的,取消正在装的也不会丢掉后面排队的(它会自动接着装)
60
+ - 面板比对用的是 npm 官方 registry,而安装走你本机 pnpm 配置的源(如 npmmirror 镜像)。镜像尚未同步到的新版本会出现「面板说有新版、装时报找不到版本」,稍后镜像同步后重试即可
59
61
  - 个别插件(如社区插件管理页扩展)自带单独的「检查更新」入口,与本插件的检查互不关联,结果可能不同
60
62
  - 与启动器 dsh-web-shell 的分工:对话区滚动条、宽表格修复等界面增强已由本插件接管,dsh-web-shell 自身不再注入;其「检查更新」按钮仍负责 **dsh 本体**的比对与升级提示
61
63
 
@@ -65,4 +67,4 @@ MIT
65
67
 
66
68
  ---
67
69
 
68
- 想自己改这个插件、或者想了解它在内部怎么实现的,见 [DEVELOPING.md](./DEVELOPING.md)。
70
+ 想自己改这个插件、或者想了解它在内部怎么实现的,见 [DEVELOPMENT.md](./DEVELOPMENT.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.2";
17
+ const BUILD = "0.1.4";
18
18
 
19
19
  /* 每个模块的注册各套一层 guard:失败只记 warn(座位/模块失败模式是「静默消失」,
20
20
  必须主动吭声;但不能让一个模块的异常打断同文件里其它模块的 apply)。 */
@@ -474,6 +474,8 @@ window.__ModuleLoader__.load({
474
474
  var queue = []; // 待更新名单(串行队列)
475
475
  var busyName = ""; // 正在更新的那个
476
476
  var busyReq = "";
477
+ var busyBefore = ""; // 本次安装前该行的版本(回读比对基准,不受中途重查影响)
478
+ var busyLatest = ""; // 本次安装解析出的目标版本(取消后退回「有新版本」要用)
477
479
  var cancelling = false;
478
480
  var checking = false;
479
481
  var listeners = []; // 面板重绘订阅(面板未开时也维护状态)
@@ -526,11 +528,13 @@ window.__ModuleLoader__.load({
526
528
  return {
527
529
  name: name, version: ver, latest: "",
528
530
  state: isLocalSrc(src) ? "local" : (isRemoteSrc(src) ? "remote" : (ver ? "checking" : "unknown")),
529
- src: src, why: ""
531
+ src: src, enabled: b.enabled !== false, why: ""
530
532
  };
531
533
  }).filter(function (x) { return x.name; })
532
534
  /* 官方 @deepseek-ai 插件由 dsh 本体更新流程负责,直接从检查列表里剔除 */
533
535
  .filter(function (x) { return !/^@deepseek-ai\//.test(x.name); });
536
+ /* 与在飞的安装对齐(见 reconcileInstallStates):重查不得抹掉「更新中/等待更新」 */
537
+ reconcileInstallStates();
534
538
  checking = false;
535
539
  emit();
536
540
  // 先向 host 半区要非 registry 直装名单,命中的行直接标「本地链接」或「Git 直装」
@@ -577,144 +581,228 @@ window.__ModuleLoader__.load({
577
581
  waitScope();
578
582
  }
579
583
 
580
- /* ---- 更新执行:串行队列;装完回读实际版本判定真实结果 ---- */
584
+ /* ---- 更新执行:串行队列 ------------------------------------------------
585
+ * installBundle 是「等到装完才返回」的调用:返回信封的 .value 就是终态对象
586
+ * (application / stage / target / error / packageResult / pendingBuilds …),
587
+ * 官方桌面端 UI 也是 await 它之后直接拿 .value 定案。
588
+ * waitForInstall 只用于**响应丢失后的恢复**:请求不在活动中时返回 null,
589
+ * 且「已完成的结果不保留」。把它当进度轮询器(旧实现)必然永远读到 null、
590
+ * 一路空转到 240s 看门狗 —— 这就是「更新总是超时」的真因。
591
+ * 正常路径只读 res.value;仅当信封本身失败(res.ok === false,结果未知)才用
592
+ * waitForInstall 兜一次,仍为 null 就如实报「结果未知」。
593
+ * ------------------------------------------------------------------ */
594
+ function findRow(name) {
595
+ for (var i = 0; i < rows.length; i++) { if (rows[i].name === name) { return rows[i]; } }
596
+ return null;
597
+ }
598
+ function msgOf(e) { return String((e && e.message) || e); }
599
+
600
+ /* 重新检查后把在飞状态盖回新行:否则「更新中/等待更新」会被抹成「有新版本」,
601
+ 点它会撞上 enqueue 的 busyName 保护而毫无反应(表现为「重新检查后无法更新」),
602
+ 真正在装的那一项也失去取消入口。busy / queuing 行不参与 registry 比对。 */
603
+ function reconcileInstallStates() {
604
+ if (busyName) { var b = findRow(busyName); if (b) { b.state = "busy"; b.why = ""; } }
605
+ for (var i = 0; i < queue.length; i++) {
606
+ var q = findRow(queue[i]);
607
+ if (q) { q.state = "queuing"; q.why = ""; }
608
+ }
609
+ }
610
+
611
+ var ERROR_CODE_TEXT = {
612
+ "management-required": "该 profile 由桌面端独占管理,不能从这里安装",
613
+ "unaddressable": "插件地址无法解析",
614
+ "unknown-plugin": "未知插件",
615
+ "invalid-spec": "安装 spec 非法",
616
+ "ambiguous-install": "目标不明确,无法确定要装哪个包",
617
+ "not-bundle": "该包不是 dsh 组合包(缺少 bundle patch)",
618
+ "not-removable": "该包不可卸载",
619
+ "stop-profile": "profile 正在停止,请稍后重试",
620
+ "bundle-in-use": "插件正在使用中,无法替换",
621
+ "stale-approval": "构建脚本授权已失效,需重新授权",
622
+ "incompatible-version": "版本与当前 dsh 不兼容",
623
+ "operation-error": "安装命令执行失败(pnpm 非零退出)"
624
+ };
625
+ /* 失败原因必须落到面板上:旧实现只回一句「安装失败」,把 pnpm 的真实报错
626
+ (如 ERR_PNPM_UNEXPECTED_STORE)连同 logPath 一起丢掉,用户只看到「超时」。 */
627
+ function failureText(st) {
628
+ var parts = [];
629
+ var code = st && st.error && st.error.code;
630
+ parts.push("安装失败" + (code ? ":" + (ERROR_CODE_TEXT[code] || code) : ""));
631
+ var diag = st && st.error && st.error.diagnostic;
632
+ if (diag) { parts.push(String(diag)); }
633
+ var pr = st && st.packageResult;
634
+ if (pr) {
635
+ if (pr.kind) { parts.push("分类:" + pr.kind); }
636
+ if (pr.exitCode) { parts.push("pnpm 退出码 " + pr.exitCode); }
637
+ var out = String(pr.output || "").trim();
638
+ if (out) { parts.push("输出:" + out.slice(-800)); }
639
+ if (pr.logPath) { parts.push("日志:" + pr.logPath); }
640
+ }
641
+ var pend = st && st.pendingBuilds;
642
+ if (pend && pend.length) { parts.push("待授权的构建脚本:" + pend.join("、") + "(需在官方插件页授权后重试)"); }
643
+ if (st && st.failedAt) { parts.push("失败环节:" + st.failedAt); }
644
+ return parts.join(" | ");
645
+ }
646
+ function lostReplyText(res) {
647
+ var m = res && res.error && res.error.message;
648
+ return "安装请求未返回结果" + (m ? ":" + String(m) : "");
649
+ }
650
+
581
651
  function enqueue(name) {
652
+ if (name === busyName) { return; } // 正在装它(行内已是「更新中」态)
582
653
  if (busyName) {
583
- if (queue.indexOf(name) < 0 && name !== busyName) { queue.push(name); setRow(name, { state: "queuing" }); }
654
+ if (queue.indexOf(name) < 0) { queue.push(name); setRow(name, { state: "queuing" }); }
584
655
  return;
585
656
  }
586
657
  queue.push(name);
587
658
  pump();
588
659
  }
589
660
  function pump() {
661
+ if (busyName) { return; }
590
662
  var name = queue.shift();
591
663
  if (!name) { return; }
592
664
  startOne(name);
593
665
  }
666
+ /* 排队项取消:只把它自己摘出队列,其余照旧等待。 */
667
+ function cancelQueued(name) {
668
+ var i = queue.indexOf(name);
669
+ if (i < 0) { return; }
670
+ queue.splice(i, 1);
671
+ setRow(name, { state: "update", why: "" });
672
+ }
673
+
594
674
  function startOne(name) {
675
+ var row = findRow(name);
595
676
  busyName = name;
677
+ busyBefore = row ? row.version : "";
678
+ busyLatest = (row && row.latest) || "";
596
679
  busyReq = uuid();
597
- setRow(name, { state: "busy" });
680
+ setRow(name, { state: "busy", why: "" });
598
681
  emit();
599
682
  var pm = pickService(null, "remote.pluginManager");
600
- if (!pm) { finishOne(name, false, "remote.pluginManager 不可用"); return; }
683
+ if (!pm) { finishOne(name, "failed", "remote.pluginManager 不可用"); return; }
601
684
  var settled = false;
602
- /* 总看门狗:无论卡在 fetchLatest / installBundle / 轮询哪一段,240 秒未结束就判超时并请求取消 */
685
+ /* 看门狗只作兜底:正常路径一次调用就定案,不等它。 */
603
686
  var watchdog = setTimeout(function () {
604
- finish(false, "更新超时(240 秒未结束)");
605
- try { var pm2 = pickService(null, "remote.pluginManager"); if (pm2 && pm2.cancelInstall && busyReq) { pm2.cancelInstall(busyReq); } } catch (err) {}
687
+ finish("unknown", "更新超时(240 秒未结束);结果未知,请点「重新检查」确认");
688
+ try { var pm2 = pickService(null, "remote.pluginManager"); if (pm2 && pm2.cancelInstall && busyReq) { pm2.cancelInstall(busyReq); } } catch (e) {}
606
689
  }, 240000);
607
- function finish(ok, why) {
690
+ function finish(kind, why) {
608
691
  if (settled) { return; }
609
692
  settled = true;
610
- clearTimeout(watchdog);
611
- finishOne(name, ok, why || "");
693
+ clearTimeout(watchdog);
694
+ finishOne(name, kind, why || "");
612
695
  }
613
- /* 安装 spec 用面板已解析的精确版本(`name@版本`)而不是 @latest:
614
- * pnpm 11 的 minimumReleaseAge 门禁只拦「最新版本」解析 —— 发布不足
615
- * 24h 的新版会被静默跳过、回落到旧版;显式版本则会触发 pnpm 的
616
- * minimumReleaseAgeExclude 自动豁免并照常安装(社区分叉版
617
- * @linxin666/dsh-client-ui-plugin-manager 的 update 正是这样:先
618
- * fetchManifest 拿 registry 的 version,再按 `id@版本` 安装)。
619
- * 行内没解析过(没点过检查)则现场查 registry,仍失败退回 @latest。 */
620
- var row = null;
621
- for (var i = 0; i < rows.length; i++) { if (rows[i].name === name) { row = rows[i]; break; } }
622
- var specPromise = (row && row.latest)
623
- ? Promise.resolve(name + "@" + row.latest)
696
+ function settle(st) {
697
+ if (!st || typeof st !== "object") { finish("unknown", "安装未返回可判定的结果"); return; }
698
+ var app = st.application;
699
+ if (app === "applied") { finish("applied", ""); return; }
700
+ if (app === "restart-required") { finish("applied", "已应用,重启桌面端后生效"); return; }
701
+ if (app === "cancelled") { finish("cancelled", "已取消"); return; }
702
+ if (app === "overridden") { finish("failed", "已被另一次安装覆盖,请重试"); return; }
703
+ if (app === "failed") { finish("failed", failureText(st)); return; }
704
+ finish("unknown", "安装返回了无法识别的结果(application=" + String(app) + ")");
705
+ }
706
+ function lostReply(res) {
707
+ var pm2 = pickService(null, "remote.pluginManager");
708
+ var wait = pm2 && pm2.waitForInstall;
709
+ if (!wait) { finish("unknown", lostReplyText(res) + ",且无法恢复,请点「重新检查」确认"); return; }
710
+ Promise.resolve(wait.call(pm2, busyReq)).then(function (r) {
711
+ var st = r && r.ok ? r.value : null;
712
+ if (st) { settle(st); return; }
713
+ finish("unknown", lostReplyText(res) + ";该请求已不在活动中,结果未知,请点「重新检查」确认");
714
+ }).catch(function () {
715
+ finish("unknown", lostReplyText(res) + ";恢复结果失败,请点「重新检查」确认");
716
+ });
717
+ }
718
+ /* 安装 spec 用面板已解析的精确版本(`name@版本`),不用 @latest:pnpm 11 的
719
+ minimumReleaseAge 只拦「最新版本」解析,显式版本会触发
720
+ minimumReleaseAgeExclude 自动豁免并照常安装。行内没解析过则现场查 registry。 */
721
+ var specPromise = busyLatest
722
+ ? Promise.resolve(name + "@" + busyLatest)
624
723
  : fetchLatest(name)
625
724
  .then(function (v) { return v ? name + "@" + v : name + "@latest"; })
626
725
  .catch(function () { return name + "@latest"; });
726
+ /* 保留原本的启用选择:不再一律 enabled:true(那会把用户手动禁用的插件重新启用)。 */
727
+ var enabled = !(row && row.enabled === false);
627
728
  specPromise.then(function (spec) {
628
- try {
629
- pm.installBundle(spec, { enabled: true, requestId: busyReq })
630
- .then(function (res) {
631
- if (!res || res.ok === false) {
632
- finish(false, (res && res.failure && (res.failure.message || res.failure.reason)) || (res && res.message) || "安装请求被拒");
633
- return;
634
- }
635
- /* 轮询进度;终态形态未知,多形状都认,240s 兜底 */
636
- var t0 = Date.now();
637
- function poll() {
638
- var done = false;
639
- try {
640
- var p = pm.waitForInstall ? pm.waitForInstall(busyReq) : null;
641
- Promise.resolve(p).then(function (st) {
642
- if (!st) { return; }
643
- var app = st.application;
644
- if (app === "applied" || app === "restart-required") { done = true; finish(true, app === "restart-required" ? "已应用,重启桌面端后生效" : ""); return; }
645
- if (app === "failed") { done = true; finish(false, "安装失败" + (st.failedAt ? "(" + st.failedAt + ")" : "")); return; }
646
- if (app === "cancelled") { done = true; finish(false, "已取消"); return; }
647
- if (st.stage) { setRow(name, { state: "busy", why: "阶段:" + st.stage }); emit(); }
648
- }).catch(function () { /* 轮询失败不作为终态 */ });
649
- } catch (e) {}
650
- if (!done && !settled && Date.now() - t0 < 240000) { setTimeout(poll, 900); }
651
- else if (!settled && Date.now() - t0 >= 240000) { finish(false, "更新超时(240 秒未结束)"); }
652
- }
653
- poll();
654
- })
655
- .catch(function (e) { finish(false, String(e && e.message || e)); });
656
- } catch (e) { finish(false, String(e && e.message || e)); }
657
- });
658
- specPromise.catch(function (e) { finish(false, "解析安装版本失败:" + String(e && e.message || e)); });
729
+ return pm.installBundle(spec, { enabled: enabled, requestId: busyReq });
730
+ }).then(function (res) {
731
+ if (!res || res.ok === false) { lostReply(res); return; }
732
+ settle(res.value);
733
+ }).catch(function (e) { finish("failed", "安装请求失败:" + msgOf(e)); });
659
734
  }
660
- /* 装完回读清单:版本变了才算真的装上——安装走精确版本后 pnpm 已不会因
661
- * minimumReleaseAge 拦新版本,但如果 registry 与 pnpm 解析不一致(如
662
- * profile 配了镜像源),仍可能装回旧版,所以回读判据保留作最后兜底。
663
- * 只用 pluginManager.listBundles —— pluginInventory 没有 listBundles 方法
664
- * (只有 list()),之前先取 inventory 导致 guard 落到 reject,面板上的
665
- * 「安装已发起,但回读版本失败」假阳性就是它。装完瞬间清单可能短暂
666
- * 不可用,重试 3 次(间隔 1.5s)再下结论。 */
667
- function finishOne(name, ok, why) {
668
- var before = null;
669
- for (var i = 0; i < rows.length; i++) { if (rows[i].name === name) { before = rows[i].version; break; } }
735
+
736
+ /* 终态落定:回读清单核对真实版本(registry 解析与 pnpm 实际安装可能不一致,
737
+ 如镜像源),再按 kind 决定文案。kind: applied | failed | cancelled | unknown。
738
+ 回读只用 pluginManager.listBundles(pluginInventory 只有 list(),没有
739
+ listBundles);装完瞬间清单可能短暂不可用,重试 3 次(1.5s 间隔)再下结论。 */
740
+ function finishOne(name, kind, why) {
741
+ var before = busyBefore;
742
+ var latest = busyLatest;
670
743
  var pm = pickService(null, "remote.pluginManager");
671
744
  function done() {
672
- busyName = ""; busyReq = ""; cancelling = false;
745
+ busyName = ""; busyReq = ""; busyBefore = ""; busyLatest = "";
746
+ cancelling = false;
673
747
  emit();
674
748
  pump();
675
749
  }
676
- function attempt(n) {
677
- if (!pm || !pm.listBundles) {
678
- setRow(name, ok ? { state: "unconfirmed", why: "安装已发起;清单服务不可用,未能回读版本" } : { state: "error", why: why || "更新失败" });
679
- done();
680
- return;
750
+ function judge(ver) {
751
+ if (ver && ver !== before) {
752
+ setRow(name, { version: ver, state: "current", latest: ver, why: "" });
753
+ } else if (kind === "cancelled") {
754
+ /* 取消不是失败:该行仍有新版本,退回可再次更新的状态。 */
755
+ setRow(name, { state: "update", latest: latest, why: "" });
756
+ } else if (kind === "failed") {
757
+ setRow(name, { state: "fail", why: why || "更新失败" });
758
+ } else if (kind === "applied" && ver) {
759
+ setRow(name, { state: "unchanged", why: "安装已应用,但版本未变化:可能 registry 与 pnpm 实际解析不一致(如配置了镜像源)" });
760
+ } else {
761
+ setRow(name, { state: "unconfirmed", why: why || "未能回读版本;点击重查确认" });
681
762
  }
763
+ done();
764
+ }
765
+ function attempt(n) {
766
+ if (!pm || !pm.listBundles) { judge(""); return; }
682
767
  Promise.resolve(pm.listBundles()).then(function (r) {
683
768
  var bundles = (r && r.ok && r.value) || [];
769
+ if (!bundles.length && n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
684
770
  var ver = "";
685
771
  for (var i = 0; i < bundles.length; i++) {
686
772
  if (bundles[i].name === name) { ver = bundles[i].version != null ? String(bundles[i].version) : ""; break; }
687
773
  }
688
- if (!bundles.length && n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
689
- if (ver) {
690
- if (before && ver === before) {
691
- setRow(name, { state: "unchanged", why: "版本未变化:可能 registry 解析与 pnpm 实际安装不一致(如配置了镜像源),也可能检查时远端版本信息有误" });
692
- } else {
693
- setRow(name, { version: ver, state: "current", latest: ver, why: "" });
694
- }
695
- } else if (ok) {
696
- setRow(name, { state: "unconfirmed", why: "安装已发起,但清单里没读到它;点击重查确认" });
697
- } else {
698
- setRow(name, { state: "error", why: why || "更新失败" });
699
- }
700
- done();
774
+ judge(ver);
701
775
  }).catch(function () {
702
776
  if (n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
703
- setRow(name, ok ? { state: "unconfirmed", why: "安装已发起,但回读版本失败;点击重查确认" } : { state: "error", why: why || "更新失败" });
704
- done();
777
+ judge("");
705
778
  });
706
779
  }
707
780
  attempt(0);
708
781
  }
782
+
783
+ /* 取消:排队项只摘自己(cancelQueued);在装项请求取消**本次**,队列保留,
784
+ 等它落定后 done() → pump() 自然接着装下一项。
785
+ 旧实现把整个 queue 清空,于是「等待更新」的项全部退回「有新版本」——
786
+ 这就是「取消一项后,等待的项不再开始、反而变回有新版本」的真因。 */
709
787
  function requestCancel() {
710
788
  if (!busyName || cancelling) { return; }
711
789
  cancelling = true;
712
- queue = [];
713
- rows.forEach(function (r) { if (r.state === "queuing") { r.state = r.latest && cmpSemver(r.latest, r.version) > 0 ? "update" : "current"; } });
714
- var pm = pickService(null, "remote.pluginManager");
715
- try { if (pm && pm.cancelInstall && busyReq) { pm.cancelInstall(busyReq); } } catch (e) {}
716
- setRow(busyName, { state: "error", why: "已请求取消" });
790
+ var name = busyName;
791
+ setRow(name, { state: "busy", why: "正在取消 …" });
717
792
  emit();
793
+ var pm = pickService(null, "remote.pluginManager");
794
+ if (!pm || !pm.cancelInstall || !busyReq) {
795
+ setRow(name, { state: "busy", why: "取消服务不可用,仍在等待它自行结束" });
796
+ return;
797
+ }
798
+ Promise.resolve(pm.cancelInstall(busyReq)).then(function (r) {
799
+ var status = r && r.ok && r.value ? r.value.status : "";
800
+ if (status === "too-late") { setRow(name, { state: "busy", why: "已进入应用阶段,取消不了,正在等它结束" }); }
801
+ else if (status === "not-running") { setRow(name, { state: "busy", why: "该请求已不在运行,等待结果回传" }); }
802
+ /* status === "cancelled":安装调用会以 application:"cancelled" 落定,不在此改状态 */
803
+ }).catch(function (e) {
804
+ setRow(name, { state: "busy", why: "取消请求失败:" + msgOf(e) + ";仍在等待它结束" });
805
+ });
718
806
  }
719
807
 
720
808
  /* ---- 面板 UI:挂在 body 的 Shadow DOM(fixed 浮层不能放进 flex 工具栏)---- */
@@ -741,9 +829,10 @@ specPromise.catch(function (e) { finish(false, "解析安装版本失败:" + S
741
829
  ".x{cursor:pointer;opacity:.55;font-size:17px;line-height:1;padding:0 3px}" +
742
830
  ".x:hover{opacity:1}" +
743
831
  ".rows{flex:1 1 auto;min-height:0;overflow:auto}" +
744
- ".row{display:flex;align-items:center;gap:8px;padding:7px 12px;" +
745
- "border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.12))}" +
746
- ".row:last-child{border-bottom:0}" +
832
+ ".rowwrap{border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.12))}" +
833
+ ".rowwrap:last-child{border-bottom:0}" +
834
+ ".row{display:flex;align-items:center;gap:8px;padding:7px 12px}" +
835
+ ".why{padding:0 12px 8px;font-size:12px;line-height:1.45;opacity:.7;word-break:break-word;white-space:pre-wrap}" +
747
836
  ".nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
748
837
  ".ver{opacity:.6;font-size:12px;white-space:nowrap;font-variant-numeric:tabular-nums}" +
749
838
  ".tag{font-size:11px;padding:1px 7px;border-radius:999px;white-space:nowrap;" +
@@ -831,7 +920,11 @@ specPromise.catch(function (e) { finish(false, "解析安装版本失败:" + S
831
920
  if (nm) { enqueue(nm); }
832
921
  return;
833
922
  }
834
- if (t.classList.contains("cancel")) { requestCancel(); return; }
923
+ if (t.classList.contains("cancel")) {
924
+ var nmc = t.getAttribute("data-n");
925
+ if (nmc && nmc !== busyName) { cancelQueued(nmc); } else { requestCancel(); }
926
+ return;
927
+ }
835
928
  if (t.classList.contains("retry")) {
836
929
  var nm2 = t.getAttribute("data-n");
837
930
  checkOne(nm2);
@@ -867,7 +960,7 @@ specPromise.catch(function (e) { finish(false, "解析安装版本失败:" + S
867
960
  var TAG_TEXT = {
868
961
  checking: "比对中 …", current: "已是最新", update: "有新版本",
869
962
  local: "本地链接", remote: "Git 直装", unknown: "无法比对", error: "检查失败", queuing: "等待更新",
870
- busy: "更新中 …", unchanged: "暂缓更新", unconfirmed: "已发起·未确认"
963
+ busy: "更新中 …", unchanged: "暂缓更新", unconfirmed: "已发起·未确认", fail: "更新失败"
871
964
  };
872
965
  function tagHtml(r) {
873
966
  var cls = "tag other", act = "", title = "";
@@ -877,7 +970,9 @@ specPromise.catch(function (e) { finish(false, "解析安装版本失败:" + S
877
970
  else if (r.state === "local") { title = " title=\"" + esc(r.src || r.why || "本地直装") + ";没有可比的 registry 版本\""; }
878
971
  else if (r.state === "remote") { title = " title=\"" + esc(r.src || r.why || "Git/URL 直装") + ";没有可比的 registry 版本\""; }
879
972
  else if (r.state === "unknown") { title = " title=\"" + esc(r.why || "插件清单未提供已装版本") + ",因此无法与 registry 比对\""; }
880
- else if (r.state === "queuing" || r.state === "busy") { cls = "tag other cancel clickable"; title = " title=\"点击取消" + (r.state === "busy" ? "本次更新" : "排队") + "\""; }
973
+ else if (r.state === "queuing") { cls = "tag other cancel clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"点击取消排队\""; }
974
+ else if (r.state === "busy") { cls = "tag other cancel clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why ? r.why + ";" : "") + "点击取消本次更新\""; }
975
+ else if (r.state === "fail") { cls = "tag error act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "更新失败") + "(点击重试)\""; }
881
976
  else if (r.state === "unchanged") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "") + "(点击重试)\""; }
882
977
  else if (r.state === "unconfirmed") { cls = "tag other reverify clickable"; title = " title=\"" + esc(r.why || "") + "(点击重查)\""; }
883
978
  return "<span class=\"" + cls + "\"" + act + title + ">" + esc(TAG_TEXT[r.state] || "未比对") + "</span>";
@@ -892,11 +987,14 @@ specPromise.catch(function (e) { finish(false, "解析安装版本失败:" + S
892
987
  body = "<div class=\"msg\">" + (checking ? "正在读取插件清单…" : "点上方「重新检查」开始比对") + "</div>";
893
988
  } else {
894
989
  body = rows.map(function (r) {
895
- var ver = r.latest && (r.state === "update" || r.state === "unchanged") ? r.version + " → " + r.latest : r.version;
896
- return "<div class=\"row\">" +
990
+ var showLatest = r.state === "update" || r.state === "unchanged" || r.state === "fail";
991
+ var ver = r.latest && showLatest ? r.version + " → " + r.latest : r.version;
992
+ var sub = r.why && (r.state === "fail" || r.state === "unchanged" || r.state === "unconfirmed" || r.state === "error")
993
+ ? "<div class=\"why\">" + esc(r.why) + "</div>" : "";
994
+ return "<div class=\"rowwrap\"><div class=\"row\">" +
897
995
  "<span class=\"nm\" title=\"" + esc(r.name) + "\">" + esc(r.name) + "</span>" +
898
996
  "<span class=\"ver\" title=\"" + esc(ver) + "\">" + esc(ver) + "</span>" +
899
- tagHtml(r) + "</div>";
997
+ tagHtml(r) + "</div>" + sub + "</div>";
900
998
  }).join("");
901
999
  }
902
1000
  var html = "<div class=\"hd\"><b>插件更新 · " + (busyName ? "正在更新 " + esc(busyName) + " …" : (checking ? "检查中…" : (rows.length ? upd + " 个插件有新版本" : "插件更新检查"))) + "</b>" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimoxie/dsh-ui-tweaks",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "dsh Web UI 微调合集:宽表格越界修复、对话区左侧自绘滚动条、插件更新检查面板(不含 dsh 本体)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -17,7 +17,7 @@
17
17
  "lib",
18
18
  "cordis.patch.yml",
19
19
  "README.md",
20
- "DEVELOPING.md",
20
+ "DEVELOPMENT.md",
21
21
  "LICENSE"
22
22
  ],
23
23
  "keywords": [