dsh-life-pack 0.2.7 → 0.3.0

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
@@ -38,42 +38,48 @@ window.__ModuleLoader__.load({
38
38
  slotId: "ilife:memo",
39
39
  order: 70,
40
40
  title: "备忘录",
41
- plugin: "dsh-memo-ilife"
41
+ plugin: "dsh-memo-ilife",
42
+ channel: "/ilife-memo"
42
43
  },
43
44
  {
44
45
  skill: "calorie",
45
46
  slotId: "ilife:calorie",
46
47
  order: 75,
47
48
  title: "卡路里",
48
- plugin: "dsh-calorie"
49
+ plugin: "dsh-calorie",
50
+ channel: "/ilife-calorie"
49
51
  },
50
52
  {
51
53
  skill: "schedule",
52
54
  slotId: "ilife:schedule",
53
55
  order: 80,
54
- title: "作息",
55
- plugin: "dsh-schedule-ilife"
56
+ title: "作息管家",
57
+ plugin: "dsh-schedule-ilife",
58
+ channel: "/ilife-schedule-ilife"
56
59
  },
57
60
  {
58
61
  skill: "home",
59
62
  slotId: "ilife:home",
60
63
  order: 85,
61
- title: "居家",
62
- plugin: "dsh-home-ilife"
64
+ title: "居家管家",
65
+ plugin: "dsh-home-ilife",
66
+ channel: "/ilife-home-ilife"
63
67
  },
64
68
  {
65
69
  skill: "chef",
66
70
  slotId: "ilife:chef",
67
71
  order: 90,
68
- title: "大厨",
69
- plugin: "dsh-chef"
72
+ title: "私家大厨",
73
+ plugin: "dsh-chef",
74
+ channel: "/ilife-chef"
70
75
  },
71
76
  {
72
77
  skill: "bill",
73
78
  slotId: "ilife:cookie",
74
79
  order: 95,
75
- title: "记账",
76
- plugin: "dsh-bill-ilife"
80
+ title: "饼干记账",
81
+ plugin: "dsh-bill-ilife",
82
+ channel: "/ilife-bill-ilife"
77
83
  }
78
84
  ];
79
85
  function recoFor(tab) {
@@ -139,10 +145,17 @@ window.__ModuleLoader__.load({
139
145
  * 一处定义、两边引用(结构纪律「概念唯一」):名字改了只有这一行要动。
140
146
  * 宿主判据为什么要读这个字面量、以及它的边界,见 `readPanelRegistered` 的头注。 */
141
147
  const CONFIG_TAB_SLOT = "ilife.config-tab";
148
+ /** 载体基段:`connection.rpc.call` 的**第一段**参数,宿主注册路径与它同源(`'/api' + 通道名`)。
149
+ *
150
+ * 一处定义(#735):总管自己的电话、以及面板到各家的每一通电话,第一段都必须是它。
151
+ * 真机实现(`dsh-client-connection/lib/client.js`):URL = `${第一段}/${第二段}`、信封 `method` = 第二段。
152
+ * 传错第一段就是 404:总管自己的电话在 #678 栽过一次,各家的配置体检在 #735 又栽过一次
153
+ * (那次传的是通道名 `/ilife-<技能>`,拼出来的地址上没有注册过那条路由,六家全 404)。 */
154
+ const CARRIER_BASE = "/api";
142
155
  /** 载体:宿主注册 `connection.fetch.register` 的路径 / 面板 `connection.rpc.call` 的两段参数。 */
143
156
  const MANAGER_RPC = {
144
157
  /** 载体基段(`connection.rpc.call` 的**第一段**参数)。传错第一段就是 404:真机上踩过一次。 */
145
- base: "/api",
158
+ base: CARRIER_BASE,
146
159
  /** 单段通道名,须 match `/^\/[A-Za-z0-9._~-]+$/`(cookbook §6)。 */
147
160
  channel: "/ilife-manager",
148
161
  /** 面板侧第二段参数(等于通道名去掉前导斜杠那一段)。 */
@@ -160,8 +173,19 @@ window.__ModuleLoader__.load({
160
173
  * (`update.phoneNames`,`host.ts:335`);面板侧既不能 import 更新包(浏览器产物纯度门)
161
174
  * 也不该把名字再写一份(写两份必然走散)。宿主把名字表连同版本行一起交出去,
162
175
  * 面板一行字面量都不留。 */
163
- targets: "ilife-manager.targets"
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"
164
183
  };
184
+ /** 版本读不到时两侧共用的降级字面量(面板照原样显示,不假装知道版本)。
185
+ *
186
+ * 一处定义、两侧引用:宿主半 `manager-version.ts` 读失败时回它,面板半 `update-client.ts`
187
+ * 归一化时也认它——面板侧不许 import 宿主半(会把 node 内建带进浏览器束)。 */
188
+ const VERSION_UNKNOWN = "unknown";
165
189
  /** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。 */
166
190
  const REASON_TEXT = {
167
191
  "unknown-profile": "使用范围认不出(名字非法或目录不存在):先检查使用范围名与目录,这种情形不给手工命令。",
@@ -187,14 +211,15 @@ window.__ModuleLoader__.load({
187
211
  }
188
212
  //#endregion
189
213
  //#region src/update-client.ts
190
- /** 面板到宿主的三条流程(纯异步函数:无 DOM、无 react;传输口由调用方注入,便于单测)。
214
+ /** 面板到宿主的四条流程(纯异步函数:无 DOM、无 react;传输口由调用方注入,便于单测)。
191
215
  *
192
- * 三条流程与更新包 README 的对应关系:
216
+ * 四条流程与更新包 README 的对应关系:
193
217
  * - 检查更新:`updateCheck`(第 3 节第 3 步的 `host.call(UPD_CHECK, {})`);
194
218
  * - 装上更新:`updateCheck` 拿凭证 → `updateInstall` 提交 → **按间隔轮询 `updateStatus`**
195
219
  * 直到任务离开 `installing`/`verifying`(第 3 节第 3 步与第 9 节:安装是后台跑,
196
220
  * 调用方不干等;凭证过期 `check-expired` 是正常错误码,重查一次再提交,见第 11 节);
197
- * - 装上缺席的包:走总管自有的 `ilife-manager.install`(更新包的三个电话只管已装的包)。
221
+ * - 装上缺席的包:走总管自有的 `ilife-manager.install`(更新包的三个电话只管已装的包);
222
+ * - 总管自述版本:走总管自有的 `ilife-manager.version`(宿主读自己那份包描述文件,见 `manager-version.ts`)。
198
223
  */
199
224
  /** 轮询上限:1 秒一次、最多 900 次(覆盖更新包 15 分钟的安装时限)。 */
200
225
  const MAX_POLL_TRIES = 900;
@@ -253,6 +278,16 @@ window.__ModuleLoader__.load({
253
278
  }
254
279
  };
255
280
  }
281
+ /** 取总管自述版本(票 #737):宿主读**自己这份已安装包**的 `package.json`,面板只渲染读到的值。
282
+ *
283
+ * 单次、无轮询;宿主读不到会回 `unknown`,传输层失败也回同一个字面量——面板照原样显示,
284
+ * 不假装知道版本(与卡路里 #130 的降级口径一致)。 */
285
+ async function loadManagerVersion(call) {
286
+ const out = await callManager(call, MANAGER_ACTIONS.version, {});
287
+ if (!out.ok) return VERSION_UNKNOWN;
288
+ const raw = out.value?.version;
289
+ return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : VERSION_UNKNOWN;
290
+ }
256
291
  /** 查一家:默认查新版(`updateCheck`,顺带回最新版本与凭证);给 `phoneName` 时查那个电话。 */
257
292
  async function checkTarget(call, target, phoneName) {
258
293
  const name = phoneName ?? target.phones?.check;
@@ -998,12 +1033,20 @@ window.__ModuleLoader__.load({
998
1033
  /** 面板到各家的配置体检:一次取数、六份报告(纯异步函数:无 DOM、无 react;传输口注入,便于单测)。
999
1034
  *
1000
1035
  * 三条纪律:
1001
- * · **只走各家自己的通道**(`rpc.call(channel, HEALTH_ENDPOINT, …)`)——总管不认任何一家、也不加
1002
- * 任何单品的依赖;通道名从「爱生活页签槽」的账本条目带出来(各家注册时写进 options)。
1036
+ * · **只走各家自己的电话**(`rpc.call(CARRIER_BASE, <电话名>, {method, payload}, …)`)——总管不认任何
1037
+ * 一家的端点、也不加任何单品的依赖;电话名与端点名都是各家契约件公开的事实。
1038
+ * ⚠ **载体形状**(#735 真机踩到):第一段必须是**载体基段** `/api`,第二段是那家的电话名
1039
+ * (=它注册在 `/api<通道>` 上的那一段),端点名写进载荷的 `method`。写错第一段就是 404,
1040
+ * 而 404 只落成「那一家取不到数」,屏上看起来像按钮坏了(先例:总管自己的电话在 #678 栽过同一处,
1041
+ * 见 `update-contract.ts` 的 `CARRIER_BASE`)。
1003
1042
  * · **一次取数,六份报告**:总览那行与各家那张表读的是同一份结果(见 `health-panel.ts` 的单一状态源),
1004
1043
  * 不许两处各算一遍。
1005
1044
  * · 有界超时:一家不回不该拖死整屏(技能侧 spawn 上限 20 s,这里给 25 s)。
1006
1045
  */
1046
+ /** 一家的电话名:通道名去掉前导斜杠那一段(各家宿主半注册在 `/api` + 通道名,信封 `method` 就是它)。 */
1047
+ function phoneOf(channel) {
1048
+ return channel.startsWith("/") ? channel.slice(1) : channel;
1049
+ }
1007
1050
  /** 一次取数的时限(毫秒):技能侧 spawn 上限 20 s,这里留一点余量。 */
1008
1051
  const HEALTH_TIMEOUT_MS = 25e3;
1009
1052
  function timeoutText() {
@@ -1011,7 +1054,10 @@ window.__ModuleLoader__.load({
1011
1054
  }
1012
1055
  async function fetchOne(call, row) {
1013
1056
  try {
1014
- const result = await call(row.channel, HEALTH_ENDPOINT, {}, AbortSignal.timeout(HEALTH_TIMEOUT_MS));
1057
+ const result = await call(CARRIER_BASE, phoneOf(row.channel), {
1058
+ method: HEALTH_ENDPOINT,
1059
+ payload: {}
1060
+ }, AbortSignal.timeout(HEALTH_TIMEOUT_MS));
1015
1061
  if (!result || typeof result !== "object" || typeof result.ok !== "boolean") return {
1016
1062
  report: null,
1017
1063
  error: "回执信封异常(非 ok 信封)。"
@@ -1118,6 +1164,17 @@ window.__ModuleLoader__.load({
1118
1164
  } catch {}
1119
1165
  return face;
1120
1166
  }
1167
+ /** 汇总行该显示哪一句错(票 #735):取数过程本身的错优先;**一家报告都没回来**时,把第一条家错误顶上来。
1168
+ *
1169
+ * 为什么要有这一格:各家的错只画在那家页签里的体检表上,一行摘要看不见 ⇒ 用户点完只见「体检中…」闪一下、
1170
+ * 接着仍旧「还没体检」,会以为按钮坏了(真机上就是这么被误判的;那次六家全 404,汇总行一声不吭)。
1171
+ * 有报告回来时不顶:那时各家那张表里已经把错说清楚了,摘要行只管读数。 */
1172
+ function summaryErrorOf(face) {
1173
+ if (face.error !== null) return face.error;
1174
+ const rows = Object.values(face.rows);
1175
+ if (rows.some((row) => row.report !== null)) return null;
1176
+ return rows.find((row) => row.error !== null)?.error ?? null;
1177
+ }
1121
1178
  //#endregion
1122
1179
  //#region src/health-view.ts
1123
1180
  /** 配置体检的两个画法:面板顶部那行总览(六家一盏灯)+ 各家那张表。
@@ -1339,15 +1396,26 @@ window.__ModuleLoader__.load({
1339
1396
  }
1340
1397
  /** 几截读数之间那个分隔符:不许染档位色(它不属于任何一档)。 */
1341
1398
  const SEG_SEP = " · ";
1342
- /** 汇总行那一句:绿要显眼(它是「正常」这件事本身),没通道的那几家不冒充绿。 */
1399
+ /** 汇总行那一句:绿要显眼(它是「正常」这件事本身),没通道的那几家不冒充绿。
1400
+ *
1401
+ * 「没有体检出口」与「还没跑过体检」是**两件事**(票 #735):前者那颗「体检一次」按下去不会有任何反应,
1402
+ * 必须自己说出来——两态共用一句「还没体检」时,用户与诊断都会被引到「点了没跑」上去。 */
1343
1403
  function overallText(lights) {
1344
1404
  const pending = lights.filter((light) => !light.hasChannel).length;
1345
- const withReport = lights.filter((light) => light.hasChannel);
1346
- const tail = pending > 0 ? " · " + String(pending) + " 家还没体检" : "";
1347
- if (withReport.length > 0 && withReport.every((light) => light.status === "green")) return react.createElement("span", { style: {
1405
+ const withChannel = lights.filter((light) => light.hasChannel);
1406
+ const tail = pending > 0 ? " · " + String(pending) + " 家没有体检出口" : "";
1407
+ if (withChannel.length === 0) {
1408
+ const what = lights.length === 0 ? "还没有任何一家登记体检出口" : String(lights.length) + " 家都没有体检出口(通道名没登记上)";
1409
+ return react.createElement("span", { style: {
1410
+ color: STATUS_TEXT.red,
1411
+ fontWeight: 700
1412
+ } }, "没有可体检的家:" + what);
1413
+ }
1414
+ const allGreen = withChannel.every((light) => light.status === "green");
1415
+ if (allGreen && pending === 0) return react.createElement("span", { style: {
1348
1416
  color: STATUS_COLOR.green,
1349
1417
  fontWeight: 700
1350
- } }, "六家正常" + tail);
1418
+ } }, "六家正常");
1351
1419
  const parts = [];
1352
1420
  for (const status of ["red", "yellow"]) {
1353
1421
  let count = 0;
@@ -1365,7 +1433,7 @@ window.__ModuleLoader__.load({
1365
1433
  }
1366
1434
  }, STATUS_LABEL[status] + " " + String(count)));
1367
1435
  }
1368
- if (parts.length === 0) return react.createElement("span", { style: { color: INK_DIM } }, "还没体检" + tail);
1436
+ if (parts.length === 0) return react.createElement("span", { style: { color: INK_DIM } }, (allGreen ? "有出口的都正常" : "还没体检") + tail);
1369
1437
  parts.push(react.createElement("span", {
1370
1438
  key: "todo",
1371
1439
  style: { color: INK }
@@ -1409,11 +1477,10 @@ window.__ModuleLoader__.load({
1409
1477
  * 与页签的分工:这行只答「一共几条要处理」;「哪几家、各几条」由页签的圆点与小数字答;
1410
1478
  * 「具体哪一条坏了、去哪修」由那家页签里的 `HealthTable` 答。 */
1411
1479
  function HealthSummaryLine(props) {
1412
- const hasLights = props.lights.length > 0;
1413
1480
  return react.createElement("div", {
1414
1481
  style: HEALTH_STYLE.summaryRow,
1415
1482
  "data-ilife-health": "summary"
1416
- }, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检"), react.createElement("div", { style: HEALTH_STYLE.summaryText }, props.error !== null ? react.createElement("span", { style: { color: STATUS_TEXT.red } }, props.error) : hasLights ? overallText(props.lights) : react.createElement("span", { style: { color: INK_DIM } }, "还没体检")), react.createElement("button", {
1483
+ }, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检"), react.createElement("div", { style: HEALTH_STYLE.summaryText }, props.error !== null ? react.createElement("span", { style: { color: STATUS_TEXT.red } }, props.error) : overallText(props.lights)), react.createElement("button", {
1417
1484
  type: "button",
1418
1485
  style: props.running ? {
1419
1486
  ...HEALTH_STYLE.btn,
@@ -1533,6 +1600,9 @@ window.__ModuleLoader__.load({
1533
1600
  * 文案与网址都取自 nav.ts 的两张静态表,本文件只画不算。
1534
1601
  */
1535
1602
  const inject = ["slots", "connection"];
1603
+ /** 包名 → 本家那条客户端通道(票 #735)。取值面是导航表那份镜像,不再从页签槽账本的自定义选项里读:
1604
+ * 装机槽位面不透传自定义键,读了恒是空串(详见 `nav.ts` 上 `ManagerTab.channel` 的注释)。 */
1605
+ const CHANNEL_BY_PLUGIN = new Map(MANAGER_TABS.map((tab) => [tab.plugin, tab.channel]));
1536
1606
  /** 总管视觉(内联 style;颜色走 DSH 主题别名,深浅主题自适应,写死值只做回退;与技能面板同语言)。 */
1537
1607
  const S = {
1538
1608
  headRow: {
@@ -1710,6 +1780,23 @@ window.__ModuleLoader__.load({
1710
1780
  style: S.moreRow
1711
1781
  }, 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)))));
1712
1782
  }
1783
+ /** 总管自述版本(票 #737):**不手写**,问宿主——宿主读自己这份已安装包的 `package.json`。
1784
+ *
1785
+ * 单次取数(`loadManagerVersion`),无轮询;初值 `unknown`,读不到也停在 `unknown`。
1786
+ * 与卡路里 #130 同一条路:面板是浏览器产物、禁 node 内建,读盘只许在宿主半。 */
1787
+ function useManagerVersion(getCall) {
1788
+ const [version, setVersion] = react.useState(VERSION_UNKNOWN);
1789
+ react.useEffect(() => {
1790
+ let alive = true;
1791
+ loadManagerVersion(getCall()).then((next) => {
1792
+ if (alive) setVersion(next);
1793
+ });
1794
+ return () => {
1795
+ alive = false;
1796
+ };
1797
+ }, [getCall]);
1798
+ return version;
1799
+ }
1713
1800
  /** 爱生活面板:总设置区 + 检查更新(七家)+ 爱生活页签条(slot 驱动)+ 技能设置页投影/缺席卡。 */
1714
1801
  function LifePackSection(props) {
1715
1802
  const tabsId = react.useId();
@@ -1717,6 +1804,7 @@ window.__ModuleLoader__.load({
1717
1804
  const rows = props.useTabs((value) => value);
1718
1805
  const present = new Set(rows.map((r) => r.id));
1719
1806
  const face = useUpdateRows(props.getCall);
1807
+ const managerVersion = useManagerVersion(props.getCall);
1720
1808
  const health = useHealthPanel(() => props.getCall(), react.useMemo(() => rows.filter((row) => row.channel.length > 0).map((row) => ({
1721
1809
  id: row.id,
1722
1810
  channel: row.channel
@@ -1766,10 +1854,10 @@ window.__ModuleLoader__.load({
1766
1854
  setActiveId(next.plugin);
1767
1855
  tabRefs.current[nextIndex]?.focus();
1768
1856
  }
1769
- 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, "总开关 · 开关(缺省启用,只读)"), react.createElement("div", null, "总管 dsh-life-pack · 0.2.6")), react.createElement(UpdateResults, { face }), react.createElement(HealthSummaryLine, {
1857
+ 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, {
1770
1858
  lights,
1771
1859
  running: health.running,
1772
- error: health.error,
1860
+ error: summaryErrorOf(health),
1773
1861
  onRun: health.run
1774
1862
  }), react.createElement("div", {
1775
1863
  role: "tablist",
@@ -1853,7 +1941,7 @@ window.__ModuleLoader__.load({
1853
1941
  id: entry.options.id ?? "",
1854
1942
  order: entry.options.order ?? 0,
1855
1943
  label: resolveLabel(entry.options.label),
1856
- channel: entry.options.channel ?? ""
1944
+ channel: CHANNEL_BY_PLUGIN.get(entry.options.id ?? "") ?? ""
1857
1945
  })).sort((a, b) => a.order - b.order);
1858
1946
  }
1859
1947
  return tabs;