dsh-life-pack 0.3.0 → 0.3.1

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/client.js CHANGED
@@ -186,28 +186,37 @@ window.__ModuleLoader__.load({
186
186
  * 一处定义、两侧引用:宿主半 `manager-version.ts` 读失败时回它,面板半 `update-client.ts`
187
187
  * 归一化时也认它——面板侧不许 import 宿主半(会把 node 内建带进浏览器束)。 */
188
188
  const VERSION_UNKNOWN = "unknown";
189
- /** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。 */
189
+ /** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。
190
+ *
191
+ * 每条都要三段齐全(票 #740 的文案判据):**事实**(哪里对不上)→ **后果**(这家现在能不能装)
192
+ * → **动作**(用户做得到、且面板上真有的那一步;面板代劳不了的走「下面这条命令」)。
193
+ * 禁用口语与自造词:「半截任务」「电话没接上」「回执异常」「还出现就重装」「查宿主日志」一律不写。
194
+ *
195
+ * 点名的按钮必须是**这一行上真够得着**的那颗(票 #740 第三轮):结果浮层一开就盖住题头的
196
+ * 「检查更新」,所以「改完再读一次」这类下一步一律写「重新检查」(画在那一行上);
197
+ * 只有发生在**重启之后**的下一步才写「检查更新」。反过来,凡是写着「下面这条命令」的那几句,
198
+ * 那一行的命令块必须真出来(判据 `Verdict.manualHint`)——两处都由门禁守着。 */
190
199
  const REASON_TEXT = {
191
- "unknown-profile": "使用范围认不出(名字非法或目录不存在):先检查使用范围名与目录,这种情形不给手工命令。",
192
- "source-install": "当前是从源码装的,不是按版本号装的:想走更新,先按版本号重装一次。",
193
- "invalid-installation": "已装的包不完整(名字对不上、版本非法、入口文件缺失):重装当前版本,修好已装目录再查。",
194
- "installation-changed": "安装位置在使用中途变了:重新打开宿主再查一次;还出现就重装。",
195
- "pending-restart": "新版已装到磁盘,正在跑的还是旧版:重启宿主后生效。",
196
- "registry-conflict": "本地声明的版本与磁盘实际版本互相矛盾:打开使用范围的清单,把目标包名那行改成版本号再试。",
197
- "incompatible-node": "新版要求的 Node 与当前运行的 Node 对不上:先把 Node 升到 22 或更高,再查。",
198
- "recovery-required": "上次安装被打断,留下一个半截任务:重新点一次安装;一直出现就查宿主日志。",
199
- "check-failed": "查新版失败(联网不通或官方源不可达):检查网络与官方源地址,再查一次。",
200
- "invalid-release": "远端发行信息不合规(名字对不上、版本非法、包地址或完整性校验不过):换个版本再查。",
201
- "check-expired": "上次查新版的凭证过期了:重新查一次再点安装(不要重试旧编号)。",
202
- "update-busy": "同一个使用范围同时只装一个:等当前任务结束再点。",
203
- "install-failed": "安装没跑成:按下面那条手工命令在终端执行一次。",
204
- "manager-unreachable": "总管的电话没接上(宿主半没注册或版本对不上):重启宿主后再试;仍不行看宿主日志。",
205
- "bad-request": "回执异常(请求形状不对):把宿主日志里的事件名报给作者。",
206
- internal: "回执异常(总管内部错误):把宿主日志里的事件名报给作者。"
200
+ "unknown-profile": "认不出这台机器上的安装位置(使用范围的目录不存在,或名字不合法)。把使用范围目录建好,再点「重新检查」。",
201
+ "source-install": "这一份不是从 npm 发行包装的(源码装),面板不能更新它。要更新,照下面这条命令重装,装完再点「重新检查」。",
202
+ "invalid-installation": "磁盘上这一份装得不完整(包名或版本对不上、入口文件缺失)。照下面这条命令重装,装完再点「重新检查」。",
203
+ "installation-changed": "这台机器的安装清单变了(在别处装过、或清单被手工改过),这一家现在不能装。点「重新检查」重新读一次,再点「装上更新」。",
204
+ "pending-restart": "新版已装到磁盘,正在运行的是旧版本。重启 DSH(退出后重新打开)后生效。",
205
+ "registry-conflict": "使用范围清单里写的版本与磁盘上的实际版本对不上。打开使用范围的 `package.json`,把这一行改成磁盘上的版本号,再点「重新检查」。",
206
+ "incompatible-node": "新版本要求 Node 22 或更高,当前运行的是更低版本。升级 Node、重启 DSH 后,再点「检查更新」。",
207
+ "recovery-required": "上次安装没有收尾。点「重试安装」再跑一次;还出现这一条,就用下面这条命令重装。",
208
+ "check-failed": "查不到最新版本:联网失败,或官方源(registry.npmjs.org)不可达。确认网络与源地址后,再点「重新检查」。",
209
+ "invalid-release": "官方源上这个版本的元数据不完整(包名或版本不合法、包地址或校验值缺失)。换一个版本,或稍后重试。",
210
+ "check-expired": "上次检查的结果已过期。点「重新检查」重查一次,再点「装上更新」。",
211
+ "update-busy": "同一时间只能安装一个插件。等这次装完,再点「装上更新」。",
212
+ "install-failed": "安装没有完成。在终端里执行下面这条命令把它装完,装完再点「重新检查」。",
213
+ "manager-unreachable": "面板连不上总管插件(宿主里没注册,或版本对不上)。重启 DSH 后重试;仍不行就在 DSH 日志里找 `[dsh-life-pack]` 的报错。",
214
+ "bad-request": "面板与总管之间的请求出错了(属程序缺陷)。请在 DSH 日志里找 `[dsh-life-pack]` 的报错,连同这家插件的名字报到本仓 issue。",
215
+ internal: "面板与总管之间的请求出错了(属程序缺陷)。请在 DSH 日志里找 `[dsh-life-pack]` 的报错,连同这家插件的名字报到本仓 issue。"
207
216
  };
208
217
  /** 原因码 → 人话(认不出的码不猜、原样回码并说明这是未知原因)。 */
209
218
  function reasonText(code) {
210
- return REASON_TEXT[code] ?? "未知原因(" + code + "):把宿主日志里的事件名报给作者。";
219
+ return REASON_TEXT[code] ?? "面板不认识这个原因码(" + code + ")。请在 DSH 日志里找同一处报错,报到本仓 issue。";
211
220
  }
212
221
  //#endregion
213
222
  //#region src/update-client.ts
@@ -357,57 +366,136 @@ window.__ModuleLoader__.load({
357
366
  if (target.panelRegistered === false) return "unregistered-product";
358
367
  return "not-connected";
359
368
  }
360
- /** 把快照翻成一行结论:还没查 / 已是最新 / 有新版 / 装不了(附人话原因)。 */
369
+ /** 这一家是不是「装好了、只差重启」——判据是**事实**(磁盘版本 ≠ 正在跑的版本),不是原因码。
370
+ *
371
+ * 为什么不许按原因码判(票 #740 真机):同一种事实在更新核心里可能报成两种码——
372
+ * `pending-restart`,或者被上一次中断的任务盖成 `recovery-required`。按码判会把「该重启」
373
+ * 漏成一句「装不上」,用户看到的正是那种读不懂的屏。 */
374
+ function restartPendingOf(snapshot) {
375
+ const installed = snapshot.installedVersion;
376
+ return installed !== null && installed !== snapshot.runningVersion;
377
+ }
378
+ /** 待重启那一句(三段齐全:事实 → 后果 → 动作)。
379
+ *
380
+ * 措辞不写「新版」:磁盘上那份也可能比正在跑的**低**(手工降级),「新版」就不实了;
381
+ * 只照事实说「磁盘上装的是 X、正在运行的是 Y」。
382
+ * 动作只给「重启 DSH」:这一态下面板**做不了别的**——更新包的可装判据要求最新版严格高于
383
+ * **正在运行**的版本(`service.js:219`),而凭证只发在可装态(`service.js:256`:`receipt` 只在
384
+ * `canInstall` 时给),所以这一态里「重试安装」点了也是白点。 */
385
+ function restartLine(snapshot) {
386
+ const installed = snapshot.installedVersion ?? snapshot.job?.targetVersion ?? "新版本";
387
+ return "磁盘上装的是 " + String(installed) + ";正在运行的是 " + snapshot.runningVersion + "。重启 DSH(退出后重新打开)后生效。";
388
+ }
389
+ /** 「条件在别处(目录/清单/别处装的),改完回来重读一次」的原因码:面板能给的下一步就是「重新检查」。
390
+ *
391
+ * 为什么要逐码列(票 #740 第三轮的朗读表):`unknown-profile`/`registry-conflict`/`invalid-installation`/
392
+ * `source-install` 这四态原先一个动作都不给,屏上那句却写着「再点「检查更新」」——那颗按钮住在面板题头,
393
+ * 结果浮层一开(点「检查更新」就开)就被浮层盖住,够不着。跟两张「还没查过」的卡是同一类病。 */
394
+ const RECHECK_REASONS = /* @__PURE__ */ new Set([
395
+ "installation-changed",
396
+ "unknown-profile",
397
+ "registry-conflict",
398
+ "invalid-installation",
399
+ "source-install"
400
+ ]);
401
+ /** 「这一步面板代劳不了、得照下面那条命令做」的原因码:这些话下面必须真有命令块。 */
402
+ const NEEDS_MANUAL = /* @__PURE__ */ new Set([
403
+ "source-install",
404
+ "invalid-installation",
405
+ "recovery-required"
406
+ ]);
407
+ /** 拦截态各自给得出的下一步:面板能代劳的给按钮,代劳不了的给 null(那种走手工命令块)。 */
408
+ function actionForBlocked(reason) {
409
+ if (RECHECK_REASONS.has(reason)) return "recheck";
410
+ if (reason === "recovery-required") return "retry";
411
+ return null;
412
+ }
413
+ /** 这一行此刻该画哪颗按钮:结论自带的动作优先,**失败的行一律给「重新检查」**(票 #740 第三轮)。
414
+ *
415
+ * 失败的行(查不到最新版/检查过期/装失败)没有快照、也就没有结论,原先一颗按钮都不画:
416
+ * 屏上写着「再点「检查更新」」,那颗按钮却在浮层底下 ⇒ 无路可走。重新查一次对每一种失败都成立。 */
417
+ function cardActionOf(verdict, phase) {
418
+ if (verdict?.action) return verdict.action;
419
+ return phase === "failed" ? "recheck" : null;
420
+ }
421
+ /** 装成一家之后,其余各家的读数一律作废(票 #740):七家共用同一份使用范围清单,
422
+ * 刚那次安装已经改写了它,旧快照再拿去装必然被守卫拦下。这句话就是那条守卫的人话。 */
423
+ function staleVerdict() {
424
+ return {
425
+ kind: "blocked",
426
+ text: "刚在本面板装过别的插件,共用的安装清单变了,这一家要先重读一次。点「重新检查」,再点「装上更新」。",
427
+ action: "recheck"
428
+ };
429
+ }
430
+ /** 把除 `keepKey` 之外的行全标成过期(票 #740):装成一家 ⇒ 七家共用的安装清单被改写,
431
+ * 其余各家的旧快照再拿去装必然被守卫拦下。纯函数:状态源与单测共用同一份判据,
432
+ * 不关心行里还有什么(泛型只要求有 `stale` 这一格)。 */
433
+ function markStaleOthers(rows, keepKey) {
434
+ const next = {};
435
+ for (const [key, row] of Object.entries(rows)) next[key] = key === keepKey ? row : {
436
+ ...row,
437
+ stale: true
438
+ };
439
+ return next;
440
+ }
441
+ /** 把快照翻成一行结论:还没查 / 已是最新 / 有新版 / 待重启 / 装不了(附人话原因与下一步)。 */
361
442
  function verdictOf(target, snapshot) {
362
443
  const absent = isAbsent(target);
363
444
  const latest = snapshot.latestVersion;
364
445
  if (absent && latest === null) return {
365
446
  kind: "unknown",
366
- text: "未安装;点「检查更新」查最新版本后装上。",
367
- action: null
447
+ text: "未安装。点「重新检查」查最新版本,再点「装上」。",
448
+ action: "recheck"
368
449
  };
369
450
  if (absent) return {
370
451
  kind: "update-available",
371
- text: "未安装;最新版本 " + String(latest) + "。",
452
+ text: "未安装;最新版本 " + String(latest) + "。点「装上」安装。",
372
453
  action: "install"
373
454
  };
374
- if (snapshot.blockedReason !== null) {
455
+ if (restartPendingOf(snapshot)) return {
456
+ kind: "blocked",
457
+ text: restartLine(snapshot),
458
+ action: null
459
+ };
460
+ const installed = snapshot.installedVersion;
461
+ const hasNewer = latest !== null && latest !== installed;
462
+ if (snapshot.blockedReason !== null && hasNewer) {
375
463
  const blocked = snapshot.blockedReason;
376
- if (blocked === "pending-restart") return {
377
- kind: "blocked",
378
- text: reasonText(blocked),
379
- action: null
380
- };
381
464
  return {
382
465
  kind: "blocked",
383
466
  text: reasonText(blocked),
384
- action: null
467
+ action: actionForBlocked(blocked),
468
+ manualHint: NEEDS_MANUAL.has(blocked)
385
469
  };
386
470
  }
387
471
  if (latest === null) return {
388
472
  kind: "unknown",
389
- text: "还没查过最新版本;点「检查更新」。",
390
- action: null
473
+ text: "还没查过最新版本。点「重新检查」。",
474
+ action: "recheck"
391
475
  };
392
476
  if (snapshot.canInstall) return {
393
477
  kind: "update-available",
394
- text: "有新版:" + String(latest) + ",点「装上更新」。",
478
+ text: "有新版本 " + String(latest) + "(正在运行 " + snapshot.runningVersion + ")。点「装上更新」。",
395
479
  action: "update"
396
480
  };
481
+ if (hasNewer) return {
482
+ kind: "update-available",
483
+ text: "有新版本 " + String(latest) + "(正在运行 " + snapshot.runningVersion + ")。点「重新检查」,再点「装上更新」。",
484
+ action: "recheck"
485
+ };
397
486
  return {
398
487
  kind: "up-to-date",
399
- text: "已是最新(" + String(latest) + ")。",
488
+ text: "已是最新版本 " + String(latest) + "。",
400
489
  action: null
401
490
  };
402
491
  }
403
- /** 待重启横幅文案(更新包 README 第 10 节:说清新版号与「重启后才生效」两件事)。 */
404
- function pendingRestartText(target, snapshot) {
405
- if (snapshot.blockedReason !== "pending-restart") return null;
406
- const installed = snapshot.installedVersion ?? snapshot.job?.targetVersion ?? null;
407
- const running = snapshot.runningVersion;
408
- if (installed === null) return "⚠️ 新版已装好,重启宿主后生效。";
409
- if (installed === running) return "⚠️ 新版 " + installed + " 已装好,重启宿主后生效。";
410
- return "⚠️ 新版 " + installed + " 已装好,正在跑的还是 " + running + ",重启宿主后生效。";
492
+ /** 待重启横幅:**一行总账**,只点名哪几家要做「重启 DSH」这件事。
493
+ *
494
+ * 为什么不再逐家重复那句话(票 #740 对抗式审查第三轮的朗读表逮到):同一句「重启 DSH…后生效」
495
+ * 会同时出现在横幅和那家卡片上,一屏两遍。分工定死:**横幅答「谁要重启」,卡片答「为什么、怎么做」**。 */
496
+ function restartBannerText(titles) {
497
+ if (titles.length === 0) return null;
498
+ return "⚠️ 待重启 DSH(退出后重新打开):" + titles.join("、");
411
499
  }
412
500
  /** 面板展示的那条可复制命令(用户会照着敲的那一条)。
413
501
  *
@@ -692,6 +780,13 @@ window.__ModuleLoader__.load({
692
780
  default: return "idle";
693
781
  }
694
782
  }
783
+ /** 四个动作在屏上的名字:按钮文案与判据同源(票 #740)。 */
784
+ const ACTION_LABEL = {
785
+ install: "装上",
786
+ update: "装上更新",
787
+ retry: "重试安装",
788
+ recheck: "重新检查"
789
+ };
695
790
  const IDLE$1 = {
696
791
  phase: "idle",
697
792
  outcome: null,
@@ -728,6 +823,10 @@ window.__ModuleLoader__.load({
728
823
  [key]: next
729
824
  }));
730
825
  }, []);
826
+ /** 装成一家 ⇒ 其余各家的旧快照一律作废(票 #740):判据见 `markStaleOthers`。 */
827
+ const staleOthers = react.useCallback((keepKey) => {
828
+ setRows((previous) => markStaleOthers(previous, keepKey));
829
+ }, []);
731
830
  return {
732
831
  targets,
733
832
  rows,
@@ -743,18 +842,21 @@ window.__ModuleLoader__.load({
743
842
  for (const target of list) patch(target.key, {
744
843
  phase: "checking",
745
844
  outcome: rows[target.key]?.outcome ?? null,
746
- failure: null
845
+ failure: null,
846
+ stale: false
747
847
  });
748
848
  Promise.all(list.map(async (target) => {
749
849
  const result = await checkTarget(getCall(), target);
750
850
  patch(target.key, result.ok ? {
751
851
  phase: "ready",
752
852
  outcome: result.value,
753
- failure: null
853
+ failure: null,
854
+ stale: false
754
855
  } : {
755
856
  phase: "failed",
756
857
  outcome: null,
757
- failure: result
858
+ failure: result,
859
+ stale: false
758
860
  });
759
861
  })).finally(() => setChecking(false));
760
862
  }, [
@@ -763,12 +865,36 @@ window.__ModuleLoader__.load({
763
865
  rows,
764
866
  targets
765
867
  ]),
766
- act: react.useCallback((target) => {
868
+ act: react.useCallback((target, action) => {
767
869
  const current = rows[target.key] ?? IDLE$1;
870
+ if (action === "recheck") {
871
+ patch(target.key, {
872
+ phase: "checking",
873
+ outcome: current.outcome,
874
+ failure: null,
875
+ stale: false
876
+ });
877
+ (async () => {
878
+ const checked = await checkTarget(getCall(), target);
879
+ patch(target.key, checked.ok ? {
880
+ phase: "ready",
881
+ outcome: checked.value,
882
+ failure: null,
883
+ stale: false
884
+ } : {
885
+ phase: "failed",
886
+ outcome: null,
887
+ failure: checked,
888
+ stale: false
889
+ });
890
+ })();
891
+ return;
892
+ }
768
893
  patch(target.key, {
769
894
  ...current,
770
895
  phase: "installing",
771
- failure: null
896
+ failure: null,
897
+ stale: false
772
898
  });
773
899
  (async () => {
774
900
  const absent = isAbsent(target);
@@ -779,29 +905,36 @@ window.__ModuleLoader__.load({
779
905
  patch(target.key, {
780
906
  phase: "failed",
781
907
  outcome: null,
782
- failure: checked
908
+ failure: checked,
909
+ stale: false
783
910
  });
784
911
  return;
785
912
  }
786
913
  patch(target.key, {
787
914
  phase: "installing",
788
915
  outcome: checked.value,
789
- failure: null
916
+ failure: null,
917
+ stale: false
790
918
  });
791
919
  version = checked.value.snapshot.latestVersion;
792
920
  }
793
921
  const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMs);
794
922
  if (result.ok) {
923
+ const before = current.outcome?.snapshot.installedVersion ?? null;
795
924
  const refreshed = await checkTarget(getCall(), target, target.phones?.status);
925
+ const after = refreshed.ok ? refreshed.value.snapshot.installedVersion ?? null : null;
796
926
  patch(target.key, refreshed.ok ? {
797
927
  phase: "ready",
798
928
  outcome: refreshed.value,
799
- failure: null
929
+ failure: null,
930
+ stale: false
800
931
  } : {
801
932
  phase: "failed",
802
933
  outcome: null,
803
- failure: refreshed
934
+ failure: refreshed,
935
+ stale: false
804
936
  });
937
+ if (after !== null && after !== before) staleOthers(target.key);
805
938
  await reload();
806
939
  return;
807
940
  }
@@ -811,7 +944,8 @@ window.__ModuleLoader__.load({
811
944
  failure: {
812
945
  ...result,
813
946
  manual: result.manual ?? current.outcome?.manual ?? null
814
- }
947
+ },
948
+ stale: false
815
949
  });
816
950
  })();
817
951
  }, [
@@ -819,7 +953,8 @@ window.__ModuleLoader__.load({
819
953
  patch,
820
954
  pollMs,
821
955
  reload,
822
- rows
956
+ rows,
957
+ staleOthers
823
958
  ])
824
959
  };
825
960
  }
@@ -836,20 +971,20 @@ window.__ModuleLoader__.load({
836
971
  } : PANEL_STYLE.btn,
837
972
  disabled,
838
973
  onClick: checkAll,
839
- title: "一次查七家(总管自己 + 六个单品)的插件包版本"
974
+ title: "检查总管与六家插件的版本"
840
975
  }, checking ? "检查中…" : "检查更新");
841
976
  }
842
- /** 待重启横幅:任何一家成 `pending-restart` 就常驻显示,重启宿主后自动消失(更新包 README 第 10 节)。 */
977
+ /** 待重启横幅:一行总账,只点名哪几家要重启(理由与做法写在各家卡片上,见 `restartBannerText`)。 */
843
978
  function RestartBanners(props) {
844
- const lines = [];
979
+ const titles = [];
845
980
  for (const target of props.face.targets) {
846
981
  const outcome = props.face.rows[target.key]?.outcome;
847
982
  if (!outcome) continue;
848
- const text = pendingRestartText(target, outcome.snapshot);
849
- if (text) lines.push(text);
983
+ if (restartPendingOf(outcome.snapshot)) titles.push(target.title);
850
984
  }
851
- if (lines.length === 0) return null;
852
- return react.createElement("div", { style: PANEL_STYLE.banner }, lines.map((line, index) => react.createElement("div", { key: String(index) }, line)));
985
+ const text = restartBannerText(titles);
986
+ if (text === null) return null;
987
+ return react.createElement("div", { style: PANEL_STYLE.banner }, text);
853
988
  }
854
989
  /** 七家结果:装不了的与待重启的常驻(横幅),详细结果放**居中浮层**(点「检查更新」开)。
855
990
  *
@@ -872,10 +1007,11 @@ window.__ModuleLoader__.load({
872
1007
  const body = shown.map((target) => {
873
1008
  const row = rows[target.key];
874
1009
  const snapshot = row.outcome?.snapshot ?? null;
875
- const verdict = snapshot ? verdictOf(target, snapshot) : null;
1010
+ const verdict = row.stale === true ? staleVerdict() : snapshot ? verdictOf(target, snapshot) : null;
876
1011
  const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
877
1012
  const busy = row.phase === "installing";
878
- const showManual = manual !== null && (row.phase === "failed" || verdict?.kind === "blocked");
1013
+ const showManual = manual !== null && (row.phase === "failed" || verdict?.manualHint === true || verdict?.kind === "blocked" && verdict.action === null);
1014
+ const buttonAction = cardActionOf(verdict, row.phase);
879
1015
  const tone = rowToneOf(row, verdict);
880
1016
  const paint = ROW_TONE[tone];
881
1017
  /** 这一行「当前/最新」两个版本号:不等才算有新版,把「最新」那格上色(用户扫一眼先看有没有箭头)。 */
@@ -906,7 +1042,7 @@ window.__ModuleLoader__.load({
906
1042
  } : {
907
1043
  ...PANEL_STYLE.verdictLine,
908
1044
  color: "var(--dsw-alias-label-secondary, #9a9a9a)"
909
- } }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), verdict?.action ? react.createElement("button", {
1045
+ } }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), buttonAction ? react.createElement("button", {
910
1046
  type: "button",
911
1047
  style: busy ? {
912
1048
  ...PANEL_STYLE.btnPrimary,
@@ -914,11 +1050,13 @@ window.__ModuleLoader__.load({
914
1050
  cursor: "default"
915
1051
  } : PANEL_STYLE.btnPrimary,
916
1052
  disabled: busy,
917
- onClick: () => props.face.act(target)
918
- }, verdict.action === "install" ? "装上" : "装上更新") : null, row.failure ? react.createElement("div", { style: PANEL_STYLE.reason }, "装不上:" + row.failure.message) : null, versionLines(target, snapshot).map((line, index) => react.createElement("div", {
1053
+ onClick: () => {
1054
+ props.face.act(target, buttonAction);
1055
+ }
1056
+ }, ACTION_LABEL[buttonAction]) : null, row.failure ? react.createElement("div", { style: PANEL_STYLE.reason }, "装不上:" + row.failure.message) : null, versionLines(target, snapshot).map((line, index) => react.createElement("div", {
919
1057
  key: String(index),
920
1058
  style: PANEL_STYLE.skill
921
- }, line)), showManual ? react.createElement("code", { style: PANEL_STYLE.cmd }, manual) : null);
1059
+ }, line)), showManual ? react.createElement("div", { style: PANEL_STYLE.skill }, "在终端里执行这条命令可以手动完成这一步:") : null, showManual ? react.createElement("code", { style: PANEL_STYLE.cmd }, manual) : null);
922
1060
  });
923
1061
  return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), open ? react.createElement("div", {
924
1062
  style: PANEL_STYLE.overlay,
@@ -940,7 +1078,7 @@ window.__ModuleLoader__.load({
940
1078
  onClick: () => close(),
941
1079
  title: "关闭",
942
1080
  "aria-label": "关闭"
943
- }, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。")))) : null);
1081
+ }, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "安装在宿主后台继续:关掉这块不会中断它。再点「检查更新」看最新进度。")))) : null);
944
1082
  }
945
1083
  /** 缺席卡:**按两处独立事实分三态**,每态只给能改变状态的动作。
946
1084
  *
@@ -961,17 +1099,19 @@ window.__ModuleLoader__.load({
961
1099
  const manual = (target ? manualForDisplay(target, row?.failure?.manual ?? row?.outcome?.manual ?? null) : null) ?? props.fallbackCommand;
962
1100
  const installed = target ? target.installedVersion ?? target.runningVersion : null;
963
1101
  const state = slotStateOf(target, props.inLedger);
1102
+ const blockedVerdict = target && row?.outcome && row.outcome.snapshot.blockedReason !== null ? verdictOf(target, row.outcome.snapshot) : null;
964
1103
  let reason = row?.failure?.message ?? null;
965
- if (target && reason === null && row?.outcome && row.outcome.snapshot.blockedReason !== null) reason = verdictOf(target, row.outcome.snapshot).text;
1104
+ if (reason === null && blockedVerdict) reason = blockedVerdict.text;
1105
+ const retryAction = row ? cardActionOf(blockedVerdict, row.phase) : null;
966
1106
  let headline;
967
1107
  switch (state) {
968
1108
  case "absent":
969
1109
  headline = target ? "未安装[" + target.packageName + "],装上后这个页签就能用了。" : "这个页签对应的插件还没装。";
970
1110
  break;
971
1111
  case "unregistered-product":
972
- headline = target.packageName + " " + String(installed) + " 装着,但它这一版的产物里没有爱生活页签的注册代码:得等它发一个带注册代码的新版才对得上。重启与重装都不会变(重装只是把同一版再装一次)。";
1112
+ headline = target.packageName + " " + String(installed) + " 已安装,但这一版的插件包里没有「爱生活页签」的注册代码:要等它发布带注册代码的新版本。重启和重装都不会改变这一点。";
973
1113
  break;
974
- default: headline = target.packageName + " " + String(installed) + " 装着、产物里也有注册代码,但它的设置页没送到浏览器:重启宿主后再看这个页签;仍然这样就去宿主日志里找它的报错。";
1114
+ default: headline = target.packageName + " " + String(installed) + " 已安装,插件包里也有注册代码,但它的设置页没有加载到面板:重启 DSH 后再看这个页签;仍然这样,就在 DSH 日志里找它的报错。";
975
1115
  }
976
1116
  return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
977
1117
  type: "button",
@@ -985,9 +1125,23 @@ window.__ModuleLoader__.load({
985
1125
  },
986
1126
  disabled: busy,
987
1127
  onClick: () => {
988
- if (target) props.face.act(target);
1128
+ if (target) props.face.act(target, "install");
989
1129
  }
990
- }, busy ? "正在装,请稍候…" : "装上") : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
1130
+ }, busy ? "正在装,请稍候…" : "装上") : null, retryAction === "recheck" && target ? react.createElement("button", {
1131
+ type: "button",
1132
+ style: busy ? {
1133
+ ...PANEL_STYLE.btn,
1134
+ marginTop: 8,
1135
+ marginLeft: 8,
1136
+ opacity: .55
1137
+ } : {
1138
+ ...PANEL_STYLE.btn,
1139
+ marginTop: 8,
1140
+ marginLeft: 8
1141
+ },
1142
+ disabled: busy,
1143
+ onClick: () => props.face.act(target, "recheck")
1144
+ }, ACTION_LABEL.recheck) : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
991
1145
  }
992
1146
  //#endregion
993
1147
  //#region src/health-contract.ts
@@ -1047,8 +1201,15 @@ window.__ModuleLoader__.load({
1047
1201
  function phoneOf(channel) {
1048
1202
  return channel.startsWith("/") ? channel.slice(1) : channel;
1049
1203
  }
1050
- /** 一次取数的时限(毫秒):技能侧 spawn 上限 20 s,这里留一点余量。 */
1051
- const HEALTH_TIMEOUT_MS = 25e3;
1204
+ /** 整批取数的时限(毫秒),**不是**一家一份:六家的活是在**同一个宿主线程**上排队干的。
1205
+ *
1206
+ * 为什么不是「技能侧 spawn 上限 20 s + 一点余量」(票 #740 对抗式审查第二轮逮到):
1207
+ * 各家的宿主半用的是 `spawnSync`(各单品的 `src/bridge.ts` 里 `SPAWN_TIMEOUT_MS = 20_000`),
1208
+ * 它**阻塞**宿主的事件循环——六通电话同时打进来,真正干活的是一个接一个串起来的。
1209
+ * 于是「六家之和」才是这一批的真实上界:按一家 20 s 算最坏 120 s,常见情形(一家 1~3 s)约 10 s。
1210
+ * 60 s 取两者之间:给常见情形留足余量,又不至于让一次卡死把按钮按成两分钟不动。
1211
+ * 单家的错照样逐家报(那家页签里的体检表会写「体检超时(60s)…」)。 */
1212
+ const HEALTH_TIMEOUT_MS = 6e4;
1052
1213
  function timeoutText() {
1053
1214
  return "体检超时(" + String(Math.round(HEALTH_TIMEOUT_MS / 1e3)) + "s):技能没在时限内回话。";
1054
1215
  }