dsh-life-pack 0.3.12 → 0.3.14

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.
Files changed (45) hide show
  1. package/dist/client.js +291 -60
  2. package/dist/client.js.map +1 -1
  3. package/dist/config-panel-api.d.ts +5 -2
  4. package/dist/config-panel-api.d.ts.map +1 -1
  5. package/dist/config-panel-api.js +4 -2
  6. package/dist/config-panel-api.js.map +1 -1
  7. package/dist/config-panel-contract.d.ts +13 -2
  8. package/dist/config-panel-contract.d.ts.map +1 -1
  9. package/dist/config-panel-contract.js +5 -2
  10. package/dist/config-panel-contract.js.map +1 -1
  11. package/dist/config-panel-value.d.ts +0 -12
  12. package/dist/config-panel-value.d.ts.map +1 -1
  13. package/dist/config-panel-value.js +6 -16
  14. package/dist/config-panel-value.js.map +1 -1
  15. package/dist/config-panel-view.d.ts +230 -13
  16. package/dist/config-panel-view.d.ts.map +1 -1
  17. package/dist/config-panel-view.js +598 -99
  18. package/dist/config-panel-view.js.map +1 -1
  19. package/dist/config-panel.d.ts +7 -0
  20. package/dist/config-panel.d.ts.map +1 -1
  21. package/dist/config-panel.js +21 -0
  22. package/dist/config-panel.js.map +1 -1
  23. package/dist/directory-browser-parts.d.ts.map +1 -1
  24. package/dist/directory-browser-parts.js +1 -1
  25. package/dist/directory-browser-parts.js.map +1 -1
  26. package/dist/health-contract.d.ts +7 -0
  27. package/dist/health-contract.d.ts.map +1 -1
  28. package/dist/health-contract.js.map +1 -1
  29. package/dist/health-view.js +1 -1
  30. package/dist/health-view.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/manager-version.d.ts +11 -0
  36. package/dist/manager-version.d.ts.map +1 -1
  37. package/dist/manager-version.js +39 -6
  38. package/dist/manager-version.js.map +1 -1
  39. package/dist/nav.js +1 -1
  40. package/dist/nav.js.map +1 -1
  41. package/dist/update-queue.d.ts +42 -0
  42. package/dist/update-queue.d.ts.map +1 -0
  43. package/dist/update-queue.js +91 -0
  44. package/dist/update-queue.js.map +1 -0
  45. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
100
100
  const PANEL_LINKS = [{
101
101
  key: "star",
102
102
  glyph: "⭐",
103
- tip: "去 GitHub 点 Star",
103
+ tip: "你的🌟是我夜空中最亮的星。",
104
104
  url: "https://github.com/FeatherHunter/ilife"
105
105
  }, {
106
106
  key: "feedback",
@@ -518,6 +518,112 @@ window.__ModuleLoader__.load({
518
518
  return lines;
519
519
  }
520
520
  //#endregion
521
+ //#region src/update-queue.ts
522
+ /** 任一行 installing 即忙:题头检查更新禁用、行按钮全局互斥、checkAll/act 入口直接返回的同一判据。 */
523
+ function hasInstallingRow(rows) {
524
+ for (const key of Object.keys(rows)) if (rows[key]?.phase === "installing") return true;
525
+ return false;
526
+ }
527
+ /** 按目标顺序逐家查→装→收尾:一家收尾才起下一家;失败记 failed 继续;待重启跳过安装。
528
+ *
529
+ * 跳过安装只有一种:快照事实已是待重启(`restartPendingOf`)——由横幅点名,不在这里装第二遍。
530
+ * 不可装(已是最新/拦下且无安装动作)记 ready 收尾;缺席且无最新版本号记 ready 收尾(等下一次检查)。
531
+ * 本函数不汇总排队数、不碰他行(`patch` 只收本家 key)。
532
+ */
533
+ async function runSerialUpdateAll(targets, deps) {
534
+ for (const target of targets) {
535
+ const current = deps.getRow(target.key);
536
+ deps.patch(target.key, {
537
+ phase: "checking",
538
+ outcome: current?.outcome ?? null,
539
+ failure: null
540
+ });
541
+ const checked = await deps.check(target);
542
+ if (!checked.ok) {
543
+ deps.patch(target.key, {
544
+ phase: "failed",
545
+ outcome: null,
546
+ failure: checked
547
+ });
548
+ continue;
549
+ }
550
+ if (restartPendingOf(checked.value.snapshot)) {
551
+ deps.patch(target.key, {
552
+ phase: "ready",
553
+ outcome: checked.value,
554
+ failure: null
555
+ });
556
+ continue;
557
+ }
558
+ const verdict = verdictOf(target, checked.value.snapshot);
559
+ if (!(verdict.action === "update" || verdict.action === "install" || verdict.action === "retry")) {
560
+ deps.patch(target.key, {
561
+ phase: "ready",
562
+ outcome: checked.value,
563
+ failure: null
564
+ });
565
+ continue;
566
+ }
567
+ deps.patch(target.key, {
568
+ phase: "installing",
569
+ outcome: checked.value,
570
+ failure: null
571
+ });
572
+ if (isAbsent(target)) {
573
+ const version = checked.value.snapshot.latestVersion;
574
+ if (!version) {
575
+ deps.patch(target.key, {
576
+ phase: "ready",
577
+ outcome: checked.value,
578
+ failure: null
579
+ });
580
+ continue;
581
+ }
582
+ const done = await deps.installAbsent(target, version);
583
+ if (done.ok) {
584
+ const refreshed = await deps.checkStatus(target);
585
+ deps.patch(target.key, refreshed.ok ? {
586
+ phase: "ready",
587
+ outcome: refreshed.value,
588
+ failure: null
589
+ } : {
590
+ phase: "failed",
591
+ outcome: null,
592
+ failure: refreshed
593
+ });
594
+ } else deps.patch(target.key, {
595
+ phase: "failed",
596
+ outcome: checked.value,
597
+ failure: {
598
+ ...done,
599
+ manual: done.manual ?? checked.value.manual ?? null
600
+ }
601
+ });
602
+ continue;
603
+ }
604
+ const done = await deps.installPresent(target);
605
+ if (done.ok) {
606
+ const refreshed = await deps.checkStatus(target);
607
+ deps.patch(target.key, refreshed.ok ? {
608
+ phase: "ready",
609
+ outcome: refreshed.value,
610
+ failure: null
611
+ } : {
612
+ phase: "failed",
613
+ outcome: null,
614
+ failure: refreshed
615
+ });
616
+ } else deps.patch(target.key, {
617
+ phase: "failed",
618
+ outcome: checked.value,
619
+ failure: {
620
+ ...done,
621
+ manual: done.manual ?? checked.value.manual ?? null
622
+ }
623
+ });
624
+ }
625
+ }
626
+ //#endregion
521
627
  //#region src/update-panel.ts
522
628
  /** 爱生活面板的更新界面:检查更新按钮、七家结果行、待重启横幅、缺席卡的「装上」。
523
629
  *
@@ -562,7 +668,7 @@ window.__ModuleLoader__.load({
562
668
  marginBottom: 10
563
669
  },
564
670
  tab: {
565
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
671
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35))",
566
672
  background: "transparent",
567
673
  color: "var(--dsw-alias-label-primary, inherit)",
568
674
  borderRadius: 999,
@@ -573,13 +679,13 @@ window.__ModuleLoader__.load({
573
679
  tabActive: {
574
680
  background: "var(--dsw-alias-brand-primary, #0a84ff)",
575
681
  borderColor: "transparent",
576
- color: "#fff",
682
+ color: "var(--dsw-alias-label-primary-foreground, #0f1115)",
577
683
  fontWeight: 700
578
684
  },
579
685
  reco: {
580
686
  padding: "12px 14px",
581
687
  borderRadius: 10,
582
- border: "1px dashed var(--dsw-alias-border, rgba(128,128,128,.45))",
688
+ border: "1px dashed var(--dsw-alias-border-l1, rgba(128,128,128,.45))",
583
689
  color: "var(--dsw-alias-label-secondary, #9a9a9a)",
584
690
  fontSize: 13,
585
691
  lineHeight: 1.7
@@ -596,7 +702,7 @@ window.__ModuleLoader__.load({
596
702
  userSelect: "all"
597
703
  },
598
704
  btn: {
599
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
705
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35))",
600
706
  background: "transparent",
601
707
  color: "var(--dsw-alias-label-primary, inherit)",
602
708
  borderRadius: 8,
@@ -608,7 +714,7 @@ window.__ModuleLoader__.load({
608
714
  padding: "10px 12px",
609
715
  marginBottom: 10,
610
716
  borderRadius: 8,
611
- border: "1px solid var(--dsw-alias-state-warning-primary, #d8a300)",
717
+ border: "1px solid var(--dsw-alias-state-warn-primary, #d8a300)",
612
718
  background: "var(--dsw-alias-bg-base, rgba(216,163,0,.12))",
613
719
  color: "var(--dsw-alias-label-primary, inherit)",
614
720
  fontSize: 13,
@@ -618,7 +724,7 @@ window.__ModuleLoader__.load({
618
724
  padding: "10px 12px",
619
725
  marginBottom: 8,
620
726
  borderRadius: 10,
621
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))",
727
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3))",
622
728
  borderLeftWidth: 3,
623
729
  fontSize: 13,
624
730
  lineHeight: 1.7
@@ -668,7 +774,7 @@ window.__ModuleLoader__.load({
668
774
  btnPrimary: {
669
775
  border: "1px solid transparent",
670
776
  background: "var(--dsw-alias-brand-primary, #0a84ff)",
671
- color: "#fff",
777
+ color: "var(--dsw-alias-label-primary-foreground, #0f1115)",
672
778
  borderRadius: 8,
673
779
  padding: "5px 14px",
674
780
  fontSize: 13,
@@ -701,8 +807,8 @@ window.__ModuleLoader__.load({
701
807
  flexDirection: "column",
702
808
  overflow: "hidden",
703
809
  borderRadius: 12,
704
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.4))",
705
- background: "var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #222))",
810
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.4))",
811
+ background: "var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base, #222))",
706
812
  color: "var(--dsw-alias-label-primary, inherit)",
707
813
  boxShadow: "0 12px 32px rgba(0,0,0,.35)"
708
814
  },
@@ -713,7 +819,7 @@ window.__ModuleLoader__.load({
713
819
  gap: 8,
714
820
  flex: "0 0 auto",
715
821
  padding: "12px 14px",
716
- borderBottom: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))"
822
+ borderBottom: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3))"
717
823
  },
718
824
  dialogTitle: {
719
825
  fontSize: 14,
@@ -727,7 +833,7 @@ window.__ModuleLoader__.load({
727
833
  padding: "12px 14px"
728
834
  },
729
835
  close: {
730
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
836
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35))",
731
837
  background: "transparent",
732
838
  color: "var(--dsw-alias-label-primary, inherit)",
733
839
  borderRadius: 8,
@@ -751,7 +857,7 @@ window.__ModuleLoader__.load({
751
857
  },
752
858
  blocked: {
753
859
  mark: "!",
754
- color: "var(--dsw-alias-state-warning-primary, #d8a300)",
860
+ color: "var(--dsw-alias-state-warn-primary, #d8a300)",
755
861
  tint: "rgba(216,163,0,.10)"
756
862
  },
757
863
  failed: {
@@ -794,13 +900,21 @@ window.__ModuleLoader__.load({
794
900
  outcome: null,
795
901
  failure: null
796
902
  };
797
- /** 面板的更新状态源:挂载时取七个目标的表,之后按需查/装(组件与按钮共用一个 face)。 */
903
+ /** 面板的更新状态源:挂载时取七个目标的表,之后按需查/装(组件与按钮共用一个 face)。
904
+ *
905
+ * 并发形状(#925 拍板四条,用户已确认):
906
+ * - 一键全部更新按目标顺序串行(查→装→轮询到收尾,一家收尾才起下一家;失败记 failed 继续);
907
+ * - 任一行 installing 时 checkAll 入口直接返回、题头禁用;
908
+ * - act 入口有忙直接返回(有忙拒 update-busy 且不改 rows,含同家第二点);
909
+ * - 每行各自显示(patch 只写本家 key),题头不汇总排队数。
910
+ */
798
911
  function useUpdateRows(getCall) {
799
912
  const [targets, setTargets] = react.useState([]);
800
913
  const [rows, setRows] = react.useState({});
801
914
  const [pollMs, setPollMs] = react.useState(1e3);
802
915
  const [loadError, setLoadError] = react.useState(null);
803
916
  const [checking, setChecking] = react.useState(false);
917
+ const [updatingAll, setUpdatingAll] = react.useState(false);
804
918
  const [open, setOpen] = react.useState(false);
805
919
  const alive = react.useRef(true);
806
920
  react.useEffect(() => () => {
@@ -825,21 +939,31 @@ window.__ModuleLoader__.load({
825
939
  [key]: next
826
940
  }));
827
941
  }, []);
942
+ const rowsRef = react.useRef(rows);
943
+ rowsRef.current = rows;
944
+ const targetsRef = react.useRef(targets);
945
+ targetsRef.current = targets;
946
+ const pollMsRef = react.useRef(pollMs);
947
+ pollMsRef.current = pollMs;
948
+ const updatingAllRef = react.useRef(false);
828
949
  return {
829
950
  targets,
830
951
  rows,
831
952
  pollMs,
832
953
  loadError,
833
954
  checking,
955
+ updatingAll,
834
956
  open,
835
957
  close: () => setOpen(false),
836
958
  checkAll: react.useCallback(() => {
959
+ if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
837
960
  setChecking(true);
838
961
  setOpen(true);
839
- const list = targets;
962
+ const list = targetsRef.current;
963
+ const snapshot = rowsRef.current;
840
964
  for (const target of list) patch(target.key, {
841
965
  phase: "checking",
842
- outcome: rows[target.key]?.outcome ?? null,
966
+ outcome: snapshot[target.key]?.outcome ?? null,
843
967
  failure: null
844
968
  });
845
969
  Promise.all(list.map(async (target) => {
@@ -853,15 +977,41 @@ window.__ModuleLoader__.load({
853
977
  outcome: null,
854
978
  failure: result
855
979
  });
856
- })).finally(() => setChecking(false));
980
+ })).finally(() => {
981
+ if (alive.current) setChecking(false);
982
+ });
983
+ }, [getCall, patch]),
984
+ updateAll: react.useCallback(() => {
985
+ if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
986
+ const list = targetsRef.current;
987
+ if (list.length === 0) return;
988
+ updatingAllRef.current = true;
989
+ setUpdatingAll(true);
990
+ setOpen(true);
991
+ (async () => {
992
+ try {
993
+ await runSerialUpdateAll(list, {
994
+ check: (target) => checkTarget(getCall(), target),
995
+ checkStatus: (target) => checkTarget(getCall(), target, target.phones?.status),
996
+ installAbsent: (target, version) => installAbsent(getCall(), target, version),
997
+ installPresent: (target) => updateInstalled(getCall(), target, pollMsRef.current),
998
+ patch,
999
+ getRow: (key) => rowsRef.current[key]
1000
+ });
1001
+ await reload();
1002
+ } finally {
1003
+ updatingAllRef.current = false;
1004
+ if (alive.current) setUpdatingAll(false);
1005
+ }
1006
+ })();
857
1007
  }, [
858
1008
  getCall,
859
1009
  patch,
860
- rows,
861
- targets
1010
+ reload
862
1011
  ]),
863
1012
  act: react.useCallback((target, action) => {
864
- const current = rows[target.key] ?? IDLE$1;
1013
+ if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
1014
+ const current = rowsRef.current[target.key] ?? IDLE$1;
865
1015
  if (action === "recheck") {
866
1016
  patch(target.key, {
867
1017
  phase: "checking",
@@ -907,7 +1057,7 @@ window.__ModuleLoader__.load({
907
1057
  });
908
1058
  version = checked.value.snapshot.latestVersion;
909
1059
  }
910
- const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMs);
1060
+ const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMsRef.current);
911
1061
  if (result.ok) {
912
1062
  const refreshed = await checkTarget(getCall(), target, target.phones?.status);
913
1063
  patch(target.key, refreshed.ok ? {
@@ -934,27 +1084,42 @@ window.__ModuleLoader__.load({
934
1084
  }, [
935
1085
  getCall,
936
1086
  patch,
937
- pollMs,
938
- reload,
939
- rows
1087
+ reload
940
1088
  ])
941
1089
  };
942
1090
  }
943
- /** 标题右侧的「检查更新」:一个按钮一次查七家(吃 `useUpdateRows` 的表)。 */
1091
+ /** 标题右侧两件:「检查更新」一次查七家 + 「全部更新」按顺序串行装(吃 `useUpdateRows` 的表)。
1092
+ *
1093
+ * 禁用形状(#925 第 2 条):任一行 installing 时两件都点不得;人话沿用 `update-busy` 原句
1094
+ * (挂在 title 上,按钮文本仍是动作名)。题头不汇总排队数:串行进度只在各行自己显示。
1095
+ */
944
1096
  function CheckUpdateButton(props) {
945
- const { checking, checkAll, targets } = props.face;
946
- const disabled = checking || targets.length === 0;
947
- return react.createElement("button", {
1097
+ const { checking, updatingAll, checkAll, updateAll, targets, rows } = props.face;
1098
+ const busy = hasInstallingRow(rows) || updatingAll;
1099
+ const checkDisabled = checking || updatingAll || busy || targets.length === 0;
1100
+ const allDisabled = checking || updatingAll || busy || targets.length === 0;
1101
+ const busyText = reasonText("update-busy");
1102
+ return react.createElement("span", { style: PANEL_STYLE.actions }, react.createElement("button", {
948
1103
  type: "button",
949
- style: disabled ? {
1104
+ style: checkDisabled ? {
950
1105
  ...PANEL_STYLE.btn,
951
1106
  opacity: .55,
952
1107
  cursor: "default"
953
1108
  } : PANEL_STYLE.btn,
954
- disabled,
1109
+ disabled: checkDisabled,
955
1110
  onClick: checkAll,
956
- title: "检查总管与六家插件的版本"
957
- }, checking ? "检查中…" : "检查更新");
1111
+ title: busy ? busyText : "检查总管与六家插件的版本"
1112
+ }, checking ? "检查中…" : "检查更新"), react.createElement("button", {
1113
+ type: "button",
1114
+ style: allDisabled ? {
1115
+ ...PANEL_STYLE.btn,
1116
+ opacity: .55,
1117
+ cursor: "default"
1118
+ } : PANEL_STYLE.btn,
1119
+ disabled: allDisabled,
1120
+ onClick: updateAll,
1121
+ title: busy ? busyText : "按顺序逐家装上更新(一家收尾才起下一家)"
1122
+ }, updatingAll ? "更新中…" : "全部更新"));
958
1123
  }
959
1124
  /** 待重启横幅:一行总账,只点名哪几家要重启(理由与做法写在各家卡片上,见 `restartBannerText`)。 */
960
1125
  function RestartBanners(props) {
@@ -977,7 +1142,7 @@ window.__ModuleLoader__.load({
977
1142
  * 手工命令只在「装不了」或「装失败」的行显示:正常用户用不到,摊在每行上既吵又误导。
978
1143
  */
979
1144
  function UpdateResults(props) {
980
- const { targets, rows, loadError, checking, open, close } = props.face;
1145
+ const { targets, rows, loadError, checking, updatingAll, open, close } = props.face;
981
1146
  const dialogRef = react.useRef(null);
982
1147
  react.useEffect(() => {
983
1148
  if (open) dialogRef.current?.focus();
@@ -986,6 +1151,7 @@ window.__ModuleLoader__.load({
986
1151
  if (loadError) return react.createElement("div", { style: PANEL_STYLE.reason }, "更新能力没接上:" + loadError);
987
1152
  if (targets.length === 0) return null;
988
1153
  if (shown.length === 0 && !open) return null;
1154
+ const locked = hasInstallingRow(rows) || updatingAll;
989
1155
  const body = shown.map((target) => {
990
1156
  const row = rows[target.key];
991
1157
  const snapshot = row.outcome?.snapshot ?? null;
@@ -1026,12 +1192,13 @@ window.__ModuleLoader__.load({
1026
1192
  color: "var(--dsw-alias-label-secondary, #9a9a9a)"
1027
1193
  } }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), buttonAction ? react.createElement("button", {
1028
1194
  type: "button",
1029
- style: busy ? {
1195
+ style: locked ? {
1030
1196
  ...PANEL_STYLE.btnPrimary,
1031
1197
  opacity: .55,
1032
1198
  cursor: "default"
1033
1199
  } : PANEL_STYLE.btnPrimary,
1034
- disabled: busy,
1200
+ disabled: locked,
1201
+ title: locked ? reasonText("update-busy") : void 0,
1035
1202
  onClick: () => {
1036
1203
  props.face.act(target, buttonAction);
1037
1204
  }
@@ -1078,6 +1245,7 @@ window.__ModuleLoader__.load({
1078
1245
  const target = props.target;
1079
1246
  const row = target ? props.face.rows[target.key] : null;
1080
1247
  const busy = row?.phase === "installing";
1248
+ const locked = busy || hasInstallingRow(props.face.rows) || props.face.updatingAll;
1081
1249
  const manual = (target ? manualForDisplay(target, row?.failure?.manual ?? row?.outcome?.manual ?? null) : null) ?? props.fallbackCommand;
1082
1250
  const installed = target ? target.installedVersion ?? target.runningVersion : null;
1083
1251
  const state = slotStateOf(target, props.inLedger);
@@ -1097,7 +1265,7 @@ window.__ModuleLoader__.load({
1097
1265
  }
1098
1266
  return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
1099
1267
  type: "button",
1100
- style: busy ? {
1268
+ style: locked ? {
1101
1269
  ...PANEL_STYLE.btn,
1102
1270
  marginTop: 8,
1103
1271
  opacity: .55
@@ -1105,13 +1273,14 @@ window.__ModuleLoader__.load({
1105
1273
  ...PANEL_STYLE.btn,
1106
1274
  marginTop: 8
1107
1275
  },
1108
- disabled: busy,
1276
+ disabled: locked,
1277
+ title: locked ? reasonText("update-busy") : void 0,
1109
1278
  onClick: () => {
1110
1279
  if (target) props.face.act(target, "install");
1111
1280
  }
1112
1281
  }, busy ? "正在装,请稍候…" : "装上") : null, retryAction === "recheck" && target ? react.createElement("button", {
1113
1282
  type: "button",
1114
- style: busy ? {
1283
+ style: locked ? {
1115
1284
  ...PANEL_STYLE.btn,
1116
1285
  marginTop: 8,
1117
1286
  marginLeft: 8,
@@ -1121,7 +1290,8 @@ window.__ModuleLoader__.load({
1121
1290
  marginTop: 8,
1122
1291
  marginLeft: 8
1123
1292
  },
1124
- disabled: busy,
1293
+ disabled: locked,
1294
+ title: locked ? reasonText("update-busy") : void 0,
1125
1295
  onClick: () => props.face.act(target, "recheck")
1126
1296
  }, ACTION_LABEL.recheck) : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
1127
1297
  }
@@ -1386,7 +1556,7 @@ window.__ModuleLoader__.load({
1386
1556
  /** 面板上的文字色与分隔线(与 `client.ts` 同一套别名)。 */
1387
1557
  const INK = "var(--dsw-alias-label-primary, inherit)";
1388
1558
  const INK_DIM = "var(--dsw-alias-label-secondary, #9a9a9a)";
1389
- const BORDER = "var(--dsw-alias-border, rgba(128,128,128,.35))";
1559
+ const BORDER = "var(--dsw-alias-border-l1, rgba(128,128,128,.35))";
1390
1560
  const HEALTH_STYLE = {
1391
1561
  box: {
1392
1562
  padding: "10px 12px",
@@ -2262,7 +2432,7 @@ window.__ModuleLoader__.load({
2262
2432
  textOverflow: "ellipsis",
2263
2433
  whiteSpace: "nowrap"
2264
2434
  },
2265
- error: { color: "var(--dsw-alias-label-error, #e66)" }
2435
+ error: { color: "var(--dsw-alias-state-error-primary, #e66)" }
2266
2436
  };
2267
2437
  /** 面包屑:从文件系统根到当前层,每一格都是跳转目标(`listing.crumbs` 由取数方给)。 */
2268
2438
  function crumbRow(state, onEnter) {
@@ -2572,11 +2742,23 @@ window.__ModuleLoader__.load({
2572
2742
  fontSize: 14,
2573
2743
  lineHeight: 1
2574
2744
  },
2575
- meta: {
2576
- color: "var(--dsw-alias-label-secondary, #9a9a9a)",
2577
- fontSize: 12,
2578
- lineHeight: 1.7,
2579
- marginBottom: 10
2745
+ /** #933:页签格里「配置卡 + 体检卡」这一列的堆叠间距(唯一口径,别再给某一张卡加 margin)。 */
2746
+ tabStack: {
2747
+ display: "flex",
2748
+ flexDirection: "column",
2749
+ gap: 10
2750
+ },
2751
+ /** #937:标题行里那枚版本胶囊(形状照真源 v3.1 的 `.ic-ver`:10px 字、1px 描边、999 圆角)。 */
2752
+ versionCapsule: {
2753
+ fontSize: 10.5,
2754
+ lineHeight: 1.6,
2755
+ color: "var(--dsw-alias-label-tertiary, #adb2b8)",
2756
+ border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
2757
+ borderRadius: 999,
2758
+ padding: "0 7px",
2759
+ whiteSpace: "nowrap",
2760
+ flex: "0 0 auto",
2761
+ fontWeight: 400
2580
2762
  },
2581
2763
  tablist: {
2582
2764
  display: "flex",
@@ -2585,7 +2767,7 @@ window.__ModuleLoader__.load({
2585
2767
  marginBottom: 10
2586
2768
  },
2587
2769
  tab: {
2588
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
2770
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35))",
2589
2771
  background: "transparent",
2590
2772
  color: "var(--dsw-alias-label-primary, inherit)",
2591
2773
  borderRadius: 999,
@@ -2596,13 +2778,13 @@ window.__ModuleLoader__.load({
2596
2778
  tabActive: {
2597
2779
  background: "var(--dsw-alias-brand-primary, #0a84ff)",
2598
2780
  borderColor: "transparent",
2599
- color: "#fff",
2781
+ color: "var(--dsw-alias-label-primary-foreground, #0f1115)",
2600
2782
  fontWeight: 700
2601
2783
  },
2602
2784
  reco: {
2603
2785
  padding: "12px 14px",
2604
2786
  borderRadius: 10,
2605
- border: "1px dashed var(--dsw-alias-border, rgba(128,128,128,.45))",
2787
+ border: "1px dashed var(--dsw-alias-border-l1, rgba(128,128,128,.45))",
2606
2788
  color: "var(--dsw-alias-label-secondary, #9a9a9a)",
2607
2789
  fontSize: 13,
2608
2790
  lineHeight: 1.7
@@ -2622,7 +2804,7 @@ window.__ModuleLoader__.load({
2622
2804
  marginTop: 16,
2623
2805
  padding: "10px 12px",
2624
2806
  borderRadius: 10,
2625
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))"
2807
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35))"
2626
2808
  },
2627
2809
  moreTitle: {
2628
2810
  fontSize: 13,
@@ -2714,22 +2896,47 @@ window.__ModuleLoader__.load({
2714
2896
  style: S.moreRow
2715
2897
  }, react.createElement("span", { style: S.morePkg }, row.pkg), react.createElement("span", { style: S.moreDesc }, row.desc), react.createElement("span", { style: S.moreIcon }, react.createElement(ExternalIcon, null)))));
2716
2898
  }
2717
- /** 总管自述版本(票 #737):**不手写**,问宿主——宿主读自己这份已安装包的 `package.json`。
2899
+ /** 总管自述版本(票 #737;#937 改口):**不手写**,问宿主——宿主读自己这份已安装包的 `package.json`。
2718
2900
  *
2719
- * 单次取数(`loadManagerVersion`),无轮询;初值 `unknown`,读不到也停在 `unknown`。
2901
+ * 三态(#937):`loading`(首帧就画胶囊占位 `…`)/`ready`(读到,画包名 · 版本)/`missing`(读不到,画「版本未知」)。
2902
+ * **失败要重试**:原先只取一次、一失败就永远停在未知(#937 查出来的真缺陷);这里按 1s/3s/8s 退避重试三次。
2720
2903
  * 与卡路里 #130 同一条路:面板是浏览器产物、禁 node 内建,读盘只许在宿主半。 */
2904
+ /** 读中 / 读不到 两种胶囊文案(#937:占位符用 `…`,读不到给人话,不把 `unknown` 印给人看)。 */
2905
+ const VERSION_PENDING_TEXT = "…";
2906
+ const VERSION_MISSING_TEXT = "版本未知";
2907
+ /** 重试间隔(#937):三次退避,之后不再打扰宿主。 */
2908
+ const VERSION_RETRY_MS = [
2909
+ 1e3,
2910
+ 3e3,
2911
+ 8e3
2912
+ ];
2721
2913
  function useManagerVersion(getCall) {
2722
- const [version, setVersion] = react.useState(VERSION_UNKNOWN);
2914
+ const [state, setState] = react.useState({ kind: "loading" });
2723
2915
  react.useEffect(() => {
2724
2916
  let alive = true;
2725
- loadManagerVersion(getCall()).then((next) => {
2726
- if (alive) setVersion(next);
2727
- });
2917
+ const timers = [];
2918
+ const attempt = (index) => {
2919
+ loadManagerVersion(getCall()).then((next) => {
2920
+ if (!alive) return;
2921
+ if (next !== "unknown") {
2922
+ setState({
2923
+ kind: "ready",
2924
+ version: next
2925
+ });
2926
+ return;
2927
+ }
2928
+ setState({ kind: "missing" });
2929
+ const wait = VERSION_RETRY_MS[index];
2930
+ if (wait !== void 0) timers.push(setTimeout(() => attempt(index + 1), wait));
2931
+ });
2932
+ };
2933
+ attempt(0);
2728
2934
  return () => {
2729
2935
  alive = false;
2936
+ for (const id of timers) clearTimeout(id);
2730
2937
  };
2731
2938
  }, [getCall]);
2732
- return version;
2939
+ return state;
2733
2940
  }
2734
2941
  /** 爱生活面板:总设置区 + 检查更新(七家)+ 爱生活页签条(slot 驱动)+ 技能设置页投影/缺席卡。 */
2735
2942
  function LifePackSection(props) {
@@ -2759,6 +2966,27 @@ window.__ModuleLoader__.load({
2759
2966
  return /* @__PURE__ */ new Set([...previous, active]);
2760
2967
  });
2761
2968
  }, [active]);
2969
+ /** 预热:section 一挂载,就把其余几家**错峰**挂上(隐藏面板照常挂载、只切 `hidden`),
2970
+ * 让各家那一次配置读提前跑掉——用户点哪个页签,多半值已经在了。
2971
+ * 错峰(每 150ms 一家)是为了不让六家子进程同时抢开工:首帧那一口气优先留给框架。
2972
+ * 只在页签账本非空时起一次(`rows` 要等各家 slot 注入后才齐)。
2973
+ *
2974
+ * **一次性**(p10 定时器门的例外依据):每个页签只挂一次(`warmed` 闸门 + 一批固定数量的定时器),
2975
+ * 不是轮询、也不是重试——它没有「次数」可言;卸载即 `clearTimeout` 清干净。
2976
+ * 不做轮询是本意:这里只补一次提前量,取数与刷新仍旧由各家用例自己的通道走。 */
2977
+ const warmed = react.useRef(false);
2978
+ const tabCount = rows.length;
2979
+ react.useEffect(() => {
2980
+ if (warmed.current || tabCount === 0) return;
2981
+ warmed.current = true;
2982
+ const STAGGER_MS = 150;
2983
+ const timers = MANAGER_TABS.filter((tab) => rows.some((row) => row.id === tab.plugin)).map((tab, index) => setTimeout(() => {
2984
+ setVisitedIds((previous) => previous.has(tab.plugin) ? previous : /* @__PURE__ */ new Set([...previous, tab.plugin]));
2985
+ }, STAGGER_MS * (index + 1)));
2986
+ return () => {
2987
+ for (const timer of timers) clearTimeout(timer);
2988
+ };
2989
+ }, [tabCount]);
2762
2990
  function labelFor(tab) {
2763
2991
  const row = rows.find((r) => r.id === tab.plugin);
2764
2992
  return row && row.label.length > 0 ? row.label : tab.title;
@@ -2788,7 +3016,10 @@ window.__ModuleLoader__.load({
2788
3016
  setActiveId(next.plugin);
2789
3017
  tabRefs.current[nextIndex]?.focus();
2790
3018
  }
2791
- return react.createElement("div", null, react.createElement("div", { style: S.headRow }, react.createElement("div", { style: S.head }, "爱生活"), react.createElement("div", { style: S.headActions }, react.createElement(CheckUpdateButton, { face }), react.createElement(PanelActions, null))), react.createElement("div", { style: S.meta }, react.createElement("div", null, "总管 dsh-life-pack · " + managerVersion)), react.createElement(UpdateResults, { face }), react.createElement(HealthSummaryLine, {
3019
+ return react.createElement("div", null, react.createElement("div", { style: S.headRow }, react.createElement("div", { style: S.head }, "爱生活"), react.createElement("span", {
3020
+ style: S.versionCapsule,
3021
+ "data-ilife-version": "capsule"
3022
+ }, managerVersion.kind === "ready" ? "dsh-life-pack · " + managerVersion.version : managerVersion.kind === "missing" ? VERSION_MISSING_TEXT : VERSION_PENDING_TEXT), react.createElement("div", { style: S.headActions }, react.createElement(CheckUpdateButton, { face }), react.createElement(PanelActions, null))), react.createElement(UpdateResults, { face }), react.createElement(HealthSummaryLine, {
2792
3023
  lights,
2793
3024
  running: health.running,
2794
3025
  error: summaryErrorOf(health),
@@ -2848,8 +3079,8 @@ window.__ModuleLoader__.load({
2848
3079
  id: tabsId + "-panel-" + tab.plugin,
2849
3080
  role: "tabpanel",
2850
3081
  "aria-labelledby": tabsId + "-tab-" + tab.plugin,
2851
- hidden: !selected
2852
- }, present.has(tab.plugin) ? react.createElement("div", null, props.renderSlot(CONFIG_TAB_SLOT, {}, { only: tab.plugin }), react.createElement(HealthTable, {
3082
+ style: selected ? void 0 : { display: "none" }
3083
+ }, present.has(tab.plugin) ? react.createElement("div", { style: S.tabStack }, props.renderSlot(CONFIG_TAB_SLOT, {}, { only: tab.plugin }), react.createElement(HealthTable, {
2853
3084
  title: labelFor(tab),
2854
3085
  phase: health.rows[tab.plugin]?.phase ?? "idle",
2855
3086
  report: health.rows[tab.plugin]?.report ?? null,