dsh-plugin-update 0.5.8 → 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.
@@ -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
  }
@@ -55,13 +55,22 @@ export interface BatchPanelOptions {
55
55
  */
56
56
  onCloseRequested?: () => void | Promise<void>;
57
57
  /**
58
- * 语言覆盖(#60 窗口期占位:本票只加选项不消费,传了暂不生效,消费留给 #61-#66)。
58
+ * 语言覆盖(#59 起消费:新文案走集中字典单语渲染;既有散落文案保持基线不变)。
59
59
  * 形态与入口件一致:'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void }。
60
60
  */
61
61
  locale?: unknown;
62
+ /**
63
+ * 打开面板自动查(#59 D3):缺省 true。false 即手动挡(行为回今天,只修文案)。
64
+ * 优先级:用户偏好(prefs.json)> 本选项 > 缺省。
65
+ */
66
+ checkOnOpen?: boolean;
67
+ /**
68
+ * 自动继续未终态轮次(#59 Q3:与显式按钮并存,常开)。false 可关(测试与特殊集成用)。
69
+ */
70
+ autoResume?: boolean;
62
71
  }
63
72
  /** 面板可点的动作(HTML 上 data-act 一一对应;测试走同一条路)。 */
64
- 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';
73
+ 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' | 'toggle-check-on-open';
65
74
  /** 挂载点:只要有 innerHTML 的容器即可(浏览器元素或测试替身都行)。 */
66
75
  export interface BatchPanelContainer {
67
76
  innerHTML: string;
@@ -76,20 +85,22 @@ export interface BatchPanelController {
76
85
  setMode(mode: BatchPanelMode): void;
77
86
  unmount(): void;
78
87
  }
79
- /** 批量五个电话名(与宿主侧 MultiHostUpdate.phoneNames 同一形状)。 */
88
+ /** 批量七个电话名(与宿主侧 MultiHostUpdate.phoneNames 同一形状)。 */
80
89
  export interface BatchPhoneNames {
81
90
  status: string;
82
91
  check: string;
83
92
  install: string;
84
93
  resume: string;
85
94
  cancel: string;
95
+ prefs: string;
96
+ prefsSave: string;
86
97
  }
87
98
  /** 批量面板轮询口径(默认 1.5 秒、下限 250 毫秒,与单插件面板同一套下限)。 */
88
99
  export declare const BATCH_PANEL_POLL: {
89
100
  readonly defaultMs: 1500;
90
101
  readonly minMs: 250;
91
102
  };
92
- /** 批量电话名拼法:前缀 + 五个固定动作名(前缀形状校验与单插件面板同一套)。 */
103
+ /** 批量电话名拼法:前缀 + 七个固定动作名(前缀形状校验与单插件面板同一套)。 */
93
104
  export declare function buildBatchPhoneNames(prefix: string): BatchPhoneNames;
94
105
  export interface BatchLedgerCounts {
95
106
  /** phase=ready:有新版、还没安装。 */
@@ -114,20 +125,37 @@ export interface BatchLedgerCounts {
114
125
  * 宿主忙时先写占位再回 update-busy,面板把它翻回「可更新」,免得总账把它报成失败。
115
126
  */
116
127
  export declare function batchLedgerCounts(rows: readonly BatchRowView[]): BatchLedgerCounts;
117
- /** 分类计数 → 一句话总账(只出现非零档,顺序固定:可更新 · 安装中 · 待查 · 待重启 · 失败 · 已跳过 · 已最新)。 */
118
- export declare function batchLedgerText(counts: BatchLedgerCounts): string;
128
+ /** 分类计数 → 一句话总账(只出现非零档,顺序固定:可更新 · 安装中 · 待查 · 待重启 · 失败 · 已跳过 · 已最新;#64 单语:缺省 zh 零回归)。 */
129
+ export declare function batchLedgerText(counts: BatchLedgerCounts, lang?: unknown): string;
119
130
  /**
120
131
  * 排队中的状态词:只回答「还要等多久」。位置从该行的队列视图来。
121
132
  * queue.ts 的 position 是 waiting 顺位(队首为 1、0 = 自己正在装),而正在装的那一家也压在这一家前面,
122
133
  * 所以「前方 N 个」= position N(队首那家前方正好 1 个:正在装的拥有者)。
123
134
  * 位置取不到(宿主只记了忙、没给占位读数)就说「等前面安装完」,不猜数字。
124
135
  */
125
- export declare function batchQueuedStatus(position: number | null): string;
136
+ export declare function batchQueuedStatus(position: number | null, lang?: unknown): string;
137
+ /** 知识账本里的一行(只做展示,不参与安装决策)。 */
138
+ export interface BatchKnowledgeEntry {
139
+ lastCheckedAt: number;
140
+ installedVersion: string | null;
141
+ latestVersion: string | null;
142
+ canInstall: boolean | null;
143
+ error: string | null;
144
+ }
145
+ /** 有未终态行(控制区出现条件:盘上有没做完的一轮)。 */
146
+ export declare function hasUnfinishedRows(rows: readonly BatchRowView[], session?: unknown): boolean;
147
+ export declare function unfinishedCount(rows: readonly BatchRowView[], session?: unknown): number;
126
148
  /**
127
149
  * 一行的状态词:只回答一个问题——这家的**下一步**是什么。
128
150
  * 中文可执行,不写相位英文(相位只留在 data-phase 属性上,给人看的这句永远是动作)。
151
+ * (#59 D2:调用方在无轮次 + 有知识时优先用 batchRowKnowledgeText;本函数语义冻结,旧快照不动。)
152
+ */
153
+ export declare function batchRowStatus(row: BatchRowView, skippedVersion?: string | null, lang?: unknown): string;
154
+ /**
155
+ * 无轮次时的知识行(#59 D2):pending + 有知识即按知识展示;有轮次未做完仍走执行态。
156
+ * 无知识(从没查过)回 null,调用方回退旧 pending 文案。
129
157
  */
130
- export declare function batchRowStatus(row: BatchRowView, skippedVersion?: string | null): string;
158
+ export declare function batchRowKnowledgeText(row: BatchRowView, entry: BatchKnowledgeEntry | null, lang?: unknown): string | null;
131
159
  export interface BatchPanelRenderInput {
132
160
  rows?: readonly BatchRowView[];
133
161
  theme?: UpdatePanelTheme;
@@ -151,15 +179,23 @@ export interface BatchPanelRenderInput {
151
179
  noticeKey?: string | null;
152
180
  /** 键 -> 该行已取到的日志全文(只传有文本的;取不到与没展开即中性提示,不挡安装)。 */
153
181
  changelogs?: Record<string, string | null>;
182
+ /** 轮次会话(有未终态行才画控制区;不传按行相位回退旧口径,兼容旧快照)。 */
183
+ session?: unknown;
184
+ /** 知识账本(无轮次时行按知识展示;只做展示,不参与安装决策)。 */
185
+ inventory?: unknown;
186
+ /** 偏好(底部勾选的受控状态;不传不画勾选)。 */
187
+ prefs?: unknown;
188
+ /** 语言(单语渲染新文案用;不传跟随全局信号)。 */
189
+ lang?: unknown;
154
190
  }
155
191
  /** 整面板 HTML(含样式;重绘即整体替换 innerHTML,故每次都带 style 也只留一份)。 */
156
192
  export declare function renderBatchPanelHTML(input: BatchPanelRenderInput): string;
157
193
  export declare const UPDATE_BATCH_PANEL_CSS: string;
158
194
  /**
159
- * 批量面板的 D5 皮肤(#20 同一套口径:只换颜色/字体/间距,不改顺序、不藏东西)。
195
+ * 批量面板的 Archive 皮肤(#20 同一套口径:只换颜色/字体/间距,不改顺序、不藏东西)。
160
196
  * 卷宗抬头右侧给大印章留 120px(与单插件面板档案头同一处理)。
161
197
  */
162
- export declare const UPDATE_BATCH_PANEL_D5_CSS: string;
198
+ export declare const UPDATE_BATCH_PANEL_ARCHIVE_CSS: string;
163
199
  /**
164
200
  * 挂批量面板(调用方一行即跑):
165
201
  * ```js