dsh-life-pack 0.3.18 → 0.3.20

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
@@ -174,12 +174,6 @@ window.__ModuleLoader__.load({
174
174
  * 也不该把名字再写一份(写两份必然走散)。宿主把名字表连同版本行一起交出去,
175
175
  * 面板一行字面量都不留。 */
176
176
  targets: "ilife-manager.targets",
177
- /** 总管自述版本(票 #737):入参 `{}`,回包 `{version}`。
178
- *
179
- * 宿主读**自己这份已安装包**的 `package.json`(`manager-version.ts`),面板只渲染读到的值。
180
- * 为什么走电话:面板是浏览器产物,禁 node 内建(`test/client-bundle-48.test.mjs` 看门),
181
- * 读盘只许在宿主半;与卡路里 #130 同一条路(host 读 → RPC → client 纯渲染)。 */
182
- version: "ilife-manager.version",
183
177
  /** 本机「根」清单(票 #744):入参 `{}`,回包 `{roots: [{path, kind}]}`。
184
178
  *
185
179
  * 为什么这条能力住在总管:浏览器里没有卷清单接口,而宿主那条目录选择接缝只有
@@ -188,15 +182,9 @@ window.__ModuleLoader__.load({
188
182
  * 回包**恒成功**:列不出来是空清单,不是错误码(界面据此只是不画那一行)。 */
189
183
  roots: "ilife-manager.roots"
190
184
  };
191
- /** 版本读不到时两侧共用的降级字面量(面板照原样显示,不假装知道版本)。
192
- *
193
- * 一处定义、两侧引用:宿主半 `manager-version.ts` 读失败时回它,面板半 `update-client.ts`
194
- * 归一化时也认它——面板侧不许 import 宿主半(会把 node 内建带进浏览器束)。 */
195
- const VERSION_UNKNOWN = "unknown";
196
185
  /** 挂载取数失败的退避重试表(票 #982,更新目标表用)。
197
186
  *
198
- * 一处定义:与版本读取重试同形(`client.ts` 的退避三次)——挂载时首次传输失败
199
- * (宿主路由尚未就绪)会使错误态常驻整个会话;单次直返是当时的缺口。
187
+ * 挂载时首次传输失败(宿主路由尚未就绪)会使错误态常驻整个会话;单次直返是当时的缺口。
200
188
  * 次数有界、调用方负责卸载清理(见 `update-panel.ts` 的取数),不是无界轮询。 */
201
189
  const LOAD_RETRY_MS = [
202
190
  1e3,
@@ -237,15 +225,17 @@ window.__ModuleLoader__.load({
237
225
  }
238
226
  //#endregion
239
227
  //#region src/update-client.ts
240
- /** 面板到宿主的四条流程(纯异步函数:无 DOM、无 react;传输口由调用方注入,便于单测)。
228
+ /** 面板到宿主的三条流程(纯异步函数:无 DOM、无 react;传输口由调用方注入,便于单测)。
241
229
  *
242
- * 四条流程与更新包 README 的对应关系:
230
+ * 三条流程与更新包 README 的对应关系:
243
231
  * - 检查更新:`updateCheck`(第 3 节第 3 步的 `host.call(UPD_CHECK, {})`);
244
232
  * - 装上更新:`updateCheck` 拿凭证 → `updateInstall` 提交 → **按间隔轮询 `updateStatus`**
245
233
  * 直到任务离开 `installing`/`verifying`(第 3 节第 3 步与第 9 节:安装是后台跑,
246
234
  * 调用方不干等;凭证过期 `check-expired` 是正常错误码,重查一次再提交,见第 11 节);
247
- * - 装上缺席的包:走总管自有的 `ilife-manager.install`(更新包的三个电话只管已装的包);
248
- * - 总管自述版本:走总管自有的 `ilife-manager.version`(宿主读自己那份包描述文件,见 `manager-version.ts`)。
235
+ * - 装上缺席的包:走总管自有的 `ilife-manager.install`(更新包的三个电话只管已装的包)。
236
+ *
237
+ * 版本号**不在这三条里**(票 #986):面板那行版本由构建期注入产物(见 `client.ts`),
238
+ * 不再经 `ilife-manager.version` 往返——标签的延迟不依赖宿主可用性。
249
239
  */
250
240
  /** 轮询上限:1 秒一次、最多 900 次(覆盖更新包 15 分钟的安装时限)。 */
251
241
  const MAX_POLL_TRIES = 900;
@@ -323,16 +313,6 @@ window.__ModuleLoader__.load({
323
313
  }
324
314
  };
325
315
  }
326
- /** 取总管自述版本(票 #737):宿主读**自己这份已安装包**的 `package.json`,面板只渲染读到的值。
327
- *
328
- * 单次、无轮询;宿主读不到会回 `unknown`,传输层失败也回同一个字面量——面板照原样显示,
329
- * 不假装知道版本(与卡路里 #130 的降级口径一致)。 */
330
- async function loadManagerVersion(call) {
331
- const out = await callManager(call, MANAGER_ACTIONS.version, {});
332
- if (!out.ok) return VERSION_UNKNOWN;
333
- const raw = out.value?.version;
334
- return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : VERSION_UNKNOWN;
335
- }
336
316
  /** 查一家:默认查新版(`updateCheck`,顺带回最新版本与凭证);给 `phoneName` 时查那个电话。 */
337
317
  async function checkTarget(call, target, phoneName) {
338
318
  const name = phoneName ?? target.phones?.check;
@@ -555,6 +535,29 @@ window.__ModuleLoader__.load({
555
535
  if (typeof raw === "string" && raw.length > 0) return failure.code + " detail=" + raw.slice(0, 300);
556
536
  return failure.code;
557
537
  }
538
+ /** 失败原文那一行(票 #988):宿主回包 `details.detail` 里那句人话,面板把它摊在原因句下面。
539
+ *
540
+ * 为什么要摊:`install-failed` 这句人话对每一种装不上都长一个样,光看它分不出「这个宿主没有安装出口」
541
+ * 与「pnpm 退出码非零」。原文由宿主半取好并回传(`update-host.ts` 的 `installErrorDetail`),
542
+ * 这里只取值与截断(长文本会撑爆那一行)。没有原文回 null——面板不自己编原因。 */
543
+ function failureDetailOf(failure) {
544
+ const raw = failure?.details?.detail;
545
+ if (typeof raw !== "string") return null;
546
+ const text = raw.trim();
547
+ return text.length === 0 ? null : text.slice(0, 300);
548
+ }
549
+ /** 题头两颗按钮的悬停标题(票 #986 第二半:目标表未就绪时说真因)。
550
+ *
551
+ * 住这里(不是面板组件旁边):标题口径只有一个定义地,面板与单测共用同一份;
552
+ * 忙时沿用 update-busy 原句(与禁用判据同源),未就绪且空闲时说「还在读更新目标」,
553
+ * 就绪后回到调用方给的动作说明(本函数不写死那两句动作说明,由调用方传入)。
554
+ *
555
+ * 入参只取判据形状(blocked 与 empty),不 import 面板钩子。 */
556
+ function headerButtonTitle(input) {
557
+ if (input.blocked) return reasonText("update-busy");
558
+ if (input.empty) return "还在读更新目标,稍候再试";
559
+ return input.readyText ?? "";
560
+ }
558
561
  //#endregion
559
562
  //#region src/update-queue.ts
560
563
  /** 任一行 installing 即忙:题头检查更新禁用、行按钮全局互斥、checkAll/act 入口直接返回的同一判据。 */
@@ -921,6 +924,13 @@ window.__ModuleLoader__.load({
921
924
  marginTop: 4
922
925
  }
923
926
  };
927
+ /** 失败原文那一块(票 #988):宿主回来的细节单放一个可复制块(`cmd` 样式带 `userSelect: all`,
928
+ * 点一下全选即拷走),不与可执行的手工命令拼在同一块里——拼一起用户会整块拷去终端敲。
929
+ * 两处失败行(结果行与缺席卡)共用这一块,口径一处。 */
930
+ function failureDetailCode(detail) {
931
+ if (!detail) return null;
932
+ return react.createElement("code", { style: PANEL_STYLE.cmd }, "宿主原话:" + detail);
933
+ }
924
934
  const ROW_TONE = {
925
935
  update: {
926
936
  mark: "↑",
@@ -1053,6 +1063,10 @@ window.__ModuleLoader__.load({
1053
1063
  react.useEffect(() => {
1054
1064
  reload(true);
1055
1065
  }, [reload]);
1066
+ /** 取数失败后的手动重试(票 #986 第二半):不依赖目标表,挂载那次的退避表复用。 */
1067
+ const retry = react.useCallback(() => {
1068
+ reload(true);
1069
+ }, [reload]);
1056
1070
  const patch = react.useCallback((key, next) => {
1057
1071
  setRows((previous) => ({
1058
1072
  ...previous,
@@ -1072,6 +1086,7 @@ window.__ModuleLoader__.load({
1072
1086
  checking,
1073
1087
  updatingAll,
1074
1088
  open,
1089
+ retry,
1075
1090
  close: () => setOpen(resolveDialogOpen("hide")),
1076
1091
  showProgress: () => setOpen(resolveDialogOpen("show")),
1077
1092
  checkAll: react.useCallback(() => {
@@ -1213,6 +1228,21 @@ window.__ModuleLoader__.load({
1213
1228
  ])
1214
1229
  };
1215
1230
  }
1231
+ /** 取数失败的出口卡(票 #986 第二半:失败必须有出口)。
1232
+ *
1233
+ * 纯组件(无钩子):错误原文+一颗可点的「重试」。重试不依赖目标表——
1234
+ * 目标表为空时两题头按钮仍禁用,但这颗可点;点它重走取数,成功后按钮与行恢复。
1235
+ */
1236
+ function TargetsLoadError(props) {
1237
+ return react.createElement("div", { style: PANEL_STYLE.reason }, "更新能力没接上:" + props.message, react.createElement("button", {
1238
+ type: "button",
1239
+ style: {
1240
+ ...PANEL_STYLE.btn,
1241
+ marginLeft: 8
1242
+ },
1243
+ onClick: props.onRetry
1244
+ }, "重试"));
1245
+ }
1216
1246
  /** 标题右侧两件:「检查更新」一次查七家 + 「全部更新」按顺序串行装(吃 `useUpdateRows` 的表)。
1217
1247
  *
1218
1248
  * 禁用形状(#925 第 2 条 + #980 检查中):任一行 installing、一键串行中或检查轮未收尾时两件都点不得;人话沿用 `update-busy` 原句
@@ -1225,30 +1255,37 @@ window.__ModuleLoader__.load({
1225
1255
  updatingAll,
1226
1256
  checking
1227
1257
  });
1228
- const checkDisabled = blocked || targets.length === 0;
1229
- const allDisabled = blocked || targets.length === 0;
1230
- const busyText = reasonText("update-busy");
1258
+ const targetsEmpty = targets.length === 0;
1259
+ const headerDisabled = blocked || targetsEmpty;
1231
1260
  const installingNow = hasInstallingRow(rows) || updatingAll;
1232
1261
  return react.createElement("span", { style: PANEL_STYLE.actions }, react.createElement("button", {
1233
1262
  type: "button",
1234
- style: checkDisabled ? {
1263
+ style: headerDisabled ? {
1235
1264
  ...PANEL_STYLE.btn,
1236
1265
  opacity: .55,
1237
1266
  cursor: "default"
1238
1267
  } : PANEL_STYLE.btn,
1239
- disabled: checkDisabled,
1268
+ disabled: headerDisabled,
1240
1269
  onClick: checkAll,
1241
- title: blocked ? busyText : "检查总管与六家插件的版本"
1270
+ title: headerButtonTitle({
1271
+ blocked,
1272
+ empty: targetsEmpty,
1273
+ readyText: "检查总管与六家插件的版本"
1274
+ })
1242
1275
  }, checking ? "检查中…" : "检查更新"), react.createElement("button", {
1243
1276
  type: "button",
1244
- style: allDisabled ? {
1277
+ style: headerDisabled ? {
1245
1278
  ...PANEL_STYLE.btn,
1246
1279
  opacity: .55,
1247
1280
  cursor: "default"
1248
1281
  } : PANEL_STYLE.btn,
1249
- disabled: allDisabled,
1282
+ disabled: headerDisabled,
1250
1283
  onClick: updateAll,
1251
- title: blocked ? busyText : "按顺序逐家装上更新(一家收尾才起下一家)"
1284
+ title: headerButtonTitle({
1285
+ blocked,
1286
+ empty: targetsEmpty,
1287
+ readyText: "按顺序逐家装上更新(一家收尾才起下一家)"
1288
+ })
1252
1289
  }, installingNow ? "更新中…" : "全部更新"));
1253
1290
  }
1254
1291
  /** 待重启横幅:一行总账,只点名哪几家要重启(理由与做法写在各家卡片上,见 `restartBannerText`)。 */
@@ -1272,13 +1309,16 @@ window.__ModuleLoader__.load({
1272
1309
  * 手工命令只在「装不了」或「装失败」的行显示:正常用户用不到,摊在每行上既吵又误导。
1273
1310
  */
1274
1311
  function UpdateResults(props) {
1275
- const { targets, rows, loadError, checking, updatingAll, open, close, showProgress } = props.face;
1312
+ const { targets, rows, loadError, checking, updatingAll, open, close, showProgress, retry } = props.face;
1276
1313
  const dialogRef = react.useRef(null);
1277
1314
  react.useEffect(() => {
1278
1315
  if (open) dialogRef.current?.focus();
1279
1316
  }, [open]);
1280
1317
  const shown = targets.filter((target) => rows[target.key] && rows[target.key].phase !== "idle");
1281
- if (loadError) return react.createElement("div", { style: PANEL_STYLE.reason }, "更新能力没接上:" + loadError);
1318
+ if (loadError) return react.createElement(TargetsLoadError, {
1319
+ message: loadError,
1320
+ onRetry: retry
1321
+ });
1282
1322
  if (targets.length === 0) return null;
1283
1323
  if (shown.length === 0 && !open) return null;
1284
1324
  const locked = shouldBlockBatchStart({
@@ -1337,7 +1377,7 @@ window.__ModuleLoader__.load({
1337
1377
  onClick: () => {
1338
1378
  props.face.act(target, buttonAction);
1339
1379
  }
1340
- }, 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", {
1380
+ }, ACTION_LABEL[buttonAction]) : null, row.failure ? react.createElement("div", { style: PANEL_STYLE.reason }, "装不上:" + row.failure.message) : null, failureDetailCode(failureDetailOf(row.failure)), versionLines(target, snapshot).map((line, index) => react.createElement("div", {
1341
1381
  key: String(index),
1342
1382
  style: PANEL_STYLE.skill
1343
1383
  }, line)), showManual ? react.createElement("div", { style: PANEL_STYLE.skill }, "在终端里执行这条命令可以手动完成这一步:") : null, showManual ? react.createElement("code", { style: PANEL_STYLE.cmd }, manual) : null);
@@ -1441,7 +1481,7 @@ window.__ModuleLoader__.load({
1441
1481
  disabled: locked,
1442
1482
  title: locked ? reasonText("update-busy") : void 0,
1443
1483
  onClick: () => props.face.act(target, "recheck")
1444
- }, ACTION_LABEL.recheck) : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
1484
+ }, ACTION_LABEL.recheck) : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, failureDetailCode(failureDetailOf(row?.failure)), react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
1445
1485
  }
1446
1486
  //#endregion
1447
1487
  //#region src/health-contract.ts
@@ -3069,48 +3109,8 @@ window.__ModuleLoader__.load({
3069
3109
  style: S.moreRow
3070
3110
  }, 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)))));
3071
3111
  }
3072
- /** 总管自述版本(票 #737;#937 改口):**不手写**,问宿主——宿主读自己这份已安装包的 `package.json`。
3073
- *
3074
- * 三态(#937):`loading`(首帧就画胶囊占位 `…`)/`ready`(读到,画包名 · 版本)/`missing`(读不到,画「版本未知」)。
3075
- * **失败要重试**:原先只取一次、一失败就永远停在未知(#937 查出来的真缺陷);这里按 1s/3s/8s 退避重试三次。
3076
- * 与卡路里 #130 同一条路:面板是浏览器产物、禁 node 内建,读盘只许在宿主半。 */
3077
- /** 读中 / 读不到 两种胶囊文案(#937:占位符用 `…`,读不到给人话,不把 `unknown` 印给人看)。 */
3078
- const VERSION_PENDING_TEXT = "…";
3079
- const VERSION_MISSING_TEXT = "版本未知";
3080
- /** 重试间隔(#937):三次退避,之后不再打扰宿主。 */
3081
- const VERSION_RETRY_MS = [
3082
- 1e3,
3083
- 3e3,
3084
- 8e3
3085
- ];
3086
- function useManagerVersion(getCall) {
3087
- const [state, setState] = react.useState({ kind: "loading" });
3088
- react.useEffect(() => {
3089
- let alive = true;
3090
- const timers = [];
3091
- const attempt = (index) => {
3092
- loadManagerVersion(getCall()).then((next) => {
3093
- if (!alive) return;
3094
- if (next !== "unknown") {
3095
- setState({
3096
- kind: "ready",
3097
- version: next
3098
- });
3099
- return;
3100
- }
3101
- setState({ kind: "missing" });
3102
- const wait = VERSION_RETRY_MS[index];
3103
- if (wait !== void 0) timers.push(setTimeout(() => attempt(index + 1), wait));
3104
- });
3105
- };
3106
- attempt(0);
3107
- return () => {
3108
- alive = false;
3109
- for (const id of timers) clearTimeout(id);
3110
- };
3111
- }, [getCall]);
3112
- return state;
3113
- }
3112
+ /** 版本胶囊那一行字:只在这里拼一次,屏上与用例读的是同一串。 */
3113
+ const MANAGER_VERSION_TEXT = "dsh-life-pack · 0.3.20";
3114
3114
  /** 爱生活面板:总设置区 + 检查更新(七家)+ 爱生活页签条(slot 驱动)+ 技能设置页投影/缺席卡。 */
3115
3115
  function LifePackSection(props) {
3116
3116
  const tabsId = react.useId();
@@ -3118,7 +3118,6 @@ window.__ModuleLoader__.load({
3118
3118
  const rows = props.useTabs((value) => value);
3119
3119
  const present = new Set(rows.map((r) => r.id));
3120
3120
  const face = useUpdateRows(props.getCall);
3121
- const managerVersion = useManagerVersion(props.getCall);
3122
3121
  const health = useHealthPanel(() => props.getCall(), react.useMemo(() => rows.filter((row) => row.channel.length > 0).map((row) => ({
3123
3122
  id: row.id,
3124
3123
  channel: row.channel
@@ -3192,7 +3191,7 @@ window.__ModuleLoader__.load({
3192
3191
  return react.createElement("div", null, react.createElement("div", { style: S.headRow }, react.createElement("div", { style: S.head }, "爱生活"), react.createElement("span", {
3193
3192
  style: S.versionCapsule,
3194
3193
  "data-ilife-version": "capsule"
3195
- }, 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, {
3194
+ }, MANAGER_VERSION_TEXT), react.createElement("div", { style: S.headActions }, react.createElement(CheckUpdateButton, { face }), react.createElement(PanelActions, null))), react.createElement(UpdateResults, { face }), react.createElement(HealthSummaryLine, {
3196
3195
  lights,
3197
3196
  running: health.running,
3198
3197
  error: summaryErrorOf(health),