dsh-life-pack 0.3.21 → 0.3.22

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
@@ -560,11 +560,36 @@ window.__ModuleLoader__.load({
560
560
  }
561
561
  //#endregion
562
562
  //#region src/update-queue.ts
563
+ /** 一键全部更新的串行队列与并发门禁(纯函数:无 DOM、无 react;面板钩子与单测共用同一份)。
564
+ *
565
+ * 拍板出处(#925 决议,用户已逐条确认,不可改语义):
566
+ * 1. 一键排队串行:按目标顺序逐家查→装→轮询到收尾,一家收尾才起下一家;失败一家记 failed
567
+ * 继续下一家;单家重试走行按钮 recheck/retry;删并行发起与交叉轮询。
568
+ * 2. 安装中题头点不得:任一行 installing 时题头检查更新禁用;人话沿用 reasonText('update-busy') 原句。
569
+ * 3. 行按钮全局互斥:installing 行自锁,其余行禁用不进队列;act 入口有忙拒 update-busy 且不改 rows。
570
+ * 4. 进度各行自显:每行各自显示,他行保持 phase;题头不汇总排队数;待重启跳过安装继续下一家。
571
+ *
572
+ * 本文件只管「排队形状与忙判据」:传输动作由调用方注入(查/装/轮询的真实现住 `update-client.ts`,
573
+ * 面板钩子转交,单测给假件),故改坏串行形状必被门禁逮到。
574
+ *
575
+ * 忙判据的另一半(#980):忙时什么被拦、什么不被拦——批量入口(查/装/一键)被拦,
576
+ * 对话框重开(只读进度、不干活)永远不被拦。两者同住这里,面板与单测共用同一份。
577
+ */
563
578
  /** 任一行 installing 即忙:题头检查更新禁用、行按钮全局互斥、checkAll/act 入口直接返回的同一判据。 */
564
579
  function hasInstallingRow(rows) {
565
580
  for (const key of Object.keys(rows)) if (rows[key]?.phase === "installing") return true;
566
581
  return false;
567
582
  }
583
+ /** 忙时拒绝的那枚失败:码与人话都取 `update-busy` 原句(#678/#740 冻结文案,本文件不写第二套话)。 */
584
+ function updateBusyFailure(targetKey) {
585
+ return {
586
+ ok: false,
587
+ code: "update-busy",
588
+ message: reasonText("update-busy"),
589
+ details: { target: targetKey },
590
+ manual: null
591
+ };
592
+ }
568
593
  /** 批量入口点不得:一键串行中、任一行安装中、检查轮未收尾,三者任一即拦。
569
594
  *
570
595
  * 面板 `checkAll`/`updateAll`/`act` 共用同一判据:双轮同时写同一行即错(#980:
@@ -931,6 +956,11 @@ window.__ModuleLoader__.load({
931
956
  if (!detail) return null;
932
957
  return react.createElement("code", { style: PANEL_STYLE.cmd }, "宿主原话:" + detail);
933
958
  }
959
+ /** 忙注那一行(票 #989):守卫拦下点击时的可视回响。题头与缺席卡共用这一块,口径一处。 */
960
+ function busyNoteLine(note) {
961
+ if (!note) return null;
962
+ return react.createElement("div", { style: PANEL_STYLE.dialogNote }, note);
963
+ }
934
964
  const ROW_TONE = {
935
965
  update: {
936
966
  mark: "↑",
@@ -989,6 +1019,9 @@ window.__ModuleLoader__.load({
989
1019
  * - 任一行 installing 时 checkAll 入口直接返回、题头禁用;
990
1020
  * - act 入口有忙直接返回(有忙拒 update-busy 且不改 rows,含同家第二点);
991
1021
  * - 每行各自显示(patch 只写本家 key),题头不汇总排队数。
1022
+ *
1023
+ * 票 #989 补一条:上面三处"直接返回"必须留可视回响(忙注或行级失败),
1024
+ * 与"禁用的才点不得,点得动的必有回响"对齐——静默吞点击即红。
992
1025
  */
993
1026
  function useUpdateRows(getCall) {
994
1027
  const [targets, setTargets] = react.useState([]);
@@ -997,6 +1030,7 @@ window.__ModuleLoader__.load({
997
1030
  const [loadError, setLoadError] = react.useState(null);
998
1031
  const [checking, setChecking] = react.useState(false);
999
1032
  const [updatingAll, setUpdatingAll] = react.useState(false);
1033
+ const [busyNote, setBusyNote] = react.useState(null);
1000
1034
  const [open, setOpen] = react.useState(false);
1001
1035
  const alive = react.useRef(true);
1002
1036
  react.useEffect(() => () => {
@@ -1068,6 +1102,7 @@ window.__ModuleLoader__.load({
1068
1102
  reload(true);
1069
1103
  }, [reload]);
1070
1104
  const patch = react.useCallback((key, next) => {
1105
+ setBusyNote(null);
1071
1106
  setRows((previous) => ({
1072
1107
  ...previous,
1073
1108
  [key]: next
@@ -1085,12 +1120,17 @@ window.__ModuleLoader__.load({
1085
1120
  loadError,
1086
1121
  checking,
1087
1122
  updatingAll,
1123
+ busyNote,
1088
1124
  open,
1089
1125
  retry,
1090
1126
  close: () => setOpen(resolveDialogOpen("hide")),
1091
1127
  showProgress: () => setOpen(resolveDialogOpen("show")),
1092
1128
  checkAll: react.useCallback(() => {
1093
- if (batchBlocked()) return;
1129
+ if (batchBlocked()) {
1130
+ setBusyNote(reasonText("update-busy"));
1131
+ return;
1132
+ }
1133
+ setBusyNote(null);
1094
1134
  setChecking(true);
1095
1135
  setOpen(resolveDialogOpen("show"));
1096
1136
  const list = targetsRef.current;
@@ -1120,9 +1160,16 @@ window.__ModuleLoader__.load({
1120
1160
  batchBlocked
1121
1161
  ]),
1122
1162
  updateAll: react.useCallback(() => {
1123
- if (batchBlocked()) return;
1163
+ if (batchBlocked()) {
1164
+ setBusyNote(reasonText("update-busy"));
1165
+ return;
1166
+ }
1124
1167
  const list = targetsRef.current;
1125
- if (list.length === 0) return;
1168
+ if (list.length === 0) {
1169
+ setBusyNote(reasonText("update-busy"));
1170
+ return;
1171
+ }
1172
+ setBusyNote(null);
1126
1173
  updatingAllRef.current = true;
1127
1174
  setUpdatingAll(true);
1128
1175
  setOpen(resolveDialogOpen("show"));
@@ -1149,7 +1196,14 @@ window.__ModuleLoader__.load({
1149
1196
  batchBlocked
1150
1197
  ]),
1151
1198
  act: react.useCallback((target, action) => {
1152
- if (batchBlocked()) return;
1199
+ if (batchBlocked()) {
1200
+ const current = rowsRef.current[target.key] ?? IDLE$1;
1201
+ patch(target.key, {
1202
+ ...current,
1203
+ failure: updateBusyFailure(target.key)
1204
+ });
1205
+ return;
1206
+ }
1153
1207
  const current = rowsRef.current[target.key] ?? IDLE$1;
1154
1208
  if (action === "recheck") {
1155
1209
  patch(target.key, {
@@ -1177,48 +1231,62 @@ window.__ModuleLoader__.load({
1177
1231
  failure: null
1178
1232
  });
1179
1233
  (async () => {
1180
- const absent = isAbsent(target);
1181
- let version = current.outcome?.snapshot.latestVersion ?? null;
1182
- if (absent && version === null) {
1183
- const checked = await checkTarget(getCall(), target);
1184
- if (!checked.ok) {
1234
+ try {
1235
+ const absent = isAbsent(target);
1236
+ let version = current.outcome?.snapshot.latestVersion ?? null;
1237
+ if (absent && version === null) {
1238
+ const checked = await checkTarget(getCall(), target);
1239
+ if (!checked.ok) {
1240
+ patch(target.key, {
1241
+ phase: "failed",
1242
+ outcome: null,
1243
+ failure: checked
1244
+ });
1245
+ return;
1246
+ }
1185
1247
  patch(target.key, {
1248
+ phase: "installing",
1249
+ outcome: checked.value,
1250
+ failure: null
1251
+ });
1252
+ version = checked.value.snapshot.latestVersion;
1253
+ }
1254
+ const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMsRef.current);
1255
+ if (result.ok) {
1256
+ const refreshed = await checkTarget(getCall(), target, target.phones?.status);
1257
+ patch(target.key, refreshed.ok ? {
1258
+ phase: "ready",
1259
+ outcome: refreshed.value,
1260
+ failure: null
1261
+ } : {
1186
1262
  phase: "failed",
1187
1263
  outcome: null,
1188
- failure: checked
1264
+ failure: refreshed
1189
1265
  });
1266
+ await reload(false);
1190
1267
  return;
1191
1268
  }
1192
1269
  patch(target.key, {
1193
- phase: "installing",
1194
- outcome: checked.value,
1195
- failure: null
1270
+ phase: "failed",
1271
+ outcome: current.outcome,
1272
+ failure: {
1273
+ ...result,
1274
+ manual: result.manual ?? current.outcome?.manual ?? null
1275
+ }
1196
1276
  });
1197
- version = checked.value.snapshot.latestVersion;
1198
- }
1199
- const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMsRef.current);
1200
- if (result.ok) {
1201
- const refreshed = await checkTarget(getCall(), target, target.phones?.status);
1202
- patch(target.key, refreshed.ok ? {
1203
- phase: "ready",
1204
- outcome: refreshed.value,
1205
- failure: null
1206
- } : {
1277
+ } catch (error) {
1278
+ patch(target.key, {
1207
1279
  phase: "failed",
1208
- outcome: null,
1209
- failure: refreshed
1280
+ outcome: current.outcome,
1281
+ failure: {
1282
+ ok: false,
1283
+ code: "internal",
1284
+ message: reasonText("internal"),
1285
+ details: { detail: String(error?.message ?? error) },
1286
+ manual: current.outcome?.manual ?? null
1287
+ }
1210
1288
  });
1211
- await reload(false);
1212
- return;
1213
1289
  }
1214
- patch(target.key, {
1215
- phase: "failed",
1216
- outcome: current.outcome,
1217
- failure: {
1218
- ...result,
1219
- manual: result.manual ?? current.outcome?.manual ?? null
1220
- }
1221
- });
1222
1290
  })();
1223
1291
  }, [
1224
1292
  getCall,
@@ -1391,7 +1459,7 @@ window.__ModuleLoader__.load({
1391
1459
  onClick: () => showProgress()
1392
1460
  }, "查看进度")) : null;
1393
1461
  const dialogNote = installingNow ? "安装在后台继续:关掉这块不会中断它。点「查看进度」可随时重新打开。" : checking ? "正在检查更新:关掉这块不会中断它,查完点「查看进度」看结果。" : "安装在宿主后台继续:关掉这块不会中断它。再点「检查更新」看最新进度。";
1394
- return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), progressStrip, open ? react.createElement("div", {
1462
+ return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), busyNoteLine(props.face.busyNote), progressStrip, open ? react.createElement("div", {
1395
1463
  style: PANEL_STYLE.overlay,
1396
1464
  role: "presentation",
1397
1465
  onClick: () => close()
@@ -1451,7 +1519,7 @@ window.__ModuleLoader__.load({
1451
1519
  break;
1452
1520
  default: headline = target.packageName + " " + String(installed) + " 已安装,插件包里也有注册代码,但它的设置页没有加载到面板:重启 DSH 后再看这个页签;仍然这样,就在 DSH 日志里找它的报错。";
1453
1521
  }
1454
- return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
1522
+ return react.createElement("div", { style: PANEL_STYLE.reco }, busyNoteLine(props.face.busyNote), react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
1455
1523
  type: "button",
1456
1524
  style: locked ? {
1457
1525
  ...PANEL_STYLE.btn,
@@ -3110,7 +3178,7 @@ window.__ModuleLoader__.load({
3110
3178
  }, 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)))));
3111
3179
  }
3112
3180
  /** 版本胶囊那一行字:只在这里拼一次,屏上与用例读的是同一串。 */
3113
- const MANAGER_VERSION_TEXT = "dsh-life-pack · 0.3.21";
3181
+ const MANAGER_VERSION_TEXT = "dsh-life-pack · 0.3.22";
3114
3182
  /** 爱生活面板:总设置区 + 检查更新(七家)+ 爱生活页签条(slot 驱动)+ 技能设置页投影/缺席卡。 */
3115
3183
  function LifePackSection(props) {
3116
3184
  const tabsId = react.useId();