dsh-plugin-update 0.5.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/entry.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type UpdatePanelContainer, type UpdatePanelTheme } from './panel.js';
2
2
  import type { UpdateSnapshot } from './ports.js';
3
3
  import { type BilingualKey } from './bilingual.js';
4
+ import { type AppLang, type LocaleOption } from './lang.js';
4
5
  /** 入口件的形态。 */
5
6
  export type EntryVariant = 'button' | 'badge' | 'inline';
6
7
  /** 什么时候主动查一次(只读,联网一次)。 */
@@ -20,6 +21,24 @@ export interface UpdateEntryState {
20
21
  snapshot: UpdateSnapshot | null;
21
22
  error: string | null;
22
23
  }
24
+ /**
25
+ * 入口件按钮尺寸覆盖(#69):只影响 button 本体(字号/内边距/圆角/整体缩放),不碰 dialog 面板。
26
+ * 缺省(不传)保持现状、零回归;badge 圆点与 inline 内嵌不读这些变量,天然不受影响。
27
+ * 实现口径与现有 `--dsh-upd-*` 颜色变量一致:本选项只是把同样的 CSS 变量以内联方式写到容器上,
28
+ * 手写 CSS 变量(`--dsh-update-entry-font-size` / `--dsh-update-entry-padding` /
29
+ * `--dsh-update-entry-border-radius` / `--dsh-update-entry-scale`)同样生效,两者等价。
30
+ * `scale` 即宿主临时方案里容器 `zoom: 1` 开关的正式形态:改一个数即缩放(默认 1)。
31
+ */
32
+ export interface EntrySizing {
33
+ /** 按钮字号(CSS font-size 值,如 '12px'),不传即 13px 默认。 */
34
+ fontSize?: string;
35
+ /** 按钮内边距(CSS padding 值,如 '2px 8px'),不传即 '4px 12px' 默认。 */
36
+ padding?: string;
37
+ /** 按钮圆角(CSS border-radius 值,如 '8px'),不传即默认主题 6px / archive 主题 3px。 */
38
+ borderRadius?: string;
39
+ /** 整体缩放(对应 CSS `zoom`,如 1.2 放大、0.9 缩小),须为大于 0 的有限数,不传即 1。 */
40
+ scale?: number;
41
+ }
23
42
  export interface UpdateEntryOptions {
24
43
  pluginId: string;
25
44
  /** 单插件电话前缀(与宿主侧一致)。 */
@@ -27,7 +46,7 @@ export interface UpdateEntryOptions {
27
46
  /** 调宿主电话:(phoneName, args) => Promise<reply>。 */
28
47
  call: (name: string, args: Record<string, unknown>) => Promise<unknown>;
29
48
  variant?: EntryVariant;
30
- /** 主题:`default` 最小可用深色,`archive` 档案卷纸面浅色(`d5-paper` 为旧别名仍可用)。 */
49
+ /** 主题:`default` 最小可用深色,`archive` 档案卷纸面浅色(仍可用)。 */
31
50
  theme?: UpdatePanelTheme;
32
51
  /** 缺省 'mount':进页面静默查一次(只读)。'never' 则只在用户点击时查。 */
33
52
  autoCheck?: EntryAutoCheck;
@@ -35,6 +54,10 @@ export interface UpdateEntryOptions {
35
54
  openOn?: EntryOpenOn;
36
55
  /** 覆盖默认按钮文案(不传就用状态联动文案)。 */
37
56
  label?: string;
57
+ /** 按钮尺寸覆盖(#69):见 EntrySizing;不传即零回归,badge/inline 不受影响。 */
58
+ sizing?: EntrySizing;
59
+ /** 语言覆盖(#60 v2):'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void },不传即跟随 html[lang] > navigator > zh。 */
60
+ locale?: LocaleOption;
38
61
  profileName?: string;
39
62
  pollMs?: number;
40
63
  /**
@@ -68,16 +91,21 @@ export interface UpdateEntryController {
68
91
  export declare function entryStateKind(state: UpdateEntryState | null | undefined): EntryStateKind;
69
92
  /**
70
93
  * 入口件双语 key(#54 底座调用链):与 entryStateKind 同一输入(快照 + 失败码),分支只认稳定码,永不读文案。
71
- * 数据层只传 key + {version} 运行时值,表现层经 bilingualHTML 拼语义块。
94
+ * 数据层只传 key + {version} 运行时值,表现层经 copyHTML 拼单语块。
72
95
  */
73
96
  export declare function entryBilingualKeyFor(state: UpdateEntryState | null | undefined): BilingualKey;
74
- /** 双语插值(运行时值永不翻译:同一 version 在中英两 span 各出现一次)。 */
97
+ /** 双语插值(运行时值永不翻译:同一 version 在单语块里原样透传,冻结词元两语言逐字相同)。 */
75
98
  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;
80
- /** 入口件文案(唯一出处:测试与接入方都读它,不各写一份)。 */
81
- export declare function entryLabelFor(state: UpdateEntryState | null | undefined): string;
99
+ /** 一路调用链的单语块 HTML(entryHTML 按钮正文唯一来源,无覆盖时必走此函数;lang 显式入参,缺省跟随全局)。 */
100
+ export declare function entryBilingualHTMLFor(state: UpdateEntryState | null | undefined, lang?: AppLang | string | null): string;
101
+ /** 属性位纯文本单语(title / aria-label 用,放不下 HTML 时的同内容平面版;与 HTML 出口同语言)。 */
102
+ export declare function entryBilingualTextFor(state: UpdateEntryState | null | undefined, lang?: AppLang | string | null): string;
103
+ /** 入口件文案第二出口(唯一出处:测试与接入方都读它;与 HTML 出口同语言,v2 起按 lang 取字典,不再硬编码中文)。 */
104
+ export declare function entryLabelFor(state: UpdateEntryState | null | undefined, lang?: AppLang | string | null): string;
105
+ /**
106
+ * `sizing` → 容器 style 属性体(纯函数:同一输入永远算出同一串;空/缺省回空串,即不写 style)。
107
+ * 非法即抛(未知键、空串、注入字符、非正有限 scale 都不收)。
108
+ */
109
+ export declare function entrySizingStyleFor(sizing: EntrySizing | null | undefined): string;
82
110
  export declare const UPDATE_ENTRY_CSS: string;
83
111
  export declare function mountUpdateEntry(container: UpdatePanelContainer, options: UpdateEntryOptions): UpdateEntryController;
package/dist/entry.js CHANGED
@@ -5,11 +5,8 @@ import {
5
5
  mountUpdatePanel,
6
6
  normalizePanelTheme
7
7
  } from "./panel.js";
8
- import { BILINGUAL_CSS, bilingualHTML, bilingualText } from "./bilingual.js";
9
- const LABEL_IDLE = "\u68C0\u67E5\u66F4\u65B0";
10
- const LABEL_FAILED = "\u66F4\u65B0\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B";
11
- const LABEL_BUSY = "\u6B63\u5728\u5B89\u88C5\u2026";
12
- const LABEL_RESTART = "\u5F85\u91CD\u542F";
8
+ import { BILINGUAL_CSS, copyHTML, copyText } from "./bilingual.js";
9
+ import { resolveLang, subscribeLang } from "./lang.js";
13
10
  function hasUpdateOf(snapshot) {
14
11
  if (!snapshot) return false;
15
12
  const latest = snapshot.latestVersion;
@@ -53,33 +50,63 @@ function entryBilingualValuesFor(state) {
53
50
  }
54
51
  return {};
55
52
  }
56
- function entryBilingualHTMLFor(state) {
57
- return bilingualHTML(entryBilingualKeyFor(state), entryBilingualValuesFor(state));
53
+ function entryBilingualHTMLFor(state, lang) {
54
+ const l = lang ?? resolveLang();
55
+ return copyHTML(entryBilingualKeyFor(state), l, entryBilingualValuesFor(state));
58
56
  }
59
- function entryBilingualTextFor(state) {
60
- return bilingualText(entryBilingualKeyFor(state), entryBilingualValuesFor(state));
57
+ function entryBilingualTextFor(state, lang) {
58
+ const l = lang ?? resolveLang();
59
+ return copyText(entryBilingualKeyFor(state), l, entryBilingualValuesFor(state));
61
60
  }
62
- function entryLabelFor(state) {
63
- switch (entryStateKind(state)) {
64
- case "busy":
65
- return LABEL_BUSY;
66
- case "restart":
67
- return LABEL_RESTART;
68
- case "failed":
69
- return LABEL_FAILED;
70
- case "update": {
71
- const latest = state?.snapshot?.latestVersion;
72
- return typeof latest === "string" && latest.trim() ? `\u6709\u65B0\u7248 ${latest.trim()}` : LABEL_IDLE;
61
+ function entryLabelFor(state, lang) {
62
+ const l = lang ?? resolveLang();
63
+ return copyText(entryBilingualKeyFor(state), l, entryBilingualValuesFor(state));
64
+ }
65
+ function entrySizingError(raw) {
66
+ return new Error(`[dsh-plugin-update] \u5165\u53E3\u4EF6\u5C3A\u5BF8\u53C2\u6570 sizing \u975E\u6CD5\uFF1A\u53EA\u6536 fontSize / padding / borderRadius\uFF08\u975E\u7A7A CSS \u503C\uFF09\u4E0E scale\uFF08\u5927\u4E8E 0 \u7684\u6709\u9650\u6570\uFF09\uFF08\u6536\u5230 ${JSON.stringify(raw ?? null)})`);
67
+ }
68
+ function isSafeEntryCssValue(value) {
69
+ const v = value.trim();
70
+ if (!v || v.length > 200) return false;
71
+ if (/[;"'<>`{}!&]/.test(v)) return false;
72
+ if (/url\s*\(/i.test(v)) return false;
73
+ if (/expression\s*\(/i.test(v)) return false;
74
+ if (/javascript\s*:/i.test(v)) return false;
75
+ return true;
76
+ }
77
+ function entrySizingStyleFor(sizing) {
78
+ if (sizing === void 0 || sizing === null) return "";
79
+ if (typeof sizing !== "object" || Array.isArray(sizing)) throw entrySizingError(sizing);
80
+ for (const key of Object.keys(sizing)) {
81
+ if (key !== "fontSize" && key !== "padding" && key !== "borderRadius" && key !== "scale") {
82
+ throw entrySizingError(sizing);
73
83
  }
74
- default:
75
- return LABEL_IDLE;
76
84
  }
85
+ const parts = [];
86
+ const { fontSize, padding, borderRadius, scale } = sizing;
87
+ if (fontSize !== void 0) {
88
+ if (typeof fontSize !== "string" || !isSafeEntryCssValue(fontSize)) throw entrySizingError(sizing);
89
+ parts.push(`--dsh-update-entry-font-size:${fontSize.trim()}`);
90
+ }
91
+ if (padding !== void 0) {
92
+ if (typeof padding !== "string" || !isSafeEntryCssValue(padding)) throw entrySizingError(sizing);
93
+ parts.push(`--dsh-update-entry-padding:${padding.trim()}`);
94
+ }
95
+ if (borderRadius !== void 0) {
96
+ if (typeof borderRadius !== "string" || !isSafeEntryCssValue(borderRadius)) throw entrySizingError(sizing);
97
+ parts.push(`--dsh-update-entry-border-radius:${borderRadius.trim()}`);
98
+ }
99
+ if (scale !== void 0) {
100
+ if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0) throw entrySizingError(sizing);
101
+ parts.push(`--dsh-update-entry-scale:${String(scale)}`);
102
+ }
103
+ return parts.join(";");
77
104
  }
78
105
  const UPDATE_ENTRY_CSS = [
79
- '.dsh-upd-entry{display:inline-flex;align-items:center;gap:8px;font:13px/1.6 system-ui,"Microsoft YaHei",sans-serif;color:var(--dsh-upd-fg,#1f2937)}',
80
- ".dsh-upd-entry-btn{font:inherit;border:1px solid var(--dsh-upd-line,#d1d5db);border-radius:6px;",
81
- "background:var(--dsh-upd-btn,#f9fafb);color:inherit;padding:4px 12px;cursor:pointer}",
82
- ".dsh-upd-entry-btn:hover{border-color:var(--dsh-upd-primary,#2563eb)}",
106
+ '.dsh-upd-entry{display:inline-flex;align-items:center;gap:8px;font:13px/1.6 var(--dsh-update-font-sans,system-ui,"Microsoft YaHei",sans-serif);font-size:var(--dsh-update-entry-font-size,13px);color:var(--dsh-update-text,#1f2937)}',
107
+ ".dsh-upd-entry-btn{font:inherit;border:1px solid var(--dsh-update-border,#d1d5db);border-radius:var(--dsh-update-entry-border-radius,6px);",
108
+ "background:var(--dsh-update-button-bg,#f9fafb);color:inherit;padding:var(--dsh-update-entry-padding,4px 12px);zoom:var(--dsh-update-entry-scale,1);cursor:pointer}",
109
+ ".dsh-upd-entry-btn:hover{border-color:var(--dsh-update-primary,#2563eb)}",
83
110
  ".dsh-upd-entry-btn,.dsh-upd-entry-dot{transition:background-color .15s ease,border-color .15s ease,color .15s ease,transform .06s ease}",
84
111
  ".dsh-upd-entry-btn:active:not(:disabled){transform:translateY(1px)}",
85
112
  ".dsh-upd-entry-btn:disabled,.dsh-upd-entry-dot:disabled{opacity:.55;cursor:wait}",
@@ -87,25 +114,26 @@ const UPDATE_ENTRY_CSS = [
87
114
  "border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:dsh-upd-entry-spin .8s linear infinite}",
88
115
  "@keyframes dsh-upd-entry-spin{to{transform:rotate(360deg)}}",
89
116
  '@media (prefers-reduced-motion: reduce){.dsh-upd-entry-btn,.dsh-upd-entry-dot{transition:none}.dsh-upd-entry-btn[aria-busy="true"]::after{animation:none}}',
90
- ".dsh-upd-entry-btn:focus-visible,.dsh-upd-entry-dot:focus-visible{outline:2px solid var(--dsh-upd-focus,#2563eb);outline-offset:1px}",
91
- '.dsh-upd-entry[data-state="update"] .dsh-upd-entry-btn{border-color:var(--dsh-upd-ok-line,#059669);color:var(--dsh-upd-ok-line,#059669)}',
92
- '.dsh-upd-entry[data-state="restart"] .dsh-upd-entry-btn{border-color:var(--dsh-upd-warn-line,#d97706);color:var(--dsh-upd-warn-line,#d97706)}',
93
- '.dsh-upd-entry[data-state="failed"] .dsh-upd-entry-btn{border-color:var(--dsh-upd-bad-line,#dc2626);color:var(--dsh-upd-bad-line,#dc2626)}',
94
- ".dsh-upd-entry-dot{width:10px;height:10px;padding:0;border:0;border-radius:50%;background:var(--dsh-upd-line,#9ca3af);cursor:pointer}",
95
- '.dsh-upd-entry[data-state="update"] .dsh-upd-entry-dot{background:var(--dsh-upd-ok-line,#059669)}',
96
- '.dsh-upd-entry[data-state="busy"] .dsh-upd-entry-dot,.dsh-upd-entry[data-state="restart"] .dsh-upd-entry-dot{background:var(--dsh-upd-warn-line,#d97706)}',
97
- '.dsh-upd-entry[data-state="failed"] .dsh-upd-entry-dot{background:var(--dsh-upd-bad-line,#dc2626)}',
117
+ ".dsh-upd-entry-btn:focus-visible,.dsh-upd-entry-dot:focus-visible{outline:2px solid var(--dsh-update-focus,#2563eb);outline-offset:1px}",
118
+ '.dsh-upd-entry[data-state="update"] .dsh-upd-entry-btn{border-color:var(--dsh-update-ok-border,#059669);color:var(--dsh-update-ok-border,#059669)}',
119
+ '.dsh-upd-entry[data-state="restart"] .dsh-upd-entry-btn{border-color:var(--dsh-update-warn-border,#d97706);color:var(--dsh-update-warn-border,#d97706)}',
120
+ '.dsh-upd-entry[data-state="failed"] .dsh-upd-entry-btn{border-color:var(--dsh-update-bad-border,#dc2626);color:var(--dsh-update-bad-border,#dc2626)}',
121
+ ".dsh-upd-entry-dot{width:10px;height:10px;padding:0;border:0;border-radius:50%;background:var(--dsh-update-border,#9ca3af);cursor:pointer}",
122
+ '.dsh-upd-entry[data-state="update"] .dsh-upd-entry-dot{background:var(--dsh-update-ok-border,#059669)}',
123
+ '.dsh-upd-entry[data-state="busy"] .dsh-upd-entry-dot,.dsh-upd-entry[data-state="restart"] .dsh-upd-entry-dot{background:var(--dsh-update-warn-border,#d97706)}',
124
+ '.dsh-upd-entry[data-state="failed"] .dsh-upd-entry-dot{background:var(--dsh-update-bad-border,#dc2626)}',
98
125
  // 小字自带底(深色宿主 + 浅色变量时也读得出;浅底宿主上只是多一圈细线,不抢戏)。
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}",
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;',
102
- 'font-family:Georgia,"Songti SC","STSong","SimSun",serif;color:var(--d5-ink)}',
126
+ ".dsh-upd-entry-note{font-size:12.5px;opacity:.9;background:var(--dsh-update-bg,#ffffff);border:1px solid var(--dsh-update-border,#e5e7eb);border-radius:var(--dsh-update-radius-badge,4px);padding:1px 8px}",
127
+ '.dsh-upd-entry[data-theme="archive"] .dsh-upd-entry-note{background:var(--dsh-update-bg);border-color:var(--dsh-update-border-strong);color:var(--dsh-update-text)}',
128
+ '.dsh-upd-entry[data-theme="archive"]{--dsh-update-text:#1a1a1a;--dsh-update-text-muted:#6f675a;--dsh-update-border-strong:#c4b896;--dsh-update-primary:#c8402a;--dsh-update-bg:#fffdf6;',
129
+ '--dsh-update-font-serif:Georgia,"Songti SC","STSong","SimSun",serif;',
130
+ 'font-family:var(--dsh-update-font-serif,Georgia,"Songti SC","STSong","SimSun",serif);color:var(--dsh-update-text)}',
103
131
  // 按钮脸自己不透明(深色宿主 + 浅色系统变量时也读得出;hover 红在深浅底上都可见)。
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}}',
132
+ '.dsh-upd-entry[data-theme="archive"] .dsh-upd-entry-btn{border-color:var(--dsh-update-border-strong);background:var(--dsh-update-bg);color:var(--dsh-update-text);border-radius:var(--dsh-update-entry-border-radius,3px)}',
133
+ '.dsh-upd-entry[data-theme="archive"] .dsh-upd-entry-btn:hover{border-color:var(--dsh-update-primary);color:var(--dsh-update-primary)}',
134
+ '@media (prefers-color-scheme: dark){.dsh-upd-entry[data-theme="archive"]{--dsh-update-text:#ece5d3;--dsh-update-text-muted:#a89c83;--dsh-update-border-strong:#5c4e3b;--dsh-update-primary:#e0684e;--dsh-update-bg:#1e1a15}}',
107
135
  "@media (prefers-color-scheme: dark){.dsh-upd-entry{color:#e5e7eb}",
108
- ".dsh-upd-entry-btn{--dsh-upd-btn:#1f2937;--dsh-upd-line:#374151}}"
136
+ ".dsh-upd-entry-btn{--dsh-update-button-bg:#1f2937;--dsh-update-border:#374151}}"
109
137
  ].join("\n");
110
138
  const ENTRY_ATTR = "data-dsh-upd-entry";
111
139
  const ENTRY_SELECTOR = "[data-dsh-upd-entry]";
@@ -150,8 +178,8 @@ function mountUpdateEntry(container, options) {
150
178
  if (openOn !== "has-update" && openOn !== "always" && openOn !== "manual" && openOn !== "direct") {
151
179
  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`);
152
180
  }
153
- if (options.theme !== void 0 && options.theme !== "default" && options.theme !== "archive" && options.theme !== "d5-paper") {
154
- 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`);
181
+ if (options.theme !== void 0 && options.theme !== "default" && options.theme !== "archive") {
182
+ throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08\u6536\u5230 ${JSON.stringify(options.theme)}\uFF09`);
155
183
  }
156
184
  let theme = normalizePanelTheme(options.theme ?? "default");
157
185
  const pollMs = options.pollMs;
@@ -159,6 +187,22 @@ function mountUpdateEntry(container, options) {
159
187
  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`);
160
188
  }
161
189
  const labelOverride = typeof options.label === "string" && options.label ? options.label : null;
190
+ const sizingStyle = entrySizingStyleFor(options.sizing ?? void 0);
191
+ const sizingAttr = sizingStyle ? ` style="${sizingStyle}"` : "";
192
+ const localeOpt = options.locale ?? void 0;
193
+ if (localeOpt !== void 0 && localeOpt !== null) {
194
+ const isStr = typeof localeOpt === "string";
195
+ const isObj = typeof localeOpt === "object" && typeof localeOpt.getActive === "function";
196
+ if (!isStr && !isObj) {
197
+ throw new Error("[dsh-plugin-update] invalid locale: expected zh / en / BCP47 or { getActive(), subscribe? }");
198
+ }
199
+ if (isStr && !localeOpt.trim()) {
200
+ throw new Error("[dsh-plugin-update] invalid locale: empty string");
201
+ }
202
+ }
203
+ function currentLang() {
204
+ return resolveLang(localeOpt);
205
+ }
162
206
  const profileName = typeof options.profileName === "string" && options.profileName ? options.profileName : null;
163
207
  const onActivate = typeof options.onActivate === "function" ? options.onActivate : null;
164
208
  const call = options.call;
@@ -191,7 +235,7 @@ function mountUpdateEntry(container, options) {
191
235
  return { snapshot, error };
192
236
  }
193
237
  function currentLabel() {
194
- return labelOverride ?? entryLabelFor(stateOf());
238
+ return labelOverride ?? entryLabelFor(stateOf(), currentLang());
195
239
  }
196
240
  function hasUpdate() {
197
241
  return hasUpdateOf(snapshot);
@@ -201,15 +245,16 @@ function mountUpdateEntry(container, options) {
201
245
  }
202
246
  function entryHTML() {
203
247
  const kind = entryStateKind(stateOf());
204
- const labelHTML = labelOverride ? escapeHtml(labelOverride) : activating ? bilingualHTML("entry.action.checking") : entryBilingualHTMLFor(stateOf());
205
- const labelText = labelOverride ?? (activating ? bilingualText("entry.action.checking") : entryBilingualTextFor(stateOf()));
248
+ const lang = currentLang();
249
+ const labelHTML = labelOverride ? escapeHtml(labelOverride) : activating ? copyHTML("entry.action.checking", lang) : copyHTML(entryBilingualKeyFor(stateOf()), lang, entryBilingualValuesFor(stateOf()));
250
+ const labelText = labelOverride ?? (activating ? copyText("entry.action.checking", lang) : copyText(entryBilingualKeyFor(stateOf()), lang, entryBilingualValuesFor(stateOf())));
206
251
  const busyAttr = activating ? ' disabled aria-busy="true"' : "";
207
252
  const themeAttr = theme === "archive" ? ' data-theme="archive"' : "";
208
253
  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>` : "";
254
+ const noteHTML = noteVersion ? `<span class="dsh-upd-entry-note" data-dsh-upd-note="1">${copyHTML("entry.note.up-to-date", lang, { version: noteVersion })}</span>` : "";
210
255
  return `<style>${UPDATE_ENTRY_CSS}
211
256
  ${BILINGUAL_CSS}</style>
212
- <span class="dsh-upd-entry" data-variant="${variant}" data-state="${kind}"${themeAttr}>${control}${noteHTML}</span>`;
257
+ <span class="dsh-upd-entry" data-variant="${variant}" data-state="${kind}"${themeAttr}${sizingAttr}>${control}${noteHTML}</span>`;
213
258
  }
214
259
  function render() {
215
260
  if (!mounted) return;
@@ -258,6 +303,7 @@ ${BILINGUAL_CSS}</style>
258
303
  profileName,
259
304
  autoChangelog: options.autoChangelog,
260
305
  changelogMarkdown: options.changelogMarkdown ?? null,
306
+ locale: localeOpt,
261
307
  // 面板点「关闭」即走入口件的完整关闭(收 dialog + 还原按钮 + 重查一次),不再是面板自己停轮询。
262
308
  onCloseRequested: () => close(),
263
309
  call: panelCall
@@ -313,8 +359,8 @@ ${BILINGUAL_CSS}</style>
313
359
  render();
314
360
  }
315
361
  function setTheme(next) {
316
- if (next !== "default" && next !== "archive" && next !== "d5-paper") {
317
- 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`);
362
+ if (next !== "default" && next !== "archive") {
363
+ throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08\u6536\u5230 ${JSON.stringify(next)}\uFF09`);
318
364
  }
319
365
  theme = normalizePanelTheme(next);
320
366
  if (panel) void panel.setTheme(theme);
@@ -344,6 +390,10 @@ ${BILINGUAL_CSS}</style>
344
390
  function unmount() {
345
391
  if (!mounted) return;
346
392
  mounted = false;
393
+ try {
394
+ unsubLang();
395
+ } catch {
396
+ }
347
397
  const opened = panel;
348
398
  panel = null;
349
399
  panelMode = null;
@@ -360,6 +410,9 @@ ${BILINGUAL_CSS}</style>
360
410
  } catch {
361
411
  }
362
412
  }
413
+ const unsubLang = subscribeLang(() => {
414
+ render();
415
+ }, localeOpt);
363
416
  try {
364
417
  container.addEventListener?.("click", onClick);
365
418
  } catch {
@@ -380,6 +433,7 @@ export {
380
433
  entryBilingualTextFor,
381
434
  entryBilingualValuesFor,
382
435
  entryLabelFor,
436
+ entrySizingStyleFor,
383
437
  entryStateKind,
384
438
  mountUpdateEntry
385
439
  };
@@ -19,10 +19,13 @@ import { createHostUpdate } from "./host.js";
19
19
  import { defaultHomeDir } from "./reader.js";
20
20
  import { compareReleaseVersions } from "./service.js";
21
21
  import {
22
- createBatchDiskPorts,
22
+ createBatchOwnerPorts,
23
23
  createSkipDiskPorts,
24
24
  createUpdateQueuePorts,
25
- isVersionSkipped
25
+ emptyBatchInventory,
26
+ isVersionSkipped,
27
+ normalizeBatchInventory,
28
+ normalizeBatchPrefs
26
29
  } from "./store.js";
27
30
  const DEFAULT_DRAIN_INTERVAL_MS = 5e3;
28
31
  const MIN_DRAIN_INTERVAL_MS = 1e3;
@@ -405,31 +408,56 @@ function createMultiHostUpdate(deps = {}, options) {
405
408
  return runtime;
406
409
  });
407
410
  const runtimeByKey = new Map(runtimes.map((rt) => [rt.spec.key, rt]));
408
- const batchPorts = batchScope && batchScope.profileDir ? createBatchDiskPorts(batchScope.homeDir, batchScope.profileDir) : null;
411
+ const ownerPorts = batchScope && batchScope.profileDir ? createBatchOwnerPorts(batchScope.homeDir, batchScope.profileDir, prefix) : null;
409
412
  let memSession = null;
413
+ let memInventory = null;
414
+ let memPrefs = {};
410
415
  let driveActive = false;
411
416
  let liveSession = null;
412
417
  let lastRun = null;
413
418
  let drainTimer = null;
414
419
  let disposed = false;
415
420
  async function readSession() {
416
- if (batchPorts) return await batchPorts.readBatch();
421
+ if (ownerPorts) return await ownerPorts.readBatch();
417
422
  return memSession ?? emptyBatchSession();
418
423
  }
419
424
  async function saveSession(session) {
420
- if (batchPorts) {
421
- await batchPorts.writeBatch(session);
425
+ if (ownerPorts) {
426
+ await ownerPorts.writeBatch(session);
422
427
  return;
423
428
  }
424
429
  memSession = session;
425
430
  }
426
431
  async function clearSession() {
427
- if (batchPorts) {
428
- await batchPorts.clearBatch();
432
+ if (ownerPorts) {
433
+ await ownerPorts.clearBatch();
429
434
  return;
430
435
  }
431
436
  memSession = emptyBatchSession();
432
437
  }
438
+ async function readInventory() {
439
+ if (ownerPorts) return await ownerPorts.readInventory();
440
+ return memInventory ?? emptyBatchInventory();
441
+ }
442
+ async function writeInventory(inventory) {
443
+ if (ownerPorts) {
444
+ await ownerPorts.writeInventory(inventory);
445
+ return;
446
+ }
447
+ memInventory = inventory;
448
+ }
449
+ async function readPrefs() {
450
+ if (ownerPorts) return await ownerPorts.readPrefs();
451
+ return memPrefs;
452
+ }
453
+ async function writePrefs(prefs) {
454
+ const normalized = normalizeBatchPrefs(prefs);
455
+ if (ownerPorts) {
456
+ await ownerPorts.writePrefs(normalized);
457
+ return;
458
+ }
459
+ memPrefs = normalized;
460
+ }
433
461
  function newSessionId() {
434
462
  const suffix = String(randomId() ?? "").replace(/[^A-Za-z0-9._~-]/g, "").slice(0, 24);
435
463
  return now().toString(36) + "-" + (suffix || hash8(String(now())));
@@ -499,7 +527,19 @@ function createMultiHostUpdate(deps = {}, options) {
499
527
  }
500
528
  async function table(session, refresh = true) {
501
529
  const built = await buildRows(session, refresh);
502
- return { ok: true, session: built.session, rows: built.rows, progress: progressOf(built.session) };
530
+ let inventory = emptyBatchInventory();
531
+ let prefs = {};
532
+ try {
533
+ inventory = await readInventory();
534
+ } catch {
535
+ inventory = emptyBatchInventory();
536
+ }
537
+ try {
538
+ prefs = await readPrefs();
539
+ } catch {
540
+ prefs = {};
541
+ }
542
+ return { ok: true, session: built.session, rows: built.rows, progress: progressOf(built.session), inventory, prefs };
503
543
  }
504
544
  function denied() {
505
545
  if (!scopeError) return null;
@@ -609,14 +649,42 @@ function createMultiHostUpdate(deps = {}, options) {
609
649
  if (refuse) return refuse;
610
650
  const session = await readSession();
611
651
  if (!driveActive) {
652
+ const at = now();
653
+ let prev = emptyBatchInventory();
654
+ try {
655
+ prev = await readInventory();
656
+ } catch {
657
+ prev = emptyBatchInventory();
658
+ }
659
+ const entries = { ...prev.entries };
612
660
  for (const rt of runtimes) {
613
661
  try {
614
662
  const outcome = await rt.check(rt.spec.key, rt.spec);
615
663
  rt.cache = { ...rt.cache, error: outcome.kind === "failed" ? outcome.error : null };
664
+ const snapshot = asRecord(rt.cache.snapshot);
665
+ const installed = firstText(snapshot["installedVersion"], snapshot["runningVersion"]);
666
+ const canInstallRaw = snapshot["canInstall"];
667
+ const canInstall = typeof canInstallRaw === "boolean" ? canInstallRaw : null;
668
+ if (outcome.kind === "update") {
669
+ entries[rt.spec.key] = { lastCheckedAt: at, installedVersion: installed, latestVersion: outcome.version, canInstall, error: null };
670
+ } else if (outcome.kind === "current") {
671
+ entries[rt.spec.key] = { lastCheckedAt: at, installedVersion: installed, latestVersion: installed, canInstall: false, error: null };
672
+ } else if (outcome.kind === "skipped") {
673
+ entries[rt.spec.key] = { lastCheckedAt: at, installedVersion: installed, latestVersion: outcome.version, canInstall: false, error: null };
674
+ } else {
675
+ entries[rt.spec.key] = { lastCheckedAt: at, installedVersion: installed, latestVersion: firstText(snapshot["latestVersion"]), canInstall, error: outcome.error };
676
+ }
616
677
  } catch (error) {
617
- rt.cache = { ...rt.cache, error: errorPayloadOf(error).error };
678
+ const code = errorPayloadOf(error).error;
679
+ rt.cache = { ...rt.cache, error: code };
680
+ const snapshot = asRecord(rt.cache.snapshot);
681
+ entries[rt.spec.key] = { lastCheckedAt: at, installedVersion: firstText(snapshot["installedVersion"], snapshot["runningVersion"]), latestVersion: firstText(snapshot["latestVersion"]), canInstall: typeof snapshot["canInstall"] === "boolean" ? snapshot["canInstall"] : null, error: code };
618
682
  }
619
683
  }
684
+ try {
685
+ await writeInventory(normalizeBatchInventory({ version: 1, updatedAt: at, entries }, at));
686
+ } catch {
687
+ }
620
688
  }
621
689
  return await table(session, false);
622
690
  }
@@ -682,11 +750,12 @@ function createMultiHostUpdate(deps = {}, options) {
682
750
  if (refuse) return refuse;
683
751
  if (driveActive) return { ok: false, error: "update-busy", errorKind: "update-busy" };
684
752
  const disk = await readSession();
685
- if (disk.entries.length === 0) return await table(disk);
753
+ const hadUnfinished = disk.entries.length > 0 && !isBatchFinished(disk);
754
+ if (!hadUnfinished) return { ...await table(disk), resumed: false };
686
755
  const resumed = resumeBatchSession(disk, now());
687
756
  if (resumed !== disk) await saveSession(resumed);
688
757
  try {
689
- return await table(await drive(resumed, null, Number.POSITIVE_INFINITY));
758
+ return { ...await table(await drive(resumed, null, Number.POSITIVE_INFINITY)), resumed: true };
690
759
  } catch (error) {
691
760
  return { ok: false, ...errorPayloadOf(error) };
692
761
  }
@@ -727,12 +796,32 @@ function createMultiHostUpdate(deps = {}, options) {
727
796
  void tickDrain();
728
797
  }, drainIntervalMs);
729
798
  }
799
+ async function prefsPhone() {
800
+ const refuse = denied();
801
+ if (refuse) return refuse;
802
+ return { ok: true, prefs: await readPrefs() };
803
+ }
804
+ async function prefsSavePhone(args) {
805
+ const refuse = denied();
806
+ if (refuse) return refuse;
807
+ const current = await readPrefs();
808
+ const next = { ...current };
809
+ if ("checkOnOpen" in args) {
810
+ const v = args["checkOnOpen"];
811
+ if (typeof v !== "boolean") return { ok: false, error: "check-failed", errorKind: "check-failed" };
812
+ next.checkOnOpen = v;
813
+ }
814
+ await writePrefs(next);
815
+ return { ok: true, prefs: next };
816
+ }
730
817
  const phoneNames = {
731
818
  status: prefix + ".batchStatus",
732
819
  check: prefix + ".batchCheck",
733
820
  install: prefix + ".batchInstall",
734
821
  resume: prefix + ".batchResume",
735
- cancel: prefix + ".batchCancel"
822
+ cancel: prefix + ".batchCancel",
823
+ prefs: prefix + ".batchPrefs",
824
+ prefsSave: prefix + ".batchPrefsSave"
736
825
  };
737
826
  function phone(fn) {
738
827
  return async (args) => {
@@ -749,6 +838,8 @@ function createMultiHostUpdate(deps = {}, options) {
749
838
  handlers[phoneNames.install] = phone((args) => installPhone(args));
750
839
  handlers[phoneNames.resume] = phone(() => resumePhone());
751
840
  handlers[phoneNames.cancel] = phone(() => cancelPhone());
841
+ handlers[phoneNames.prefs] = phone(() => prefsPhone());
842
+ handlers[phoneNames.prefsSave] = phone((args) => prefsSavePhone(args));
752
843
  for (const rt of runtimes) {
753
844
  for (const name of Object.keys(rt.host.handlers)) {
754
845
  const handler = rt.host.handlers[name];
package/dist/http.d.ts CHANGED
@@ -11,8 +11,8 @@ export declare const DEFAULT_HTTP_STATUS_TIMEOUT_MS = 15000;
11
11
  /** 单插件电话动作名(旧三电话与 src/config.ts PHONE_ACTIONS 同字面,不另起字面;#38 加 updateChangelog)。 */
12
12
  export declare const HTTP_PHONE_ACTIONS: readonly ["updateStatus", "updateCheck", "updateInstall", "updateChangelog"];
13
13
  export type HttpPhoneAction = (typeof HTTP_PHONE_ACTIONS)[number];
14
- /** 批量五电话动作名(与 src/panel-batch.ts buildBatchPhoneNames 同字面,不另起字面;同一内核)。 */
15
- export declare const HTTP_BATCH_ACTIONS: readonly ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel"];
14
+ /** 批量七电话动作名(与 src/panel-batch.ts buildBatchPhoneNames 同字面,不另起字面;同一内核)。 */
15
+ export declare const HTTP_BATCH_ACTIONS: readonly ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel", "batchPrefs", "batchPrefsSave"];
16
16
  export type HttpBatchAction = (typeof HTTP_BATCH_ACTIONS)[number];
17
17
  /** 逐电话覆写表:键为全电话名(prefix.动作)或短动作名;未知键拒绝。 */
18
18
  export type HttpRoutes = Record<string, string>;
package/dist/http.js CHANGED
@@ -11,7 +11,7 @@ const DEFAULT_HTTP_CHECK_TIMEOUT_MS = 15e3;
11
11
  const DEFAULT_HTTP_INSTALL_TIMEOUT_MS = 16 * 6e4;
12
12
  const DEFAULT_HTTP_STATUS_TIMEOUT_MS = 15e3;
13
13
  const HTTP_PHONE_ACTIONS = ["updateStatus", "updateCheck", "updateInstall", "updateChangelog"];
14
- const HTTP_BATCH_ACTIONS = ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel"];
14
+ const HTTP_BATCH_ACTIONS = ["batchStatus", "batchCheck", "batchInstall", "batchResume", "batchCancel", "batchPrefs", "batchPrefsSave"];
15
15
  function isAbsoluteUrl(value) {
16
16
  return /^https?:\/\//i.test(value);
17
17
  }