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/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 {
@@ -7,6 +7,9 @@ import {
7
7
  parseChangelog,
8
8
  renderChangelogHTML
9
9
  } from "./changelog.js";
10
+ function normalizePanelTheme(value) {
11
+ return value === "d5-paper" || value === "archive" ? "d5-paper" : "default";
12
+ }
10
13
  const BLOCKED_COPY = {
11
14
  "unknown-profile": {
12
15
  title: "\u4F7F\u7528\u8303\u56F4\u6216\u63D2\u4EF6\u4F4D\u7F6E\u8BA4\u4E0D\u51FA",
@@ -551,8 +554,25 @@ const UPDATE_PANEL_CSS = [
551
554
  ".dsh-upd-prog-bar{display:block;height:100%;background:var(--dsh-upd-primary,#2563eb);transition:width .3s}",
552
555
  ".dsh-upd-progtxt{font-size:12.5px;opacity:.75}",
553
556
  ".dsh-upd-skipline{font-size:13px;margin-top:8px}",
557
+ // 骨架微光:只在首帧 loading 出现;reduced-motion 下静止占位,不断语义。
558
+ ".dsh-upd-skv{display:inline-block;min-width:64px;border-radius:3px;color:transparent !important;user-select:none;",
559
+ "background:linear-gradient(90deg,var(--dsh-upd-line,#e5e7eb) 25%,var(--dsh-upd-soft,#f3f4f6) 50%,var(--dsh-upd-line,#e5e7eb) 75%);",
560
+ "background-size:200% 100%;animation:dsh-upd-shimmer 1.2s linear infinite}",
561
+ "@keyframes dsh-upd-shimmer{to{background-position:-200% 0}}",
554
562
  ".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
563
  ".dsh-upd-err{font-size:13px;margin:0 0 6px}",
564
+ // —— 全按钮交互反馈(#36:悬停/按下/过渡/在途忙态;浅深双主题通用写法,不碰上面的既有串)——
565
+ ".dsh-upd button{transition:background-color .15s ease,border-color .15s ease,color .15s ease,transform .06s ease}",
566
+ ".dsh-upd button:hover:not(:disabled){border-color:var(--dsh-upd-focus,#2563eb)}",
567
+ '.dsh-upd button[data-primary="1"]:hover:not(:disabled){filter:brightness(.93)}',
568
+ ".dsh-upd button:active:not(:disabled){transform:translateY(1px)}",
569
+ '.dsh-upd button[aria-busy="true"]{cursor:wait;animation:dsh-upd-pulse 1s ease-in-out infinite}',
570
+ "@keyframes dsh-upd-pulse{0%,100%{opacity:1}50%{opacity:.55}}",
571
+ // 在途转圈:纯 CSS ::after,不加 DOM 节点(内核 DOM 冻结);转的是边框缺口,不是 emoji。
572
+ '.dsh-upd button[aria-busy="true"]::after{content:"";display:inline-block;width:11px;height:11px;margin-left:8px;vertical-align:-1px;',
573
+ "border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:dsh-upd-spin .8s linear infinite}",
574
+ "@keyframes dsh-upd-spin{to{transform:rotate(360deg)}}",
575
+ '@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
576
  "@media (prefers-color-scheme: dark){.dsh-upd{--dsh-upd-fg:#e5e7eb;--dsh-upd-bg:#111827;--dsh-upd-line:#374151;",
557
577
  "--dsh-upd-btn:#1f2937;--dsh-upd-soft:#1f2937;--dsh-upd-primary:#3b82f6;--dsh-upd-focus:#93c5fd;",
558
578
  // 横幅深色覆盖:底色用低透明度同色系(不是浅色原值),边线提亮,保证「深底浅字」可读。
@@ -681,6 +701,11 @@ function chapterOf(index, inner, note = "") {
681
701
  const title = CHAPTER_TITLES[index - 1];
682
702
  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
703
  }
704
+ function skeletonStrip(loading) {
705
+ if (!loading) return "";
706
+ 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>`;
707
+ return `<div class="dsh-upd-strip" aria-hidden="true">` + cell("\u8FD0\u884C") + cell("\u78C1\u76D8") + cell("\u8FDC\u7AEF") + `</div>`;
708
+ }
684
709
  function versionStrip(snapshot) {
685
710
  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
711
  if (!snapshot) return "";
@@ -712,31 +737,34 @@ function renderUpdatePanelKernel(input, view) {
712
737
  parts.push(`<div><strong>${escapeHtml(b.title)}</strong></div>`);
713
738
  if (b.action) parts.push(`<div>${escapeHtml(b.action)}</div>`);
714
739
  parts.push("</div>");
715
- parts.push(versionStrip(snapshot));
740
+ parts.push(snapshot ? versionStrip(snapshot) : skeletonStrip(view.banner.kind === "loading"));
716
741
  const showActions = input.actions !== "none";
717
742
  const actions = [];
718
743
  if (showActions) {
744
+ const busyAct = input.busyAct;
745
+ const checkBusy = busyAct === "check";
746
+ const installBusy = busyAct === "install";
719
747
  actions.push('<div class="dsh-upd-actions">');
720
748
  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>`
749
+ (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
750
  );
723
751
  if (snapshot?.latestVersion && !view.skippedLatest && view.banner.kind === "update") {
724
- actions.push(`<button type="button" data-action="skip">\u8DF3\u8FC7\u8BE5\u7248\u672C</button>`);
752
+ 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
753
  }
726
754
  if (view.showReset && snapshot?.latestVersion) {
727
- actions.push(`<button type="button" data-action="reset-skip">\u6062\u590D\uFF08${escapeHtml(snapshot.latestVersion)}\uFF09</button>`);
755
+ 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
756
  }
729
757
  if (view.showManual && manual) {
730
- actions.push(`<button type="button" data-action="copy-manual">\u590D\u5236\u624B\u5DE5\u547D\u4EE4</button>`);
758
+ 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
759
  }
732
760
  if (b.kind === "restart") {
733
- actions.push(`<button type="button" data-action="restart-hint" data-primary="1">\u91CD\u542F\u5BBF\u4E3B</button>`);
761
+ 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
762
  }
735
763
  if (snapshot) {
736
- actions.push(`<button type="button" data-action="copy-diag">\u590D\u5236\u8BCA\u65AD</button>`);
764
+ 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
765
  }
738
766
  if (mode === "dialog") {
739
- actions.push(`<button type="button" data-action="close-view">\u5173\u95ED</button>`);
767
+ 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
768
  }
741
769
  actions.push("</div>");
742
770
  }
@@ -838,7 +866,7 @@ ${changelogHTML}
838
866
  function renderUpdatePanelHTML(input) {
839
867
  const view = panelViewModel(input);
840
868
  const kernel = renderUpdatePanelKernel(input, view);
841
- const d5 = input.theme === "d5-paper";
869
+ const d5 = normalizePanelTheme(input.theme) === "d5-paper";
842
870
  const attr = d5 ? ' data-theme="d5-paper"' : "";
843
871
  const sealAttr = ` data-seal="${escapeHtml(view.seal.text)}" data-seal-tone="${view.seal.tone}"`;
844
872
  const body = input.mode === "dialog" ? `<div class="dsh-upd-overlay" data-mode="dialog"><div class="dsh-upd" data-mode="dialog" data-plugin="${escapeHtml(input.pluginId)}"${sealAttr}${attr}>
@@ -918,6 +946,13 @@ function mountUpdatePanel(container, options) {
918
946
  }
919
947
  const prefix = options.prefix === void 0 ? "wf" : options.prefix;
920
948
  const phoneNames = buildPhoneNames(prefix);
949
+ const changelogPhone = buildChangelogPhoneName(prefix);
950
+ const autoChangelogEnabled = typeof options.changelogMarkdown !== "string" && options.autoChangelog !== false;
951
+ let manualChangelogOverride = typeof options.changelogMarkdown === "string";
952
+ const changelogCache = /* @__PURE__ */ new Map();
953
+ const changelogInflight = /* @__PURE__ */ new Set();
954
+ const changelogFailed = /* @__PURE__ */ new Set();
955
+ let autoSeq = 0;
921
956
  const pollMs = options.pollMs === void 0 ? DEFAULT_PANEL_POLL_MS : options.pollMs;
922
957
  if (typeof pollMs !== "number" || !Number.isFinite(pollMs) || pollMs < MIN_PANEL_POLL_MS) {
923
958
  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`);
@@ -926,13 +961,15 @@ function mountUpdatePanel(container, options) {
926
961
  if (mode !== "embedded" && mode !== "dialog") {
927
962
  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`);
928
963
  }
929
- let theme = options.theme ?? "default";
930
- if (theme !== "default" && theme !== "d5-paper") {
931
- 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`);
964
+ if (options.theme !== void 0 && options.theme !== "default" && options.theme !== "archive" && options.theme !== "d5-paper") {
965
+ 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`);
932
966
  }
967
+ let theme = normalizePanelTheme(options.theme ?? "default");
933
968
  let showOthers = options.showOthers === true;
934
969
  const call = options.call;
935
970
  const onRestartRequested = options.onRestartRequested;
971
+ const onCloseRequested = typeof options.onCloseRequested === "function" ? options.onCloseRequested : null;
972
+ let lastHTML = "";
936
973
  const copyText = options.copyText ?? defaultCopyText;
937
974
  const skipStore = options.skipStore ?? createBrowserSkipStore(pluginId);
938
975
  const hostKind = typeof options.hostKind === "string" && options.hostKind ? options.hostKind : null;
@@ -948,20 +985,36 @@ function mountUpdatePanel(container, options) {
948
985
  let lastErrorKind = null;
949
986
  let lastDiag = null;
950
987
  let copyNotice = null;
988
+ let noticeExpiresAt = 0;
989
+ let busyAct = null;
951
990
  let mounted = true;
952
991
  let envProfileName = null;
953
992
  let envHostKind = null;
993
+ function sayCopy(text) {
994
+ copyNotice = text;
995
+ try {
996
+ noticeExpiresAt = Date.now() + 5e3;
997
+ } catch {
998
+ noticeExpiresAt = 0;
999
+ }
1000
+ }
954
1001
  function queueArgs() {
955
1002
  return { includeQueue: true, includeEnv: true, showOthers, ...requestId ? { requestId } : {} };
956
1003
  }
957
1004
  function render() {
958
1005
  if (!mounted) return;
1006
+ if (!copyNotice) noticeExpiresAt = 0;
1007
+ else if (noticeExpiresAt && Date.now() > noticeExpiresAt) {
1008
+ copyNotice = null;
1009
+ noticeExpiresAt = 0;
1010
+ }
959
1011
  const latest = snapshot?.latestVersion ?? null;
960
1012
  const skippedLatest = !!latest && validReleaseVersion(latest) && skipStore.has(latest);
961
- container.innerHTML = renderUpdatePanelHTML({
1013
+ const nextHTML = renderUpdatePanelHTML({
962
1014
  snapshot,
963
1015
  manual,
964
1016
  queue,
1017
+ busyAct,
965
1018
  skippedLatest,
966
1019
  lastError,
967
1020
  errorKind: lastErrorKind,
@@ -975,6 +1028,10 @@ function mountUpdatePanel(container, options) {
975
1028
  profileName: profileNameOption ?? envProfileName,
976
1029
  hostKind: hostKind ?? envHostKind
977
1030
  });
1031
+ if (nextHTML !== lastHTML) {
1032
+ lastHTML = nextHTML;
1033
+ container.innerHTML = nextHTML;
1034
+ }
978
1035
  }
979
1036
  function applyStatusReply(reply) {
980
1037
  if (!isObject(reply)) return;
@@ -1003,6 +1060,44 @@ function mountUpdatePanel(container, options) {
1003
1060
  lastDiag = Object.prototype.hasOwnProperty.call(reply, "diag") ? reply["diag"] : null;
1004
1061
  }
1005
1062
  }
1063
+ function pendingAutoChangelogVersion() {
1064
+ if (!autoChangelogEnabled || manualChangelogOverride || !mounted) return null;
1065
+ const v = snapshot?.latestVersion ?? null;
1066
+ if (typeof v !== "string" || !validReleaseVersion(v)) return null;
1067
+ if (!snapshot || snapshot.canInstall !== true) return null;
1068
+ try {
1069
+ if (skipStore.has(v)) return null;
1070
+ } catch {
1071
+ }
1072
+ if (changelogCache.has(v) || changelogInflight.has(v) || changelogFailed.has(v)) return null;
1073
+ return v;
1074
+ }
1075
+ function maybeAutoChangelog() {
1076
+ const v = pendingAutoChangelogVersion();
1077
+ if (v === null) return;
1078
+ changelogInflight.add(v);
1079
+ const seq = autoSeq;
1080
+ void Promise.resolve().then(() => call(changelogPhone, { version: v })).then(
1081
+ (reply) => {
1082
+ changelogInflight.delete(v);
1083
+ if (!mounted || seq !== autoSeq) return;
1084
+ if (!reply || typeof reply !== "object" || reply["ok"] !== true) {
1085
+ changelogFailed.add(v);
1086
+ return;
1087
+ }
1088
+ const md = reply["markdown"];
1089
+ changelogCache.set(v, typeof md === "string" ? md : null);
1090
+ if (typeof md === "string") {
1091
+ changelogMarkdown = md;
1092
+ render();
1093
+ }
1094
+ },
1095
+ () => {
1096
+ changelogInflight.delete(v);
1097
+ if (mounted && seq === autoSeq) changelogFailed.add(v);
1098
+ }
1099
+ );
1100
+ }
1006
1101
  async function refresh() {
1007
1102
  if (!mounted) return;
1008
1103
  try {
@@ -1016,26 +1111,40 @@ function mountUpdatePanel(container, options) {
1016
1111
  lastDiag = null;
1017
1112
  }
1018
1113
  render();
1114
+ maybeAutoChangelog();
1019
1115
  }
1020
1116
  async function act(kind, arg) {
1021
1117
  if (!mounted) return;
1022
1118
  copyNotice = null;
1023
1119
  switch (kind) {
1024
1120
  case "check": {
1121
+ if (busyAct) return;
1122
+ busyAct = "check";
1123
+ copyNotice = "\u6B63\u5728\u67E5\u65B0\u7248\u2026";
1124
+ render();
1025
1125
  try {
1026
1126
  const reply = await call(phoneNames.updateCheck, queueArgs());
1027
1127
  if (!mounted) return;
1028
1128
  applyStatusReply(reply);
1129
+ changelogFailed.clear();
1029
1130
  } catch {
1030
1131
  if (!mounted) return;
1031
1132
  lastError = "check-failed";
1032
1133
  lastErrorKind = null;
1033
1134
  lastDiag = null;
1135
+ } finally {
1136
+ busyAct = null;
1137
+ if (copyNotice === "\u6B63\u5728\u67E5\u65B0\u7248\u2026") copyNotice = null;
1034
1138
  }
1035
1139
  render();
1140
+ maybeAutoChangelog();
1036
1141
  return;
1037
1142
  }
1038
1143
  case "install": {
1144
+ if (busyAct) return;
1145
+ busyAct = "install";
1146
+ copyNotice = "\u6B63\u5728\u5B89\u88C5\u2026";
1147
+ render();
1039
1148
  try {
1040
1149
  if (!receipt) {
1041
1150
  const checked = await call(phoneNames.updateCheck, queueArgs());
@@ -1063,8 +1172,12 @@ function mountUpdatePanel(container, options) {
1063
1172
  lastError = "check-failed";
1064
1173
  lastErrorKind = null;
1065
1174
  lastDiag = null;
1175
+ } finally {
1176
+ busyAct = null;
1177
+ if (copyNotice === "\u6B63\u5728\u5B89\u88C5\u2026") copyNotice = null;
1066
1178
  }
1067
1179
  render();
1180
+ maybeAutoChangelog();
1068
1181
  return;
1069
1182
  }
1070
1183
  case "skip": {
@@ -1090,9 +1203,9 @@ function mountUpdatePanel(container, options) {
1090
1203
  if (manual) {
1091
1204
  try {
1092
1205
  await copyText(manual);
1093
- copyNotice = "\u624B\u5DE5\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u7C98\u5230\u7EC8\u7AEF\u6574\u884C\u6267\u884C\u5373\u53EF\u3002";
1206
+ sayCopy("\u624B\u5DE5\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u7C98\u5230\u7EC8\u7AEF\u6574\u884C\u6267\u884C\u5373\u53EF\u3002");
1094
1207
  } catch {
1095
- copyNotice = "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u547D\u4EE4\u3002";
1208
+ sayCopy("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u547D\u4EE4\u3002");
1096
1209
  }
1097
1210
  }
1098
1211
  render();
@@ -1123,9 +1236,9 @@ function mountUpdatePanel(container, options) {
1123
1236
  });
1124
1237
  try {
1125
1238
  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";
1239
+ sayCopy("\u8BCA\u65AD\u5DF2\u590D\u5236\uFF0C\u76F4\u63A5\u7C98\u7ED9\u63D2\u4EF6\u4F5C\u8005\u5373\u53EF\uFF08\u5DF2\u8131\u654F\uFF09\u3002");
1127
1240
  } catch {
1128
- copyNotice = "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u4FE1\u606F\u3002";
1241
+ sayCopy("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u4FE1\u606F\u3002");
1129
1242
  }
1130
1243
  render();
1131
1244
  return;
@@ -1141,18 +1254,18 @@ function mountUpdatePanel(container, options) {
1141
1254
  try {
1142
1255
  if (typeof onRestartRequested === "function") {
1143
1256
  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";
1257
+ sayCopy("\u5DF2\u6309\u8C03\u7528\u65B9\u7684\u91CD\u542F\u6D41\u7A0B\u5904\u7406\uFF1B\u91CD\u542F\u540E\u65B0\u7248\u751F\u6548\u3002");
1145
1258
  } 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";
1259
+ 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
1260
  }
1148
1261
  } 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";
1262
+ 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
1263
  }
1151
1264
  render();
1152
1265
  return;
1153
1266
  }
1154
1267
  case "close-view": {
1155
- if (mode === "dialog") unmount();
1268
+ await requestDialogClose();
1156
1269
  return;
1157
1270
  }
1158
1271
  }
@@ -1165,10 +1278,10 @@ function mountUpdatePanel(container, options) {
1165
1278
  render();
1166
1279
  }
1167
1280
  async function setTheme(next) {
1168
- if (next !== "default" && next !== "d5-paper") {
1169
- throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 d5-paper\uFF08\u6536\u5230 ${JSON.stringify(next)}\uFF09`);
1281
+ if (next !== "default" && next !== "archive" && next !== "d5-paper") {
1282
+ 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(next)}\uFF09`);
1170
1283
  }
1171
- theme = next;
1284
+ theme = normalizePanelTheme(next);
1172
1285
  render();
1173
1286
  }
1174
1287
  async function setShowOthers(show) {
@@ -1185,13 +1298,34 @@ function mountUpdatePanel(container, options) {
1185
1298
  } catch {
1186
1299
  }
1187
1300
  }
1301
+ async function requestDialogClose() {
1302
+ if (!mounted || mode !== "dialog") return;
1303
+ if (onCloseRequested) {
1304
+ try {
1305
+ await onCloseRequested();
1306
+ } catch {
1307
+ }
1308
+ }
1309
+ unmount();
1310
+ }
1311
+ function onKeyDown(ev) {
1312
+ try {
1313
+ const e = ev;
1314
+ if (!mounted || mode !== "dialog" || !e || e.key !== "Escape") return;
1315
+ void requestDialogClose();
1316
+ } catch {
1317
+ }
1318
+ }
1188
1319
  function setChangelogMarkdown(markdown) {
1189
1320
  changelogMarkdown = typeof markdown === "string" ? markdown : null;
1321
+ if (typeof markdown === "string") manualChangelogOverride = true;
1190
1322
  render();
1191
1323
  }
1192
1324
  function unmount() {
1193
1325
  if (!mounted) return;
1194
1326
  mounted = false;
1327
+ autoSeq++;
1328
+ changelogInflight.clear();
1195
1329
  try {
1196
1330
  timer.clear(handle);
1197
1331
  } catch {
@@ -1200,12 +1334,20 @@ function mountUpdatePanel(container, options) {
1200
1334
  container.removeEventListener?.("click", onClick);
1201
1335
  } catch {
1202
1336
  }
1337
+ try {
1338
+ container.removeEventListener?.("keydown", onKeyDown);
1339
+ } catch {
1340
+ }
1203
1341
  }
1204
1342
  render();
1205
1343
  try {
1206
1344
  container.addEventListener?.("click", onClick);
1207
1345
  } catch {
1208
1346
  }
1347
+ try {
1348
+ container.addEventListener?.("keydown", onKeyDown);
1349
+ } catch {
1350
+ }
1209
1351
  const timer = getTimer();
1210
1352
  const handle = timer.set(() => {
1211
1353
  void refresh();
@@ -1219,6 +1361,7 @@ function mountUpdatePanel(container, options) {
1219
1361
  return { refresh, act, setMode, setTheme, setShowOthers, setChangelogMarkdown, unmount };
1220
1362
  }
1221
1363
  import { buildPhoneNames as buildPhoneNames2 } from "./config.js";
1364
+ import { buildChangelogPhoneName as buildChangelogPhoneName2 } from "./config.js";
1222
1365
  const PANEL_POLL = {
1223
1366
  defaultMs: DEFAULT_PANEL_POLL_MS,
1224
1367
  minMs: MIN_PANEL_POLL_MS
@@ -1270,6 +1413,7 @@ export {
1270
1413
  UPDATE_PANEL_D5_CSS,
1271
1414
  blockedCopy,
1272
1415
  buildDiagnosticText,
1416
+ buildChangelogPhoneName2 as buildPanelChangelogPhoneName,
1273
1417
  buildPhoneNames2 as buildPanelPhoneNames,
1274
1418
  buildUpdateDiagCopy,
1275
1419
  changelogForUpdate2 as changelogForUpdate,
@@ -1284,6 +1428,7 @@ export {
1284
1428
  isQueueBusy,
1285
1429
  isUnreleasedVersion,
1286
1430
  mountUpdatePanel,
1431
+ normalizePanelTheme,
1287
1432
  normalizeQueueState,
1288
1433
  panelViewModel,
1289
1434
  parseChangelog2 as parseChangelog,
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;
package/dist/service.js CHANGED
@@ -226,6 +226,55 @@ async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
226
226
  throw checkFailedWithStatus(response);
227
227
  }
228
228
  }
229
+ async function fetchNpmVersionRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
230
+ const targetName = opts?.targetPackageName ?? PACKAGE_NAME;
231
+ const registry = opts?.registryUrl ?? NPM_REGISTRY;
232
+ const channel = opts?.releaseChannel === "prerelease" ? "prerelease" : "stable";
233
+ const version = opts?.version;
234
+ if (typeof version !== "string" || !validReleaseVersion(version) || !isVersionAllowedInChannel(version, channel)) {
235
+ throw updateError("invalid-release");
236
+ }
237
+ let response = null;
238
+ try {
239
+ response = await fetchImpl(`${registry}${encodeURIComponent(targetName)}/${encodeURIComponent(version)}`, {
240
+ headers: { accept: "application/json" },
241
+ redirect: "error",
242
+ signal: timeoutSignal(timeoutMs)
243
+ });
244
+ } catch {
245
+ throw updateError("check-failed");
246
+ }
247
+ try {
248
+ if (!response.ok) throw checkFailedWithStatus(response);
249
+ const declared = Number(response.headers.get("content-length"));
250
+ if (Number.isFinite(declared) && declared > MAX_METADATA_BYTES) throw updateError("invalid-release");
251
+ const text = await response.text();
252
+ if (byteLength(text) > MAX_METADATA_BYTES) throw updateError("invalid-release");
253
+ const value = JSON.parse(text);
254
+ if (value.name !== targetName || value.version !== version || !isVersionAllowedInChannel(value.version, channel)) {
255
+ throw updateError("invalid-release");
256
+ }
257
+ const nodeRange = value.engines?.node;
258
+ if (nodeRange !== void 0 && typeof nodeRange !== "string") throw updateError("invalid-release");
259
+ const tarballText = value.dist?.tarball;
260
+ const integrity = value.dist?.integrity;
261
+ let tarball;
262
+ try {
263
+ tarball = new URL(String(tarballText));
264
+ } catch {
265
+ throw updateError("invalid-release");
266
+ }
267
+ const registryOrigin = new URL(registry).origin;
268
+ const shapeOk = tarball.origin === registryOrigin && !tarball.username && !tarball.password && !tarball.search && !tarball.hash && tarball.pathname === `/${targetName}/-/${targetName}-${version}.tgz` && typeof integrity === "string" && new RegExp(INTEGRITY_PATTERN).test(integrity);
269
+ if (!shapeOk) throw updateError("invalid-release");
270
+ return { version, nodeRange: typeof nodeRange === "string" ? nodeRange : "*", integrity, tarball: tarball.href };
271
+ } catch (error) {
272
+ if (error?.code === "check-failed") throw error;
273
+ if (error?.code === "invalid-release") throw error;
274
+ if (response !== null && response.ok) throw updateError("invalid-release");
275
+ throw checkFailedWithStatus(response);
276
+ }
277
+ }
229
278
  function createUpdateCore(ports) {
230
279
  const checkTimeoutMs = ports.checkTimeoutMs ?? CHECK_TIMEOUT_MS;
231
280
  const confirmationTtlMs = ports.confirmationTtlMs ?? CONFIRMATION_TTL_MS;
@@ -446,6 +495,7 @@ export {
446
495
  compareVersions,
447
496
  createUpdateCore,
448
497
  fetchNpmRelease,
498
+ fetchNpmVersionRelease,
449
499
  isPrereleaseVersion,
450
500
  isVersionAllowedInChannel,
451
501
  satisfiesNodeRange,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-update",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
5
  "license": "MIT",
6
6
  "author": "王辰浩",