dsh-life-pack 0.3.15 → 0.3.17

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
@@ -193,6 +193,16 @@ window.__ModuleLoader__.load({
193
193
  * 一处定义、两侧引用:宿主半 `manager-version.ts` 读失败时回它,面板半 `update-client.ts`
194
194
  * 归一化时也认它——面板侧不许 import 宿主半(会把 node 内建带进浏览器束)。 */
195
195
  const VERSION_UNKNOWN = "unknown";
196
+ /** 挂载取数失败的退避重试表(票 #982,更新目标表用)。
197
+ *
198
+ * 一处定义:与版本读取重试同形(`client.ts` 的退避三次)——挂载时首次传输失败
199
+ * (宿主路由尚未就绪)会使错误态常驻整个会话;单次直返是当时的缺口。
200
+ * 次数有界、调用方负责卸载清理(见 `update-panel.ts` 的取数),不是无界轮询。 */
201
+ const LOAD_RETRY_MS = [
202
+ 1e3,
203
+ 3e3,
204
+ 8e3
205
+ ];
196
206
  /** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。
197
207
  *
198
208
  * 每条都要三段齐全(票 #740 的文案判据):**事实**(哪里对不上)→ **后果**(这家现在能不能装)
@@ -282,8 +292,27 @@ window.__ModuleLoader__.load({
282
292
  const error = result.error;
283
293
  return failure(typeof error?.code === "string" ? error.code : "internal", error && typeof error.details === "object" && error.details !== null ? error.details : {});
284
294
  }
285
- /** 取七个更新目标的表(宿主转交更新包的三个电话名与版本行,面板不写死任何电话名)。 */
286
- async function loadTargets(call) {
295
+ /** 取七个更新目标的表(宿主转交更新包的三个电话名与版本行,面板不写死任何电话名)。
296
+ *
297
+ * 重试参数(票 #982):缺省单次(与旧行为一致);给 `delays` 即按退避重试——
298
+ * 挂载时首次传输失败不再直接进入错误态。`sleep`/`isAlive` 可注入(测试与卸载清理用),
299
+ * 缺省走本模块同形的等待与恒真。首次成功即返回,否则返回最后一次失败。 */
300
+ async function loadTargets(call, retry) {
301
+ const delays = retry?.delays ?? [];
302
+ const sleepFn = retry?.sleep ?? sleep;
303
+ const alive = retry?.isAlive ?? (() => true);
304
+ let last = await loadTargetsOnce(call);
305
+ for (let i = 0; i < delays.length && !last.ok; i += 1) {
306
+ if (!alive()) return last;
307
+ const wait = delays[i];
308
+ if (wait > 0) await sleepFn(wait);
309
+ if (!alive()) return last;
310
+ last = await loadTargetsOnce(call);
311
+ }
312
+ return last;
313
+ }
314
+ /** 取七个更新目标的表:单次(重试循环的每一步都是它)。 */
315
+ async function loadTargetsOnce(call) {
287
316
  const out = await callManager(call, MANAGER_ACTIONS.targets, {});
288
317
  if (!out.ok) return out;
289
318
  return {
@@ -517,6 +546,15 @@ window.__ModuleLoader__.load({
517
546
  else lines.push("技能包随插件(不单独查更新)");
518
547
  return lines;
519
548
  }
549
+ /** 取数失败的日志细节(票 #982):传输层返回的原文优先,否则返回原因码——短文本,不带用户数据。
550
+ *
551
+ * 住这里(不是取数钩子旁边):本文件是面板纯函数与单测共用的那一份,判据可单测;
552
+ * 参数只取失败体的形状(`code`+`details`),不 import 取数模块。 */
553
+ function loadFailureDetail(failure) {
554
+ const raw = failure.details?.detail;
555
+ if (typeof raw === "string" && raw.length > 0) return failure.code + " detail=" + raw.slice(0, 300);
556
+ return failure.code;
557
+ }
520
558
  //#endregion
521
559
  //#region src/update-queue.ts
522
560
  /** 任一行 installing 即忙:题头检查更新禁用、行按钮全局互斥、checkAll/act 入口直接返回的同一判据。 */
@@ -524,6 +562,40 @@ window.__ModuleLoader__.load({
524
562
  for (const key of Object.keys(rows)) if (rows[key]?.phase === "installing") return true;
525
563
  return false;
526
564
  }
565
+ /** 批量入口点不得:一键串行中、任一行安装中、检查轮未收尾,三者任一即拦。
566
+ *
567
+ * 面板 `checkAll`/`updateAll`/`act` 共用同一判据:双轮同时写同一行即错(#980:
568
+ * 原先缺 `checking`,检查未收尾即点一键会另起一轮覆盖行状态)。
569
+ * 人话仍沿用 `update-busy` 原句,本函数只回答拦不拦。 */
570
+ function shouldBlockBatchStart(state) {
571
+ return state.hasInstallingRow || state.updatingAll || state.checking;
572
+ }
573
+ /** 重取目标表时保不保留旧表:新表为空且屏上有进度(任一行非空闲或一键串行中)
574
+ * 则保留旧表——否则可见行计算短暂为空,整块闪掉如重载(#980)。
575
+ * 首挂全空闲与正常非空表不受影响,照常替换。 */
576
+ function shouldKeepTargetsOnReload(loadedCount, rows, updatingAll) {
577
+ if (loadedCount > 0) return false;
578
+ if (updatingAll) return true;
579
+ for (const key of Object.keys(rows)) if (rows[key]?.phase !== "idle") return true;
580
+ return false;
581
+ }
582
+ /** 重取目标表后的下一张表:空表保旧(见 `shouldKeepTargetsOnReload`);非空表照常换,
583
+ * 但批量进行中(串行中或有安装中)时在新表里缺失、屏上又有进度的旧目标会被追加到表尾——
584
+ * 键乱序或短暂缺键时不断进度、不闪掉整块(#980 验收“顺序变化时不把整块置空”)。
585
+ * 空闲时不追加:正常下架由新表说了算。 */
586
+ function mergeTargetsOnReload(previous, loaded, rows, updatingAll) {
587
+ if (shouldKeepTargetsOnReload(loaded.length, rows, updatingAll)) return previous;
588
+ if (!updatingAll && !hasInstallingRow(rows)) return loaded;
589
+ const keys = new Set(loaded.map((target) => target.key));
590
+ const kept = previous.filter((target) => {
591
+ const row = rows[target.key];
592
+ return row !== void 0 && !keys.has(target.key) && row.phase !== "idle";
593
+ });
594
+ return kept.length > 0 ? [...loaded, ...kept] : loaded;
595
+ }
596
+ function resolveDialogOpen(event) {
597
+ return event === "show";
598
+ }
527
599
  /** 按目标顺序逐家查→装→收尾:一家收尾才起下一家;失败记 failed 继续;待重启跳过安装。
528
600
  *
529
601
  * 跳过安装只有一种:快照事实已是待重启(`restartPendingOf`)——由横幅点名,不在这里装第二遍。
@@ -920,18 +992,66 @@ window.__ModuleLoader__.load({
920
992
  react.useEffect(() => () => {
921
993
  alive.current = false;
922
994
  }, []);
923
- /** 重取七个目标的表(装机读数变了就重取:缺席卡的态是拿这张表算的,缓存住它会继续说旧话)。 */
924
- const reload = react.useCallback(async () => {
925
- const loaded = await loadTargets(getCall());
995
+ const rowsRef = react.useRef(rows);
996
+ rowsRef.current = rows;
997
+ const targetsRef = react.useRef(targets);
998
+ targetsRef.current = targets;
999
+ const pollMsRef = react.useRef(pollMs);
1000
+ pollMsRef.current = pollMs;
1001
+ const checkingRef = react.useRef(false);
1002
+ checkingRef.current = checking;
1003
+ const updatingAllRef = react.useRef(false);
1004
+ const retryTimers = react.useRef([]);
1005
+ react.useEffect(() => () => {
1006
+ for (const id of retryTimers.current) clearTimeout(id);
1007
+ retryTimers.current = [];
1008
+ }, []);
1009
+ /** 重取七个目标的表(装机读数变了就重取:缺席卡的态是拿这张表算的,缓存住它会继续说旧话)。
1010
+ *
1011
+ * 进度在屏上时不断进度(#980):空表保旧表,非空表把批量中缺失的旧目标追加到表尾——
1012
+ * 否则可见行计算短暂为空或缺键,整块闪掉如重载。
1013
+ *
1014
+ * 首帧失败重试(票 #982):挂载时首次传输失败(宿主路由尚未就绪)不再直接进入错误态,
1015
+ * 只在挂载那一次按退避表重试;装后重取走单次(此前刚成功调用过宿主,不必等)。
1016
+ * 重试用尽仍失败才显示红色错误行,并把传输层细节打到 `[dsh-life-pack]` 前缀的日志里——
1017
+ * 面板指引用户去日志里找的那一行,此前在这里从未输出过(浏览器侧只能打到控制台)。
1018
+ *
1019
+ * 已超线,需要根据规则进行重构:本文件改前约 747 行(本包告警线 350 行),本次 #982 增加约 60 行
1020
+ * (重试等待、卸载清理、失败日志)。暂不拆分——拆分面板文件(取数、行渲染、浮层、缺席卡)是独立重构,
1021
+ * 与本次修复无关;下轮单独立项。 */
1022
+ const reload = react.useCallback(async (withRetry) => {
1023
+ let tries = 0;
1024
+ const sleep = (ms) => new Promise((resolve) => {
1025
+ tries += 1;
1026
+ if (tries >= LOAD_RETRY_MS.length + 1) {
1027
+ resolve();
1028
+ return;
1029
+ }
1030
+ const timer = setTimeout(() => {
1031
+ clearTimeout(timer);
1032
+ resolve();
1033
+ }, ms);
1034
+ retryTimers.current.push(timer);
1035
+ });
1036
+ const loaded = await loadTargets(getCall(), {
1037
+ delays: withRetry ? LOAD_RETRY_MS : [],
1038
+ sleep,
1039
+ isAlive: () => alive.current
1040
+ });
1041
+ for (const id of retryTimers.current) clearTimeout(id);
1042
+ retryTimers.current = [];
926
1043
  if (!alive.current) return;
927
1044
  if (loaded.ok) {
928
- setTargets(loaded.value.targets);
1045
+ setTargets(mergeTargetsOnReload(targetsRef.current, loaded.value.targets, rowsRef.current, updatingAllRef.current));
929
1046
  setPollMs(loaded.value.pollMs);
930
1047
  setLoadError(null);
931
- } else setLoadError(loaded.message);
1048
+ } else {
1049
+ setLoadError(loaded.message);
1050
+ console.warn("[dsh-life-pack] update targets load failed: " + loadFailureDetail(loaded));
1051
+ }
932
1052
  }, [getCall]);
933
1053
  react.useEffect(() => {
934
- reload();
1054
+ reload(true);
935
1055
  }, [reload]);
936
1056
  const patch = react.useCallback((key, next) => {
937
1057
  setRows((previous) => ({
@@ -939,13 +1059,11 @@ window.__ModuleLoader__.load({
939
1059
  [key]: next
940
1060
  }));
941
1061
  }, []);
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);
1062
+ const batchBlocked = react.useCallback(() => shouldBlockBatchStart({
1063
+ hasInstallingRow: hasInstallingRow(rowsRef.current),
1064
+ updatingAll: updatingAllRef.current,
1065
+ checking: checkingRef.current
1066
+ }), []);
949
1067
  return {
950
1068
  targets,
951
1069
  rows,
@@ -954,11 +1072,12 @@ window.__ModuleLoader__.load({
954
1072
  checking,
955
1073
  updatingAll,
956
1074
  open,
957
- close: () => setOpen(false),
1075
+ close: () => setOpen(resolveDialogOpen("hide")),
1076
+ showProgress: () => setOpen(resolveDialogOpen("show")),
958
1077
  checkAll: react.useCallback(() => {
959
- if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
1078
+ if (batchBlocked()) return;
960
1079
  setChecking(true);
961
- setOpen(true);
1080
+ setOpen(resolveDialogOpen("show"));
962
1081
  const list = targetsRef.current;
963
1082
  const snapshot = rowsRef.current;
964
1083
  for (const target of list) patch(target.key, {
@@ -980,14 +1099,18 @@ window.__ModuleLoader__.load({
980
1099
  })).finally(() => {
981
1100
  if (alive.current) setChecking(false);
982
1101
  });
983
- }, [getCall, patch]),
1102
+ }, [
1103
+ getCall,
1104
+ patch,
1105
+ batchBlocked
1106
+ ]),
984
1107
  updateAll: react.useCallback(() => {
985
- if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
1108
+ if (batchBlocked()) return;
986
1109
  const list = targetsRef.current;
987
1110
  if (list.length === 0) return;
988
1111
  updatingAllRef.current = true;
989
1112
  setUpdatingAll(true);
990
- setOpen(true);
1113
+ setOpen(resolveDialogOpen("show"));
991
1114
  (async () => {
992
1115
  try {
993
1116
  await runSerialUpdateAll(list, {
@@ -998,7 +1121,7 @@ window.__ModuleLoader__.load({
998
1121
  patch,
999
1122
  getRow: (key) => rowsRef.current[key]
1000
1123
  });
1001
- await reload();
1124
+ await reload(false);
1002
1125
  } finally {
1003
1126
  updatingAllRef.current = false;
1004
1127
  if (alive.current) setUpdatingAll(false);
@@ -1007,10 +1130,11 @@ window.__ModuleLoader__.load({
1007
1130
  }, [
1008
1131
  getCall,
1009
1132
  patch,
1010
- reload
1133
+ reload,
1134
+ batchBlocked
1011
1135
  ]),
1012
1136
  act: react.useCallback((target, action) => {
1013
- if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
1137
+ if (batchBlocked()) return;
1014
1138
  const current = rowsRef.current[target.key] ?? IDLE$1;
1015
1139
  if (action === "recheck") {
1016
1140
  patch(target.key, {
@@ -1069,7 +1193,7 @@ window.__ModuleLoader__.load({
1069
1193
  outcome: null,
1070
1194
  failure: refreshed
1071
1195
  });
1072
- await reload();
1196
+ await reload(false);
1073
1197
  return;
1074
1198
  }
1075
1199
  patch(target.key, {
@@ -1084,21 +1208,27 @@ window.__ModuleLoader__.load({
1084
1208
  }, [
1085
1209
  getCall,
1086
1210
  patch,
1087
- reload
1211
+ reload,
1212
+ batchBlocked
1088
1213
  ])
1089
1214
  };
1090
1215
  }
1091
1216
  /** 标题右侧两件:「检查更新」一次查七家 + 「全部更新」按顺序串行装(吃 `useUpdateRows` 的表)。
1092
1217
  *
1093
- * 禁用形状(#925 第 2 条):任一行 installing 时两件都点不得;人话沿用 `update-busy` 原句
1218
+ * 禁用形状(#925 第 2 条 + #980 检查中):任一行 installing、一键串行中或检查轮未收尾时两件都点不得;人话沿用 `update-busy` 原句
1094
1219
  * (挂在 title 上,按钮文本仍是动作名)。题头不汇总排队数:串行进度只在各行自己显示。
1095
1220
  */
1096
1221
  function CheckUpdateButton(props) {
1097
1222
  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;
1223
+ const blocked = shouldBlockBatchStart({
1224
+ hasInstallingRow: hasInstallingRow(rows),
1225
+ updatingAll,
1226
+ checking
1227
+ });
1228
+ const checkDisabled = blocked || targets.length === 0;
1229
+ const allDisabled = blocked || targets.length === 0;
1101
1230
  const busyText = reasonText("update-busy");
1231
+ const installingNow = hasInstallingRow(rows) || updatingAll;
1102
1232
  return react.createElement("span", { style: PANEL_STYLE.actions }, react.createElement("button", {
1103
1233
  type: "button",
1104
1234
  style: checkDisabled ? {
@@ -1108,7 +1238,7 @@ window.__ModuleLoader__.load({
1108
1238
  } : PANEL_STYLE.btn,
1109
1239
  disabled: checkDisabled,
1110
1240
  onClick: checkAll,
1111
- title: busy ? busyText : "检查总管与六家插件的版本"
1241
+ title: blocked ? busyText : "检查总管与六家插件的版本"
1112
1242
  }, checking ? "检查中…" : "检查更新"), react.createElement("button", {
1113
1243
  type: "button",
1114
1244
  style: allDisabled ? {
@@ -1118,8 +1248,8 @@ window.__ModuleLoader__.load({
1118
1248
  } : PANEL_STYLE.btn,
1119
1249
  disabled: allDisabled,
1120
1250
  onClick: updateAll,
1121
- title: busy ? busyText : "按顺序逐家装上更新(一家收尾才起下一家)"
1122
- }, updatingAll ? "更新中…" : "全部更新"));
1251
+ title: blocked ? busyText : "按顺序逐家装上更新(一家收尾才起下一家)"
1252
+ }, installingNow ? "更新中…" : "全部更新"));
1123
1253
  }
1124
1254
  /** 待重启横幅:一行总账,只点名哪几家要重启(理由与做法写在各家卡片上,见 `restartBannerText`)。 */
1125
1255
  function RestartBanners(props) {
@@ -1142,7 +1272,7 @@ window.__ModuleLoader__.load({
1142
1272
  * 手工命令只在「装不了」或「装失败」的行显示:正常用户用不到,摊在每行上既吵又误导。
1143
1273
  */
1144
1274
  function UpdateResults(props) {
1145
- const { targets, rows, loadError, checking, updatingAll, open, close } = props.face;
1275
+ const { targets, rows, loadError, checking, updatingAll, open, close, showProgress } = props.face;
1146
1276
  const dialogRef = react.useRef(null);
1147
1277
  react.useEffect(() => {
1148
1278
  if (open) dialogRef.current?.focus();
@@ -1151,7 +1281,12 @@ window.__ModuleLoader__.load({
1151
1281
  if (loadError) return react.createElement("div", { style: PANEL_STYLE.reason }, "更新能力没接上:" + loadError);
1152
1282
  if (targets.length === 0) return null;
1153
1283
  if (shown.length === 0 && !open) return null;
1154
- const locked = hasInstallingRow(rows) || updatingAll;
1284
+ const locked = shouldBlockBatchStart({
1285
+ hasInstallingRow: hasInstallingRow(rows),
1286
+ updatingAll,
1287
+ checking
1288
+ });
1289
+ const installingNow = hasInstallingRow(rows) || updatingAll;
1155
1290
  const body = shown.map((target) => {
1156
1291
  const row = rows[target.key];
1157
1292
  const snapshot = row.outcome?.snapshot ?? null;
@@ -1207,7 +1342,16 @@ window.__ModuleLoader__.load({
1207
1342
  style: PANEL_STYLE.skill
1208
1343
  }, line)), showManual ? react.createElement("div", { style: PANEL_STYLE.skill }, "在终端里执行这条命令可以手动完成这一步:") : null, showManual ? react.createElement("code", { style: PANEL_STYLE.cmd }, manual) : null);
1209
1344
  });
1210
- return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), open ? react.createElement("div", {
1345
+ const progressStrip = !open && (locked || shown.length > 0) ? react.createElement("div", { style: PANEL_STYLE.banner }, installingNow ? "有更新正在安装:关掉对话框不会中断。" : checking ? "正在检查更新,稍候。" : "上次检查的结果还在,直接打开看。", react.createElement("button", {
1346
+ type: "button",
1347
+ style: {
1348
+ ...PANEL_STYLE.btn,
1349
+ marginLeft: 8
1350
+ },
1351
+ onClick: () => showProgress()
1352
+ }, "查看进度")) : null;
1353
+ const dialogNote = installingNow ? "安装在后台继续:关掉这块不会中断它。点「查看进度」可随时重新打开。" : checking ? "正在检查更新:关掉这块不会中断它,查完点「查看进度」看结果。" : "安装在宿主后台继续:关掉这块不会中断它。再点「检查更新」看最新进度。";
1354
+ return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), progressStrip, open ? react.createElement("div", {
1211
1355
  style: PANEL_STYLE.overlay,
1212
1356
  role: "presentation",
1213
1357
  onClick: () => close()
@@ -1227,7 +1371,7 @@ window.__ModuleLoader__.load({
1227
1371
  onClick: () => close(),
1228
1372
  title: "关闭",
1229
1373
  "aria-label": "关闭"
1230
- }, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "安装在宿主后台继续:关掉这块不会中断它。再点「检查更新」看最新进度。")))) : null);
1374
+ }, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, dialogNote)))) : null);
1231
1375
  }
1232
1376
  /** 缺席卡:**按两处独立事实分三态**,每态只给能改变状态的动作。
1233
1377
  *
@@ -1245,7 +1389,11 @@ window.__ModuleLoader__.load({
1245
1389
  const target = props.target;
1246
1390
  const row = target ? props.face.rows[target.key] : null;
1247
1391
  const busy = row?.phase === "installing";
1248
- const locked = busy || hasInstallingRow(props.face.rows) || props.face.updatingAll;
1392
+ const locked = shouldBlockBatchStart({
1393
+ hasInstallingRow: hasInstallingRow(props.face.rows),
1394
+ updatingAll: props.face.updatingAll,
1395
+ checking: props.face.checking
1396
+ });
1249
1397
  const manual = (target ? manualForDisplay(target, row?.failure?.manual ?? row?.outcome?.manual ?? null) : null) ?? props.fallbackCommand;
1250
1398
  const installed = target ? target.installedVersion ?? target.runningVersion : null;
1251
1399
  const state = slotStateOf(target, props.inLedger);