dsh-plugin-update 0.3.1 → 0.4.0

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.
@@ -1,5 +1,6 @@
1
1
  // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/panel-batch.ts
2
2
  import { assertPrefix, MIN_PANEL_POLL_MS } from "./config.js";
3
+ import { validReleaseVersion } from "./service.js";
3
4
  import {
4
5
  isTerminalPhase,
5
6
  normalizeBatchSession
@@ -111,6 +112,7 @@ function renderBatchPanelHTML(input) {
111
112
  const notice = typeof input.notice === "string" && input.notice ? input.notice : null;
112
113
  const noticeKey = typeof input.noticeKey === "string" && input.noticeKey ? input.noticeKey : null;
113
114
  const detailNoticeKey = notice !== null && noticeKey !== null && noticeKey === expandedKey ? noticeKey : null;
115
+ const changelogs = input.changelogs && typeof input.changelogs === "object" ? input.changelogs : {};
114
116
  const ctx = {
115
117
  expanded: false,
116
118
  busy: installing,
@@ -118,15 +120,16 @@ function renderBatchPanelHTML(input) {
118
120
  theme,
119
121
  titles,
120
122
  skipped: null,
121
- notice: null
123
+ notice: null,
124
+ changelogMarkdown: null
122
125
  };
123
126
  const parts = [];
124
127
  parts.push('<div class="dsh-upd-batch-head">');
125
128
  parts.push('<div class="dsh-upd-batch-title">\u66F4\u65B0\u6863\u6848 \xB7 <i>\u603B\u8D26</i></div>');
126
129
  parts.push('<div class="dsh-upd-batch-macros">');
127
- parts.push('<button type="button" data-act="check"' + disabled + ">\u68C0\u67E5\u66F4\u65B0</button>");
128
- parts.push('<button type="button" data-act="install" data-primary="1"' + disabled + ">\u5168\u90E8\u66F4\u65B0</button>");
129
- if (mode === "dialog") parts.push('<button type="button" data-act="close">\u5173\u95ED</button>');
130
+ parts.push('<button type="button" data-act="check"' + disabled + ' title="\u91CD\u65B0\u8BFB\u53D6\u6279\u91CF\u72B6\u6001\uFF08\u53EA\u8BFB\uFF09">\u68C0\u67E5\u66F4\u65B0</button>');
131
+ parts.push('<button type="button" data-act="install" data-primary="1"' + disabled + ' title="\u628A\u6709\u65B0\u7248\u7684\u51E0\u5BB6\u4E00\u6B21\u63D0\u4EA4\uFF1B\u540C\u4E00\u4F1A\u8BDD\u540C\u4E00\u5E42\u7B49\u7F16\u53F7">\u5168\u90E8\u66F4\u65B0</button>');
132
+ if (mode === "dialog") parts.push('<button type="button" data-act="close" title="\u5173\u95ED\u9762\u677F\uFF08\u6279\u91CF\u63A8\u8FDB\u5728\u5BBF\u4E3B\u4FA7\u7EE7\u7EED\u8DD1\uFF09">\u5173\u95ED</button>');
130
133
  parts.push("</div>");
131
134
  parts.push("</div>");
132
135
  const summary = loaded ? batchSummaryText(counts, rows, total, installing, lastError !== null) : "\u6B63\u5728\u8BFB\u53D6\u6279\u91CF\u66F4\u65B0\u72B6\u6001\u2026";
@@ -142,7 +145,8 @@ function renderBatchPanelHTML(input) {
142
145
  ...ctx,
143
146
  expanded,
144
147
  skipped,
145
- notice: expanded && row.key === detailNoticeKey ? notice : null
148
+ notice: expanded && row.key === detailNoticeKey ? notice : null,
149
+ changelogMarkdown: expanded && typeof changelogs[row.key] === "string" ? changelogs[row.key] : null
146
150
  })
147
151
  );
148
152
  }
@@ -153,7 +157,10 @@ function renderBatchPanelHTML(input) {
153
157
  if (total > 0 && !terminal) {
154
158
  const more = [];
155
159
  if (stalled) more.push('<button type="button" data-act="resume">\u63A5\u7740\u4E0A\u6B21</button>');
156
- more.push('<button type="button" data-act="cancel"' + disabled + ">\u53D6\u6D88\u8FD9\u4E00\u6279</button>");
160
+ const confirmCancel = input.confirmCancel === true;
161
+ more.push(
162
+ '<button type="button" data-act="cancel"' + disabled + (confirmCancel ? ' data-confirm="1" title="\u518D\u70B9\u4E00\u6B21\u786E\u8BA4\u53D6\u6D88"' : ' title="\u53D6\u6D88\u8FD9\u4E00\u6279\uFF08\u8981\u70B9\u4E24\u6B21\u786E\u8BA4\uFF0C\u9632\u8BEF\u89E6\uFF09"') + ">" + (confirmCancel ? "\u786E\u8BA4\u53D6\u6D88\u8FD9\u4E00\u6279" : "\u53D6\u6D88\u8FD9\u4E00\u6279") + "</button>"
163
+ );
157
164
  parts.push('<div class="dsh-upd-batch-more">' + more.join("") + "</div>");
158
165
  }
159
166
  if (notice !== null && detailNoticeKey === null) {
@@ -263,6 +270,7 @@ function detailHTML(row, ctx) {
263
270
  const html = renderUpdatePanelHTML({
264
271
  snapshot: asSnapshotLike(row.snapshot),
265
272
  manual: typeof row.manual === "string" ? row.manual : null,
273
+ changelogMarkdown: typeof ctx.changelogMarkdown === "string" ? ctx.changelogMarkdown : null,
266
274
  queue: asQueueLike(row.queue),
267
275
  // 跳过是面板本地记录(与单插件面板同一套语义):内核按「已跳过」画 tag 与提示行,
268
276
  // 恢复按钮由上面的动作行提供,措辞与内核提示里的「恢复」对得上。
@@ -363,6 +371,8 @@ const UPDATE_BATCH_PANEL_CSS = [
363
371
  ".dsh-upd-batch-ledger{margin:8px 0 0;font-size:13px;opacity:.85}",
364
372
  ".dsh-upd-batch-more{margin-top:6px}",
365
373
  ".dsh-upd-batch-more button{font-size:12px}",
374
+ // 确认态:红框红字,与「关闭」等中性按钮一眼区分;disabled 照旧置灰。
375
+ '.dsh-upd-batch-more button[data-confirm="1"]{border-color:var(--dsh-upd-bad-line,#dc2626);color:var(--dsh-upd-bad-line,#dc2626)}',
366
376
  ".dsh-upd-batch-notice{margin-top:6px;font-size:12.5px;opacity:.85}",
367
377
  ".dsh-upd-bdetail-actions{margin:0 0 6px}",
368
378
  ".dsh-upd .dsh-upd-bdetail-actions button{font-size:12.5px}",
@@ -617,6 +627,11 @@ function mountUpdateBatchPanel(container, options) {
617
627
  const onRestartRequested = options.onRestartRequested;
618
628
  const copyText = options.copyText ?? defaultCopyText;
619
629
  const skipStores = /* @__PURE__ */ new Map();
630
+ const autoChangelogEnabled = options.autoChangelog !== false;
631
+ const rowChangelog = /* @__PURE__ */ new Map();
632
+ const rowInflight = /* @__PURE__ */ new Set();
633
+ const rowFailed = /* @__PURE__ */ new Map();
634
+ let rowAutoSeq = 0;
620
635
  let rows = [];
621
636
  let expandedKey = null;
622
637
  let lastError = null;
@@ -625,7 +640,60 @@ function mountUpdateBatchPanel(container, options) {
625
640
  let inFlight = false;
626
641
  let loaded = false;
627
642
  let refreshing = false;
643
+ let confirmCancelArmed = false;
628
644
  let mounted = true;
645
+ function changelogView() {
646
+ const out = {};
647
+ for (const row of rows) {
648
+ const cached = rowChangelog.get(row.key);
649
+ if (!cached || typeof cached.markdown !== "string") continue;
650
+ if (latestVersionOf(row) !== cached.version) continue;
651
+ out[row.key] = cached.markdown;
652
+ }
653
+ return out;
654
+ }
655
+ function changelogPhoneOf(row) {
656
+ const phones2 = isObject(row.phoneNames) ? row.phoneNames : null;
657
+ if (!phones2) return null;
658
+ const name = phones2["updateChangelog"];
659
+ return typeof name === "string" && name ? name : null;
660
+ }
661
+ function maybeAutoRowChangelog(key) {
662
+ if (!autoChangelogEnabled || !mounted || !key) return;
663
+ const row = rowOfKey(key);
664
+ if (!row || asBatchPhase(row.phase) !== "ready") return;
665
+ const version = latestVersionOf(row);
666
+ if (!version || !validReleaseVersion(version)) return;
667
+ try {
668
+ if (skipStoreFor(pluginIdOf(row)).has(version)) return;
669
+ } catch {
670
+ }
671
+ const cached = rowChangelog.get(key);
672
+ if (cached && cached.version === version) return;
673
+ if (rowInflight.has(key)) return;
674
+ if (rowFailed.get(key) === version) return;
675
+ const phone2 = changelogPhoneOf(row);
676
+ if (!phone2) return;
677
+ rowInflight.add(key);
678
+ const seq = rowAutoSeq;
679
+ void Promise.resolve().then(() => call(phone2, { version })).then(
680
+ (reply) => {
681
+ rowInflight.delete(key);
682
+ if (!mounted || seq !== rowAutoSeq) return;
683
+ if (!isObject(reply) || reply["ok"] !== true) {
684
+ rowFailed.set(key, version);
685
+ return;
686
+ }
687
+ const md = reply["markdown"];
688
+ rowChangelog.set(key, { version, markdown: typeof md === "string" ? md : null });
689
+ if (typeof md === "string") render();
690
+ },
691
+ () => {
692
+ rowInflight.delete(key);
693
+ if (mounted && seq === rowAutoSeq) rowFailed.set(key, version);
694
+ }
695
+ );
696
+ }
629
697
  function render() {
630
698
  if (!mounted) return;
631
699
  container.innerHTML = renderBatchPanelHTML({
@@ -639,7 +707,9 @@ function mountUpdateBatchPanel(container, options) {
639
707
  skippedVersions: skippedVersionsOf(),
640
708
  inFlight,
641
709
  loaded,
642
- titles
710
+ titles,
711
+ confirmCancel: confirmCancelArmed,
712
+ changelogs: changelogView()
643
713
  });
644
714
  }
645
715
  function rowOfKey(key) {
@@ -717,6 +787,7 @@ function mountUpdateBatchPanel(container, options) {
717
787
  refreshing = false;
718
788
  }
719
789
  render();
790
+ maybeAutoRowChangelog(expandedKey);
720
791
  }
721
792
  async function phone(name, args, busyRowKey = null) {
722
793
  if (!mounted || inFlight) return;
@@ -731,6 +802,7 @@ function mountUpdateBatchPanel(container, options) {
731
802
  return;
732
803
  }
733
804
  applyReply(reply);
805
+ rowFailed.clear();
734
806
  } catch {
735
807
  if (!mounted) return;
736
808
  lastError = "check-failed";
@@ -738,9 +810,14 @@ function mountUpdateBatchPanel(container, options) {
738
810
  inFlight = false;
739
811
  if (mounted) render();
740
812
  }
813
+ maybeAutoRowChangelog(expandedKey);
741
814
  }
742
815
  async function act(kind, key) {
743
816
  if (!mounted) return;
817
+ if (kind !== "cancel" && confirmCancelArmed) {
818
+ confirmCancelArmed = false;
819
+ render();
820
+ }
744
821
  switch (kind) {
745
822
  case "check":
746
823
  return phone(phones.check, {});
@@ -757,7 +834,10 @@ function mountUpdateBatchPanel(container, options) {
757
834
  render();
758
835
  return;
759
836
  }
837
+ // 程序调用(controller.act)即执行:调它就是明确意图,不上膛。
838
+ // 鼠标误触的防护在 onClick 那一层(第一次点击只上膛)。
760
839
  case "cancel": {
840
+ confirmCancelArmed = false;
761
841
  await phone(phones.cancel, {});
762
842
  if (!mounted) return;
763
843
  say("\u5DF2\u53D6\u6D88\u8FD9\u4E00\u6279\uFF1A\u5269\u4E0B\u7684\u4E0D\u518D\u63A8\u8FDB\uFF1B\u8981\u91CD\u6765\u70B9\u300C\u68C0\u67E5\u66F4\u65B0\u300D\u3002");
@@ -833,6 +913,7 @@ function mountUpdateBatchPanel(container, options) {
833
913
  if (typeof key !== "string" || !key) return;
834
914
  expandedKey = expandedKey === key ? null : key;
835
915
  render();
916
+ maybeAutoRowChangelog(expandedKey);
836
917
  return;
837
918
  }
838
919
  // 「重启宿主」:宿主没有重启自己的电话,只做入口——调用方给了 onRestartRequested 就交给它,
@@ -859,6 +940,14 @@ function mountUpdateBatchPanel(container, options) {
859
940
  return;
860
941
  }
861
942
  }
943
+ function onKeyDown(ev) {
944
+ try {
945
+ const e = ev;
946
+ if (!mounted || mode !== "dialog" || !e || e.key !== "Escape") return;
947
+ unmount();
948
+ } catch {
949
+ }
950
+ }
862
951
  function setTheme(next) {
863
952
  if (next !== "default" && next !== "d5-paper") {
864
953
  throw new Error("[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 d5-paper\uFF08\u6536\u5230 " + JSON.stringify(next) + "\uFF09");
@@ -879,6 +968,13 @@ function mountUpdateBatchPanel(container, options) {
879
968
  const btn = t?.target && typeof t.target.closest === "function" ? t.target.closest("[data-act]") : null;
880
969
  const kind = btn?.getAttribute ? btn.getAttribute("data-act") : null;
881
970
  if (!kind) return;
971
+ if (kind === "cancel" && !confirmCancelArmed) {
972
+ confirmCancelArmed = true;
973
+ say("\u518D\u70B9\u4E00\u6B21\u300C\u786E\u8BA4\u53D6\u6D88\u8FD9\u4E00\u6279\u300D\u624D\u771F\u7684\u53D6\u6D88\uFF1B\u70B9\u522B\u7684\u6309\u94AE\u53EF\u64A4\u9500\u8FD9\u6B21\u786E\u8BA4\u3002");
974
+ render();
975
+ return;
976
+ }
977
+ if (kind === "cancel") confirmCancelArmed = false;
882
978
  const key = btn?.getAttribute ? btn.getAttribute("data-key") : null;
883
979
  void act(kind, key === null ? void 0 : key);
884
980
  } catch {
@@ -887,6 +983,8 @@ function mountUpdateBatchPanel(container, options) {
887
983
  function unmount() {
888
984
  if (!mounted) return;
889
985
  mounted = false;
986
+ rowAutoSeq++;
987
+ rowInflight.clear();
890
988
  try {
891
989
  timer.clear(handle);
892
990
  } catch {
@@ -895,12 +993,20 @@ function mountUpdateBatchPanel(container, options) {
895
993
  container.removeEventListener?.("click", onClick);
896
994
  } catch {
897
995
  }
996
+ try {
997
+ container.removeEventListener?.("keydown", onKeyDown);
998
+ } catch {
999
+ }
898
1000
  }
899
1001
  render();
900
1002
  try {
901
1003
  container.addEventListener?.("click", onClick);
902
1004
  } catch {
903
1005
  }
1006
+ try {
1007
+ container.addEventListener?.("keydown", onKeyDown);
1008
+ } catch {
1009
+ }
904
1010
  const timer = getTimer();
905
1011
  const handle = timer.set(() => {
906
1012
  void refresh();
package/dist/panel.d.ts CHANGED
@@ -43,9 +43,15 @@ export interface UpdatePanelOptions {
43
43
  /**
44
44
  * 更新日志 Markdown(#23 包内 CHANGELOG 展示):
45
45
  * 调用方按“已装版离线读、新版按需取 tarball”备好后传入(取不到传 null/空串即中性提示);
46
- * 面板只渲染不取数,缺日志永不挡安装、不写 blockedReason。
46
+ * 缺日志永不挡安装、不写 blockedReason。
47
+ * 显式传入即赢:自动链路(见 autoChangelog)永不覆盖它。
47
48
  */
48
49
  changelogMarkdown?: string | null;
50
+ /**
51
+ * 自动取日志(#38):默认开。有新版时面板按 latestVersion 调一次宿主更新日志电话,
52
+ * 回来经内部通道换日志节;显式传过 changelogMarkdown 或传 false 即退回手动模式。
53
+ */
54
+ autoChangelog?: boolean;
49
55
  }
50
56
  /** 挂载点:只要有 innerHTML 的容器即可(浏览器元素或测试替身都行)。 */
51
57
  export interface UpdatePanelContainer {
@@ -221,6 +227,11 @@ export interface PanelRenderInput extends PanelViewInput {
221
227
  profileName?: string | null;
222
228
  /** 宿主种类:进诊断文本;缺省显示“未知”。 */
223
229
  hostKind?: string | null;
230
+ /**
231
+ * 在途动作(仅挂载态内部用:点下查新版/安装到回包前的那一帧,按钮置忙)。
232
+ * 缺省即静默(输出与旧版一字不差);只在挂载器置忙的那次 render 里传。
233
+ */
234
+ busyAct?: 'check' | 'install' | null;
224
235
  /**
225
236
  * 动作面归谁:`default`(缺省)由内核画动作按钮;`none` 只画内容、不画按钮。
226
237
  * 给「调用方自己提供动作面」的场景(如批量面板的详情:动作由批量面板经自己的通道提供)。
@@ -242,6 +253,8 @@ export declare function renderUpdatePanelHTML(input: PanelRenderInput): string;
242
253
  export declare function mountUpdatePanel(container: UpdatePanelContainer, options: UpdatePanelOptions): UpdatePanelController;
243
254
  export { buildPhoneNames as buildPanelPhoneNames } from './config.js';
244
255
  export type { PhoneAction as PanelPhoneAction } from './config.js';
256
+ export { buildChangelogPhoneName as buildPanelChangelogPhoneName } from './config.js';
257
+ export type { ChangelogPhoneAction as PanelChangelogPhoneAction } from './config.js';
245
258
  /** 面板轮询口径(默认 1 秒、下限 250 毫秒,与宿主侧同一套)。 */
246
259
  export declare const PANEL_POLL: {
247
260
  readonly defaultMs: 1000;
package/dist/panel.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/panel.ts
2
- import { buildPhoneNames, DEFAULT_PANEL_POLL_MS, MIN_PANEL_POLL_MS } from "./config.js";
2
+ import { buildChangelogPhoneName, buildPhoneNames, DEFAULT_PANEL_POLL_MS, MIN_PANEL_POLL_MS } from "./config.js";
3
3
  import { COPY_BUDGET_CHARS, sanitizeForCopy } from "./redaction.js";
4
4
  import { validReleaseVersion } from "./service.js";
5
5
  import {
@@ -551,8 +551,25 @@ const UPDATE_PANEL_CSS = [
551
551
  ".dsh-upd-prog-bar{display:block;height:100%;background:var(--dsh-upd-primary,#2563eb);transition:width .3s}",
552
552
  ".dsh-upd-progtxt{font-size:12.5px;opacity:.75}",
553
553
  ".dsh-upd-skipline{font-size:13px;margin-top:8px}",
554
+ // 骨架微光:只在首帧 loading 出现;reduced-motion 下静止占位,不断语义。
555
+ ".dsh-upd-skv{display:inline-block;min-width:64px;border-radius:3px;color:transparent !important;user-select:none;",
556
+ "background:linear-gradient(90deg,var(--dsh-upd-line,#e5e7eb) 25%,var(--dsh-upd-soft,#f3f4f6) 50%,var(--dsh-upd-line,#e5e7eb) 75%);",
557
+ "background-size:200% 100%;animation:dsh-upd-shimmer 1.2s linear infinite}",
558
+ "@keyframes dsh-upd-shimmer{to{background-position:-200% 0}}",
554
559
  ".dsh-upd-tag{display:inline-block;border:1px dashed currentColor;border-radius:3px;padding:1px 8px;margin-right:8px;font-family:Consolas,Menlo,monospace;font-size:12px}",
555
560
  ".dsh-upd-err{font-size:13px;margin:0 0 6px}",
561
+ // —— 全按钮交互反馈(#36:悬停/按下/过渡/在途忙态;浅深双主题通用写法,不碰上面的既有串)——
562
+ ".dsh-upd button{transition:background-color .15s ease,border-color .15s ease,color .15s ease,transform .06s ease}",
563
+ ".dsh-upd button:hover:not(:disabled){border-color:var(--dsh-upd-focus,#2563eb)}",
564
+ '.dsh-upd button[data-primary="1"]:hover:not(:disabled){filter:brightness(.93)}',
565
+ ".dsh-upd button:active:not(:disabled){transform:translateY(1px)}",
566
+ '.dsh-upd button[aria-busy="true"]{cursor:wait;animation:dsh-upd-pulse 1s ease-in-out infinite}',
567
+ "@keyframes dsh-upd-pulse{0%,100%{opacity:1}50%{opacity:.55}}",
568
+ // 在途转圈:纯 CSS ::after,不加 DOM 节点(内核 DOM 冻结);转的是边框缺口,不是 emoji。
569
+ '.dsh-upd button[aria-busy="true"]::after{content:"";display:inline-block;width:11px;height:11px;margin-left:8px;vertical-align:-1px;',
570
+ "border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:dsh-upd-spin .8s linear infinite}",
571
+ "@keyframes dsh-upd-spin{to{transform:rotate(360deg)}}",
572
+ '@media (prefers-reduced-motion: reduce){.dsh-upd button{transition:none}.dsh-upd button:active:not(:disabled){transform:none}.dsh-upd button[aria-busy="true"]{animation:none}.dsh-upd-skv{animation:none}}',
556
573
  "@media (prefers-color-scheme: dark){.dsh-upd{--dsh-upd-fg:#e5e7eb;--dsh-upd-bg:#111827;--dsh-upd-line:#374151;",
557
574
  "--dsh-upd-btn:#1f2937;--dsh-upd-soft:#1f2937;--dsh-upd-primary:#3b82f6;--dsh-upd-focus:#93c5fd;",
558
575
  // 横幅深色覆盖:底色用低透明度同色系(不是浅色原值),边线提亮,保证「深底浅字」可读。
@@ -681,6 +698,11 @@ function chapterOf(index, inner, note = "") {
681
698
  const title = CHAPTER_TITLES[index - 1];
682
699
  return `<section class="dsh-upd-chapter" data-chapter="${no}"><div class="dsh-upd-chap-head"><span class="dsh-upd-chap-no">${no}</span><h3 class="dsh-upd-chap-title">${escapeHtml(title)}</h3><span class="dsh-upd-chap-rule"></span>${note}</div>${inner}</section>`;
683
700
  }
701
+ function skeletonStrip(loading) {
702
+ if (!loading) return "";
703
+ const cell = (k) => `<div><span class="dsh-upd-strip-k">${escapeHtml(k)}</span><span class="dsh-upd-strip-v dsh-upd-skv" aria-hidden="true">\u2026</span></div>`;
704
+ return `<div class="dsh-upd-strip" aria-hidden="true">` + cell("\u8FD0\u884C") + cell("\u78C1\u76D8") + cell("\u8FDC\u7AEF") + `</div>`;
705
+ }
684
706
  function versionStrip(snapshot) {
685
707
  const cell = (k, v) => `<div><span class="dsh-upd-strip-k">${escapeHtml(k)}</span><span class="dsh-upd-strip-v">${escapeHtml(v ?? "\u672A\u77E5")}</span></div>`;
686
708
  if (!snapshot) return "";
@@ -712,31 +734,34 @@ function renderUpdatePanelKernel(input, view) {
712
734
  parts.push(`<div><strong>${escapeHtml(b.title)}</strong></div>`);
713
735
  if (b.action) parts.push(`<div>${escapeHtml(b.action)}</div>`);
714
736
  parts.push("</div>");
715
- parts.push(versionStrip(snapshot));
737
+ parts.push(snapshot ? versionStrip(snapshot) : skeletonStrip(view.banner.kind === "loading"));
716
738
  const showActions = input.actions !== "none";
717
739
  const actions = [];
718
740
  if (showActions) {
741
+ const busyAct = input.busyAct;
742
+ const checkBusy = busyAct === "check";
743
+ const installBusy = busyAct === "install";
719
744
  actions.push('<div class="dsh-upd-actions">');
720
745
  actions.push(
721
- `<button type="button" data-action="check">\u67E5\u65B0\u7248</button><button type="button" data-action="install" data-primary="1"${view.installEnabled ? "" : " disabled"}>${escapeHtml(view.installLabel)}</button>`
746
+ (checkBusy ? `<button type="button" data-action="check" disabled aria-busy="true" title="\u6B63\u5728\u5411\u5B98\u65B9\u6E90\u67E5\u8BE2\uFF0C\u8BF7\u7A0D\u5019">\u6B63\u5728\u67E5\u65B0\u7248\u2026</button>` : `<button type="button" data-action="check" title="\u91CD\u65B0\u5411\u5B98\u65B9\u6E90\u67E5\u4E00\u6B21\u65B0\u7248\uFF08\u53EA\u8BFB\uFF0C\u4E0D\u5B89\u88C5\uFF09">\u67E5\u65B0\u7248</button>`) + (installBusy ? `<button type="button" data-action="install" data-primary="1" disabled aria-busy="true" title="\u6B63\u5728\u5B89\u88C5\uFF0C\u8BF7\u7A0D\u5019">\u6B63\u5728\u5B89\u88C5\u2026</button>` : `<button type="button" data-action="install" data-primary="1" title="\u7528\u7CBE\u786E\u7248\u672C\u5B89\u88C5\uFF1B\u540C\u4E00\u4F7F\u7528\u8303\u56F4\u540C\u65F6\u53EA\u88C5\u4E00\u4E2A"${view.installEnabled ? "" : " disabled"}>${escapeHtml(view.installLabel)}</button>`)
722
747
  );
723
748
  if (snapshot?.latestVersion && !view.skippedLatest && view.banner.kind === "update") {
724
- actions.push(`<button type="button" data-action="skip">\u8DF3\u8FC7\u8BE5\u7248\u672C</button>`);
749
+ actions.push(`<button type="button" data-action="skip" title="\u8BE5\u7248\u672C\u4E0D\u518D\u63D0\u9192\uFF1B\u6709\u66F4\u65B0\u7684\u65B0\u7248\u672C\u7167\u5E38\u63D0\u9192">\u8DF3\u8FC7\u8BE5\u7248\u672C</button>`);
725
750
  }
726
751
  if (view.showReset && snapshot?.latestVersion) {
727
- actions.push(`<button type="button" data-action="reset-skip">\u6062\u590D\uFF08${escapeHtml(snapshot.latestVersion)}\uFF09</button>`);
752
+ actions.push(`<button type="button" data-action="reset-skip" title="\u64A4\u9500\u8DF3\u8FC7\uFF0C\u8BE5\u7248\u672C\u91CD\u65B0\u63D0\u9192">\u6062\u590D\uFF08${escapeHtml(snapshot.latestVersion)}\uFF09</button>`);
728
753
  }
729
754
  if (view.showManual && manual) {
730
- actions.push(`<button type="button" data-action="copy-manual">\u590D\u5236\u624B\u5DE5\u547D\u4EE4</button>`);
755
+ actions.push(`<button type="button" data-action="copy-manual" title="\u590D\u5236\u624B\u5DE5\u547D\u4EE4\uFF0C\u7C98\u5230\u7EC8\u7AEF\u6574\u884C\u6267\u884C">\u590D\u5236\u624B\u5DE5\u547D\u4EE4</button>`);
731
756
  }
732
757
  if (b.kind === "restart") {
733
- actions.push(`<button type="button" data-action="restart-hint" data-primary="1">\u91CD\u542F\u5BBF\u4E3B</button>`);
758
+ actions.push(`<button type="button" data-action="restart-hint" data-primary="1" title="\u5BBF\u4E3B\u6CA1\u6709\u81EA\u91CD\u542F\u7535\u8BDD\uFF1A\u8BF7\u624B\u52A8\u91CD\u542F\u5BBF\u4E3B">\u91CD\u542F\u5BBF\u4E3B</button>`);
734
759
  }
735
760
  if (snapshot) {
736
- actions.push(`<button type="button" data-action="copy-diag">\u590D\u5236\u8BCA\u65AD</button>`);
761
+ actions.push(`<button type="button" data-action="copy-diag" title="\u590D\u5236\u5DF2\u8131\u654F\u8BCA\u65AD\uFF0C\u76F4\u63A5\u7C98\u7ED9\u63D2\u4EF6\u4F5C\u8005">\u590D\u5236\u8BCA\u65AD</button>`);
737
762
  }
738
763
  if (mode === "dialog") {
739
- actions.push(`<button type="button" data-action="close-view">\u5173\u95ED</button>`);
764
+ actions.push(`<button type="button" data-action="close-view" title="\u5173\u95ED\u9762\u677F\uFF08\u5B89\u88C5\u5728\u5BBF\u4E3B\u4FA7\u7EE7\u7EED\u8DD1\uFF0C\u53EF\u91CD\u5F00\u6062\u590D\u663E\u793A\uFF09">\u5173\u95ED</button>`);
740
765
  }
741
766
  actions.push("</div>");
742
767
  }
@@ -918,6 +943,13 @@ function mountUpdatePanel(container, options) {
918
943
  }
919
944
  const prefix = options.prefix === void 0 ? "wf" : options.prefix;
920
945
  const phoneNames = buildPhoneNames(prefix);
946
+ const changelogPhone = buildChangelogPhoneName(prefix);
947
+ const autoChangelogEnabled = typeof options.changelogMarkdown !== "string" && options.autoChangelog !== false;
948
+ let manualChangelogOverride = typeof options.changelogMarkdown === "string";
949
+ const changelogCache = /* @__PURE__ */ new Map();
950
+ const changelogInflight = /* @__PURE__ */ new Set();
951
+ const changelogFailed = /* @__PURE__ */ new Set();
952
+ let autoSeq = 0;
921
953
  const pollMs = options.pollMs === void 0 ? DEFAULT_PANEL_POLL_MS : options.pollMs;
922
954
  if (typeof pollMs !== "number" || !Number.isFinite(pollMs) || pollMs < MIN_PANEL_POLL_MS) {
923
955
  throw new Error(`[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2\u95F4\u9694\u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 ${JSON.stringify(options.pollMs)}\uFF09`);
@@ -948,20 +980,36 @@ function mountUpdatePanel(container, options) {
948
980
  let lastErrorKind = null;
949
981
  let lastDiag = null;
950
982
  let copyNotice = null;
983
+ let noticeExpiresAt = 0;
984
+ let busyAct = null;
951
985
  let mounted = true;
952
986
  let envProfileName = null;
953
987
  let envHostKind = null;
988
+ function sayCopy(text) {
989
+ copyNotice = text;
990
+ try {
991
+ noticeExpiresAt = Date.now() + 5e3;
992
+ } catch {
993
+ noticeExpiresAt = 0;
994
+ }
995
+ }
954
996
  function queueArgs() {
955
997
  return { includeQueue: true, includeEnv: true, showOthers, ...requestId ? { requestId } : {} };
956
998
  }
957
999
  function render() {
958
1000
  if (!mounted) return;
1001
+ if (!copyNotice) noticeExpiresAt = 0;
1002
+ else if (noticeExpiresAt && Date.now() > noticeExpiresAt) {
1003
+ copyNotice = null;
1004
+ noticeExpiresAt = 0;
1005
+ }
959
1006
  const latest = snapshot?.latestVersion ?? null;
960
1007
  const skippedLatest = !!latest && validReleaseVersion(latest) && skipStore.has(latest);
961
1008
  container.innerHTML = renderUpdatePanelHTML({
962
1009
  snapshot,
963
1010
  manual,
964
1011
  queue,
1012
+ busyAct,
965
1013
  skippedLatest,
966
1014
  lastError,
967
1015
  errorKind: lastErrorKind,
@@ -1003,6 +1051,44 @@ function mountUpdatePanel(container, options) {
1003
1051
  lastDiag = Object.prototype.hasOwnProperty.call(reply, "diag") ? reply["diag"] : null;
1004
1052
  }
1005
1053
  }
1054
+ function pendingAutoChangelogVersion() {
1055
+ if (!autoChangelogEnabled || manualChangelogOverride || !mounted) return null;
1056
+ const v = snapshot?.latestVersion ?? null;
1057
+ if (typeof v !== "string" || !validReleaseVersion(v)) return null;
1058
+ if (!snapshot || snapshot.canInstall !== true) return null;
1059
+ try {
1060
+ if (skipStore.has(v)) return null;
1061
+ } catch {
1062
+ }
1063
+ if (changelogCache.has(v) || changelogInflight.has(v) || changelogFailed.has(v)) return null;
1064
+ return v;
1065
+ }
1066
+ function maybeAutoChangelog() {
1067
+ const v = pendingAutoChangelogVersion();
1068
+ if (v === null) return;
1069
+ changelogInflight.add(v);
1070
+ const seq = autoSeq;
1071
+ void Promise.resolve().then(() => call(changelogPhone, { version: v })).then(
1072
+ (reply) => {
1073
+ changelogInflight.delete(v);
1074
+ if (!mounted || seq !== autoSeq) return;
1075
+ if (!reply || typeof reply !== "object" || reply["ok"] !== true) {
1076
+ changelogFailed.add(v);
1077
+ return;
1078
+ }
1079
+ const md = reply["markdown"];
1080
+ changelogCache.set(v, typeof md === "string" ? md : null);
1081
+ if (typeof md === "string") {
1082
+ changelogMarkdown = md;
1083
+ render();
1084
+ }
1085
+ },
1086
+ () => {
1087
+ changelogInflight.delete(v);
1088
+ if (mounted && seq === autoSeq) changelogFailed.add(v);
1089
+ }
1090
+ );
1091
+ }
1006
1092
  async function refresh() {
1007
1093
  if (!mounted) return;
1008
1094
  try {
@@ -1016,26 +1102,40 @@ function mountUpdatePanel(container, options) {
1016
1102
  lastDiag = null;
1017
1103
  }
1018
1104
  render();
1105
+ maybeAutoChangelog();
1019
1106
  }
1020
1107
  async function act(kind, arg) {
1021
1108
  if (!mounted) return;
1022
1109
  copyNotice = null;
1023
1110
  switch (kind) {
1024
1111
  case "check": {
1112
+ if (busyAct) return;
1113
+ busyAct = "check";
1114
+ copyNotice = "\u6B63\u5728\u67E5\u65B0\u7248\u2026";
1115
+ render();
1025
1116
  try {
1026
1117
  const reply = await call(phoneNames.updateCheck, queueArgs());
1027
1118
  if (!mounted) return;
1028
1119
  applyStatusReply(reply);
1120
+ changelogFailed.clear();
1029
1121
  } catch {
1030
1122
  if (!mounted) return;
1031
1123
  lastError = "check-failed";
1032
1124
  lastErrorKind = null;
1033
1125
  lastDiag = null;
1126
+ } finally {
1127
+ busyAct = null;
1128
+ if (copyNotice === "\u6B63\u5728\u67E5\u65B0\u7248\u2026") copyNotice = null;
1034
1129
  }
1035
1130
  render();
1131
+ maybeAutoChangelog();
1036
1132
  return;
1037
1133
  }
1038
1134
  case "install": {
1135
+ if (busyAct) return;
1136
+ busyAct = "install";
1137
+ copyNotice = "\u6B63\u5728\u5B89\u88C5\u2026";
1138
+ render();
1039
1139
  try {
1040
1140
  if (!receipt) {
1041
1141
  const checked = await call(phoneNames.updateCheck, queueArgs());
@@ -1063,8 +1163,12 @@ function mountUpdatePanel(container, options) {
1063
1163
  lastError = "check-failed";
1064
1164
  lastErrorKind = null;
1065
1165
  lastDiag = null;
1166
+ } finally {
1167
+ busyAct = null;
1168
+ if (copyNotice === "\u6B63\u5728\u5B89\u88C5\u2026") copyNotice = null;
1066
1169
  }
1067
1170
  render();
1171
+ maybeAutoChangelog();
1068
1172
  return;
1069
1173
  }
1070
1174
  case "skip": {
@@ -1090,9 +1194,9 @@ function mountUpdatePanel(container, options) {
1090
1194
  if (manual) {
1091
1195
  try {
1092
1196
  await copyText(manual);
1093
- copyNotice = "\u624B\u5DE5\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u7C98\u5230\u7EC8\u7AEF\u6574\u884C\u6267\u884C\u5373\u53EF\u3002";
1197
+ sayCopy("\u624B\u5DE5\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u7C98\u5230\u7EC8\u7AEF\u6574\u884C\u6267\u884C\u5373\u53EF\u3002");
1094
1198
  } catch {
1095
- copyNotice = "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u547D\u4EE4\u3002";
1199
+ sayCopy("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u547D\u4EE4\u3002");
1096
1200
  }
1097
1201
  }
1098
1202
  render();
@@ -1123,9 +1227,9 @@ function mountUpdatePanel(container, options) {
1123
1227
  });
1124
1228
  try {
1125
1229
  await copyText(text);
1126
- copyNotice = "\u8BCA\u65AD\u5DF2\u590D\u5236\uFF0C\u76F4\u63A5\u7C98\u7ED9\u63D2\u4EF6\u4F5C\u8005\u5373\u53EF\uFF08\u5DF2\u8131\u654F\uFF09\u3002";
1230
+ sayCopy("\u8BCA\u65AD\u5DF2\u590D\u5236\uFF0C\u76F4\u63A5\u7C98\u7ED9\u63D2\u4EF6\u4F5C\u8005\u5373\u53EF\uFF08\u5DF2\u8131\u654F\uFF09\u3002");
1127
1231
  } catch {
1128
- copyNotice = "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u4FE1\u606F\u3002";
1232
+ sayCopy("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u4FE1\u606F\u3002");
1129
1233
  }
1130
1234
  render();
1131
1235
  return;
@@ -1141,12 +1245,12 @@ function mountUpdatePanel(container, options) {
1141
1245
  try {
1142
1246
  if (typeof onRestartRequested === "function") {
1143
1247
  await onRestartRequested();
1144
- copyNotice = "\u5DF2\u6309\u8C03\u7528\u65B9\u7684\u91CD\u542F\u6D41\u7A0B\u5904\u7406\uFF1B\u91CD\u542F\u540E\u65B0\u7248\u751F\u6548\u3002";
1248
+ sayCopy("\u5DF2\u6309\u8C03\u7528\u65B9\u7684\u91CD\u542F\u6D41\u7A0B\u5904\u7406\uFF1B\u91CD\u542F\u540E\u65B0\u7248\u751F\u6548\u3002");
1145
1249
  } else {
1146
- copyNotice = "\u672C\u5BBF\u4E3B\u672A\u63D0\u4F9B\u91CD\u542F\u5165\u53E3\uFF1A\u8BF7\u624B\u52A8\u91CD\u542F\u5BBF\u4E3B\uFF0C\u91CD\u542F\u540E\u65B0\u7248\u751F\u6548\u3002";
1250
+ sayCopy("\u672C\u5BBF\u4E3B\u672A\u63D0\u4F9B\u91CD\u542F\u5165\u53E3\uFF1A\u8BF7\u624B\u52A8\u91CD\u542F\u5BBF\u4E3B\uFF0C\u91CD\u542F\u540E\u65B0\u7248\u751F\u6548\u3002");
1147
1251
  }
1148
1252
  } catch {
1149
- copyNotice = "\u91CD\u542F\u5165\u53E3\u8C03\u7528\u5931\u8D25\uFF1A\u8BF7\u624B\u52A8\u91CD\u542F\u5BBF\u4E3B\uFF0C\u91CD\u542F\u540E\u65B0\u7248\u751F\u6548\u3002";
1253
+ sayCopy("\u91CD\u542F\u5165\u53E3\u8C03\u7528\u5931\u8D25\uFF1A\u8BF7\u624B\u52A8\u91CD\u542F\u5BBF\u4E3B\uFF0C\u91CD\u542F\u540E\u65B0\u7248\u751F\u6548\u3002");
1150
1254
  }
1151
1255
  render();
1152
1256
  return;
@@ -1185,13 +1289,24 @@ function mountUpdatePanel(container, options) {
1185
1289
  } catch {
1186
1290
  }
1187
1291
  }
1292
+ function onKeyDown(ev) {
1293
+ try {
1294
+ const e = ev;
1295
+ if (!mounted || mode !== "dialog" || !e || e.key !== "Escape") return;
1296
+ unmount();
1297
+ } catch {
1298
+ }
1299
+ }
1188
1300
  function setChangelogMarkdown(markdown) {
1189
1301
  changelogMarkdown = typeof markdown === "string" ? markdown : null;
1302
+ if (typeof markdown === "string") manualChangelogOverride = true;
1190
1303
  render();
1191
1304
  }
1192
1305
  function unmount() {
1193
1306
  if (!mounted) return;
1194
1307
  mounted = false;
1308
+ autoSeq++;
1309
+ changelogInflight.clear();
1195
1310
  try {
1196
1311
  timer.clear(handle);
1197
1312
  } catch {
@@ -1200,12 +1315,20 @@ function mountUpdatePanel(container, options) {
1200
1315
  container.removeEventListener?.("click", onClick);
1201
1316
  } catch {
1202
1317
  }
1318
+ try {
1319
+ container.removeEventListener?.("keydown", onKeyDown);
1320
+ } catch {
1321
+ }
1203
1322
  }
1204
1323
  render();
1205
1324
  try {
1206
1325
  container.addEventListener?.("click", onClick);
1207
1326
  } catch {
1208
1327
  }
1328
+ try {
1329
+ container.addEventListener?.("keydown", onKeyDown);
1330
+ } catch {
1331
+ }
1209
1332
  const timer = getTimer();
1210
1333
  const handle = timer.set(() => {
1211
1334
  void refresh();
@@ -1219,6 +1342,7 @@ function mountUpdatePanel(container, options) {
1219
1342
  return { refresh, act, setMode, setTheme, setShowOthers, setChangelogMarkdown, unmount };
1220
1343
  }
1221
1344
  import { buildPhoneNames as buildPhoneNames2 } from "./config.js";
1345
+ import { buildChangelogPhoneName as buildChangelogPhoneName2 } from "./config.js";
1222
1346
  const PANEL_POLL = {
1223
1347
  defaultMs: DEFAULT_PANEL_POLL_MS,
1224
1348
  minMs: MIN_PANEL_POLL_MS
@@ -1270,6 +1394,7 @@ export {
1270
1394
  UPDATE_PANEL_D5_CSS,
1271
1395
  blockedCopy,
1272
1396
  buildDiagnosticText,
1397
+ buildChangelogPhoneName2 as buildPanelChangelogPhoneName,
1273
1398
  buildPhoneNames2 as buildPanelPhoneNames,
1274
1399
  buildUpdateDiagCopy,
1275
1400
  changelogForUpdate2 as changelogForUpdate,
package/dist/service.d.ts CHANGED
@@ -69,4 +69,18 @@ export declare function fetchNpmRelease(fetchImpl: FetchImpl, timeoutMs?: number
69
69
  registryUrl?: string;
70
70
  releaseChannel?: ReleaseChannel;
71
71
  }): Promise<ReleaseInfo>;
72
+ /**
73
+ * 按版本号取发行信息(#38 更新日志电话用:面板说要哪版就取哪版)。
74
+ *
75
+ * 与 fetchNpmRelease 同一信任根:只接受名字对得上、版本与请求完全一致且发行合法、
76
+ * 包地址与完整性校验全合规的返回,否则按版本信息无效处理。通道门禁同样复用
77
+ * (stable 只要纯三段,prerelease 才收预发布)。取的是版本文档(与 latest 文档同量级),
78
+ * 不是全量包元数据。
79
+ */
80
+ export declare function fetchNpmVersionRelease(fetchImpl: FetchImpl, timeoutMs: number | undefined, opts: {
81
+ targetPackageName?: string;
82
+ registryUrl?: string;
83
+ releaseChannel?: ReleaseChannel;
84
+ version: string;
85
+ }): Promise<ReleaseInfo>;
72
86
  export declare function createUpdateCore(ports: UpdatePorts): UpdateCore;