dsh-plugin-update 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  格式为 Keep-a-Changelog 子集:`Added/Fixed/Changed` 必写,`Deprecated/Removed/Security` 有则透传,`Unreleased` 面板忽略,禁止 git-log 直倒。面板缺日志时中性提示,不挡安装。
4
4
 
5
+ ## [0.5.4] - 2026-10-06
6
+
7
+ ### Added
8
+ - 批量感知入口件(#49):新子路径 `dsh-plugin-update/entry-batch`,一颗按钮看 N 家聚合(button/badge/inline;openOn 缺省 always,autoCheck 缺省 mount;只调 batchStatus/batchCheck,绝不自动装;聚合与批量总账同一份数法,忙失败占位翻回可更新)。
9
+ - 批量 dialog 关闭落地与单面板同口径(#50):`BatchPanelOptions` 新增可选 `onCloseRequested`,点关闭/Esc 先交调用方撤 DOM 再停轮询;入口件打开的 dialog 已内置;不传回退只停轮询;HTTP 直挂透传。
10
+
11
+ ## [0.5.3] - 2026-10-06
12
+
13
+ ### Added
14
+ - 进入面板自动查一次新版(#46 结论固化 #48):首绘 loading 后先只读本地刷新,拿到活体快照后条件自动查一次;轮询心跳永远只读本地、不触发查新版;抑制谓词 `pendingAutoCheck` 为纯函数,仅快照任务态 installing/verifying 或已有查/装在途时不发起,同范围忙/凭证过期/各类阻拦一律不抑制,无活体快照不发起;复用手动查同一通路与“正在查新版…”提示;并发只信服务端真相源(面板 busyAct 互斥 + checking 在途复用 + 2s 复用窗口),卸载靠 mounted 丢弃;失败无退避,只渲染既有失败文案与复制诊断,手动查清日志退避、关闭重开立即重问;embedded/dialog 一致;pollMs 不影响时机;四电话形状与快照六字段零变更。
15
+ - 关闭按钮住右下角独立 footer 区(#47 定案 A):第一章 actions 不再画 close-view,内核末尾 05 章之后新增 `.dsh-upd-footer`(dialog + 非只读渲染才有),按钮原 title/data-action 原样搬入;embedded 无面板自带关闭(宿主框架自带关);默认 + D5 只换肤、DOM 同序;act/Esc/onCloseRequested 语义一字未动。
16
+
17
+ ### Changed
18
+ - 入口件 direct 断言收紧:区分入口自身空参预查与面板挂载带 includeQueue 自查(面板挂载即自查归面板)。
19
+ - 面板安装复用断言更新:mount 自动查已拿凭证时安装直接复用,不另查(无 receipt 回退见 panel-auto-check 用例)。
20
+ - 测试:新增 tests/panel-auto-check.test.mjs 11 项(纯函数、正常/抑制/阻拦仍查、慢网单发、卸载丢弃、手动重问、轮询不查、失败无退避、安装复用凭证),tests/panel-close-footer-47.test.mjs 5 项随前一提交;全量 637 计、636 过、0 败、1 跳(既有 derive-client 跳过)。
21
+
5
22
  ## [0.5.2] - 2026-10-06
6
23
 
7
24
  ### Added
package/README.md CHANGED
@@ -195,6 +195,27 @@ mountUpdateEntry(el, { pluginId: 'p', prefix: 'notes', call, variant: 'badge',
195
195
  onActivate: ({ hasUpdate, latestVersion }) => { /* 自己跳自己的页面 */ } })
196
196
  ```
197
197
 
198
+ #### 批量感知的入口件(一颗按钮看 N 家,点开即批量面板)
199
+
200
+ 一个总管替 N 家管更新时,别再用单入口的 `manual` 桥接批量面板——徽标只反映总管自己一行,不是七家聚合。换这一颗:
201
+
202
+ ```js
203
+ import { mountUpdateBatchEntry } from 'dsh-plugin-update/entry-batch'
204
+
205
+ const entry = mountUpdateBatchEntry(document.getElementById('upd-entry'), {
206
+ prefix: 'life', // 批量电话前缀(五个批量电话从它派生)
207
+ call: (name, args) => host.call(name, args),
208
+ theme: 'archive', // 与单入口/面板同一套皮肤
209
+ })
210
+ // 点击先调 `life.batchCheck` 查一次 N 家,再以 dialog 形态开批量面板;徽标即七家聚合。
211
+ // 下游删掉 `openOn: 'manual' + onActivate` 桥接即跑,总管单行不再当状态源。
212
+ ```
213
+
214
+ 聚合口径与批量面板总账同一份数法(含「忙失败占位翻回可更新」):`忙 > 失败 > 待重启 > 可更新 > 待查`,
215
+ 文案为 `检查更新` / `N 家可更新` / `正在安装…` / `N 家待重启` / `N 家失败,点此查看`。
216
+ 三自由度与单入口同取值,缺省不同:`openOn` 缺省 `'always'`(查完总是开批量面板;`has-update` 下有事才开,否则原地给总账一句)。
217
+ `autoCheck` 缺省 `'mount'`(只调 `.batchStatus`,只读);铁律同单入口:任何路径都不自动安装。
218
+
198
219
  ### 第 2.6 节:多目标批量更新(一个插件管 N 个插件的更新)
199
220
 
200
221
  一个插件替自己**和另外几个插件**管更新时,别把 N 个面板并排——用户在那块界面上只问三件事:
@@ -239,6 +260,7 @@ const panel = mountUpdateBatchPanel(el, {
239
260
  theme: 'archive', // 与单插件面板同一套皮肤(archive = 档案卷,旧值 d5-paper 仍可用)
240
261
  })
241
262
  ```
263
+ // 弹窗版与单面板同口径:传 `onCloseRequested`(点关闭/Esc 时先调它撤 DOM,再停轮询;入口件打开的 dialog 已内置;不传即只停轮询)。
242
264
 
243
265
  批量面板 HTTP 版(无 `host.call` 环境即跑:同一传输内核,五电话走同一映射,取消走 `batchCancel`):
244
266
 
@@ -0,0 +1,92 @@
1
+ import { type BatchRowView } from './panel-batch.js';
2
+ import { type UpdatePanelTheme } from './panel.js';
3
+ /** 批量入口件的形态(与单入口同一套取值)。 */
4
+ export type BatchEntryVariant = 'button' | 'badge' | 'inline';
5
+ /** 什么时候主动查一次(只读;与单入口同一口径)。 */
6
+ export type BatchEntryAutoCheck = 'mount' | 'never';
7
+ /**
8
+ * 用户点下去之后做什么(与单入口同一套取值,缺省不同:批量缺省 'always')。
9
+ * 单入口缺省 'has-update'(有新版才开单面板);批量入口缺省 'always'(查完总是开批量面板——
10
+ * 批量面板的总账本身就是答案,空着也值得看一眼;下游删 manual 桥接即按此缺省跑)。
11
+ * 'has-update' 下只有聚合档非 idle 才开面板(忙/失败/待重启/可更新都算有事),否则原地给总账一句。
12
+ */
13
+ export type BatchEntryOpenOn = 'has-update' | 'always' | 'manual' | 'direct';
14
+ /** 批量入口件的状态档:只用来算文案与 data-state,不另立状态机。 */
15
+ export type BatchEntryStateKind = 'idle' | 'update' | 'busy' | 'restart' | 'failed';
16
+ /** 算文案要的全部输入:批量七行 + 最近一次批量电话的失败稳定码(没查过、没失败为 null)。 */
17
+ export interface UpdateBatchEntryState {
18
+ rows: readonly BatchRowView[] | null;
19
+ error: string | null;
20
+ }
21
+ /** 聚合后的读数(onActivate 与测试共用同一份,不各算一份)。 */
22
+ export interface BatchEntrySummary {
23
+ kind: BatchEntryStateKind;
24
+ label: string;
25
+ updatable: number;
26
+ installing: number;
27
+ pending: number;
28
+ restart: number;
29
+ failed: number;
30
+ skipped: number;
31
+ settled: number;
32
+ total: number;
33
+ hasUpdate: boolean;
34
+ }
35
+ export interface UpdateBatchEntryOptions {
36
+ /** 批量电话前缀(与宿主侧一致,例 'life')。 */
37
+ prefix: string;
38
+ /** 调宿主电话:(phoneName, args) => Promise<reply>。 */
39
+ call: (name: string, args: Record<string, unknown>) => Promise<unknown>;
40
+ variant?: BatchEntryVariant;
41
+ /** 主题:与单入口/面板同一套(default/archive,d5-paper 为旧别名仍可用)。 */
42
+ theme?: UpdatePanelTheme;
43
+ /** 缺省 'mount':进页面静默查一次(只调 .batchStatus,只读)。'never' 则只在用户点击时查。 */
44
+ autoCheck?: BatchEntryAutoCheck;
45
+ /** 缺省 'always':查完总是开批量面板(见本类型说明)。 */
46
+ openOn?: BatchEntryOpenOn;
47
+ /** 覆盖默认按钮文案(不传就用聚合文案)。 */
48
+ label?: string;
49
+ /** 可选:键 -> 中文名覆盖(透传给批量面板)。 */
50
+ titles?: Record<string, string>;
51
+ /** 轮询间隔(毫秒,透传给批量面板;缺省 1500,下限 250)。 */
52
+ pollMs?: number;
53
+ /**
54
+ * 详情行自动取日志(透传给批量面板,缺省开)。
55
+ * 展开行有新版时调该行自己的更新日志电话按需取;false 关闭。
56
+ */
57
+ autoChangelog?: boolean;
58
+ /** 「重启宿主」的落地(透传给批量面板):不传就只提示手动重启,不假装能重启。 */
59
+ onRestartRequested?: () => void | Promise<void>;
60
+ /** 复制文本的出口(透传给批量面板):不传即试浏览器剪贴板,都没有也不抛错。 */
61
+ copyText?: (text: string) => void | Promise<void>;
62
+ /** variant='badge' 或 openOn='manual' 时,点击交给接入方(自己跳自己的页面)。 */
63
+ onActivate?: (state: BatchEntrySummary) => void;
64
+ }
65
+ export interface UpdateBatchEntryController {
66
+ /** 立刻查一次并把状态刷到界面上(只读:调 .batchStatus)。 */
67
+ refresh(): Promise<void>;
68
+ /** 打开批量面板(dialog 形态)。 */
69
+ open(): void;
70
+ /** 关掉批量面板。 */
71
+ close(): void;
72
+ /** 当前按钮上的聚合文案(接入方做自定义排版时读它)。 */
73
+ label(): string;
74
+ /** 当前聚合读数(含七档计数,接入方排版与 onActivate 共用)。 */
75
+ summary(): BatchEntrySummary;
76
+ setTheme(theme: UpdatePanelTheme): void;
77
+ unmount(): void;
78
+ }
79
+ /**
80
+ * 聚合读数:计数唯一出处 batchLedgerCounts(与面板总账同一份,忙失败占位翻回可更新)。
81
+ * hasUpdate = 忙/失败/待重启/可更新任一非零(openOn has-update 按它开面板;全 settled/待查/跳过即 idle)。
82
+ */
83
+ export declare function batchEntrySummary(state: UpdateBatchEntryState | null | undefined): BatchEntrySummary;
84
+ /** 状态档(唯一出处:batchEntrySummary,不各写一份)。 */
85
+ export declare function batchEntryStateKind(state: UpdateBatchEntryState | null | undefined): BatchEntryStateKind;
86
+ /** 批量入口件文案(唯一出处:测试与接入方都读它,不各写一份)。 */
87
+ export declare function batchEntryLabelFor(state: UpdateBatchEntryState | null | undefined): string;
88
+ export declare function mountUpdateBatchEntry(container: {
89
+ innerHTML: string;
90
+ addEventListener?: (type: string, listener: (ev: unknown) => void) => void;
91
+ removeEventListener?: (type: string, listener: (ev: unknown) => void) => void;
92
+ }, options: UpdateBatchEntryOptions): UpdateBatchEntryController;
@@ -0,0 +1,355 @@
1
+ // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/entry-batch.ts
2
+ import { MIN_PANEL_POLL_MS } from "./config.js";
3
+ import { UPDATE_ENTRY_CSS } from "./entry.js";
4
+ import {
5
+ batchLedgerCounts,
6
+ batchLedgerText,
7
+ buildBatchPhoneNames,
8
+ mountUpdateBatchPanel
9
+ } from "./panel-batch.js";
10
+ import {
11
+ failureCodeOf,
12
+ normalizePanelTheme
13
+ } from "./panel.js";
14
+ const LABEL_IDLE = "\u68C0\u67E5\u66F4\u65B0";
15
+ const LABEL_BUSY = "\u6B63\u5728\u5B89\u88C5\u2026";
16
+ function isObject(value) {
17
+ return !!value && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+ function asRows(value) {
20
+ if (!Array.isArray(value)) return null;
21
+ return value;
22
+ }
23
+ function escapeHtml(value) {
24
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
25
+ }
26
+ function batchEntrySummary(state) {
27
+ const rows = state?.rows ?? null;
28
+ const counts = batchLedgerCounts(Array.isArray(rows) ? rows : []);
29
+ const total = Array.isArray(rows) ? rows.length : 0;
30
+ let kind;
31
+ if (!Array.isArray(rows) || rows.length === 0) {
32
+ kind = state?.error ? "failed" : "idle";
33
+ } else if (counts.installing > 0) {
34
+ kind = "busy";
35
+ } else if (counts.failed > 0) {
36
+ kind = "failed";
37
+ } else if (counts.restart > 0) {
38
+ kind = "restart";
39
+ } else if (counts.updatable > 0) {
40
+ kind = "update";
41
+ } else {
42
+ kind = "idle";
43
+ }
44
+ if (kind !== "busy" && state?.error) {
45
+ if (Array.isArray(rows) && rows.length > 0 && counts.failed === 0 && counts.installing === 0) {
46
+ kind = "failed";
47
+ }
48
+ }
49
+ const hasUpdate = counts.installing > 0 || counts.failed > 0 || counts.restart > 0 || counts.updatable > 0;
50
+ let label;
51
+ switch (kind) {
52
+ case "busy":
53
+ label = LABEL_BUSY;
54
+ break;
55
+ case "failed":
56
+ label = counts.failed > 0 ? counts.failed + " \u5BB6\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B" : "\u66F4\u65B0\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B";
57
+ break;
58
+ case "restart":
59
+ label = counts.restart + " \u5BB6\u5F85\u91CD\u542F";
60
+ break;
61
+ case "update":
62
+ label = counts.updatable + " \u5BB6\u53EF\u66F4\u65B0";
63
+ break;
64
+ default:
65
+ label = LABEL_IDLE;
66
+ break;
67
+ }
68
+ return {
69
+ kind,
70
+ label,
71
+ updatable: counts.updatable,
72
+ installing: counts.installing,
73
+ pending: counts.pending,
74
+ restart: counts.restart,
75
+ failed: counts.failed,
76
+ skipped: counts.skipped,
77
+ settled: counts.settled,
78
+ total,
79
+ hasUpdate
80
+ };
81
+ }
82
+ function batchEntryStateKind(state) {
83
+ return batchEntrySummary(state).kind;
84
+ }
85
+ function batchEntryLabelFor(state) {
86
+ return batchEntrySummary(state).label;
87
+ }
88
+ function mountUpdateBatchEntry(container, options) {
89
+ if (!container || typeof container.innerHTML !== "string") {
90
+ throw new Error("[dsh-plugin-update] \u6302\u6279\u91CF\u5165\u53E3\u4EF6\u9700\u8981\u4E00\u4E2A\u6709 innerHTML \u7684\u5BB9\u5668");
91
+ }
92
+ if (!options || typeof options !== "object") {
93
+ throw new Error("[dsh-plugin-update] \u6302\u6279\u91CF\u5165\u53E3\u4EF6\u7F3A\u5C11\u914D\u7F6E\uFF1A\u6279\u91CF\u7535\u8BDD\u524D\u7F00 prefix \u5FC5\u586B");
94
+ }
95
+ if (typeof options.call !== "function") {
96
+ throw new Error("[dsh-plugin-update] \u6302\u6279\u91CF\u5165\u53E3\u4EF6\u9700\u8981\u4F20\u8F93\u51FD\u6570 call\uFF08\u5165\u53E3\u4EF6\u8C03\u5BBF\u4E3B\u7535\u8BDD\u7684\u552F\u4E00\u63A5\u89E6\u9762\uFF09");
97
+ }
98
+ const phones = buildBatchPhoneNames(options.prefix);
99
+ const variant = options.variant ?? "button";
100
+ if (variant !== "button" && variant !== "badge" && variant !== "inline") {
101
+ throw new Error("[dsh-plugin-update] \u5165\u53E3\u4EF6\u5F62\u6001\u975E\u6CD5\uFF1A\u53EA\u6536 button / badge / inline\uFF08\u6536\u5230 " + JSON.stringify(options.variant) + "\uFF09");
102
+ }
103
+ const autoCheck = options.autoCheck ?? "mount";
104
+ if (autoCheck !== "mount" && autoCheck !== "never") {
105
+ throw new Error("[dsh-plugin-update] \u81EA\u52A8\u68C0\u67E5\u65F6\u673A\u975E\u6CD5\uFF1A\u53EA\u6536 mount \u6216 never\uFF08\u6536\u5230 " + JSON.stringify(options.autoCheck) + "\uFF09");
106
+ }
107
+ const openOn = options.openOn ?? "always";
108
+ if (openOn !== "has-update" && openOn !== "always" && openOn !== "manual" && openOn !== "direct") {
109
+ throw new Error("[dsh-plugin-update] \u70B9\u51FB\u53BB\u5411\u975E\u6CD5\uFF1A\u53EA\u6536 has-update / always / manual / direct\uFF08\u6536\u5230 " + JSON.stringify(options.openOn) + "\uFF09");
110
+ }
111
+ if (options.theme !== void 0 && options.theme !== "default" && options.theme !== "archive" && options.theme !== "d5-paper") {
112
+ 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");
113
+ }
114
+ let theme = normalizePanelTheme(options.theme ?? "default");
115
+ const pollMs = options.pollMs;
116
+ if (pollMs !== void 0 && (typeof pollMs !== "number" || !Number.isFinite(pollMs) || pollMs < MIN_PANEL_POLL_MS)) {
117
+ 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");
118
+ }
119
+ const labelOverride = typeof options.label === "string" && options.label ? options.label : null;
120
+ const titles = options.titles && typeof options.titles === "object" ? options.titles : {};
121
+ const onActivate = typeof options.onActivate === "function" ? options.onActivate : null;
122
+ const call = options.call;
123
+ const ENTRY_ATTR = "data-dsh-upd-entry";
124
+ const ENTRY_SELECTOR = "[data-dsh-upd-entry]";
125
+ let rows = null;
126
+ let error = null;
127
+ let note = null;
128
+ let loaded = false;
129
+ let activating = false;
130
+ let mounted = true;
131
+ let panel = null;
132
+ let panelMode = null;
133
+ const panelHost = {
134
+ get innerHTML() {
135
+ return container.innerHTML;
136
+ },
137
+ set innerHTML(value) {
138
+ container.innerHTML = value;
139
+ },
140
+ addEventListener(type, listener) {
141
+ ;
142
+ container.addEventListener?.(type, listener);
143
+ },
144
+ removeEventListener(type, listener) {
145
+ ;
146
+ container.removeEventListener?.(type, listener);
147
+ }
148
+ };
149
+ async function panelCall(name, args) {
150
+ const reply = await call(name, args);
151
+ return isObject(reply) ? reply : {};
152
+ }
153
+ function stateOf() {
154
+ return { rows, error };
155
+ }
156
+ function summaryOf() {
157
+ return batchEntrySummary(stateOf());
158
+ }
159
+ function currentLabel() {
160
+ return labelOverride ?? summaryOf().label;
161
+ }
162
+ function phoneArgs() {
163
+ return {};
164
+ }
165
+ function entryHTML() {
166
+ const summary = summaryOf();
167
+ const text = activating ? "\u6B63\u5728\u67E5\u65B0\u7248\u2026" : currentLabel();
168
+ const busyAttr = activating ? ' disabled aria-busy="true"' : "";
169
+ const themeAttr = theme === "d5-paper" ? ' data-theme="d5-paper"' : "";
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
+ const noteHTML = note ? '<span class="dsh-upd-entry-note" data-dsh-upd-note="1">' + escapeHtml(note) + "</span>" : "";
172
+ return "<style>" + UPDATE_ENTRY_CSS + '</style>\n<span class="dsh-upd-entry" data-variant="' + variant + '" data-state="' + summary.kind + '"' + themeAttr + ">" + control + noteHTML + "</span>";
173
+ }
174
+ function render() {
175
+ if (!mounted) return;
176
+ if (panelMode !== null) return;
177
+ container.innerHTML = entryHTML();
178
+ }
179
+ function applyReply(reply) {
180
+ if (!isObject(reply) || reply["ok"] !== true) {
181
+ error = failureCodeOf(isObject(reply) ? reply : null, "check-failed");
182
+ return;
183
+ }
184
+ const next = asRows(reply["rows"]);
185
+ if (next) rows = next;
186
+ error = null;
187
+ loaded = true;
188
+ }
189
+ async function refresh() {
190
+ if (!mounted) return;
191
+ try {
192
+ const reply = await call(phones.status, phoneArgs());
193
+ if (!mounted) return;
194
+ applyReply(reply);
195
+ } catch {
196
+ if (!mounted) return;
197
+ error = "check-failed";
198
+ }
199
+ render();
200
+ }
201
+ async function checkNow() {
202
+ try {
203
+ const reply = await call(phones.check, phoneArgs());
204
+ if (!mounted) return;
205
+ applyReply(reply);
206
+ } catch {
207
+ if (!mounted) return;
208
+ error = "check-failed";
209
+ }
210
+ }
211
+ function mountPanel(mode) {
212
+ if (!mounted || panelMode !== null) return;
213
+ panel = mountUpdateBatchPanel(panelHost, {
214
+ prefix: options.prefix,
215
+ mode,
216
+ theme,
217
+ pollMs,
218
+ titles,
219
+ onRestartRequested: options.onRestartRequested,
220
+ copyText: options.copyText,
221
+ autoChangelog: options.autoChangelog,
222
+ // 面板点「关闭」即走入口件的完整关闭(收 dialog + 还原按钮 + 重查一次),不再是面板自己停轮询。
223
+ onCloseRequested: () => close(),
224
+ call: panelCall
225
+ });
226
+ panelMode = mode;
227
+ }
228
+ function openDialog() {
229
+ mountPanel("dialog");
230
+ }
231
+ function open() {
232
+ if (!mounted || panelMode !== null) return;
233
+ if (variant === "inline") return;
234
+ openDialog();
235
+ }
236
+ function close() {
237
+ if (!mounted || panelMode !== "dialog" || !panel) return;
238
+ const opened = panel;
239
+ panel = null;
240
+ panelMode = null;
241
+ try {
242
+ opened.unmount();
243
+ } catch {
244
+ }
245
+ render();
246
+ void refresh();
247
+ }
248
+ async function activate() {
249
+ if (!mounted || panelMode !== null || activating) return;
250
+ if (openOn === "direct" && variant === "button") {
251
+ openDialog();
252
+ return;
253
+ }
254
+ activating = true;
255
+ note = null;
256
+ render();
257
+ try {
258
+ await checkNow();
259
+ } finally {
260
+ activating = false;
261
+ }
262
+ if (!mounted) return;
263
+ if (variant === "badge" || openOn === "manual") {
264
+ if (onActivate) onActivate(summaryOf());
265
+ else openDialog();
266
+ render();
267
+ return;
268
+ }
269
+ const summary = summaryOf();
270
+ if (openOn === "always" || summary.hasUpdate || !loaded || error) {
271
+ openDialog();
272
+ return;
273
+ }
274
+ note = batchLedgerText({
275
+ updatable: summary.updatable,
276
+ installing: summary.installing,
277
+ pending: summary.pending,
278
+ restart: summary.restart,
279
+ failed: summary.failed,
280
+ skipped: summary.skipped,
281
+ settled: summary.settled
282
+ });
283
+ render();
284
+ }
285
+ function setTheme(next) {
286
+ if (next !== "default" && next !== "archive" && next !== "d5-paper") {
287
+ 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");
288
+ }
289
+ theme = normalizePanelTheme(next);
290
+ if (panel) void panel.setTheme(theme);
291
+ else render();
292
+ }
293
+ function onClick(ev) {
294
+ if (!mounted) return;
295
+ try {
296
+ const target = ev?.target;
297
+ const closest = target && typeof target.closest === "function" ? target.closest.bind(target) : null;
298
+ if (!closest) return;
299
+ if (closest(ENTRY_SELECTOR)) {
300
+ void activate();
301
+ return;
302
+ }
303
+ } catch {
304
+ }
305
+ }
306
+ function onKeyDown(ev) {
307
+ try {
308
+ const e = ev;
309
+ if (!mounted || panelMode !== "dialog" || !e || e.key !== "Escape") return;
310
+ close();
311
+ } catch {
312
+ }
313
+ }
314
+ function unmount() {
315
+ if (!mounted) return;
316
+ mounted = false;
317
+ const opened = panel;
318
+ panel = null;
319
+ panelMode = null;
320
+ try {
321
+ opened?.unmount();
322
+ } catch {
323
+ }
324
+ try {
325
+ ;
326
+ container.removeEventListener?.("click", onClick);
327
+ } catch {
328
+ }
329
+ try {
330
+ ;
331
+ container.removeEventListener?.("keydown", onKeyDown);
332
+ } catch {
333
+ }
334
+ }
335
+ try {
336
+ ;
337
+ container.addEventListener?.("click", onClick);
338
+ } catch {
339
+ }
340
+ try {
341
+ ;
342
+ container.addEventListener?.("keydown", onKeyDown);
343
+ } catch {
344
+ }
345
+ if (variant === "inline") mountPanel("embedded");
346
+ else render();
347
+ if (autoCheck === "mount") void refresh();
348
+ return { refresh, open, close, label: currentLabel, summary: summaryOf, setTheme, unmount };
349
+ }
350
+ export {
351
+ batchEntryLabelFor,
352
+ batchEntryStateKind,
353
+ batchEntrySummary,
354
+ mountUpdateBatchEntry
355
+ };
package/dist/host.js CHANGED
@@ -532,7 +532,40 @@ function loggedPhone(method, kind, pluginId, fn, diagCtx) {
532
532
  } catch {
533
533
  diag = void 0;
534
534
  }
535
- return { ok: false, ...payload, ...diag ? { diag } : {} };
535
+ let failReader = null;
536
+ try {
537
+ const failConfig = diagCtx?.config;
538
+ const failOverrides = diagCtx?.readerOverrides ?? {};
539
+ if (failConfig && (wantsEnv(safeArgs) || queueArgsOf(safeArgs).includeQueue)) {
540
+ failReader = await getSharedReader(pluginId, failConfig, {
541
+ ...failOverrides,
542
+ profileDir: safeArgs && safeArgs.profileDir ? String(safeArgs.profileDir) : failOverrides.profileDir
543
+ });
544
+ }
545
+ } catch {
546
+ failReader = null;
547
+ }
548
+ let envPatch = {};
549
+ try {
550
+ if (failReader && wantsEnv(safeArgs)) {
551
+ const env = await readEnvForPanel(failReader);
552
+ if (env) envPatch = { env };
553
+ }
554
+ } catch {
555
+ envPatch = {};
556
+ }
557
+ let queuePatch = {};
558
+ try {
559
+ const { includeQueue, showOthers } = queueArgsOf(safeArgs);
560
+ if (failReader && includeQueue) {
561
+ const requestIdForQueue = safeArgs && typeof safeArgs.requestId === "string" ? String(safeArgs.requestId) : void 0;
562
+ const q = visibleQueueFor(await failReader.readQueuePruned(), pluginId, showOthers, requestIdForQueue);
563
+ if (q) queuePatch = { queue: q };
564
+ }
565
+ } catch {
566
+ queuePatch = {};
567
+ }
568
+ return { ok: false, ...payload, ...diag ? { diag } : {}, ...envPatch, ...queuePatch };
536
569
  }
537
570
  };
538
571
  }
@@ -49,6 +49,11 @@ export interface BatchPanelOptions {
49
49
  * 老宿主没给行电话名即回中性提示;false 关闭。
50
50
  */
51
51
  autoChangelog?: boolean;
52
+ /**
53
+ * 弹窗关闭的落地(与单插件面板同一口径):只在 dialog 下点「关闭」/按 Esc 时调用;调用方在此撤掉弹窗 DOM。
54
+ * 不传则回退为只停轮询(unmount),DOM 留给调用方处理。入口件打开的 dialog 已内置(收 dialog + 还原按钮)。
55
+ */
56
+ onCloseRequested?: () => void | Promise<void>;
52
57
  }
53
58
  /** 面板可点的动作(HTML 上 data-act 一一对应;测试走同一条路)。 */
54
59
  export type BatchPanelActionKind = 'check' | 'install' | 'resume' | 'cancel' | 'row-install' | 'row-skip' | 'row-resume-skip' | 'row-cancel-queue' | 'row-copy-manual' | 'row-copy-diag' | 'toggle-details' | 'restart' | 'close';
@@ -627,6 +627,7 @@ function mountUpdateBatchPanel(container, options) {
627
627
  const call = options.call;
628
628
  const titles = options.titles && typeof options.titles === "object" ? options.titles : {};
629
629
  const onRestartRequested = options.onRestartRequested;
630
+ const onCloseRequested = typeof options.onCloseRequested === "function" ? options.onCloseRequested : null;
630
631
  const copyText = options.copyText ?? defaultCopyText;
631
632
  const skipStores = /* @__PURE__ */ new Map();
632
633
  const autoChangelogEnabled = options.autoChangelog !== false;
@@ -968,18 +969,28 @@ function mountUpdateBatchPanel(container, options) {
968
969
  return;
969
970
  }
970
971
  case "close": {
971
- if (mode === "dialog") unmount();
972
+ await requestDialogClose();
972
973
  return;
973
974
  }
974
975
  default:
975
976
  return;
976
977
  }
977
978
  }
979
+ async function requestDialogClose() {
980
+ if (!mounted || mode !== "dialog") return;
981
+ if (onCloseRequested) {
982
+ try {
983
+ await onCloseRequested();
984
+ } catch {
985
+ }
986
+ }
987
+ unmount();
988
+ }
978
989
  function onKeyDown(ev) {
979
990
  try {
980
991
  const e = ev;
981
992
  if (!mounted || mode !== "dialog" || !e || e.key !== "Escape") return;
982
- unmount();
993
+ void requestDialogClose();
983
994
  } catch {
984
995
  }
985
996
  }
package/dist/panel.d.ts CHANGED
@@ -139,7 +139,7 @@ export interface PanelDiagnosticInput {
139
139
  installedVersion?: string | null;
140
140
  latestVersion?: string | null;
141
141
  hostKind?: string | null;
142
- /** 使用范围名:装到哪个 profile 是排错第一信息;不传则那一段不出现(输出与旧版一字不差)。 */
142
+ /** 使用范围名:装到哪个 profile 是排错第一信息;旧复制不传则该段不出现,新复制块恒显(缺省为未知)。 */
143
143
  profileName?: string | null;
144
144
  queuePosition?: number | null;
145
145
  requestId?: string | null;
@@ -182,7 +182,7 @@ export interface UpdateDiagCopyInput extends PanelDiagnosticInput {
182
182
  * 组出 [update-diag] 复制块(双形态同序;调用方直接拿去粘工单/issue,已脱敏)。
183
183
  * 第一性:顺序码→摘要→来源(#18 定),怎么办是面板页脚放最后,不插断三元组;
184
184
  * 缺省即省略(#18):路由/请求/检查/阶段等缺失即不出现,不占位“未知”;
185
- * 插件/版本/宿主/队列恒显(面板侧显式值兜底),摘要缺省给人话,源缺省给人话(省略本身即信息,人读不懂所以必须说)。
185
+ * 插件/版本/宿主/使用范围/队列恒显(面板侧显式值兜底),摘要缺省给人话,源缺省给人话(省略本身即信息,人读不懂所以必须说)。
186
186
  */
187
187
  export declare function buildUpdateDiagCopy(input: UpdateDiagCopyInput): string;
188
188
  export declare function createMemorySkipStore(now?: () => number): PanelSkipStore;
@@ -257,6 +257,13 @@ export interface PanelRenderInput extends PanelViewInput {
257
257
  export declare function renderUpdatePanelKernel(input: PanelRenderInput, view: PanelView): string;
258
258
  /** 整面板 HTML(含样式;重绘即整体替换 innerHTML,故每次都带 style 也只留一份)。 */
259
259
  export declare function renderUpdatePanelHTML(input: PanelRenderInput): string;
260
+ /**
261
+ * mount 自动查抑制谓词(#48:#46 结论固化,纯函数):
262
+ * 仅当快照任务态为 installing/verifying,或已有查/装在途动作时不发起;
263
+ * 同范围忙、凭证过期、各类阻拦一律不抑制(前两者正是要刷新/重建凭证时)。
264
+ * 无活体快照(null)也不发起:首个只读刷新没拿到快照时不猜。
265
+ */
266
+ export declare function pendingAutoCheck(snapshot: UpdateSnapshot | null, busyAct: 'check' | 'install' | null): boolean;
260
267
  /**
261
268
  * 挂载整组件(面板侧一行即跑):
262
269
  * ```js
package/dist/panel.js CHANGED
@@ -206,6 +206,7 @@ function buildUpdateDiagCopy(input) {
206
206
  const checkRaw = diag.checkId ?? input.checkId;
207
207
  const queue = queueTextOf(input.queuePosition ?? null);
208
208
  const prov = [`\u63D2\u4EF6=${pluginName}`, `\u7248\u672C=${runVer}\u2192${instVer}`, `\u5BBF\u4E3B=${host}`];
209
+ prov.push(`\u4F7F\u7528\u8303\u56F4=${pickText(input.profileName, "\u672A\u77E5")}`);
209
210
  if (typeof routeRaw === "string" && routeRaw.trim()) prov.push(`\u8DEF\u7531=${routeRaw.trim()}`);
210
211
  if (diag.stage) prov.push(`\u9636\u6BB5=${diag.stage}`);
211
212
  if (diag.method) prov.push(`\u65B9\u6CD5=${diag.method}`);
@@ -541,6 +542,10 @@ const UPDATE_PANEL_CSS = [
541
542
  ".dsh-upd-strip-k{display:block;font-size:11px;letter-spacing:.14em;opacity:.7}",
542
543
  ".dsh-upd-strip-v{font-family:Consolas,Menlo,monospace;font-size:12.5px}",
543
544
  ".dsh-upd-chapter{margin-top:14px;padding-top:10px;border-top:1px solid var(--dsh-upd-line,#e5e7eb)}",
545
+ // 右下角独立 footer 区(#47 定案 A):与第一章 actions 脱钩,右对齐,一次找到。
546
+ ".dsh-upd-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:14px;padding-top:10px;border-top:1px solid var(--dsh-upd-line,#e5e7eb)}",
547
+ ".dsh-upd-foot-note{margin-right:auto;font-size:12px;opacity:.7}",
548
+ ".dsh-upd-footer button{margin:0}",
544
549
  ".dsh-upd-chap-head{display:flex;align-items:baseline;gap:10px;margin-bottom:6px}",
545
550
  ".dsh-upd-chap-no{font-size:13px;font-style:italic;opacity:.6}",
546
551
  ".dsh-upd-chap-title{font-size:14px;margin:0}",
@@ -665,6 +670,9 @@ const UPDATE_PANEL_D5_CSS = [
665
670
  '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}',
666
671
  '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner>div:first-child{overflow:hidden;text-overflow:ellipsis}',
667
672
  '.dsh-upd[data-theme="d5-paper"] .dsh-upd-actions{flex-wrap:wrap}',
673
+ // 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)}',
668
676
  // —— 档案头(原型 :208-211 `.filehead`:serif 插件名 22px + 使用范围 + profile 牌;右侧留章位)——
669
677
  '.dsh-upd[data-theme="d5-paper"] .dsh-upd-head{display:flex;gap:16px;align-items:baseline;flex-wrap:wrap;padding-right:120px}',
670
678
  '.dsh-upd[data-theme="d5-paper"] .dsh-upd-name{font-family:var(--d5-serif);font-size:22px;font-weight:700}',
@@ -776,9 +784,6 @@ function renderUpdatePanelKernel(input, view) {
776
784
  if (snapshot) {
777
785
  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>`);
778
786
  }
779
- if (mode === "dialog") {
780
- 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>`);
781
- }
782
787
  actions.push("</div>");
783
788
  }
784
789
  actions.push(progressBar(snapshot));
@@ -889,6 +894,11 @@ ${changelogHTML}
889
894
  view.showManual && manual ? `<div class="dsh-upd-manual"><div>\u624B\u5DE5\u515C\u5E95\u547D\u4EE4\uFF08\u590D\u5236\u6574\u884C\u6267\u884C\uFF09\uFF1A</div><code>${escapeHtml(manual)}</code></div>` : `<div class="dsh-upd-manual"><div class="dsh-upd-changelog-neutral">\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u624B\u5DE5\u547D\u4EE4\uFF08\u8BA4\u4E0D\u51FA\u4F7F\u7528\u8303\u56F4\u6216\u5C5E\u6E90\u7801\u5B89\u88C5\u65F6\u4E0D\u7ED9\uFF09\u3002</div></div>`
890
895
  )
891
896
  );
897
+ if (showActions && mode === "dialog") {
898
+ 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>`
900
+ );
901
+ }
892
902
  return parts.join("\n");
893
903
  }
894
904
  function renderUpdatePanelHTML(input) {
@@ -958,6 +968,13 @@ async function defaultCopyText(text) {
958
968
  } catch {
959
969
  }
960
970
  }
971
+ function pendingAutoCheck(snapshot, busyAct) {
972
+ if (busyAct === "check" || busyAct === "install") return false;
973
+ if (!snapshot) return false;
974
+ const st = snapshot?.job?.state;
975
+ if (st === "installing" || st === "verifying") return false;
976
+ return true;
977
+ }
961
978
  function mountUpdatePanel(container, options) {
962
979
  if (!container || typeof container.innerHTML !== "string") {
963
980
  throw new Error("[dsh-plugin-update] \u6302\u8F7D\u9762\u677F\u9700\u8981\u4E00\u4E2A\u6709 innerHTML \u7684\u5BB9\u5668");
@@ -1076,8 +1093,8 @@ function mountUpdatePanel(container, options) {
1076
1093
  if (isObject(env)) {
1077
1094
  const pn = env["profileName"];
1078
1095
  const hk = env["environmentKind"];
1079
- if (typeof pn === "string" && pn) envProfileName = pn;
1080
- if (typeof hk === "string" && hk) envHostKind = hk;
1096
+ if (typeof pn === "string" && pn.trim()) envProfileName = pn.trim();
1097
+ if (typeof hk === "string" && hk.trim()) envHostKind = hk.trim();
1081
1098
  }
1082
1099
  lastError = null;
1083
1100
  lastErrorKind = null;
@@ -1086,6 +1103,21 @@ function mountUpdatePanel(container, options) {
1086
1103
  lastError = typeof reply["error"] === "string" ? reply["error"] : "check-failed";
1087
1104
  lastErrorKind = typeof reply["errorKind"] === "string" && reply["errorKind"].trim() ? reply["errorKind"].trim() : null;
1088
1105
  lastDiag = Object.prototype.hasOwnProperty.call(reply, "diag") ? reply["diag"] : null;
1106
+ try {
1107
+ const env = reply["env"];
1108
+ if (isObject(env)) {
1109
+ const pn = env["profileName"];
1110
+ const hk = env["environmentKind"];
1111
+ if (typeof pn === "string" && pn.trim()) envProfileName = pn.trim();
1112
+ if (typeof hk === "string" && hk.trim()) envHostKind = hk.trim();
1113
+ }
1114
+ } catch {
1115
+ }
1116
+ try {
1117
+ const q = asQueue(reply["queue"]);
1118
+ if (q) queue = q;
1119
+ } catch {
1120
+ }
1089
1121
  }
1090
1122
  }
1091
1123
  function pendingAutoChangelogVersion() {
@@ -1278,7 +1310,8 @@ function mountUpdatePanel(container, options) {
1278
1310
  runningVersion: snapshot?.runningVersion ?? null,
1279
1311
  installedVersion: snapshot?.installedVersion ?? null,
1280
1312
  latestVersion: snapshot?.latestVersion ?? null,
1281
- hostKind,
1313
+ hostKind: hostKind ?? envHostKind,
1314
+ profileName: profileNameOption ?? envProfileName,
1282
1315
  queuePosition: queue?.position ?? null,
1283
1316
  requestId,
1284
1317
  checkId: receipt?.checkId ?? null,
@@ -1410,7 +1443,13 @@ function mountUpdatePanel(container, options) {
1410
1443
  if (h && typeof h.unref === "function") h.unref();
1411
1444
  } catch {
1412
1445
  }
1413
- void refresh();
1446
+ void (async () => {
1447
+ await refresh();
1448
+ try {
1449
+ if (pendingAutoCheck(snapshot, busyAct)) void act("check");
1450
+ } catch {
1451
+ }
1452
+ })();
1414
1453
  return { refresh, act, setMode, setTheme, setShowOthers, setChangelogMarkdown, unmount };
1415
1454
  }
1416
1455
  import { buildPhoneNames as buildPhoneNames2 } from "./config.js";
@@ -1485,6 +1524,7 @@ export {
1485
1524
  normalizeQueueState,
1486
1525
  panelViewModel,
1487
1526
  parseChangelog2 as parseChangelog,
1527
+ pendingAutoCheck,
1488
1528
  queuePositionOf,
1489
1529
  readDiagTolerant,
1490
1530
  redactForCopy,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-update",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
5
  "license": "MIT",
6
6
  "author": "王辰浩",
@@ -38,6 +38,10 @@
38
38
  "types": "./dist/entry.d.ts",
39
39
  "default": "./dist/entry.js"
40
40
  },
41
+ "./entry-batch": {
42
+ "types": "./dist/entry-batch.d.ts",
43
+ "default": "./dist/entry-batch.js"
44
+ },
41
45
  "./http": {
42
46
  "types": "./dist/http.d.ts",
43
47
  "default": "./dist/http.js"