dsh-plugin-update 0.5.4 → 0.5.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  格式为 Keep-a-Changelog 子集:`Added/Fixed/Changed` 必写,`Deprecated/Removed/Security` 有则透传,`Unreleased` 面板忽略,禁止 git-log 直倒。面板缺日志时中性提示,不挡安装。
4
4
 
5
+ ## [0.5.5] - 2026-10-06
6
+
7
+ ### Added
8
+ - 档案卷主题收敛 archive 与面板紧凑化:运行时 data-theme 统一为 archive(d5-paper 仅作输入旧别名归一),entry/entry-batch/panel-batch 输出与 CSS 选择器同步,panel-batch setTheme 补 archive 并归一存储;根/卷头/横幅/版本条/章节/章头/队列行/footer 间距收紧;删 changelog-wrap 与章节线重影内边框;页脚去技术词,批量关闭 title 同步。
9
+ - 双语底座 #54(先行):bilingual 字典模块、entry key 映射链、noteVersion 运行时值;research/52-inventory 双语盘点与原型矩阵随附。
10
+
11
+ ### Fixed
12
+ - 安装/查失败常驻与复制诊断锁定(#58):失败横幅保留到下一次用户主动查/装的新结论、任务态或版本变化,只读轮询不再洗回可装页;「复制诊断」锁定致命那次回包(稳定码、摘要、版本、宿主、请求与检查编号冻结在失败时刻);失败旁新增「知道了」可显式确认回到可装页;健康无失败时复制改为当前状态快照,不再伪造失败码;04 章失败时变为卷宗(真实稳定码、本次查询键与失败时刻、证据冻结声明、凭编号对日志的现成查询,平时为路牌+中性行,可用 `showLogHint: false` 藏路牌);日志事件名收归 `log-events.ts` 单源(宿主发射与面板路牌同源);对抗审查补强:瞬态读失败不跨重挂、查锁存不再错配旧安装编号、瞬态失败换诚实文案、跨挂载表有界 100 家;`update-busy` 仍为瞬态不锁存;四电话形状与快照六字段零变更。
13
+
5
14
  ## [0.5.4] - 2026-10-06
6
15
 
7
16
  ### Added
package/README.md CHANGED
@@ -131,7 +131,7 @@ const panel = mountUpdatePanel(document.getElementById('update-slot'), {
131
131
  // 弹窗版想让「关闭」按钮真把弹窗撤掉:传 onCloseRequested(面板点关闭/Esc 时先调它撤 DOM,再停轮询;入口件打开的 dialog 已内置)。
132
132
  ```
133
133
 
134
- 组件内部消化的事(调用者不再写):按 `panelPollMs` 轮询查状态(下限 250 毫秒);安装按钮状态跟随快照的 `canInstall`,不另写门控规则;装不了的原因按第 5.2 节展示中文一句话;`pending-restart` 单独横幅加重启指引,不再给安装按钮;手工命令展示与复制;排队位置展示与 `showOthers` 开关;跳过按版本记(“已跳过 X.Y.Z · 恢复”在同一行,不藏进设置页);失败时旁边的“复制诊断”一键给出脱敏后的自包含文本(稳定码、版本、宿主、队列位置),深挖仍看日志(组件里留着第 6 节第 11 条的过滤口径)。
134
+ 组件内部消化的事(调用者不再写):按 `panelPollMs` 轮询查状态(下限 250 毫秒);安装按钮状态跟随快照的 `canInstall`,不另写门控规则;装不了的原因按第 5.2 节展示中文一句话;`pending-restart` 单独横幅加重启指引,不再给安装按钮;手工命令展示与复制;排队位置展示与 `showOthers` 开关;跳过按版本记(“已跳过 X.Y.Z · 恢复”在同一行,不藏进设置页);失败时旁边的“复制诊断”一键给出脱敏后的自包含文本(稳定码、版本、宿主、队列位置),深挖仍看日志(组件里留着第 6 节第 11 条的过滤口径)。失败横幅常驻:查/装失败后保留到下一次用户主动查/装的新结论、任务态或版本变化,只读轮询不再洗掉;复制诊断锁定致命那次回包的证据(码、摘要、版本、宿主、编号冻结在失败时刻);失败旁有「知道了」可显式确认回到可装页(只清面板提示,不调电话)。无失败时复制诊断给当前状态快照(版本、宿主、范围、队列),不伪造失败码。04 章平时是路牌(中性行+日志过滤口径,可用 `showLogHint: false` 隐藏);失败时变为卷宗:真实稳定码、本次查询键(请求/检查编号+失败时刻)、证据冻结声明、凭编号对日志的现成查询。日志事件名(`host.call`/`host.call.fail`/`update.install.exec`)由宿主与面板共用单源,改名即全改。
135
135
 
136
136
  待重启横幅上有个「重启宿主」入口。宿主没有“重启自己”的电话,所以默认点击只如实提示「请手动重启宿主」;想把重启流程接进来就传 `onRestartRequested`(面板调它,成败都回执):
137
137
 
@@ -0,0 +1,39 @@
1
+ /** 字典版本钉(#53 Q3 解耦):key 冻即解阻塞 #54,文案按词标 draft/locked;开发钉此版本,转正只换文案不换 key。 */
2
+ export declare const BILINGUAL_DICT_VERSION = "2026-10-06-pin53";
3
+ /** 首批 key 全集(entry 7 键,命名沿 #52 §1 key 提案,area.group.name)。 */
4
+ export type BilingualKey = 'entry.label.idle' | 'entry.label.failed' | 'entry.label.busy' | 'entry.label.restart' | 'entry.label.has-update' | 'entry.action.checking' | 'entry.note.up-to-date';
5
+ /** 一条双语:en/zh 模板 + 成熟度。模板内具名槽如 {version},运行时值填入(值永不翻译)。 */
6
+ export interface BilingualEntry {
7
+ en: string;
8
+ zh: string;
9
+ /** true=待母语+域内双签(#53 Q3),false=已双签转正。首批全 draft(无评审不假锁)。 */
10
+ draft: boolean;
11
+ }
12
+ /**
13
+ * 集中字典(机器源,类型锁死:Record<BilingualKey, BilingualEntry> 缺键即 tsc 报错)。
14
+ * 人读源为 #57 规格 + #53 结论英文定调;英文终稿待母语评审,故全标 draft。
15
+ * zh 逐字等于 entry.ts 现有散落文案(兼容既有测试与接入方 label() 口径)。
16
+ */
17
+ export declare const BILINGUAL_STRINGS: Record<BilingualKey, BilingualEntry>;
18
+ /** 取一条双语原文(类型锁死:未知 key 在 tsc 即拦;运行时未知亦抛,不静默回退)。 */
19
+ export declare function bilingualEntry(key: BilingualKey): BilingualEntry;
20
+ /**
21
+ * 纯文本双语(一行,英文在前中文在后,空白分隔):用于 title / aria-label 等放不下 HTML 的属性位。
22
+ * 返回原文(未转义),调用方按上下文转义后再拼入属性。
23
+ */
24
+ export declare function bilingualText(key: BilingualKey, values?: Record<string, unknown>): string;
25
+ /**
26
+ * 双语语义块 HTML(唯一渲染出口):英文在前中文在后,空白分隔,lang 属性齐全。
27
+ * 返回已转义可直接拼入 innerHTML 的片段;调用方不得再对整体转义,不得拆 span 重组。
28
+ */
29
+ export declare function bilingualHTML(key: BilingualKey, values?: Record<string, unknown>): string;
30
+ /** 是否 draft(供 #56 门禁读零 draft 放行条件;#54 自身不断言零 draft)。 */
31
+ export declare function isDraft(key: BilingualKey): boolean;
32
+ /** 全表 draft key 一览(#56 门禁:空数组才放行合入主分支)。 */
33
+ export declare function draftKeys(): BilingualKey[];
34
+ /**
35
+ * 双语块最小样式(与 UPDATE_ENTRY_CSS 同缝注入):
36
+ * 平时行内双语(英文前中文后,空白分隔),窄处竖排不断行(@media 360px 以下 column)。
37
+ * 只管排版,不管字重字号(字重间距留给 #55 视觉矩阵验收)。
38
+ */
39
+ export declare const BILINGUAL_CSS: string;
@@ -0,0 +1,58 @@
1
+ // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/bilingual.ts
2
+ const BILINGUAL_DICT_VERSION = "2026-10-06-pin53";
3
+ const BILINGUAL_STRINGS = {
4
+ "entry.label.idle": { en: "Check for updates", zh: "\u68C0\u67E5\u66F4\u65B0", draft: true },
5
+ "entry.label.failed": { en: "Update failed \u2014 View details", zh: "\u66F4\u65B0\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B", draft: true },
6
+ "entry.label.busy": { en: "Installing\u2026", zh: "\u6B63\u5728\u5B89\u88C5\u2026", draft: true },
7
+ "entry.label.restart": { en: "Restart required", zh: "\u5F85\u91CD\u542F", draft: true },
8
+ "entry.label.has-update": { en: "Update available {version}", zh: "\u6709\u65B0\u7248 {version}", draft: true },
9
+ "entry.action.checking": { en: "Checking for updates\u2026", zh: "\u6B63\u5728\u67E5\u65B0\u7248\u2026", draft: true },
10
+ "entry.note.up-to-date": { en: "Up to date {version}", zh: "\u5DF2\u662F\u6700\u65B0 {version}", draft: true }
11
+ };
12
+ function escapeHtml(value) {
13
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
14
+ }
15
+ function formatTemplate(template, values) {
16
+ const vals = values ?? {};
17
+ return String(template).replace(/\{([A-Za-z0-9_]+)\}/g, (_, name) => {
18
+ const v = vals[name];
19
+ if (v === null || v === void 0) return "";
20
+ return String(v).trim();
21
+ });
22
+ }
23
+ function bilingualEntry(key) {
24
+ const hit = BILINGUAL_STRINGS[key];
25
+ if (!hit) throw new Error("[dsh-plugin-update] \u672A\u77E5\u6587\u6848 key\uFF1A" + String(key));
26
+ return hit;
27
+ }
28
+ function bilingualText(key, values) {
29
+ const e = bilingualEntry(key);
30
+ return formatTemplate(e.en, values) + " " + formatTemplate(e.zh, values);
31
+ }
32
+ function bilingualHTML(key, values) {
33
+ const e = bilingualEntry(key);
34
+ const en = escapeHtml(formatTemplate(e.en, values));
35
+ const zh = escapeHtml(formatTemplate(e.zh, values));
36
+ return '<span class="dsh-upd-bi"><span lang="en">' + en + '</span> <span lang="zh">' + zh + "</span></span>";
37
+ }
38
+ function isDraft(key) {
39
+ return bilingualEntry(key).draft;
40
+ }
41
+ function draftKeys() {
42
+ return Object.keys(BILINGUAL_STRINGS).filter((k) => BILINGUAL_STRINGS[k].draft);
43
+ }
44
+ const BILINGUAL_CSS = [
45
+ ".dsh-upd-bi{display:inline-flex;align-items:baseline;gap:.5em;max-width:100%}",
46
+ '.dsh-upd-bi [lang="en"],.dsh-upd-bi [lang="zh"]{overflow-wrap:anywhere}',
47
+ "@media (max-width:360px){.dsh-upd-bi{flex-direction:column;gap:0}}"
48
+ ].join("\n");
49
+ export {
50
+ BILINGUAL_CSS,
51
+ BILINGUAL_DICT_VERSION,
52
+ BILINGUAL_STRINGS,
53
+ bilingualEntry,
54
+ bilingualHTML,
55
+ bilingualText,
56
+ draftKeys,
57
+ isDraft
58
+ };
@@ -166,7 +166,7 @@ function mountUpdateBatchEntry(container, options) {
166
166
  const summary = summaryOf();
167
167
  const text = activating ? "\u6B63\u5728\u67E5\u65B0\u7248\u2026" : currentLabel();
168
168
  const busyAttr = activating ? ' disabled aria-busy="true"' : "";
169
- const themeAttr = theme === "d5-paper" ? ' data-theme="d5-paper"' : "";
169
+ const themeAttr = theme === "archive" ? ' data-theme="archive"' : "";
170
170
  const control = variant === "badge" ? '<button type="button" class="dsh-upd-entry-dot" ' + ENTRY_ATTR + '="activate" title="' + escapeHtml(text) + '" aria-label="' + escapeHtml(text) + '"' + busyAttr + "></button>" : '<button type="button" class="dsh-upd-entry-btn" ' + ENTRY_ATTR + '="activate"' + busyAttr + ">" + escapeHtml(text) + "</button>";
171
171
  const noteHTML = note ? '<span class="dsh-upd-entry-note" data-dsh-upd-note="1">' + escapeHtml(note) + "</span>" : "";
172
172
  return "<style>" + UPDATE_ENTRY_CSS + '</style>\n<span class="dsh-upd-entry" data-variant="' + variant + '" data-state="' + summary.kind + '"' + themeAttr + ">" + control + noteHTML + "</span>";
package/dist/entry.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type UpdatePanelContainer, type UpdatePanelTheme } from './panel.js';
2
2
  import type { UpdateSnapshot } from './ports.js';
3
+ import { type BilingualKey } from './bilingual.js';
3
4
  /** 入口件的形态。 */
4
5
  export type EntryVariant = 'button' | 'badge' | 'inline';
5
6
  /** 什么时候主动查一次(只读,联网一次)。 */
@@ -65,6 +66,17 @@ export interface UpdateEntryController {
65
66
  * 待重启是正常终态(不是失败),所以排在失败前面;正在装的任务比一条陈旧的失败码更可信。
66
67
  */
67
68
  export declare function entryStateKind(state: UpdateEntryState | null | undefined): EntryStateKind;
69
+ /**
70
+ * 入口件双语 key(#54 底座调用链):与 entryStateKind 同一输入(快照 + 失败码),分支只认稳定码,永不读文案。
71
+ * 数据层只传 key + {version} 运行时值,表现层经 bilingualHTML 拼语义块。
72
+ */
73
+ export declare function entryBilingualKeyFor(state: UpdateEntryState | null | undefined): BilingualKey;
74
+ /** 双语插值(运行时值永不翻译:同一 version 在中英两 span 各出现一次)。 */
75
+ export declare function entryBilingualValuesFor(state: UpdateEntryState | null | undefined): Record<string, string>;
76
+ /** 一路调用链的语义块 HTML(entryHTML 按钮正文唯一来源,无覆盖时必走此函数)。 */
77
+ export declare function entryBilingualHTMLFor(state: UpdateEntryState | null | undefined): string;
78
+ /** 属性位纯文本双语(title / aria-label 用,放不下 HTML 时的同内容平面版)。 */
79
+ export declare function entryBilingualTextFor(state: UpdateEntryState | null | undefined): string;
68
80
  /** 入口件文案(唯一出处:测试与接入方都读它,不各写一份)。 */
69
81
  export declare function entryLabelFor(state: UpdateEntryState | null | undefined): string;
70
82
  export declare const UPDATE_ENTRY_CSS: string;
package/dist/entry.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  mountUpdatePanel,
6
6
  normalizePanelTheme
7
7
  } from "./panel.js";
8
+ import { BILINGUAL_CSS, bilingualHTML, bilingualText } from "./bilingual.js";
8
9
  const LABEL_IDLE = "\u68C0\u67E5\u66F4\u65B0";
9
10
  const LABEL_FAILED = "\u66F4\u65B0\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B";
10
11
  const LABEL_BUSY = "\u6B63\u5728\u5B89\u88C5\u2026";
@@ -28,6 +29,36 @@ function entryStateKind(state) {
28
29
  if (hasUpdateOf(snapshot)) return "update";
29
30
  return "idle";
30
31
  }
32
+ function entryBilingualKeyFor(state) {
33
+ switch (entryStateKind(state)) {
34
+ case "busy":
35
+ return "entry.label.busy";
36
+ case "restart":
37
+ return "entry.label.restart";
38
+ case "failed":
39
+ return "entry.label.failed";
40
+ case "update": {
41
+ const latest = state?.snapshot?.latestVersion;
42
+ return typeof latest === "string" && latest.trim() ? "entry.label.has-update" : "entry.label.idle";
43
+ }
44
+ default:
45
+ return "entry.label.idle";
46
+ }
47
+ }
48
+ function entryBilingualValuesFor(state) {
49
+ const key = entryBilingualKeyFor(state);
50
+ if (key === "entry.label.has-update") {
51
+ const v = state?.snapshot?.latestVersion;
52
+ return { version: typeof v === "string" ? v.trim() : "" };
53
+ }
54
+ return {};
55
+ }
56
+ function entryBilingualHTMLFor(state) {
57
+ return bilingualHTML(entryBilingualKeyFor(state), entryBilingualValuesFor(state));
58
+ }
59
+ function entryBilingualTextFor(state) {
60
+ return bilingualText(entryBilingualKeyFor(state), entryBilingualValuesFor(state));
61
+ }
31
62
  function entryLabelFor(state) {
32
63
  switch (entryStateKind(state)) {
33
64
  case "busy":
@@ -66,13 +97,13 @@ const UPDATE_ENTRY_CSS = [
66
97
  '.dsh-upd-entry[data-state="failed"] .dsh-upd-entry-dot{background:var(--dsh-upd-bad-line,#dc2626)}',
67
98
  // 小字自带底(深色宿主 + 浅色变量时也读得出;浅底宿主上只是多一圈细线,不抢戏)。
68
99
  ".dsh-upd-entry-note{font-size:12.5px;opacity:.9;background:var(--dsh-upd-bg,#ffffff);border:1px solid var(--dsh-upd-line,#e5e7eb);border-radius:4px;padding:1px 8px}",
69
- '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-note{background:var(--d5-card);border-color:var(--d5-line-strong);color:var(--d5-ink)}',
70
- '.dsh-upd-entry[data-theme="d5-paper"]{--d5-ink:#1a1a1a;--d5-muted:#6f675a;--d5-line-strong:#c4b896;--d5-accent:#c8402a;--d5-card:#fffdf6;',
100
+ '.dsh-upd-entry[data-theme="archive"] .dsh-upd-entry-note{background:var(--d5-card);border-color:var(--d5-line-strong);color:var(--d5-ink)}',
101
+ '.dsh-upd-entry[data-theme="archive"]{--d5-ink:#1a1a1a;--d5-muted:#6f675a;--d5-line-strong:#c4b896;--d5-accent:#c8402a;--d5-card:#fffdf6;',
71
102
  'font-family:Georgia,"Songti SC","STSong","SimSun",serif;color:var(--d5-ink)}',
72
103
  // 按钮脸自己不透明(深色宿主 + 浅色系统变量时也读得出;hover 红在深浅底上都可见)。
73
- '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn{border-color:var(--d5-line-strong);background:var(--d5-card);color:var(--d5-ink);border-radius:3px}',
74
- '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn:hover{border-color:var(--d5-accent);color:var(--d5-accent)}',
75
- '@media (prefers-color-scheme: dark){.dsh-upd-entry[data-theme="d5-paper"]{--d5-ink:#ece5d3;--d5-muted:#a89c83;--d5-line-strong:#5c4e3b;--d5-accent:#e0684e;--d5-card:#1e1a15}}',
104
+ '.dsh-upd-entry[data-theme="archive"] .dsh-upd-entry-btn{border-color:var(--d5-line-strong);background:var(--d5-card);color:var(--d5-ink);border-radius:3px}',
105
+ '.dsh-upd-entry[data-theme="archive"] .dsh-upd-entry-btn:hover{border-color:var(--d5-accent);color:var(--d5-accent)}',
106
+ '@media (prefers-color-scheme: dark){.dsh-upd-entry[data-theme="archive"]{--d5-ink:#ece5d3;--d5-muted:#a89c83;--d5-line-strong:#5c4e3b;--d5-accent:#e0684e;--d5-card:#1e1a15}}',
76
107
  "@media (prefers-color-scheme: dark){.dsh-upd-entry{color:#e5e7eb}",
77
108
  ".dsh-upd-entry-btn{--dsh-upd-btn:#1f2937;--dsh-upd-line:#374151}}"
78
109
  ].join("\n");
@@ -133,7 +164,7 @@ function mountUpdateEntry(container, options) {
133
164
  const call = options.call;
134
165
  let snapshot = null;
135
166
  let error = null;
136
- let note = null;
167
+ let noteVersion = null;
137
168
  let activating = false;
138
169
  let mounted = true;
139
170
  let panel = null;
@@ -170,12 +201,14 @@ function mountUpdateEntry(container, options) {
170
201
  }
171
202
  function entryHTML() {
172
203
  const kind = entryStateKind(stateOf());
173
- const text = activating ? "\u6B63\u5728\u67E5\u65B0\u7248\u2026" : currentLabel();
204
+ const labelHTML = labelOverride ? escapeHtml(labelOverride) : activating ? bilingualHTML("entry.action.checking") : entryBilingualHTMLFor(stateOf());
205
+ const labelText = labelOverride ?? (activating ? bilingualText("entry.action.checking") : entryBilingualTextFor(stateOf()));
174
206
  const busyAttr = activating ? ' disabled aria-busy="true"' : "";
175
- const themeAttr = theme === "d5-paper" ? ' data-theme="d5-paper"' : "";
176
- const control = variant === "badge" ? `<button type="button" class="dsh-upd-entry-dot" ${ENTRY_ATTR}="activate" title="${escapeHtml(text)}" aria-label="${escapeHtml(text)}"${busyAttr}></button>` : `<button type="button" class="dsh-upd-entry-btn" ${ENTRY_ATTR}="activate"${busyAttr}>${escapeHtml(text)}</button>`;
177
- const noteHTML = note ? `<span class="dsh-upd-entry-note" data-dsh-upd-note="1">${escapeHtml(note)}</span>` : "";
178
- return `<style>${UPDATE_ENTRY_CSS}</style>
207
+ const themeAttr = theme === "archive" ? ' data-theme="archive"' : "";
208
+ const control = variant === "badge" ? `<button type="button" class="dsh-upd-entry-dot" ${ENTRY_ATTR}="activate" title="${escapeHtml(labelText)}" aria-label="${escapeHtml(labelText)}"${busyAttr}></button>` : `<button type="button" class="dsh-upd-entry-btn" ${ENTRY_ATTR}="activate"${busyAttr}>${labelHTML}</button>`;
209
+ const noteHTML = noteVersion ? `<span class="dsh-upd-entry-note" data-dsh-upd-note="1">${bilingualHTML("entry.note.up-to-date", { version: noteVersion })}</span>` : "";
210
+ return `<style>${UPDATE_ENTRY_CSS}
211
+ ${BILINGUAL_CSS}</style>
179
212
  <span class="dsh-upd-entry" data-variant="${variant}" data-state="${kind}"${themeAttr}>${control}${noteHTML}</span>`;
180
213
  }
181
214
  function render() {
@@ -258,7 +291,7 @@ function mountUpdateEntry(container, options) {
258
291
  return;
259
292
  }
260
293
  activating = true;
261
- note = null;
294
+ noteVersion = null;
262
295
  render();
263
296
  try {
264
297
  await checkNow();
@@ -276,7 +309,7 @@ function mountUpdateEntry(container, options) {
276
309
  openDialog();
277
310
  return;
278
311
  }
279
- note = `\u5DF2\u662F\u6700\u65B0 ${snapshot.runningVersion}`;
312
+ noteVersion = snapshot.runningVersion;
280
313
  render();
281
314
  }
282
315
  function setTheme(next) {
@@ -342,6 +375,10 @@ function mountUpdateEntry(container, options) {
342
375
  }
343
376
  export {
344
377
  UPDATE_ENTRY_CSS,
378
+ entryBilingualHTMLFor,
379
+ entryBilingualKeyFor,
380
+ entryBilingualTextFor,
381
+ entryBilingualValuesFor,
345
382
  entryLabelFor,
346
383
  entryStateKind,
347
384
  mountUpdateEntry
package/dist/host.js CHANGED
@@ -13,6 +13,7 @@ import { manualCommand } from "./commands.js";
13
13
  import { fetchReleaseChangelogText } from "./changelog-io.js";
14
14
  import { shouldFetchChangelog } from "./changelog.js";
15
15
  import { buildDiag } from "./diag.js";
16
+ import { LOG_EVENT_CALL, LOG_EVENT_CALL_FAIL } from "./log-events.js";
16
17
  import { fetchNpmVersionRelease, isVersionAllowedInChannel, updateError, validReleaseVersion, validRequestId } from "./service.js";
17
18
  import { createUpdateDiskPorts, createUpdateExecutor, createUpdateQueuePorts } from "./store.js";
18
19
  import { createUpdateReader, defaultHomeDir, resolveProfileName, resolveTargetPackage } from "./reader.js";
@@ -496,7 +497,7 @@ function loggedPhone(method, kind, pluginId, fn, diagCtx) {
496
497
  try {
497
498
  const out = await fn(safeArgs);
498
499
  const snapshot = out && out.snapshot ? out.snapshot : out;
499
- emit("info", "host.call", { method, latencyMs: Date.now() - t0, ok: true, kind, pluginId });
500
+ emit("info", LOG_EVENT_CALL, { method, latencyMs: Date.now() - t0, ok: true, kind, pluginId });
500
501
  return {
501
502
  ok: true,
502
503
  snapshot,
@@ -509,7 +510,7 @@ function loggedPhone(method, kind, pluginId, fn, diagCtx) {
509
510
  };
510
511
  } catch (error) {
511
512
  const payload = toUpdateErrorPayload(error);
512
- emit("warn", "host.call.fail", { method, kind, errorHash: hash8(String(error?.message || payload.error)), pluginId });
513
+ emit("warn", LOG_EVENT_CALL_FAIL, { method, kind, errorHash: hash8(String(error?.message || payload.error)), pluginId });
513
514
  let diag;
514
515
  try {
515
516
  const cfg = diagCtx?.config;
@@ -581,11 +582,11 @@ function loggedChangelogPhone(method, pluginId, fn, diagCtx) {
581
582
  const safeArgs = args && typeof args === "object" ? args : {};
582
583
  try {
583
584
  const out = await fn(safeArgs);
584
- emit("info", "host.call", { method, latencyMs: Date.now() - t0, ok: true, kind: "update-changelog", pluginId });
585
+ emit("info", LOG_EVENT_CALL, { method, latencyMs: Date.now() - t0, ok: true, kind: "update-changelog", pluginId });
585
586
  return { ok: true, version: out.version, markdown: out.markdown };
586
587
  } catch (error) {
587
588
  const payload = toUpdateErrorPayload(error);
588
- emit("warn", "host.call.fail", { method, kind: "update-changelog", errorHash: hash8(String(error?.message || payload.error)), pluginId });
589
+ emit("warn", LOG_EVENT_CALL_FAIL, { method, kind: "update-changelog", errorHash: hash8(String(error?.message || payload.error)), pluginId });
589
590
  let diag;
590
591
  try {
591
592
  const cfg = diagCtx?.config;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * src/log-events.ts —— 日志事件名(单源)。
3
+ *
4
+ * 宿主发射(host/store)、面板路牌(panel)、测试断言都从这里取:
5
+ * 改名即全改,不存在“发射侧改了、路牌还是旧名”的静默分叉。
6
+ * 本文件零导入,宿主/面板同缝(与 redaction 同口径)。
7
+ */
8
+ export declare const LOG_EVENT_CALL = "host.call";
9
+ export declare const LOG_EVENT_CALL_FAIL = "host.call.fail";
10
+ export declare const LOG_EVENT_INSTALL_EXEC = "update.install.exec";
@@ -0,0 +1,9 @@
1
+ // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/log-events.ts
2
+ const LOG_EVENT_CALL = "host.call";
3
+ const LOG_EVENT_CALL_FAIL = "host.call.fail";
4
+ const LOG_EVENT_INSTALL_EXEC = "update.install.exec";
5
+ export {
6
+ LOG_EVENT_CALL,
7
+ LOG_EVENT_CALL_FAIL,
8
+ LOG_EVENT_INSTALL_EXEC
9
+ };
@@ -131,7 +131,7 @@ function renderBatchPanelHTML(input) {
131
131
  parts.push('<div class="dsh-upd-batch-macros">');
132
132
  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>');
133
133
  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>');
134
- 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>');
134
+ if (mode === "dialog") parts.push('<button type="button" data-act="close" title="\u5173\u95ED\u7A97\u53E3\uFF0C\u6279\u91CF\u66F4\u65B0\u4E0D\u53D7\u5F71\u54CD">\u5173\u95ED</button>');
135
135
  parts.push("</div>");
136
136
  parts.push("</div>");
137
137
  const summary = loaded ? batchSummaryText(counts, rows, total, installing, lastError !== null) : "\u6B63\u5728\u8BFB\u53D6\u6279\u91CF\u66F4\u65B0\u72B6\u6001\u2026";
@@ -169,8 +169,8 @@ function renderBatchPanelHTML(input) {
169
169
  parts.push('<div class="dsh-upd-batch-notice" role="status">' + escapeHtml(notice) + "</div>");
170
170
  }
171
171
  const kernel = parts.join(String.fromCharCode(10));
172
- const d5 = normalizePanelTheme(theme) === "d5-paper";
173
- const attr = d5 ? ' data-theme="d5-paper"' : "";
172
+ const d5 = normalizePanelTheme(theme) === "archive";
173
+ const attr = d5 ? ' data-theme="archive"' : "";
174
174
  const seal = batchSealOf(counts);
175
175
  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>";
176
176
  const body = mode === "dialog" ? '<div class="dsh-upd-overlay" data-mode="dialog">' + root + "</div>" : root;
@@ -388,16 +388,16 @@ const UPDATE_BATCH_PANEL_CSS = [
388
388
  ].join(String.fromCharCode(10));
389
389
  const UPDATE_BATCH_PANEL_D5_CSS = [
390
390
  "/* \u6279\u91CF\u9762\u677F\u7684 D5 \u76AE\u80A4\uFF1A\u53EA\u6362\u989C\u8272/\u5B57\u4F53/\u95F4\u8DDD\uFF1B\u6839\u4E0A\u7684\u5927\u5370\u7AE0\u7531 UPDATE_PANEL_D5_CSS \u8D1F\u8D23\u3002 */",
391
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-batch-head{padding-right:120px}',
392
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-batch-title{font-family:var(--d5-serif);font-size:21px;letter-spacing:.04em}',
393
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-batch-title i{color:var(--d5-accent)}',
394
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-brow-main{background:transparent;border:0;color:var(--d5-ink)}',
395
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-brow-main:hover{color:var(--d5-accent)}',
396
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-bname{font-family:var(--d5-serif)}',
397
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-bver{font-family:var(--d5-mono);color:var(--d5-muted)}',
398
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-batch-ledger{color:var(--d5-muted)}',
399
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-batch-sum{color:var(--d5-ink)}',
400
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-brow-set{border-top-color:var(--d5-line)}'
391
+ '.dsh-upd[data-theme="archive"] .dsh-upd-batch-head{padding-right:120px}',
392
+ '.dsh-upd[data-theme="archive"] .dsh-upd-batch-title{font-family:var(--d5-serif);font-size:21px;letter-spacing:.04em}',
393
+ '.dsh-upd[data-theme="archive"] .dsh-upd-batch-title i{color:var(--d5-accent)}',
394
+ '.dsh-upd[data-theme="archive"] .dsh-upd-brow-main{background:transparent;border:0;color:var(--d5-ink)}',
395
+ '.dsh-upd[data-theme="archive"] .dsh-upd-brow-main:hover{color:var(--d5-accent)}',
396
+ '.dsh-upd[data-theme="archive"] .dsh-upd-bname{font-family:var(--d5-serif)}',
397
+ '.dsh-upd[data-theme="archive"] .dsh-upd-bver{font-family:var(--d5-mono);color:var(--d5-muted)}',
398
+ '.dsh-upd[data-theme="archive"] .dsh-upd-batch-ledger{color:var(--d5-muted)}',
399
+ '.dsh-upd[data-theme="archive"] .dsh-upd-batch-sum{color:var(--d5-ink)}',
400
+ '.dsh-upd[data-theme="archive"] .dsh-upd-brow-set{border-top-color:var(--d5-line)}'
401
401
  ].join(String.fromCharCode(10));
402
402
  function escapeHtml(text) {
403
403
  return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -995,10 +995,10 @@ function mountUpdateBatchPanel(container, options) {
995
995
  }
996
996
  }
997
997
  function setTheme(next) {
998
- if (next !== "default" && next !== "d5-paper") {
999
- throw new Error("[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 d5-paper\uFF08\u6536\u5230 " + JSON.stringify(next) + "\uFF09");
998
+ if (next !== "default" && next !== "archive" && next !== "d5-paper") {
999
+ 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");
1000
1000
  }
1001
- theme = next;
1001
+ theme = normalizePanelTheme(next);
1002
1002
  render();
1003
1003
  }
1004
1004
  function setMode(next) {
package/dist/panel.d.ts CHANGED
@@ -9,16 +9,16 @@ export type UpdatePanelMode = 'embedded' | 'dialog';
9
9
  */
10
10
  export type UpdatePanelTheme = 'default' | 'archive' | 'd5-paper';
11
11
  /**
12
- * 主题归一:archive 与旧别名 d5-paper 走同一套渲染(DOM 属性仍为 d5-paper,既有覆盖样式不断);
12
+ * 主题归一:archive 为首选名,旧别名 d5-paper 仍收并归到 archive(DOM 属性只出 archive);
13
13
  * 其余一律回 default(纯渲染函数永不抛;挂载/setTheme 的非法值另行抛错)。
14
14
  */
15
- export declare function normalizePanelTheme(value: unknown): 'default' | 'd5-paper';
15
+ export declare function normalizePanelTheme(value: unknown): 'default' | 'archive';
16
16
  /** 与宿主通话的传输函数:面板只认这个签名,不认任何宿主对象的具体形状。 */
17
17
  export type UpdatePanelCall = (phoneName: string, args: Record<string, unknown>) => Promise<Record<string, unknown>>;
18
18
  /** 复制文本的出口:不传即尝试浏览器剪贴板,都没有则只留“已生成、请手动复制”提示。 */
19
19
  export type UpdatePanelCopyText = (text: string) => void | Promise<void>;
20
20
  /** 面板可点的动作(与 HTML 里 data-action 一一对应,测试走同一条路)。 */
21
- export type UpdatePanelActionKind = 'check' | 'install' | 'skip' | 'reset-skip' | 'copy-manual' | 'copy-diag' | 'toggle-queue' | 'restart-hint' | 'close-view';
21
+ export type UpdatePanelActionKind = 'check' | 'install' | 'skip' | 'reset-skip' | 'copy-manual' | 'copy-diag' | 'toggle-queue' | 'restart-hint' | 'close-view' | 'dismiss-failure';
22
22
  export interface UpdatePanelOptions {
23
23
  /** 插件标识:必填,与宿主侧 createHostUpdate 传的 pluginId 一致。 */
24
24
  pluginId: string;
@@ -30,6 +30,8 @@ export interface UpdatePanelOptions {
30
30
  theme?: UpdatePanelTheme;
31
31
  /** 是否看他人排队明细:默认只看自己的(他人仅露正忙占位,位置照给)。 */
32
32
  showOthers?: boolean;
33
+ /** 是否显示深挖日志指引:默认显示;面向纯终端用户的嵌入可关(失败证据行不受影响)。 */
34
+ showLogHint?: boolean;
33
35
  /** 轮询间隔毫秒:默认 1000,不得小于 250。 */
34
36
  pollMs?: number;
35
37
  /** 与宿主通话的函数(面板侧唯一的宿主接触面)。 */
@@ -235,7 +237,7 @@ export interface PanelRenderInput extends PanelViewInput {
235
237
  showOthers: boolean;
236
238
  pluginId: string;
237
239
  copyNotice: string | null;
238
- /** 可选主题:不传即默认(输出与旧版一字不差);`d5-paper` 切 D5 档案卷。 */
240
+ /** 可选主题:不传即默认(输出与旧版一字不差);`archive` 切档案卷(旧别名 `d5-paper` 仍收)。 */
239
241
  theme?: UpdatePanelTheme;
240
242
  /** 使用范围名(profile):面板「使用范围」一栏的唯一来源,缺省显示“未知”,不猜。 */
241
243
  profileName?: string | null;
@@ -246,6 +248,10 @@ export interface PanelRenderInput extends PanelViewInput {
246
248
  * 缺省即静默(输出与旧版一字不差);只在挂载器置忙的那次 render 里传。
247
249
  */
248
250
  busyAct?: 'check' | 'install' | null;
251
+ /** 失败引用(失败档案的查询键;缺省即无失败,不画卷宗)。 */
252
+ failure?: PanelFailureRef | null;
253
+ /** 日志指引开关:false 即藏通用日志行(失败证据行不受影响)。 */
254
+ showLogHint?: boolean;
249
255
  /**
250
256
  * 动作面归谁:`default`(缺省)由内核画动作按钮;`none` 只画内容、不画按钮。
251
257
  * 给「调用方自己提供动作面」的场景(如批量面板的详情:动作由批量面板经自己的通道提供)。
@@ -253,6 +259,16 @@ export interface PanelRenderInput extends PanelViewInput {
253
259
  */
254
260
  actions?: 'default' | 'none';
255
261
  }
262
+ /** 内核渲染用的失败引用(只读快照:证据仍在锁存里,这里只传展示键)。 */
263
+ export interface PanelFailureRef {
264
+ requestId: string | null;
265
+ checkId: string | null;
266
+ atMs: number | null;
267
+ source: 'check' | 'install' | null;
268
+ volatile: boolean;
269
+ }
270
+ /** 锁存时刻 → 本地 HH:MM:SS(失败档案“失败于”用;非法值回 null,不画时间)。 */
271
+ export declare function formatLatchTime(atMs: unknown): string | null;
256
272
  /** 内核 HTML(双形态行为等价的根:同一视图产出同一内核,只换外层)。 */
257
273
  export declare function renderUpdatePanelKernel(input: PanelRenderInput, view: PanelView): string;
258
274
  /** 整面板 HTML(含样式;重绘即整体替换 innerHTML,故每次都带 style 也只留一份)。 */
package/dist/panel.js CHANGED
@@ -9,8 +9,9 @@ import {
9
9
  shouldFetchChangelog,
10
10
  yankedBannerHTML
11
11
  } from "./changelog.js";
12
+ import { LOG_EVENT_CALL, LOG_EVENT_CALL_FAIL, LOG_EVENT_INSTALL_EXEC } from "./log-events.js";
12
13
  function normalizePanelTheme(value) {
13
- return value === "d5-paper" || value === "archive" ? "d5-paper" : "default";
14
+ return value === "archive" || value === "d5-paper" ? "archive" : "default";
14
15
  }
15
16
  const BLOCKED_COPY = {
16
17
  "unknown-profile": {
@@ -510,8 +511,8 @@ const UPDATE_PANEL_CSS = [
510
511
  '.dsh-upd-banner[data-kind="update"]{border-color:var(--dsh-upd-ok-line);background:var(--dsh-upd-ok-bg)}',
511
512
  '.dsh-upd-banner[data-kind="busy"]{border-color:var(--dsh-upd-busy-line);background:var(--dsh-upd-busy-bg)}',
512
513
  ".dsh-upd code{font-family:Consolas,Menlo,monospace;font-size:12px;word-break:break-all}",
513
- ".dsh-upd-manual,.dsh-upd-queue,.dsh-upd-log{margin:8px 0;font-size:13px}",
514
- ".dsh-upd-changelog-wrap{margin:8px 0;font-size:13px;border-top:1px solid var(--dsh-upd-line,#e5e7eb);padding-top:8px}",
514
+ ".dsh-upd-manual,.dsh-upd-queue,.dsh-upd-log{margin:4px 0;font-size:13px}",
515
+ ".dsh-upd-changelog-wrap{margin:4px 0 0;font-size:13px}",
515
516
  ".dsh-upd-changelog-title{font-weight:700;margin:0 0 4px}",
516
517
  ".dsh-upd-changelog-version{margin:6px 0}",
517
518
  ".dsh-upd-changelog-catname{font-weight:600;margin:6px 0 2px}",
@@ -596,7 +597,7 @@ const UPDATE_PANEL_CSS = [
596
597
  ].join("\n");
597
598
  const UPDATE_PANEL_D5_CSS = [
598
599
  "/* D5 \u6863\u6848\u5377\u53EF\u9009\u4E3B\u9898\uFF1A\u53EA\u6362\u989C\u8272/\u5B57\u4F53/\u95F4\u8DDD\uFF1B\u5185\u6838 DOM \u987A\u5E8F\u4E00\u5B57\u4E0D\u52A8\uFF0C\u4E0D\u65AD\u590D\u5236\u8BCA\u65AD\u3002 */",
599
- '.dsh-upd[data-theme="d5-paper"]{--d5-bg:#f7f3ea;--d5-card:#fffdf6;--d5-ink:#1a1a1a;--d5-muted:#6f675a;',
600
+ '.dsh-upd[data-theme="archive"]{--d5-bg:#f7f3ea;--d5-card:#fffdf6;--d5-ink:#1a1a1a;--d5-muted:#6f675a;',
600
601
  "--d5-line:#e3d9c4;--d5-line-strong:#c4b896;--d5-accent:#c8402a;--d5-accent-deep:#9c2e1d;",
601
602
  "--d5-ok:#1a7f37;--d5-ok-bg:#e9f4ea;--d5-warn:#8a5a00;--d5-warn-bg:#fbf0d0;",
602
603
  "--d5-bad:#b3261e;--d5-bad-bg:#fbe9e5;",
@@ -607,116 +608,125 @@ const UPDATE_PANEL_D5_CSS = [
607
608
  "font-family:var(--d5-sans);color:var(--d5-ink);background:var(--d5-card);",
608
609
  "border:1px solid var(--d5-line-strong);border-radius:4px;box-shadow:var(--d5-shadow)}",
609
610
  // 按钮脸自己不透明(宿主底色未知时也读得出;卡片上渲染与 transparent 逐字同色)。
610
- '.dsh-upd[data-theme="d5-paper"] button{border-color:var(--d5-line-strong);background:var(--d5-card);color:var(--d5-ink);border-radius:3px;font-family:var(--d5-sans)}',
611
- '.dsh-upd[data-theme="d5-paper"] button:hover:not(:disabled){border-color:var(--d5-accent);color:var(--d5-accent)}',
612
- '.dsh-upd[data-theme="d5-paper"] button[data-primary="1"]{background:var(--d5-accent);border-color:var(--d5-accent);color:#fff}',
613
- '.dsh-upd[data-theme="d5-paper"] button[data-primary="1"]:hover:not(:disabled){background:var(--d5-accent-deep);color:#fff}',
614
- '.dsh-upd[data-theme="d5-paper"] button:focus-visible{outline:2px solid var(--d5-accent);outline-offset:2px}',
615
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner{background:var(--d5-bg);border-color:var(--d5-line-strong)}',
616
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="update"]{border-color:var(--d5-ok);background:var(--d5-ok-bg)}',
617
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="busy"]{border-color:var(--d5-warn);background:var(--d5-warn-bg)}',
618
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="restart"]{border-color:var(--d5-warn);background:var(--d5-warn-bg);font-family:var(--d5-serif);border-width:2px}',
619
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="failed"],.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="blocked"]{border-color:var(--d5-bad);background:var(--d5-bad-bg)}',
620
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="done"]{border-color:var(--d5-ok);background:var(--d5-ok-bg)}',
621
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-changelog-yanked{border-color:var(--d5-warn);background:var(--d5-warn-bg);color:var(--d5-ink)}',
622
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-breaking-badge{color:var(--d5-accent);border-color:var(--d5-accent)}',
623
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-changelog-count{color:var(--d5-muted)}',
624
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-changelog-more-note{color:var(--d5-muted)}',
611
+ '.dsh-upd[data-theme="archive"] button{border-color:var(--d5-line-strong);background:var(--d5-card);color:var(--d5-ink);border-radius:3px;font-family:var(--d5-sans)}',
612
+ '.dsh-upd[data-theme="archive"] button:hover:not(:disabled){border-color:var(--d5-accent);color:var(--d5-accent)}',
613
+ '.dsh-upd[data-theme="archive"] button[data-primary="1"]{background:var(--d5-accent);border-color:var(--d5-accent);color:#fff}',
614
+ '.dsh-upd[data-theme="archive"] button[data-primary="1"]:hover:not(:disabled){background:var(--d5-accent-deep);color:#fff}',
615
+ '.dsh-upd[data-theme="archive"] button:focus-visible{outline:2px solid var(--d5-accent);outline-offset:2px}',
616
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner{background:var(--d5-bg);border-color:var(--d5-line-strong)}',
617
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="update"]{border-color:var(--d5-ok);background:var(--d5-ok-bg)}',
618
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="busy"]{border-color:var(--d5-warn);background:var(--d5-warn-bg)}',
619
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="restart"]{border-color:var(--d5-warn);background:var(--d5-warn-bg);font-family:var(--d5-serif);border-width:2px}',
620
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="failed"],.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="blocked"]{border-color:var(--d5-bad);background:var(--d5-bad-bg)}',
621
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="done"]{border-color:var(--d5-ok);background:var(--d5-ok-bg)}',
622
+ '.dsh-upd[data-theme="archive"] .dsh-upd-changelog-yanked{border-color:var(--d5-warn);background:var(--d5-warn-bg);color:var(--d5-ink)}',
623
+ '.dsh-upd[data-theme="archive"] .dsh-upd-breaking-badge{color:var(--d5-accent);border-color:var(--d5-accent)}',
624
+ '.dsh-upd[data-theme="archive"] .dsh-upd-changelog-count{color:var(--d5-muted)}',
625
+ '.dsh-upd[data-theme="archive"] .dsh-upd-changelog-more-note{color:var(--d5-muted)}',
625
626
  // —— 大印章(原型 :206 `.seal`:右上 88px、旋转 -7°、双细框;内容与色调来自根属性,不加节点)——
626
- '.dsh-upd[data-theme="d5-paper"]{position:relative;padding:22px 26px 20px}',
627
- '.dsh-upd[data-theme="d5-paper"]::before{content:attr(data-seal);position:absolute;top:20px;right:24px;width:88px;height:88px;',
627
+ '.dsh-upd[data-theme="archive"]{position:relative;padding:16px 20px 14px}',
628
+ '.dsh-upd[data-theme="archive"]::before{content:attr(data-seal);position:absolute;top:20px;right:24px;width:88px;height:88px;',
628
629
  "display:flex;align-items:center;justify-content:center;text-align:center;letter-spacing:.18em;text-indent:.18em;line-height:1.35;",
629
630
  "border:3px solid currentColor;border-radius:14px;transform:rotate(-7deg);font-family:var(--d5-serif);font-weight:700;font-size:21px;",
630
631
  "background:color-mix(in srgb,currentColor 8%,transparent);user-select:none;pointer-events:none;",
631
632
  "box-shadow:inset 0 0 0 5px var(--d5-card),inset 0 0 0 6px currentColor,0 2px 6px rgba(0,0,0,.12)}",
632
- '.dsh-upd[data-theme="d5-paper"][data-seal-tone="ink"]::before{color:var(--d5-muted)}',
633
- '.dsh-upd[data-theme="d5-paper"][data-seal-tone="green"]::before{color:var(--d5-ok)}',
634
- '.dsh-upd[data-theme="d5-paper"][data-seal-tone="yellow"]::before{color:var(--d5-warn)}',
635
- '.dsh-upd[data-theme="d5-paper"][data-seal-tone="red"]::before{color:var(--d5-bad)}',
633
+ '.dsh-upd[data-theme="archive"][data-seal-tone="ink"]::before{color:var(--d5-muted)}',
634
+ '.dsh-upd[data-theme="archive"][data-seal-tone="green"]::before{color:var(--d5-ok)}',
635
+ '.dsh-upd[data-theme="archive"][data-seal-tone="yellow"]::before{color:var(--d5-warn)}',
636
+ '.dsh-upd[data-theme="archive"][data-seal-tone="red"]::before{color:var(--d5-bad)}',
636
637
  // 印章占位:首行(横幅/状态行)右侧留出 120px,文字不许压到印章上(原型 .filehead padding-right:120px)
637
638
  // —— 卷宗抬头(原型 :195-203 的刊头,主题切换按钮按用户口径去掉):只有 D5 档案卷才显示 ——
638
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-masthead{display:block;padding:0 0 10px;margin:0 0 12px;border-bottom:1px solid var(--d5-line)}',
639
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-masthead-kicker{display:block;font-size:11px;letter-spacing:.35em;color:var(--d5-muted);margin-bottom:3px}',
640
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-masthead-title{font-family:var(--d5-serif);font-size:26px;font-weight:700;line-height:1.2}',
641
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-masthead-title i{color:var(--d5-accent);font-style:normal}',
639
+ '.dsh-upd[data-theme="archive"] .dsh-upd-masthead{display:block;padding:0 0 8px;margin:0 0 8px;border-bottom:1px solid var(--d5-line)}',
640
+ '.dsh-upd[data-theme="archive"] .dsh-upd-masthead-kicker{display:block;font-size:11px;letter-spacing:.35em;color:var(--d5-muted);margin-bottom:3px}',
641
+ '.dsh-upd[data-theme="archive"] .dsh-upd-masthead-title{font-family:var(--d5-serif);font-size:26px;font-weight:700;line-height:1.2}',
642
+ '.dsh-upd[data-theme="archive"] .dsh-upd-masthead-title i{color:var(--d5-accent);font-style:normal}',
642
643
  // 横幅不再给大印章留 124px:实测(headless 量盒子)印章盒底边 y=109,横幅正文顶边 y=108、
643
644
  // 状态行那句在 y=159——印章只压到横幅顶部的留白带,压不到正文。留着反而把 27px 那句话挤成两行
644
645
  // (27px 单行需 428px,留白后只剩 366px)。档案头那 120px 保留:那里是真的重叠。
645
646
  // —— 更新队列(03 章)D5 皮肤 ——
646
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-qrow{padding:8px 0}',
647
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-qk{width:66px;letter-spacing:.18em}',
648
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-qv{font-family:var(--d5-serif);font-size:16px}',
649
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-qn{font-family:var(--d5-mono);font-size:11.5px;color:var(--d5-muted)}',
650
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-qseq{color:var(--d5-muted)}',
651
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-chap-note{color:var(--d5-muted)}',
647
+ '.dsh-upd[data-theme="archive"] .dsh-upd-qrow{padding:6px 0}',
648
+ '.dsh-upd[data-theme="archive"] .dsh-upd-qk{width:66px;letter-spacing:.18em}',
649
+ '.dsh-upd[data-theme="archive"] .dsh-upd-qv{font-family:var(--d5-serif);font-size:16px}',
650
+ '.dsh-upd[data-theme="archive"] .dsh-upd-qn{font-family:var(--d5-mono);font-size:11.5px;color:var(--d5-muted)}',
651
+ '.dsh-upd[data-theme="archive"] .dsh-upd-qseq{color:var(--d5-muted)}',
652
+ '.dsh-upd[data-theme="archive"] .dsh-upd-chap-note{color:var(--d5-muted)}',
652
653
  // —— 小印章(原型 :215 `.sealmini`:30px、旋转 -5°、一字)——
653
654
  // 待重启横幅一律不画印章:那一档的标记是左侧手绘 SVG(原型 :446 的 .mark 只有 SVG)。
654
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="loading"]::before,.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="idle"]::before,',
655
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="update"]::before,.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="busy"]::before,',
656
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="blocked"]::before,.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="failed"]::before,',
657
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="done"]::before{content:attr(data-mini);display:inline-flex;align-items:center;justify-content:center;',
655
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="loading"]::before,.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="idle"]::before,',
656
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="update"]::before,.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="busy"]::before,',
657
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="blocked"]::before,.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="failed"]::before,',
658
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="done"]::before{content:attr(data-mini);display:inline-flex;align-items:center;justify-content:center;',
658
659
  "width:30px;height:30px;margin-right:10px;vertical-align:middle;border:2px solid currentColor;border-radius:7px;",
659
660
  "font-family:var(--d5-serif);font-weight:700;font-size:16px;line-height:26px;transform:rotate(-5deg);flex:none;color:var(--d5-muted)}",
660
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="update"]::before{color:var(--d5-ok)}',
661
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="busy"]::before{color:var(--d5-warn)}',
662
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="blocked"]::before,.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="failed"]::before{color:var(--d5-bad)}',
663
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="done"]::before{color:var(--d5-ok)}',
664
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-log code{font-family:var(--d5-mono);font-size:11px;color:var(--d5-muted);border:1px solid var(--d5-line-strong);border-radius:3px;padding:0 6px;letter-spacing:.06em}',
665
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-manual code{display:block;background:var(--d5-ink);color:var(--d5-bg);font-family:var(--d5-mono);font-size:12.5px;padding:12px 14px;border-radius:4px;white-space:pre-wrap;word-break:break-all}',
661
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="update"]::before{color:var(--d5-ok)}',
662
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="busy"]::before{color:var(--d5-warn)}',
663
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="blocked"]::before,.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="failed"]::before{color:var(--d5-bad)}',
664
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="done"]::before{color:var(--d5-ok)}',
665
+ '.dsh-upd[data-theme="archive"] .dsh-upd-log code{font-family:var(--d5-mono);font-size:11px;color:var(--d5-muted);border:1px solid var(--d5-line-strong);border-radius:3px;padding:0 6px;letter-spacing:.06em}',
666
+ '.dsh-upd[data-theme="archive"] .dsh-upd-manual code{display:block;background:var(--d5-ink);color:var(--d5-bg);font-family:var(--d5-mono);font-size:12.5px;padding:12px 14px;border-radius:4px;white-space:pre-wrap;word-break:break-all}',
666
667
  // 待重启标记:手绘 SVG 当**独立 flex 标记**放在文字块左侧(原型 :446 `.mark` 是独立节点),
667
668
  // 不能用行内背景——那样换行时三角会落在句子中间把话劈开(现场回归)。
668
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="restart"]>div:first-child{display:flex;gap:10px;align-items:flex-start}',
669
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="restart"]>div:first-child::before{content:"";flex:none;width:20px;height:20px;margin-top:3px;',
669
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="restart"]>div:first-child{display:flex;gap:10px;align-items:flex-start}',
670
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="restart"]>div:first-child::before{content:"";flex:none;width:20px;height:20px;margin-top:3px;',
670
671
  'background:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2720%27 height=%2720%27 viewBox=%270 0 24 24%27 fill=%27none%27 stroke=%27%238a5a00%27 stroke-width=%272%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z%27/%3E%3Cline x1=%2712%27 y1=%279%27 x2=%2712%27 y2=%2713%27/%3E%3Cline x1=%2712%27 y1=%2717%27 x2=%2712.01%27 y2=%2717%27/%3E%3C/svg%3E") no-repeat center/20px 20px}',
671
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner>div:first-child{overflow:hidden;text-overflow:ellipsis}',
672
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-actions{flex-wrap:wrap}',
672
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner>div:first-child{overflow:hidden;text-overflow:ellipsis}',
673
+ '.dsh-upd[data-theme="archive"] .dsh-upd-actions{flex-wrap:wrap}',
673
674
  // footer 只换肤(#47 定案 A + D5 约束:不换 DOM 顺序;复制诊断永不隐藏,本串不动它)。
674
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-footer{margin-top:26px;padding-top:16px;border-top:1px solid var(--d5-line)}',
675
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-foot-note{color:var(--d5-muted)}',
675
+ '.dsh-upd[data-theme="archive"] .dsh-upd-footer{margin-top:14px;padding-top:10px;border-top:1px solid var(--d5-line)}',
676
+ '.dsh-upd[data-theme="archive"] .dsh-upd-foot-note{color:var(--d5-muted)}',
676
677
  // —— 档案头(原型 :208-211 `.filehead`:serif 插件名 22px + 使用范围 + profile 牌;右侧留章位)——
677
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-head{display:flex;gap:16px;align-items:baseline;flex-wrap:wrap;padding-right:120px}',
678
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-name{font-family:var(--d5-serif);font-size:22px;font-weight:700}',
679
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-meta{width:100%;font-size:12.5px;color:var(--d5-muted)}',
680
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-meta b{color:var(--d5-ink);font-weight:600}',
681
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-proftag{font-family:var(--d5-mono);font-size:11px;color:var(--d5-muted);border:1px solid var(--d5-line-strong);border-radius:3px;padding:0 6px;margin-left:8px;letter-spacing:.06em}',
678
+ '.dsh-upd[data-theme="archive"] .dsh-upd-head{display:flex;gap:16px;align-items:baseline;flex-wrap:wrap;padding-right:120px}',
679
+ '.dsh-upd[data-theme="archive"] .dsh-upd-name{font-family:var(--d5-serif);font-size:22px;font-weight:700}',
680
+ '.dsh-upd[data-theme="archive"] .dsh-upd-meta{width:100%;font-size:12.5px;color:var(--d5-muted)}',
681
+ '.dsh-upd[data-theme="archive"] .dsh-upd-meta b{color:var(--d5-ink);font-weight:600}',
682
+ '.dsh-upd[data-theme="archive"] .dsh-upd-proftag{font-family:var(--d5-mono);font-size:11px;color:var(--d5-muted);border:1px solid var(--d5-line-strong);border-radius:3px;padding:0 6px;margin-left:8px;letter-spacing:.06em}',
682
683
  // —— 版本条(原型 :216 `.strip`:三格,格间一线,左上小写标签 + 等宽值)——
683
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-strip{display:flex;flex-wrap:wrap;margin:10px 0 0;border:1px solid var(--d5-line);border-radius:4px;overflow:hidden;font-size:12.5px}',
684
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-strip>div{flex:1 1 120px;padding:8px 12px;border-left:1px solid var(--d5-line)}',
685
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-strip>div:first-child{border-left:0}',
686
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-strip-k{display:block;font-size:11px;letter-spacing:.2em;color:var(--d5-muted)}',
687
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-strip-v{font-family:var(--d5-mono);font-size:13px}',
684
+ '.dsh-upd[data-theme="archive"] .dsh-upd-strip{display:flex;flex-wrap:wrap;margin:8px 0 0;border:1px solid var(--d5-line);border-radius:4px;overflow:hidden;font-size:12.5px}',
685
+ '.dsh-upd[data-theme="archive"] .dsh-upd-strip>div{flex:1 1 120px;padding:6px 10px;border-left:1px solid var(--d5-line)}',
686
+ '.dsh-upd[data-theme="archive"] .dsh-upd-strip>div:first-child{border-left:0}',
687
+ '.dsh-upd[data-theme="archive"] .dsh-upd-strip-k{display:block;font-size:11px;letter-spacing:.2em;color:var(--d5-muted)}',
688
+ '.dsh-upd[data-theme="archive"] .dsh-upd-strip-v{font-family:var(--d5-mono);font-size:13px}',
688
689
  // —— 章节(原型 :218-245:01–05 编号 + 衬线标题 + 细线)——
689
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-chapter{margin-top:26px;padding-top:16px;border-top:1px solid var(--d5-line)}',
690
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-chap-head{display:flex;align-items:baseline;gap:12px;margin-bottom:10px}',
691
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-chap-no{font-family:var(--d5-serif);font-style:italic;font-size:15px;color:var(--d5-muted)}',
692
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-chap-title{font-family:var(--d5-serif);font-size:17px;margin:0;letter-spacing:.1em}',
693
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-chap-rule{flex:1;border-top:1px solid var(--d5-line);transform:translateY(-4px)}',
690
+ '.dsh-upd[data-theme="archive"] .dsh-upd-chapter{margin-top:16px;padding-top:10px;border-top:1px solid var(--d5-line)}',
691
+ '.dsh-upd[data-theme="archive"] .dsh-upd-chap-head{display:flex;align-items:baseline;gap:12px;margin-bottom:6px}',
692
+ '.dsh-upd[data-theme="archive"] .dsh-upd-chap-no{font-family:var(--d5-serif);font-style:italic;font-size:15px;color:var(--d5-muted)}',
693
+ '.dsh-upd[data-theme="archive"] .dsh-upd-chap-title{font-family:var(--d5-serif);font-size:17px;margin:0;letter-spacing:.1em}',
694
+ '.dsh-upd[data-theme="archive"] .dsh-upd-chap-rule{flex:1;border-top:1px solid var(--d5-line);transform:translateY(-4px)}',
694
695
  // —— 横幅即状态行 / 待重启横幅(原型 :81-85 `.restart-banner`:2px 边框、圆角 4、内边距 12/16、衬线;右侧留章位)——
695
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner{border:2px solid var(--d5-line-strong);border-radius:4px;padding:12px 16px;font-size:14.5px;font-family:var(--d5-serif);display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
696
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner>div:first-child{flex:1 1 auto;min-width:0}',
696
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner{border:2px solid var(--d5-line-strong);border-radius:4px;padding:10px 12px;font-size:14.5px;font-family:var(--d5-serif);display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
697
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner>div:first-child{flex:1 1 auto;min-width:0}',
697
698
  // 状态行字号照原型 .status-line=27px(实测去掉横幅右侧占位后可写 486px > 428px,一行放得下)
698
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner>div:first-child strong{font-family:var(--d5-serif);font-size:27px;font-weight:700;line-height:1.35}',
699
+ '.dsh-upd[data-theme="archive"] .dsh-upd-banner>div:first-child strong{font-family:var(--d5-serif);font-size:27px;font-weight:700;line-height:1.25}',
699
700
  // —— 进度条 / 跳过行(原型 :132-133 `.prog`、:129-131 `.skipline .tag`)——
700
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-prog{height:8px;background:var(--d5-line);border-radius:4px;overflow:hidden;margin:10px 0 4px}',
701
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-prog-bar{display:block;height:100%;background:var(--d5-accent);transition:width .3s}',
702
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-progtxt{font-size:12.5px;color:var(--d5-muted)}',
703
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-skipline{font-size:13px;margin-top:8px}',
704
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-tag{display:inline-block;border:1px dashed var(--d5-line-strong);border-radius:3px;padding:1px 8px;margin-right:8px;font-family:var(--d5-mono);font-size:12px}',
705
- '.dsh-upd[data-theme="d5-paper"] .dsh-upd-err{font-size:13px;margin:0 0 6px;color:var(--d5-muted)}',
706
- '@media (max-width:640px){.dsh-upd[data-theme="d5-paper"]{padding:10px 12px}.dsh-upd[data-theme="d5-paper"]::before{top:12px;right:12px;width:56px;height:56px;font-size:15px;box-shadow:inset 0 0 0 4px var(--d5-card),inset 0 0 0 5px currentColor}.dsh-upd[data-theme="d5-paper"] .dsh-upd-masthead-title{font-size:19px}.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner{padding-right:16px}.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner::before{width:24px;height:24px;font-size:14px;line-height:20px;flex:none}.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner>div:first-child{white-space:normal}}',
707
- '@media (prefers-color-scheme: dark){.dsh-upd[data-theme="d5-paper"]{--d5-bg:#141210;--d5-card:#1e1a15;--d5-ink:#ece5d3;--d5-muted:#a89c83;',
701
+ '.dsh-upd[data-theme="archive"] .dsh-upd-prog{height:8px;background:var(--d5-line);border-radius:4px;overflow:hidden;margin:10px 0 4px}',
702
+ '.dsh-upd[data-theme="archive"] .dsh-upd-prog-bar{display:block;height:100%;background:var(--d5-accent);transition:width .3s}',
703
+ '.dsh-upd[data-theme="archive"] .dsh-upd-progtxt{font-size:12.5px;color:var(--d5-muted)}',
704
+ '.dsh-upd[data-theme="archive"] .dsh-upd-skipline{font-size:13px;margin-top:8px}',
705
+ '.dsh-upd[data-theme="archive"] .dsh-upd-tag{display:inline-block;border:1px dashed var(--d5-line-strong);border-radius:3px;padding:1px 8px;margin-right:8px;font-family:var(--d5-mono);font-size:12px}',
706
+ '.dsh-upd[data-theme="archive"] .dsh-upd-err{font-size:13px;margin:0 0 6px;color:var(--d5-muted)}',
707
+ '@media (max-width:640px){.dsh-upd[data-theme="archive"]{padding:10px 12px}.dsh-upd[data-theme="archive"]::before{top:12px;right:12px;width:56px;height:56px;font-size:15px;box-shadow:inset 0 0 0 4px var(--d5-card),inset 0 0 0 5px currentColor}.dsh-upd[data-theme="archive"] .dsh-upd-masthead-title{font-size:19px}.dsh-upd[data-theme="archive"] .dsh-upd-banner{padding-right:16px}.dsh-upd[data-theme="archive"] .dsh-upd-banner::before{width:24px;height:24px;font-size:14px;line-height:20px;flex:none}.dsh-upd[data-theme="archive"] .dsh-upd-banner>div:first-child{white-space:normal}}',
708
+ '@media (prefers-color-scheme: dark){.dsh-upd[data-theme="archive"]{--d5-bg:#141210;--d5-card:#1e1a15;--d5-ink:#ece5d3;--d5-muted:#a89c83;',
708
709
  "--d5-line:#3a3226;--d5-line-strong:#5c4e3b;--d5-accent:#e0684e;--d5-accent-deep:#f0866b;",
709
710
  "--d5-ok:#8fd6a4;--d5-ok-bg:rgba(80,180,120,.12);--d5-warn:#e8c15a;--d5-warn-bg:rgba(232,193,90,.12);",
710
711
  "--d5-bad:#ef8a7d;--d5-bad-bg:rgba(239,138,125,.12);--d5-shadow:0 1px 2px rgba(0,0,0,.4),0 12px 32px rgba(0,0,0,.45)}}",
711
- '@media (prefers-color-scheme: dark){.dsh-upd[data-theme="d5-paper"] button[data-primary="1"]{color:#141210}.dsh-upd[data-theme="d5-paper"] button:focus-visible{outline-color:var(--d5-accent-deep)}}',
712
- '@media (prefers-color-scheme: dark){.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="restart"]>div:first-child::before{background-image:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2720%27 height=%2720%27 viewBox=%270 0 24 24%27 fill=%27none%27 stroke=%27%23e8c15a%27 stroke-width=%272%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z%27/%3E%3Cline x1=%2712%27 y1=%279%27 x2=%2712%27 y2=%2713%27/%3E%3Cline x1=%2712%27 y1=%2717%27 x2=%2712.01%27 y2=%2717%27/%3E%3C/svg%3E")}}',
713
- '@media (forced-colors: active){.dsh-upd[data-theme="d5-paper"]{box-shadow:none}.dsh-upd[data-theme="d5-paper"]::before{background:none;box-shadow:none;border-color:CanvasText;color:CanvasText}.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner{border:1px solid CanvasText}.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner::before{border-color:CanvasText;color:CanvasText;background:Canvas}.dsh-upd[data-theme="d5-paper"] button{border:1px solid ButtonText}.dsh-upd[data-theme="d5-paper"] button[data-primary="1"]{background:ButtonFace;color:ButtonText;border-color:ButtonText}.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="restart"]>div:first-child::before{background-image:none;content:"\u26A0"}}',
714
- '@media (prefers-reduced-motion: reduce){.dsh-upd[data-theme="d5-paper"] *{transition:none !important;animation:none !important}}'
712
+ '@media (prefers-color-scheme: dark){.dsh-upd[data-theme="archive"] button[data-primary="1"]{color:#141210}.dsh-upd[data-theme="archive"] button:focus-visible{outline-color:var(--d5-accent-deep)}}',
713
+ '@media (prefers-color-scheme: dark){.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="restart"]>div:first-child::before{background-image:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2720%27 height=%2720%27 viewBox=%270 0 24 24%27 fill=%27none%27 stroke=%27%23e8c15a%27 stroke-width=%272%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z%27/%3E%3Cline x1=%2712%27 y1=%279%27 x2=%2712%27 y2=%2713%27/%3E%3Cline x1=%2712%27 y1=%2717%27 x2=%2712.01%27 y2=%2717%27/%3E%3C/svg%3E")}}',
714
+ '@media (forced-colors: active){.dsh-upd[data-theme="archive"]{box-shadow:none}.dsh-upd[data-theme="archive"]::before{background:none;box-shadow:none;border-color:CanvasText;color:CanvasText}.dsh-upd[data-theme="archive"] .dsh-upd-banner{border:1px solid CanvasText}.dsh-upd[data-theme="archive"] .dsh-upd-banner::before{border-color:CanvasText;color:CanvasText;background:Canvas}.dsh-upd[data-theme="archive"] button{border:1px solid ButtonText}.dsh-upd[data-theme="archive"] button[data-primary="1"]{background:ButtonFace;color:ButtonText;border-color:ButtonText}.dsh-upd[data-theme="archive"] .dsh-upd-banner[data-kind="restart"]>div:first-child::before{background-image:none;content:"\u26A0"}}',
715
+ '@media (prefers-reduced-motion: reduce){.dsh-upd[data-theme="archive"] *{transition:none !important;animation:none !important}}'
715
716
  ].join("\n");
716
717
  function escapeHtml(text) {
717
718
  return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
718
719
  }
719
720
  const CHAPTER_TITLES = ["\u68C0\u67E5\u4E0E\u5B89\u88C5", "\u66F4\u65B0\u65E5\u5FD7", "\u66F4\u65B0\u961F\u5217", "\u9519\u8BEF\u4FE1\u606F", "\u624B\u5DE5\u547D\u4EE4"];
721
+ function formatLatchTime(atMs) {
722
+ if (typeof atMs !== "number" || !Number.isFinite(atMs) || atMs <= 0) return null;
723
+ try {
724
+ const s = new Date(atMs).toLocaleTimeString("zh-CN", { hour12: false });
725
+ return s ? s : null;
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
720
730
  function chapterOf(index, inner, note = "") {
721
731
  const no = String(index).padStart(2, "0");
722
732
  const title = CHAPTER_TITLES[index - 1];
@@ -781,6 +791,9 @@ function renderUpdatePanelKernel(input, view) {
781
791
  if (b.kind === "restart") {
782
792
  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>`);
783
793
  }
794
+ if (b.kind === "failed") {
795
+ actions.push(`<button type="button" data-action="dismiss-failure" title="\u786E\u8BA4\u5DF2\u77E5\u6653\u8BE5\u5931\u8D25\uFF1A\u56DE\u5230\u53EF\u88C5\u9875\uFF0C\u4E0B\u6B21\u67E5/\u88C5\u5C06\u91CD\u65B0\u8BC4\u4F30">\u77E5\u9053\u4E86</button>`);
796
+ }
784
797
  if (snapshot) {
785
798
  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>`);
786
799
  }
@@ -877,14 +890,45 @@ ${changelogHTML}
877
890
  }
878
891
  {
879
892
  const errLines = [];
880
- if (b.kind === "failed" || b.kind === "blocked") {
893
+ const failedNow = b.kind === "failed" || b.kind === "blocked";
894
+ const failRef = input.failure;
895
+ if (failedNow) {
896
+ const shownCode = b.kind === "blocked" ? snapshot?.blockedReason ?? b.kind : typeof input.lastError === "string" && input.lastError.trim() ? input.lastError : "install-failed";
881
897
  errLines.push(
882
- `<div class="dsh-upd-err">\u7A33\u5B9A\u7801 <code>${escapeHtml(String(b.kind === "blocked" ? snapshot?.blockedReason ?? b.kind : "install-failed"))}</code>\uFF1A\u4E0A\u4E00\u6761\u4E2D\u6587\u8BF4\u660E\u5C31\u662F\u8981\u7528\u6237\u505A\u7684\u4E8B\uFF1B\u8981\u5F80\u4E0A\u6E38\u62A5\uFF0C\u7528\u300C\u590D\u5236\u8BCA\u65AD\u300D\u6574\u6BB5\u7C98\uFF08\u5DF2\u8131\u654F\uFF09\u3002</div>`
898
+ `<div class="dsh-upd-err">\u7A33\u5B9A\u7801 <code>${escapeHtml(String(shownCode))}</code>\uFF1A\u4E0A\u4E00\u6761\u4E2D\u6587\u8BF4\u660E\u5C31\u662F\u8981\u7528\u6237\u505A\u7684\u4E8B\uFF1B\u8981\u5F80\u4E0A\u6E38\u62A5\uFF0C\u7528\u300C\u590D\u5236\u8BCA\u65AD\u300D\u6574\u6BB5\u7C98\uFF08\u5DF2\u8131\u654F\uFF09\u3002</div>`
883
899
  );
900
+ if (b.kind === "failed" && failRef) {
901
+ const keys = [];
902
+ if (typeof failRef.requestId === "string" && failRef.requestId) keys.push(`\u8BF7\u6C42 <code>${escapeHtml(failRef.requestId)}</code>`);
903
+ if (typeof failRef.checkId === "string" && failRef.checkId) keys.push(`\u68C0\u67E5 <code>${escapeHtml(failRef.checkId)}</code>`);
904
+ const at = formatLatchTime(failRef.atMs);
905
+ if (at) keys.push(`\u5931\u8D25\u4E8E ${escapeHtml(at)}`);
906
+ if (keys.length > 0) {
907
+ errLines.push(`<div class="dsh-upd-err">\u672C\u6B21\u67E5\u8BE2\u952E\uFF1A${keys.join(" \xB7 ")}\uFF08\u62FF\u7740\u5B83\u4EEC\u53BB\u65E5\u5FD7\u91CC\u5BF9\uFF09\u3002</div>`);
908
+ }
909
+ if (!failRef.volatile) {
910
+ errLines.push(
911
+ `<div class="dsh-upd-err">\u8BC1\u636E\u5DF2\u51BB\u7ED3\uFF1A\u590D\u5236\u8BCA\u65AD\u91CC\u7684\u7801\u3001\u7248\u672C\u3001\u7F16\u53F7\u90FD\u53D6\u81EA\u5931\u8D25\u65F6\u523B\uFF0C\u4E0D\u968F\u8F6E\u8BE2\u5237\u65B0\uFF1B\u4E0B\u4E00\u6B21\u67E5\u65B0\u7248\u6216\u5B89\u88C5\u4F1A\u66F4\u65B0\u5B83\u3002</div>`
912
+ );
913
+ } else {
914
+ errLines.push(
915
+ `<div class="dsh-upd-err">\u8BFB\u6570\u77AC\u6001\u5931\u8D25\uFF1A\u4E0B\u4E00\u6B21\u6210\u529F\u8BFB\u6570\u4F1A\u81EA\u52A8\u89E3\u9664\uFF1B\u4E00\u76F4\u51FA\u73B0\u518D\u6309\u7A33\u5B9A\u7801\u6392\u67E5\u3002</div>`
916
+ );
917
+ }
918
+ }
919
+ } else {
920
+ errLines.push(`<div class="dsh-upd-changelog-neutral">\u6682\u65E0\u5931\u8D25\uFF1A\u6B64\u65F6\u590D\u5236\u8BCA\u65AD\u7ED9\u51FA\u7684\u662F\u5F53\u524D\u72B6\u6001\u5FEB\u7167\u3002</div>`);
921
+ }
922
+ if (input.showLogHint !== false) {
923
+ errLines.push(
924
+ `<div class="dsh-upd-log">\u6DF1\u6316\u770B\u65E5\u5FD7\uFF1A\u6309\u63D2\u4EF6\u6807\u8BC6 <code>${escapeHtml(pluginId)}</code> \u8FC7\u6EE4 <code>${LOG_EVENT_CALL}</code>\u3001<code>${LOG_EVENT_CALL_FAIL}</code>\u3001<code>${LOG_EVENT_INSTALL_EXEC}</code> \u4E09\u4E2A\u4E8B\u4EF6\u3002</div>`
925
+ );
926
+ if (b.kind === "failed" && failRef && (failRef.requestId || failRef.checkId)) {
927
+ errLines.push(
928
+ `<div class="dsh-upd-log">\u51ED\u4E0A\u9762\u7684\u8BF7\u6C42\uFF0F\u68C0\u67E5\u7F16\u53F7\u5728 <code>${LOG_EVENT_CALL_FAIL}</code> \u91CC\u5BF9\u4E0A\uFF1B\u57FA\u7EBF\u8017\u65F6\u770B <code>${LOG_EVENT_CALL}</code>\uFF0C\u6267\u884C\u7ED3\u679C\u770B <code>${LOG_EVENT_INSTALL_EXEC}</code>\u3002</div>`
929
+ );
930
+ }
884
931
  }
885
- errLines.push(
886
- `<div class="dsh-upd-log">\u6DF1\u6316\u770B\u65E5\u5FD7\uFF1A\u6309\u63D2\u4EF6\u6807\u8BC6 <code>${escapeHtml(pluginId)}</code> \u8FC7\u6EE4 <code>host.call</code>\u3001<code>host.call.fail</code>\u3001<code>update.install.exec</code> \u4E09\u4E2A\u4E8B\u4EF6\u3002</div>`
887
- );
888
932
  if (copyNotice) errLines.push(`<div class="dsh-upd-copy" role="status">${escapeHtml(copyNotice)}</div>`);
889
933
  parts.push(chapterOf(4, errLines.join("")));
890
934
  }
@@ -896,7 +940,7 @@ ${changelogHTML}
896
940
  );
897
941
  if (showActions && mode === "dialog") {
898
942
  parts.push(
899
- `<div class="dsh-upd-footer"><span class="dsh-upd-foot-note">\u5B89\u88C5\u5728\u5BBF\u4E3B\u4FA7\u7EE7\u7EED\u8DD1\uFF0C\u91CD\u5F00\u6062\u590D\u663E\u793A</span><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></div>`
943
+ `<div class="dsh-upd-footer"><span class="dsh-upd-foot-note">\u5173\u95ED\u4E0D\u5F71\u54CD\u66F4\u65B0\uFF0C\u53EF\u968F\u65F6\u56DE\u6765\u67E5\u770B</span><button type="button" data-action="close-view" title="\u5173\u95ED\u7A97\u53E3\uFF0C\u66F4\u65B0\u4E0D\u53D7\u5F71\u54CD">\u5173\u95ED</button></div>`
900
944
  );
901
945
  }
902
946
  return parts.join("\n");
@@ -904,8 +948,8 @@ ${changelogHTML}
904
948
  function renderUpdatePanelHTML(input) {
905
949
  const view = panelViewModel(input);
906
950
  const kernel = renderUpdatePanelKernel(input, view);
907
- const d5 = normalizePanelTheme(input.theme) === "d5-paper";
908
- const attr = d5 ? ' data-theme="d5-paper"' : "";
951
+ const d5 = normalizePanelTheme(input.theme) === "archive";
952
+ const attr = d5 ? ' data-theme="archive"' : "";
909
953
  const sealAttr = ` data-seal="${escapeHtml(view.seal.text)}" data-seal-tone="${view.seal.tone}"`;
910
954
  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}>
911
955
  ${kernel}
@@ -917,6 +961,7 @@ ${UPDATE_PANEL_D5_CSS}` : UPDATE_PANEL_CSS;
917
961
  return `<style>${css}</style>
918
962
  ${body}`;
919
963
  }
964
+ const PANEL_FAILURE_LATCHES = /* @__PURE__ */ new Map();
920
965
  function isObject(value) {
921
966
  return !!value && typeof value === "object" && !Array.isArray(value);
922
967
  }
@@ -1019,6 +1064,7 @@ function mountUpdatePanel(container, options) {
1019
1064
  const skipStore = options.skipStore ?? createBrowserSkipStore(pluginId);
1020
1065
  const hostKind = typeof options.hostKind === "string" && options.hostKind ? options.hostKind : null;
1021
1066
  const profileNameOption = typeof options.profileName === "string" && options.profileName ? options.profileName : null;
1067
+ const showLogHintOption = options.showLogHint !== false;
1022
1068
  const diagCopyFormat = options.diagCopyFormat === "line" ? "line" : "block";
1023
1069
  let changelogMarkdown = typeof options.changelogMarkdown === "string" ? options.changelogMarkdown : null;
1024
1070
  let snapshot = null;
@@ -1026,9 +1072,139 @@ function mountUpdatePanel(container, options) {
1026
1072
  let queue = null;
1027
1073
  let receipt = null;
1028
1074
  let requestId = null;
1029
- let lastError = null;
1030
- let lastErrorKind = null;
1031
- let lastDiag = null;
1075
+ const latchKey = `${pluginId}${profileNameOption ?? ""}`;
1076
+ let latch = null;
1077
+ try {
1078
+ latch = PANEL_FAILURE_LATCHES.get(latchKey) ?? null;
1079
+ if (latch?.volatile) latch = null;
1080
+ } catch {
1081
+ latch = null;
1082
+ }
1083
+ function saveLatch() {
1084
+ try {
1085
+ if (latch && !latch.volatile) {
1086
+ if (!PANEL_FAILURE_LATCHES.has(latchKey) && PANEL_FAILURE_LATCHES.size >= 100) {
1087
+ const oldest = PANEL_FAILURE_LATCHES.keys().next();
1088
+ if (!oldest.done) PANEL_FAILURE_LATCHES.delete(oldest.value);
1089
+ }
1090
+ PANEL_FAILURE_LATCHES.set(latchKey, latch);
1091
+ } else PANEL_FAILURE_LATCHES.delete(latchKey);
1092
+ } catch {
1093
+ }
1094
+ }
1095
+ function latchNow() {
1096
+ try {
1097
+ const n = Date.now();
1098
+ return Number.isFinite(n) ? n : null;
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+ function setLatch(next) {
1104
+ latch = next;
1105
+ saveLatch();
1106
+ }
1107
+ function clearLatch() {
1108
+ if (!latch) return;
1109
+ latch = null;
1110
+ saveLatch();
1111
+ }
1112
+ function currentEnvPair() {
1113
+ return { hostKind: hostKind ?? envHostKind, profileName: profileNameOption ?? envProfileName };
1114
+ }
1115
+ function latchFromReply(reply, source) {
1116
+ const env = currentEnvPair();
1117
+ return {
1118
+ code: failureCodeOf(
1119
+ { error: reply["error"], errorKind: reply["errorKind"] },
1120
+ source === "install" ? "install-failed" : "check-failed"
1121
+ ),
1122
+ kind: typeof reply["errorKind"] === "string" && reply["errorKind"].trim() ? reply["errorKind"].trim() : null,
1123
+ detail: null,
1124
+ diag: Object.prototype.hasOwnProperty.call(reply, "diag") ? reply["diag"] : null,
1125
+ requestId: source === "install" ? requestId : null,
1126
+ checkId: source === "install" ? receipt?.checkId ?? null : null,
1127
+ runningVersion: snapshot?.runningVersion ?? null,
1128
+ installedVersion: snapshot?.installedVersion ?? null,
1129
+ latestVersion: snapshot?.latestVersion ?? null,
1130
+ hostKind: env.hostKind,
1131
+ profileName: env.profileName,
1132
+ source,
1133
+ volatile: false,
1134
+ jobId: null,
1135
+ atMs: latchNow()
1136
+ };
1137
+ }
1138
+ function latchFromJob(job, snap) {
1139
+ const env = currentEnvPair();
1140
+ const message = typeof job.message === "string" ? job.message : null;
1141
+ const detail = message && message.includes(":") ? message.slice(message.indexOf(":") + 1).trim() || null : message;
1142
+ return {
1143
+ code: messageCodeOf(message) || "install-failed",
1144
+ kind: null,
1145
+ detail,
1146
+ // 快照里没有 diag:保留锁存里既有的(多半是致命安装回包自带的),没有即缺省说人话
1147
+ diag: latch?.diag ?? null,
1148
+ requestId: job.requestId ?? latch?.requestId ?? requestId,
1149
+ checkId: receipt?.checkId ?? latch?.checkId ?? null,
1150
+ runningVersion: snap?.runningVersion ?? snapshot?.runningVersion ?? null,
1151
+ installedVersion: snap?.installedVersion ?? snapshot?.installedVersion ?? null,
1152
+ latestVersion: snap?.latestVersion ?? snapshot?.latestVersion ?? null,
1153
+ hostKind: env.hostKind ?? latch?.hostKind ?? null,
1154
+ profileName: env.profileName ?? latch?.profileName ?? null,
1155
+ source: "install",
1156
+ volatile: false,
1157
+ jobId: typeof job.id === "string" ? job.id : null,
1158
+ atMs: latchNow()
1159
+ };
1160
+ }
1161
+ function volatileLatch(source) {
1162
+ const env = currentEnvPair();
1163
+ return {
1164
+ code: "check-failed",
1165
+ kind: null,
1166
+ detail: null,
1167
+ diag: null,
1168
+ requestId,
1169
+ checkId: receipt?.checkId ?? null,
1170
+ runningVersion: snapshot?.runningVersion ?? null,
1171
+ installedVersion: snapshot?.installedVersion ?? null,
1172
+ latestVersion: snapshot?.latestVersion ?? null,
1173
+ hostKind: env.hostKind,
1174
+ profileName: env.profileName,
1175
+ source,
1176
+ volatile: true,
1177
+ jobId: null,
1178
+ atMs: latchNow()
1179
+ };
1180
+ }
1181
+ function backfillLatch(s) {
1182
+ if (!latch) return;
1183
+ let touched = false;
1184
+ if (latch.runningVersion === null && s.runningVersion) {
1185
+ latch.runningVersion = s.runningVersion;
1186
+ touched = true;
1187
+ }
1188
+ if (latch.installedVersion === null && s.installedVersion) {
1189
+ latch.installedVersion = s.installedVersion;
1190
+ touched = true;
1191
+ }
1192
+ if (latch.latestVersion === null && s.latestVersion) {
1193
+ latch.latestVersion = s.latestVersion;
1194
+ touched = true;
1195
+ }
1196
+ if (touched) saveLatch();
1197
+ }
1198
+ function tripleChanged(s) {
1199
+ if (!latch) return false;
1200
+ backfillLatch(s);
1201
+ const pairs = [
1202
+ [latch.runningVersion, s.runningVersion ?? null],
1203
+ [latch.installedVersion, s.installedVersion ?? null],
1204
+ [latch.latestVersion, s.latestVersion ?? null]
1205
+ ];
1206
+ return pairs.some(([a, b]) => a !== null && b !== null && a !== b);
1207
+ }
1032
1208
  let copyNotice = null;
1033
1209
  let noticeExpiresAt = 0;
1034
1210
  let busyAct = null;
@@ -1061,8 +1237,10 @@ function mountUpdatePanel(container, options) {
1061
1237
  queue,
1062
1238
  busyAct,
1063
1239
  skippedLatest,
1064
- lastError,
1065
- errorKind: lastErrorKind,
1240
+ lastError: latch?.code ?? null,
1241
+ errorKind: latch?.kind ?? null,
1242
+ failure: latch ? { requestId: latch.requestId, checkId: latch.checkId, atMs: latch.atMs, source: latch.source, volatile: latch.volatile } : null,
1243
+ showLogHint: showLogHintOption,
1066
1244
  changelogMarkdown,
1067
1245
  mode,
1068
1246
  showOthers,
@@ -1078,7 +1256,7 @@ function mountUpdatePanel(container, options) {
1078
1256
  container.innerHTML = nextHTML;
1079
1257
  }
1080
1258
  }
1081
- function applyStatusReply(reply) {
1259
+ function applyStatusReply(reply, via) {
1082
1260
  if (!isObject(reply)) return;
1083
1261
  if (reply["ok"] === true) {
1084
1262
  const s = asSnapshot(reply["snapshot"]);
@@ -1096,13 +1274,29 @@ function mountUpdatePanel(container, options) {
1096
1274
  if (typeof pn === "string" && pn.trim()) envProfileName = pn.trim();
1097
1275
  if (typeof hk === "string" && hk.trim()) envHostKind = hk.trim();
1098
1276
  }
1099
- lastError = null;
1100
- lastErrorKind = null;
1101
- lastDiag = Object.prototype.hasOwnProperty.call(reply, "diag") ? reply["diag"] : null;
1277
+ const jobState = snapshot?.job?.state ?? null;
1278
+ if (jobState === "installing" || jobState === "verifying") {
1279
+ clearLatch();
1280
+ } else if (jobState === "completed" || jobState === "restart-required") {
1281
+ clearLatch();
1282
+ } else if ((jobState === "failed" || jobState === "interrupted") && snapshot?.job) {
1283
+ setLatch(latchFromJob(snapshot.job, snapshot));
1284
+ } else if (via === "install") {
1285
+ clearLatch();
1286
+ } else if (snapshot && tripleChanged(snapshot)) {
1287
+ clearLatch();
1288
+ } else if (via === "check" && latch?.source === "check") {
1289
+ clearLatch();
1290
+ } else if (latch?.volatile) {
1291
+ clearLatch();
1292
+ }
1102
1293
  } else {
1103
- lastError = typeof reply["error"] === "string" ? reply["error"] : "check-failed";
1104
- lastErrorKind = typeof reply["errorKind"] === "string" && reply["errorKind"].trim() ? reply["errorKind"].trim() : null;
1105
- lastDiag = Object.prototype.hasOwnProperty.call(reply, "diag") ? reply["diag"] : null;
1294
+ const errText = typeof reply["error"] === "string" ? reply["error"].trim() : "";
1295
+ const kindText = typeof reply["errorKind"] === "string" ? reply["errorKind"].trim() : "";
1296
+ if ((kindText || errText) === "update-busy") {
1297
+ } else {
1298
+ setLatch(latchFromReply(reply, via === "install" ? "install" : "check"));
1299
+ }
1106
1300
  try {
1107
1301
  const env = reply["env"];
1108
1302
  if (isObject(env)) {
@@ -1188,12 +1382,10 @@ function mountUpdatePanel(container, options) {
1188
1382
  try {
1189
1383
  const reply = await call(phoneNames.updateStatus, queueArgs());
1190
1384
  if (!mounted) return;
1191
- applyStatusReply(reply);
1385
+ applyStatusReply(reply, "refresh");
1192
1386
  } catch {
1193
1387
  if (!mounted) return;
1194
- lastError = "check-failed";
1195
- lastErrorKind = null;
1196
- lastDiag = null;
1388
+ setLatch(volatileLatch("check"));
1197
1389
  }
1198
1390
  render();
1199
1391
  maybeAutoChangelog();
@@ -1210,13 +1402,11 @@ function mountUpdatePanel(container, options) {
1210
1402
  try {
1211
1403
  const reply = await call(phoneNames.updateCheck, queueArgs());
1212
1404
  if (!mounted) return;
1213
- applyStatusReply(reply);
1405
+ applyStatusReply(reply, "check");
1214
1406
  changelogFailedAt.clear();
1215
1407
  } catch {
1216
1408
  if (!mounted) return;
1217
- lastError = "check-failed";
1218
- lastErrorKind = null;
1219
- lastDiag = null;
1409
+ setLatch(volatileLatch("check"));
1220
1410
  } finally {
1221
1411
  busyAct = null;
1222
1412
  if (copyNotice === "\u6B63\u5728\u67E5\u65B0\u7248\u2026") copyNotice = null;
@@ -1234,13 +1424,28 @@ function mountUpdatePanel(container, options) {
1234
1424
  if (!receipt) {
1235
1425
  const checked = await call(phoneNames.updateCheck, queueArgs());
1236
1426
  if (!mounted) return;
1237
- applyStatusReply(checked);
1427
+ applyStatusReply(checked, "check");
1238
1428
  }
1239
1429
  if (!mounted) return;
1240
1430
  if (!receipt) {
1241
- lastError = "check-expired";
1242
- lastErrorKind = "check-expired";
1243
- lastDiag = null;
1431
+ const expiredEnv = currentEnvPair();
1432
+ setLatch({
1433
+ code: "check-expired",
1434
+ kind: "check-expired",
1435
+ detail: null,
1436
+ diag: null,
1437
+ requestId,
1438
+ checkId: null,
1439
+ runningVersion: snapshot?.runningVersion ?? null,
1440
+ installedVersion: snapshot?.installedVersion ?? null,
1441
+ latestVersion: snapshot?.latestVersion ?? null,
1442
+ hostKind: expiredEnv.hostKind,
1443
+ profileName: expiredEnv.profileName,
1444
+ source: "install",
1445
+ volatile: false,
1446
+ jobId: null,
1447
+ atMs: latchNow()
1448
+ });
1244
1449
  render();
1245
1450
  return;
1246
1451
  }
@@ -1251,12 +1456,10 @@ function mountUpdatePanel(container, options) {
1251
1456
  ...queueArgs()
1252
1457
  });
1253
1458
  if (!mounted) return;
1254
- applyStatusReply(reply);
1459
+ applyStatusReply(reply, "install");
1255
1460
  } catch {
1256
1461
  if (!mounted) return;
1257
- lastError = "check-failed";
1258
- lastErrorKind = null;
1259
- lastDiag = null;
1462
+ setLatch(volatileLatch("install"));
1260
1463
  } finally {
1261
1464
  busyAct = null;
1262
1465
  if (copyNotice === "\u6B63\u5728\u5B89\u88C5\u2026") copyNotice = null;
@@ -1298,25 +1501,49 @@ function mountUpdatePanel(container, options) {
1298
1501
  }
1299
1502
  case "copy-diag": {
1300
1503
  const jobCode = snapshot?.job?.state === "failed" ? messageCodeOf(snapshot.job.message) || "install-failed" : "";
1301
- const code = failureCodeOf(
1302
- { error: lastError, errorKind: lastErrorKind },
1303
- jobCode || snapshot?.blockedReason || "check-failed"
1304
- );
1305
- const detail = snapshot?.job?.message && snapshot.job.message.includes(":") ? snapshot.job.message.slice(snapshot.job.message.indexOf(":") + 1) : snapshot?.job?.message;
1504
+ if (!latch && !jobCode && !snapshot?.blockedReason && snapshot) {
1505
+ const stateLine = snapshot.canInstall && snapshot.latestVersion ? `\u6709\u65B0\u7248 ${snapshot.latestVersion} \u53EF\u88C5\uFF08\u5F53\u524D ${snapshot.runningVersion}\uFF09` : "\u5DF2\u662F\u6700\u65B0\uFF0C\u65E0\u9700\u66F4\u65B0\u3002";
1506
+ const queueName = queueTextOf(queue?.position ?? null);
1507
+ const envBits = [
1508
+ `\u63D2\u4EF6=${pluginId}`,
1509
+ `\u5BBF\u4E3B=${hostKind ?? envHostKind ?? "\u672A\u77E5"}`,
1510
+ `\u4F7F\u7528\u8303\u56F4=${profileNameOption ?? envProfileName ?? "\u672A\u77E5"}`,
1511
+ `\u961F\u5217=${queueName}`
1512
+ ];
1513
+ if (requestId) envBits.push(`\u8BF7\u6C42=${requestId}`);
1514
+ if (receipt?.checkId) envBits.push(`\u68C0\u67E5=${receipt.checkId}`);
1515
+ const segs = [
1516
+ `[update-diag] \u5F53\u524D\u65E0\u5931\u8D25\uFF1A${stateLine}`,
1517
+ `\u7248\u672C\uFF1A\u8FD0\u884C ${snapshot.runningVersion}\uFF0F\u78C1\u76D8 ${snapshot.installedVersion ?? "\u672A\u77E5"}\uFF0F\u8FDC\u7AEF ${snapshot.latestVersion ?? "\u672A\u67E5\u8FC7"}`,
1518
+ `\u6765\u6E90\uFF1A${envBits.join(" \xB7 ")}`
1519
+ ].map((s) => redactForCopy(s));
1520
+ const text2 = diagCopyFormat === "line" ? segs.join(" \xB7 ") : segs.join("\n");
1521
+ try {
1522
+ await copyText(text2);
1523
+ sayCopy("\u5DF2\u590D\u5236\u5F53\u524D\u72B6\u6001\uFF08\u65E0\u5931\u8D25\uFF09\uFF0C\u76F4\u63A5\u7C98\u7ED9\u63D2\u4EF6\u4F5C\u8005\u5373\u53EF\uFF08\u5DF2\u8131\u654F\uFF09\u3002");
1524
+ } catch {
1525
+ sayCopy("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u4E0A\u9762\u7684\u4FE1\u606F\u3002");
1526
+ }
1527
+ render();
1528
+ return;
1529
+ }
1530
+ const frozen = latch;
1531
+ const code = frozen?.code ?? failureCodeOf({ error: null, errorKind: null }, jobCode || snapshot?.blockedReason || "check-failed");
1532
+ const detail = frozen?.detail ?? (snapshot?.job?.message && snapshot.job.message.includes(":") ? snapshot.job.message.slice(snapshot.job.message.indexOf(":") + 1) : snapshot?.job?.message);
1306
1533
  const text = buildUpdateDiagCopy({
1307
1534
  pluginId,
1308
1535
  code,
1309
1536
  detail,
1310
- runningVersion: snapshot?.runningVersion ?? null,
1311
- installedVersion: snapshot?.installedVersion ?? null,
1312
- latestVersion: snapshot?.latestVersion ?? null,
1313
- hostKind: hostKind ?? envHostKind,
1314
- profileName: profileNameOption ?? envProfileName,
1537
+ runningVersion: frozen?.runningVersion ?? snapshot?.runningVersion ?? null,
1538
+ installedVersion: frozen?.installedVersion ?? snapshot?.installedVersion ?? null,
1539
+ latestVersion: frozen?.latestVersion ?? snapshot?.latestVersion ?? null,
1540
+ hostKind: frozen?.hostKind ?? hostKind ?? envHostKind,
1541
+ profileName: frozen?.profileName ?? profileNameOption ?? envProfileName,
1315
1542
  queuePosition: queue?.position ?? null,
1316
- requestId,
1317
- checkId: receipt?.checkId ?? null,
1543
+ requestId: frozen?.requestId ?? requestId,
1544
+ checkId: frozen?.checkId ?? receipt?.checkId ?? null,
1318
1545
  route: null,
1319
- diag: lastDiag,
1546
+ diag: frozen?.diag ?? null,
1320
1547
  manual,
1321
1548
  format: diagCopyFormat
1322
1549
  });
@@ -1329,6 +1556,12 @@ function mountUpdatePanel(container, options) {
1329
1556
  render();
1330
1557
  return;
1331
1558
  }
1559
+ case "dismiss-failure": {
1560
+ clearLatch();
1561
+ sayCopy("\u5DF2\u786E\u8BA4\u8BE5\u5931\u8D25\u63D0\u793A\uFF1B\u4E0B\u6B21\u67E5\u65B0\u7248\u6216\u5B89\u88C5\u5C06\u91CD\u65B0\u8BC4\u4F30\u3002");
1562
+ render();
1563
+ return;
1564
+ }
1332
1565
  case "toggle-queue": {
1333
1566
  showOthers = !showOthers;
1334
1567
  await refresh();
@@ -1514,6 +1747,7 @@ export {
1514
1747
  emptyQueueState,
1515
1748
  failureCodeOf,
1516
1749
  failureCopy,
1750
+ formatLatchTime,
1517
1751
  hasVisibleSections,
1518
1752
  isHeadOfQueue,
1519
1753
  isKnownFailureCode,
package/dist/store.js CHANGED
@@ -8,6 +8,7 @@ import { emptyBatchSession, normalizeBatchSession } from "./batch.js";
8
8
  import { normalizeQueueState } from "./queue.js";
9
9
  import { validReleaseVersion } from "./service.js";
10
10
  import { sanitizeDetail } from "./redaction.js";
11
+ import { LOG_EVENT_INSTALL_EXEC } from "./log-events.js";
11
12
  const LEGACY_SEGMENT = "updates";
12
13
  const LEGACY_DIR_PLUGIN = LEGACY_PLUGIN_ID;
13
14
  const JOB_STATES = ["installing", "verifying", "restart-required", "completed", "failed", "interrupted"];
@@ -496,7 +497,7 @@ function emitInstall(parts, recipe, ok, exitCode, durationMs) {
496
497
  if (typeof log !== "function") return;
497
498
  try {
498
499
  const pluginId = part(parts.pluginId ?? null);
499
- log("info", "update.install.exec", {
500
+ log("info", LOG_EVENT_INSTALL_EXEC, {
500
501
  route: recipe ? recipe.route : "none",
501
502
  ok: ok === true,
502
503
  exitCode: typeof exitCode === "number" ? exitCode : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-update",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
5
  "license": "MIT",
6
6
  "author": "王辰浩",