dsh-plugin-update 0.3.1 → 0.5.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.
package/dist/http.d.ts CHANGED
@@ -8,8 +8,8 @@ export declare const HTTP_TRANSPORT_FAILED = "http-transport-failed";
8
8
  export declare const DEFAULT_HTTP_CHECK_TIMEOUT_MS = 15000;
9
9
  export declare const DEFAULT_HTTP_INSTALL_TIMEOUT_MS: number;
10
10
  export declare const DEFAULT_HTTP_STATUS_TIMEOUT_MS = 15000;
11
- /** 单插件三电话动作名(与 src/config.ts PHONE_ACTIONS 同字面,不另起字面)。 */
12
- export declare const HTTP_PHONE_ACTIONS: readonly ["updateStatus", "updateCheck", "updateInstall"];
11
+ /** 单插件电话动作名(旧三电话与 src/config.ts PHONE_ACTIONS 同字面,不另起字面;#38 加 updateChangelog)。 */
12
+ export declare const HTTP_PHONE_ACTIONS: readonly ["updateStatus", "updateCheck", "updateInstall", "updateChangelog"];
13
13
  export type HttpPhoneAction = (typeof HTTP_PHONE_ACTIONS)[number];
14
14
  /** 批量五电话动作名(与 src/panel-batch.ts buildBatchPhoneNames 同字面,不另起字面;同一内核)。 */
15
15
  export declare const HTTP_BATCH_ACTIONS: readonly ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel"];
package/dist/http.js CHANGED
@@ -10,7 +10,7 @@ const HTTP_TRANSPORT_FAILED = "http-transport-failed";
10
10
  const DEFAULT_HTTP_CHECK_TIMEOUT_MS = 15e3;
11
11
  const DEFAULT_HTTP_INSTALL_TIMEOUT_MS = 16 * 6e4;
12
12
  const DEFAULT_HTTP_STATUS_TIMEOUT_MS = 15e3;
13
- const HTTP_PHONE_ACTIONS = ["updateStatus", "updateCheck", "updateInstall"];
13
+ const HTTP_PHONE_ACTIONS = ["updateStatus", "updateCheck", "updateInstall", "updateChangelog"];
14
14
  const HTTP_BATCH_ACTIONS = ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel"];
15
15
  function isAbsoluteUrl(value) {
16
16
  return /^https?:\/\//i.test(value);
@@ -42,6 +42,7 @@ function allowedRouteKeys(prefix, batchPrefix) {
42
42
  out.add(batchPrefix + "." + a);
43
43
  }
44
44
  }
45
+ out.add("updateChangelog");
45
46
  return out;
46
47
  }
47
48
  function assertHttpRoutes(routes, prefix, batchPrefix) {
@@ -127,7 +128,7 @@ function timeoutForPhone(phoneName, timeout) {
127
128
  if (typeof t.installMs === "number" && Number.isFinite(t.installMs) && t.installMs > 0) return t.installMs;
128
129
  return DEFAULT_HTTP_INSTALL_TIMEOUT_MS;
129
130
  }
130
- if (phoneName.endsWith(".updateCheck") || phoneName.endsWith(".batchCheck")) {
131
+ if (phoneName.endsWith(".updateCheck") || phoneName.endsWith(".batchCheck") || phoneName.endsWith(".updateChangelog")) {
131
132
  if (typeof t.checkMs === "number" && Number.isFinite(t.checkMs) && t.checkMs > 0) return t.checkMs;
132
133
  return DEFAULT_HTTP_CHECK_TIMEOUT_MS;
133
134
  }
@@ -44,6 +44,11 @@ export interface BatchPanelOptions {
44
44
  onRestartRequested?: () => void | Promise<void>;
45
45
  /** 复制文本的出口(与单插件面板同一口径):不传即试浏览器剪贴板,都没有也不抛错。 */
46
46
  copyText?: (text: string) => void | Promise<void>;
47
+ /**
48
+ * 详情行自动取日志(#38):默认开。展开行有新版时调该行自己的更新日志电话按需取;
49
+ * 老宿主没给行电话名即回中性提示;false 关闭。
50
+ */
51
+ autoChangelog?: boolean;
47
52
  }
48
53
  /** 面板可点的动作(HTML 上 data-act 一一对应;测试走同一条路)。 */
49
54
  export type BatchPanelActionKind = 'check' | 'install' | 'resume' | 'cancel' | 'row-install' | 'row-skip' | 'row-resume-skip' | 'row-cancel-queue' | 'row-copy-manual' | 'row-copy-diag' | 'toggle-details' | 'restart' | 'close';
@@ -125,6 +130,8 @@ export interface BatchPanelRenderInput {
125
130
  notice?: string | null;
126
131
  /** 面板正有一次电话在飞(本地忙守卫的一半)。 */
127
132
  inFlight?: boolean;
133
+ /** 「取消这一批」已点过一次:按钮换确认文案,再点一次才真取消(防误触)。 */
134
+ confirmCancel?: boolean;
128
135
  /** 是否已经拿到过第一次回包(决定总账那句是不是「正在读取…」)。 */
129
136
  loaded?: boolean;
130
137
  titles?: Record<string, string>;
@@ -132,6 +139,8 @@ export interface BatchPanelRenderInput {
132
139
  skippedVersions?: Record<string, string | null>;
133
140
  /** notice 归哪一家:等于某行键时那条回执画在该家详情里(否则画在面板底部)。 */
134
141
  noticeKey?: string | null;
142
+ /** 键 -> 该行已取到的日志全文(只传有文本的;取不到与没展开即中性提示,不挡安装)。 */
143
+ changelogs?: Record<string, string | null>;
135
144
  }
136
145
  /** 整面板 HTML(含样式;重绘即整体替换 innerHTML,故每次都带 style 也只留一份)。 */
137
146
  export declare function renderBatchPanelHTML(input: BatchPanelRenderInput): string;
@@ -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
@@ -11,6 +12,7 @@ import {
11
12
  createBrowserSkipStore,
12
13
  failureCopy,
13
14
  isKnownFailureCode,
15
+ normalizePanelTheme,
14
16
  renderUpdatePanelHTML
15
17
  } from "./panel.js";
16
18
  const BATCH_PANEL_POLL = {
@@ -91,7 +93,7 @@ function batchRowStatus(row, skippedVersion) {
91
93
  }
92
94
  function renderBatchPanelHTML(input) {
93
95
  const rows = Array.isArray(input.rows) ? input.rows : [];
94
- const theme = input.theme === "d5-paper" ? "d5-paper" : "default";
96
+ const theme = normalizePanelTheme(input.theme);
95
97
  const mode = input.mode === "dialog" ? "dialog" : "embedded";
96
98
  const titles = input.titles && typeof input.titles === "object" ? input.titles : {};
97
99
  const skippedVersions = input.skippedVersions && typeof input.skippedVersions === "object" ? input.skippedVersions : {};
@@ -111,6 +113,7 @@ function renderBatchPanelHTML(input) {
111
113
  const notice = typeof input.notice === "string" && input.notice ? input.notice : null;
112
114
  const noticeKey = typeof input.noticeKey === "string" && input.noticeKey ? input.noticeKey : null;
113
115
  const detailNoticeKey = notice !== null && noticeKey !== null && noticeKey === expandedKey ? noticeKey : null;
116
+ const changelogs = input.changelogs && typeof input.changelogs === "object" ? input.changelogs : {};
114
117
  const ctx = {
115
118
  expanded: false,
116
119
  busy: installing,
@@ -118,15 +121,16 @@ function renderBatchPanelHTML(input) {
118
121
  theme,
119
122
  titles,
120
123
  skipped: null,
121
- notice: null
124
+ notice: null,
125
+ changelogMarkdown: null
122
126
  };
123
127
  const parts = [];
124
128
  parts.push('<div class="dsh-upd-batch-head">');
125
129
  parts.push('<div class="dsh-upd-batch-title">\u66F4\u65B0\u6863\u6848 \xB7 <i>\u603B\u8D26</i></div>');
126
130
  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>');
131
+ 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>');
132
+ 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>');
133
+ 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
134
  parts.push("</div>");
131
135
  parts.push("</div>");
132
136
  const summary = loaded ? batchSummaryText(counts, rows, total, installing, lastError !== null) : "\u6B63\u5728\u8BFB\u53D6\u6279\u91CF\u66F4\u65B0\u72B6\u6001\u2026";
@@ -142,7 +146,8 @@ function renderBatchPanelHTML(input) {
142
146
  ...ctx,
143
147
  expanded,
144
148
  skipped,
145
- notice: expanded && row.key === detailNoticeKey ? notice : null
149
+ notice: expanded && row.key === detailNoticeKey ? notice : null,
150
+ changelogMarkdown: expanded && typeof changelogs[row.key] === "string" ? changelogs[row.key] : null
146
151
  })
147
152
  );
148
153
  }
@@ -153,14 +158,17 @@ function renderBatchPanelHTML(input) {
153
158
  if (total > 0 && !terminal) {
154
159
  const more = [];
155
160
  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>");
161
+ const confirmCancel = input.confirmCancel === true;
162
+ more.push(
163
+ '<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>"
164
+ );
157
165
  parts.push('<div class="dsh-upd-batch-more">' + more.join("") + "</div>");
158
166
  }
159
167
  if (notice !== null && detailNoticeKey === null) {
160
168
  parts.push('<div class="dsh-upd-batch-notice" role="status">' + escapeHtml(notice) + "</div>");
161
169
  }
162
170
  const kernel = parts.join(String.fromCharCode(10));
163
- const d5 = theme === "d5-paper";
171
+ const d5 = normalizePanelTheme(theme) === "d5-paper";
164
172
  const attr = d5 ? ' data-theme="d5-paper"' : "";
165
173
  const seal = batchSealOf(counts);
166
174
  const root = '<div class="dsh-upd dsh-upd-batch" data-mode="' + mode + '" data-seal="' + escapeHtml(seal.text) + '" data-seal-tone="' + seal.tone + '"' + attr + ">\n" + kernel + "\n</div>";
@@ -263,6 +271,7 @@ function detailHTML(row, ctx) {
263
271
  const html = renderUpdatePanelHTML({
264
272
  snapshot: asSnapshotLike(row.snapshot),
265
273
  manual: typeof row.manual === "string" ? row.manual : null,
274
+ changelogMarkdown: typeof ctx.changelogMarkdown === "string" ? ctx.changelogMarkdown : null,
266
275
  queue: asQueueLike(row.queue),
267
276
  // 跳过是面板本地记录(与单插件面板同一套语义):内核按「已跳过」画 tag 与提示行,
268
277
  // 恢复按钮由上面的动作行提供,措辞与内核提示里的「恢复」对得上。
@@ -363,6 +372,8 @@ const UPDATE_BATCH_PANEL_CSS = [
363
372
  ".dsh-upd-batch-ledger{margin:8px 0 0;font-size:13px;opacity:.85}",
364
373
  ".dsh-upd-batch-more{margin-top:6px}",
365
374
  ".dsh-upd-batch-more button{font-size:12px}",
375
+ // 确认态:红框红字,与「关闭」等中性按钮一眼区分;disabled 照旧置灰。
376
+ '.dsh-upd-batch-more button[data-confirm="1"]{border-color:var(--dsh-upd-bad-line,#dc2626);color:var(--dsh-upd-bad-line,#dc2626)}',
366
377
  ".dsh-upd-batch-notice{margin-top:6px;font-size:12.5px;opacity:.85}",
367
378
  ".dsh-upd-bdetail-actions{margin:0 0 6px}",
368
379
  ".dsh-upd .dsh-upd-bdetail-actions button{font-size:12.5px}",
@@ -608,15 +619,20 @@ function mountUpdateBatchPanel(container, options) {
608
619
  if (mode !== "embedded" && mode !== "dialog") {
609
620
  throw new Error("[dsh-plugin-update] \u6446\u653E\u5F62\u6001\u975E\u6CD5\uFF1A\u53EA\u6536 embedded \u6216 dialog\uFF08\u6536\u5230 " + JSON.stringify(options.mode) + "\uFF09");
610
621
  }
611
- let theme = options.theme === void 0 ? "default" : options.theme;
612
- if (theme !== "default" && theme !== "d5-paper") {
613
- throw new Error("[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 d5-paper\uFF08\u6536\u5230 " + JSON.stringify(options.theme) + "\uFF09");
622
+ if (options.theme !== void 0 && options.theme !== "default" && options.theme !== "archive" && options.theme !== "d5-paper") {
623
+ throw new Error("[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08d5-paper \u4E3A\u65E7\u522B\u540D\u4ECD\u53EF\u7528\uFF09\uFF08\u6536\u5230 " + JSON.stringify(options.theme) + "\uFF09");
614
624
  }
625
+ let theme = normalizePanelTheme(options.theme ?? "default");
615
626
  const call = options.call;
616
627
  const titles = options.titles && typeof options.titles === "object" ? options.titles : {};
617
628
  const onRestartRequested = options.onRestartRequested;
618
629
  const copyText = options.copyText ?? defaultCopyText;
619
630
  const skipStores = /* @__PURE__ */ new Map();
631
+ const autoChangelogEnabled = options.autoChangelog !== false;
632
+ const rowChangelog = /* @__PURE__ */ new Map();
633
+ const rowInflight = /* @__PURE__ */ new Set();
634
+ const rowFailed = /* @__PURE__ */ new Map();
635
+ let rowAutoSeq = 0;
620
636
  let rows = [];
621
637
  let expandedKey = null;
622
638
  let lastError = null;
@@ -625,10 +641,64 @@ function mountUpdateBatchPanel(container, options) {
625
641
  let inFlight = false;
626
642
  let loaded = false;
627
643
  let refreshing = false;
644
+ let confirmCancelArmed = false;
628
645
  let mounted = true;
646
+ function changelogView() {
647
+ const out = {};
648
+ for (const row of rows) {
649
+ const cached = rowChangelog.get(row.key);
650
+ if (!cached || typeof cached.markdown !== "string") continue;
651
+ if (latestVersionOf(row) !== cached.version) continue;
652
+ out[row.key] = cached.markdown;
653
+ }
654
+ return out;
655
+ }
656
+ function changelogPhoneOf(row) {
657
+ const phones2 = isObject(row.phoneNames) ? row.phoneNames : null;
658
+ if (!phones2) return null;
659
+ const name = phones2["updateChangelog"];
660
+ return typeof name === "string" && name ? name : null;
661
+ }
662
+ function maybeAutoRowChangelog(key) {
663
+ if (!autoChangelogEnabled || !mounted || !key) return;
664
+ const row = rowOfKey(key);
665
+ if (!row || asBatchPhase(row.phase) !== "ready") return;
666
+ const version = latestVersionOf(row);
667
+ if (!version || !validReleaseVersion(version)) return;
668
+ try {
669
+ if (skipStoreFor(pluginIdOf(row)).has(version)) return;
670
+ } catch {
671
+ }
672
+ const cached = rowChangelog.get(key);
673
+ if (cached && cached.version === version) return;
674
+ if (rowInflight.has(key)) return;
675
+ if (rowFailed.get(key) === version) return;
676
+ const phone2 = changelogPhoneOf(row);
677
+ if (!phone2) return;
678
+ rowInflight.add(key);
679
+ const seq = rowAutoSeq;
680
+ void Promise.resolve().then(() => call(phone2, { version })).then(
681
+ (reply) => {
682
+ rowInflight.delete(key);
683
+ if (!mounted || seq !== rowAutoSeq) return;
684
+ if (!isObject(reply) || reply["ok"] !== true) {
685
+ rowFailed.set(key, version);
686
+ return;
687
+ }
688
+ const md = reply["markdown"];
689
+ rowChangelog.set(key, { version, markdown: typeof md === "string" ? md : null });
690
+ if (typeof md === "string") render();
691
+ },
692
+ () => {
693
+ rowInflight.delete(key);
694
+ if (mounted && seq === rowAutoSeq) rowFailed.set(key, version);
695
+ }
696
+ );
697
+ }
698
+ let lastHTML = "";
629
699
  function render() {
630
700
  if (!mounted) return;
631
- container.innerHTML = renderBatchPanelHTML({
701
+ const nextHTML = renderBatchPanelHTML({
632
702
  rows,
633
703
  theme,
634
704
  mode,
@@ -639,8 +709,14 @@ function mountUpdateBatchPanel(container, options) {
639
709
  skippedVersions: skippedVersionsOf(),
640
710
  inFlight,
641
711
  loaded,
642
- titles
712
+ titles,
713
+ confirmCancel: confirmCancelArmed,
714
+ changelogs: changelogView()
643
715
  });
716
+ if (nextHTML !== lastHTML) {
717
+ lastHTML = nextHTML;
718
+ container.innerHTML = nextHTML;
719
+ }
644
720
  }
645
721
  function rowOfKey(key) {
646
722
  return rows.find((row) => row.key === key) ?? null;
@@ -717,6 +793,7 @@ function mountUpdateBatchPanel(container, options) {
717
793
  refreshing = false;
718
794
  }
719
795
  render();
796
+ maybeAutoRowChangelog(expandedKey);
720
797
  }
721
798
  async function phone(name, args, busyRowKey = null) {
722
799
  if (!mounted || inFlight) return;
@@ -731,6 +808,7 @@ function mountUpdateBatchPanel(container, options) {
731
808
  return;
732
809
  }
733
810
  applyReply(reply);
811
+ rowFailed.clear();
734
812
  } catch {
735
813
  if (!mounted) return;
736
814
  lastError = "check-failed";
@@ -738,9 +816,14 @@ function mountUpdateBatchPanel(container, options) {
738
816
  inFlight = false;
739
817
  if (mounted) render();
740
818
  }
819
+ maybeAutoRowChangelog(expandedKey);
741
820
  }
742
821
  async function act(kind, key) {
743
822
  if (!mounted) return;
823
+ if (kind !== "cancel" && confirmCancelArmed) {
824
+ confirmCancelArmed = false;
825
+ render();
826
+ }
744
827
  switch (kind) {
745
828
  case "check":
746
829
  return phone(phones.check, {});
@@ -757,7 +840,10 @@ function mountUpdateBatchPanel(container, options) {
757
840
  render();
758
841
  return;
759
842
  }
843
+ // 程序调用(controller.act)即执行:调它就是明确意图,不上膛。
844
+ // 鼠标误触的防护在 onClick 那一层(第一次点击只上膛)。
760
845
  case "cancel": {
846
+ confirmCancelArmed = false;
761
847
  await phone(phones.cancel, {});
762
848
  if (!mounted) return;
763
849
  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 +919,7 @@ function mountUpdateBatchPanel(container, options) {
833
919
  if (typeof key !== "string" || !key) return;
834
920
  expandedKey = expandedKey === key ? null : key;
835
921
  render();
922
+ maybeAutoRowChangelog(expandedKey);
836
923
  return;
837
924
  }
838
925
  // 「重启宿主」:宿主没有重启自己的电话,只做入口——调用方给了 onRestartRequested 就交给它,
@@ -859,6 +946,14 @@ function mountUpdateBatchPanel(container, options) {
859
946
  return;
860
947
  }
861
948
  }
949
+ function onKeyDown(ev) {
950
+ try {
951
+ const e = ev;
952
+ if (!mounted || mode !== "dialog" || !e || e.key !== "Escape") return;
953
+ unmount();
954
+ } catch {
955
+ }
956
+ }
862
957
  function setTheme(next) {
863
958
  if (next !== "default" && next !== "d5-paper") {
864
959
  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 +974,13 @@ function mountUpdateBatchPanel(container, options) {
879
974
  const btn = t?.target && typeof t.target.closest === "function" ? t.target.closest("[data-act]") : null;
880
975
  const kind = btn?.getAttribute ? btn.getAttribute("data-act") : null;
881
976
  if (!kind) return;
977
+ if (kind === "cancel" && !confirmCancelArmed) {
978
+ confirmCancelArmed = true;
979
+ 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");
980
+ render();
981
+ return;
982
+ }
983
+ if (kind === "cancel") confirmCancelArmed = false;
882
984
  const key = btn?.getAttribute ? btn.getAttribute("data-key") : null;
883
985
  void act(kind, key === null ? void 0 : key);
884
986
  } catch {
@@ -887,6 +989,8 @@ function mountUpdateBatchPanel(container, options) {
887
989
  function unmount() {
888
990
  if (!mounted) return;
889
991
  mounted = false;
992
+ rowAutoSeq++;
993
+ rowInflight.clear();
890
994
  try {
891
995
  timer.clear(handle);
892
996
  } catch {
@@ -895,12 +999,20 @@ function mountUpdateBatchPanel(container, options) {
895
999
  container.removeEventListener?.("click", onClick);
896
1000
  } catch {
897
1001
  }
1002
+ try {
1003
+ container.removeEventListener?.("keydown", onKeyDown);
1004
+ } catch {
1005
+ }
898
1006
  }
899
1007
  render();
900
1008
  try {
901
1009
  container.addEventListener?.("click", onClick);
902
1010
  } catch {
903
1011
  }
1012
+ try {
1013
+ container.addEventListener?.("keydown", onKeyDown);
1014
+ } catch {
1015
+ }
904
1016
  const timer = getTimer();
905
1017
  const handle = timer.set(() => {
906
1018
  void refresh();
package/dist/panel.d.ts CHANGED
@@ -3,7 +3,16 @@ import type { BlockedReason, UpdateSnapshot } from './ports.js';
3
3
  /** 摆放形态:默认内嵌,切弹窗走同一参数。 */
4
4
  export type UpdatePanelMode = 'embedded' | 'dialog';
5
5
  /** 面板主题:默认最小可用样式;`d5-paper` 为可选 D5 档案卷专业主题(只换肤,不换 DOM 顺序)。 */
6
- export type UpdatePanelTheme = 'default' | 'd5-paper';
6
+ /**
7
+ * 面板主题:`default` 最小可用深色;`archive` 档案卷纸面浅色(原型敲定的案卷风格,只换肤)。
8
+ * `d5-paper` 是 archive 的旧别名(历史取值),仍可用,渲染逐字相同。
9
+ */
10
+ export type UpdatePanelTheme = 'default' | 'archive' | 'd5-paper';
11
+ /**
12
+ * 主题归一:archive 与旧别名 d5-paper 走同一套渲染(DOM 属性仍为 d5-paper,既有覆盖样式不断);
13
+ * 其余一律回 default(纯渲染函数永不抛;挂载/setTheme 的非法值另行抛错)。
14
+ */
15
+ export declare function normalizePanelTheme(value: unknown): 'default' | 'd5-paper';
7
16
  /** 与宿主通话的传输函数:面板只认这个签名,不认任何宿主对象的具体形状。 */
8
17
  export type UpdatePanelCall = (phoneName: string, args: Record<string, unknown>) => Promise<Record<string, unknown>>;
9
18
  /** 复制文本的出口:不传即尝试浏览器剪贴板,都没有则只留“已生成、请手动复制”提示。 */
@@ -43,9 +52,20 @@ export interface UpdatePanelOptions {
43
52
  /**
44
53
  * 更新日志 Markdown(#23 包内 CHANGELOG 展示):
45
54
  * 调用方按“已装版离线读、新版按需取 tarball”备好后传入(取不到传 null/空串即中性提示);
46
- * 面板只渲染不取数,缺日志永不挡安装、不写 blockedReason。
55
+ * 缺日志永不挡安装、不写 blockedReason。
56
+ * 显式传入即赢:自动链路(见 autoChangelog)永不覆盖它。
47
57
  */
48
58
  changelogMarkdown?: string | null;
59
+ /**
60
+ * 自动取日志(#38):默认开。有新版时面板按 latestVersion 调一次宿主更新日志电话,
61
+ * 回来经内部通道换日志节;显式传过 changelogMarkdown 或传 false 即退回手动模式。
62
+ */
63
+ autoChangelog?: boolean;
64
+ /**
65
+ * 弹窗关闭的落地:只在 dialog 下点「关闭」/按 Esc 时调用;调用方在此撤掉弹窗 DOM。
66
+ * 不传则回退为只停轮询(unmount),DOM 留给调用方处理。入口件打开的 dialog 已内置(收 dialog + 还原按钮)。
67
+ */
68
+ onCloseRequested?: () => void | Promise<void>;
49
69
  }
50
70
  /** 挂载点:只要有 innerHTML 的容器即可(浏览器元素或测试替身都行)。 */
51
71
  export interface UpdatePanelContainer {
@@ -221,6 +241,11 @@ export interface PanelRenderInput extends PanelViewInput {
221
241
  profileName?: string | null;
222
242
  /** 宿主种类:进诊断文本;缺省显示“未知”。 */
223
243
  hostKind?: string | null;
244
+ /**
245
+ * 在途动作(仅挂载态内部用:点下查新版/安装到回包前的那一帧,按钮置忙)。
246
+ * 缺省即静默(输出与旧版一字不差);只在挂载器置忙的那次 render 里传。
247
+ */
248
+ busyAct?: 'check' | 'install' | null;
224
249
  /**
225
250
  * 动作面归谁:`default`(缺省)由内核画动作按钮;`none` 只画内容、不画按钮。
226
251
  * 给「调用方自己提供动作面」的场景(如批量面板的详情:动作由批量面板经自己的通道提供)。
@@ -242,6 +267,8 @@ export declare function renderUpdatePanelHTML(input: PanelRenderInput): string;
242
267
  export declare function mountUpdatePanel(container: UpdatePanelContainer, options: UpdatePanelOptions): UpdatePanelController;
243
268
  export { buildPhoneNames as buildPanelPhoneNames } from './config.js';
244
269
  export type { PhoneAction as PanelPhoneAction } from './config.js';
270
+ export { buildChangelogPhoneName as buildPanelChangelogPhoneName } from './config.js';
271
+ export type { ChangelogPhoneAction as PanelChangelogPhoneAction } from './config.js';
245
272
  /** 面板轮询口径(默认 1 秒、下限 250 毫秒,与宿主侧同一套)。 */
246
273
  export declare const PANEL_POLL: {
247
274
  readonly defaultMs: 1000;