dsh-life-pack 0.2.7 → 0.3.1
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 +351 -102
- package/dist/client.js.map +1 -1
- package/dist/dsh-ctx.d.ts +9 -5
- package/dist/dsh-ctx.d.ts.map +1 -1
- package/dist/health-fetch.d.ts +14 -14
- package/dist/health-fetch.d.ts.map +1 -1
- package/dist/health-fetch.js +21 -5
- package/dist/health-fetch.js.map +1 -1
- package/dist/health-panel.d.ts +6 -0
- package/dist/health-panel.d.ts.map +1 -1
- package/dist/health-panel.js +13 -0
- package/dist/health-panel.js.map +1 -1
- package/dist/health-view.d.ts.map +1 -1
- package/dist/health-view.js +20 -9
- package/dist/health-view.js.map +1 -1
- package/dist/manager-version.d.ts +7 -0
- package/dist/manager-version.d.ts.map +1 -0
- package/dist/manager-version.js +62 -0
- package/dist/manager-version.js.map +1 -0
- package/dist/nav.d.ts +18 -2
- package/dist/nav.d.ts.map +1 -1
- package/dist/nav.js +10 -8
- package/dist/nav.js.map +1 -1
- package/dist/update-client.d.ts +5 -0
- package/dist/update-client.d.ts.map +1 -1
- package/dist/update-client.js +16 -4
- package/dist/update-client.js.map +1 -1
- package/dist/update-contract.d.ts +18 -0
- package/dist/update-contract.d.ts.map +1 -1
- package/dist/update-contract.js +48 -19
- package/dist/update-contract.js.map +1 -1
- package/dist/update-env.d.ts.map +1 -1
- package/dist/update-env.js +7 -11
- package/dist/update-env.js.map +1 -1
- package/dist/update-host.d.ts.map +1 -1
- package/dist/update-host.js +4 -0
- package/dist/update-host.js.map +1 -1
- package/dist/update-view.d.ts +43 -6
- package/dist/update-view.d.ts.map +1 -1
- package/dist/update-view.js +93 -21
- package/dist/update-view.js.map +1 -1
- package/package.json +1 -1
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:
|
|
158
|
+
base: CARRIER_BASE,
|
|
146
159
|
/** 单段通道名,须 match `/^\/[A-Za-z0-9._~-]+$/`(cookbook §6)。 */
|
|
147
160
|
channel: "/ilife-manager",
|
|
148
161
|
/** 面板侧第二段参数(等于通道名去掉前导斜杠那一段)。 */
|
|
@@ -160,41 +173,62 @@ 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
|
};
|
|
165
|
-
/**
|
|
184
|
+
/** 版本读不到时两侧共用的降级字面量(面板照原样显示,不假装知道版本)。
|
|
185
|
+
*
|
|
186
|
+
* 一处定义、两侧引用:宿主半 `manager-version.ts` 读失败时回它,面板半 `update-client.ts`
|
|
187
|
+
* 归一化时也认它——面板侧不许 import 宿主半(会把 node 内建带进浏览器束)。 */
|
|
188
|
+
const VERSION_UNKNOWN = "unknown";
|
|
189
|
+
/** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。
|
|
190
|
+
*
|
|
191
|
+
* 每条都要三段齐全(票 #740 的文案判据):**事实**(哪里对不上)→ **后果**(这家现在能不能装)
|
|
192
|
+
* → **动作**(用户做得到、且面板上真有的那一步;面板代劳不了的走「下面这条命令」)。
|
|
193
|
+
* 禁用口语与自造词:「半截任务」「电话没接上」「回执异常」「还出现就重装」「查宿主日志」一律不写。
|
|
194
|
+
*
|
|
195
|
+
* 点名的按钮必须是**这一行上真够得着**的那颗(票 #740 第三轮):结果浮层一开就盖住题头的
|
|
196
|
+
* 「检查更新」,所以「改完再读一次」这类下一步一律写「重新检查」(画在那一行上);
|
|
197
|
+
* 只有发生在**重启之后**的下一步才写「检查更新」。反过来,凡是写着「下面这条命令」的那几句,
|
|
198
|
+
* 那一行的命令块必须真出来(判据 `Verdict.manualHint`)——两处都由门禁守着。 */
|
|
166
199
|
const REASON_TEXT = {
|
|
167
|
-
"unknown-profile": "
|
|
168
|
-
"source-install": "
|
|
169
|
-
"invalid-installation": "
|
|
170
|
-
"installation-changed": "
|
|
171
|
-
"pending-restart": "
|
|
172
|
-
"registry-conflict": "
|
|
173
|
-
"incompatible-node": "
|
|
174
|
-
"recovery-required": "
|
|
175
|
-
"check-failed": "
|
|
176
|
-
"invalid-release": "
|
|
177
|
-
"check-expired": "
|
|
178
|
-
"update-busy": "
|
|
179
|
-
"install-failed": "
|
|
180
|
-
"manager-unreachable": "
|
|
181
|
-
"bad-request": "
|
|
182
|
-
internal: "
|
|
200
|
+
"unknown-profile": "认不出这台机器上的安装位置(使用范围的目录不存在,或名字不合法)。把使用范围目录建好,再点「重新检查」。",
|
|
201
|
+
"source-install": "这一份不是从 npm 发行包装的(源码装),面板不能更新它。要更新,照下面这条命令重装,装完再点「重新检查」。",
|
|
202
|
+
"invalid-installation": "磁盘上这一份装得不完整(包名或版本对不上、入口文件缺失)。照下面这条命令重装,装完再点「重新检查」。",
|
|
203
|
+
"installation-changed": "这台机器的安装清单变了(在别处装过、或清单被手工改过),这一家现在不能装。点「重新检查」重新读一次,再点「装上更新」。",
|
|
204
|
+
"pending-restart": "新版已装到磁盘,正在运行的是旧版本。重启 DSH(退出后重新打开)后生效。",
|
|
205
|
+
"registry-conflict": "使用范围清单里写的版本与磁盘上的实际版本对不上。打开使用范围的 `package.json`,把这一行改成磁盘上的版本号,再点「重新检查」。",
|
|
206
|
+
"incompatible-node": "新版本要求 Node 22 或更高,当前运行的是更低版本。升级 Node、重启 DSH 后,再点「检查更新」。",
|
|
207
|
+
"recovery-required": "上次安装没有收尾。点「重试安装」再跑一次;还出现这一条,就用下面这条命令重装。",
|
|
208
|
+
"check-failed": "查不到最新版本:联网失败,或官方源(registry.npmjs.org)不可达。确认网络与源地址后,再点「重新检查」。",
|
|
209
|
+
"invalid-release": "官方源上这个版本的元数据不完整(包名或版本不合法、包地址或校验值缺失)。换一个版本,或稍后重试。",
|
|
210
|
+
"check-expired": "上次检查的结果已过期。点「重新检查」重查一次,再点「装上更新」。",
|
|
211
|
+
"update-busy": "同一时间只能安装一个插件。等这次装完,再点「装上更新」。",
|
|
212
|
+
"install-failed": "安装没有完成。在终端里执行下面这条命令把它装完,装完再点「重新检查」。",
|
|
213
|
+
"manager-unreachable": "面板连不上总管插件(宿主里没注册,或版本对不上)。重启 DSH 后重试;仍不行就在 DSH 日志里找 `[dsh-life-pack]` 的报错。",
|
|
214
|
+
"bad-request": "面板与总管之间的请求出错了(属程序缺陷)。请在 DSH 日志里找 `[dsh-life-pack]` 的报错,连同这家插件的名字报到本仓 issue。",
|
|
215
|
+
internal: "面板与总管之间的请求出错了(属程序缺陷)。请在 DSH 日志里找 `[dsh-life-pack]` 的报错,连同这家插件的名字报到本仓 issue。"
|
|
183
216
|
};
|
|
184
217
|
/** 原因码 → 人话(认不出的码不猜、原样回码并说明这是未知原因)。 */
|
|
185
218
|
function reasonText(code) {
|
|
186
|
-
return REASON_TEXT[code] ?? "
|
|
219
|
+
return REASON_TEXT[code] ?? "面板不认识这个原因码(" + code + ")。请在 DSH 日志里找同一处报错,报到本仓 issue。";
|
|
187
220
|
}
|
|
188
221
|
//#endregion
|
|
189
222
|
//#region src/update-client.ts
|
|
190
|
-
/**
|
|
223
|
+
/** 面板到宿主的四条流程(纯异步函数:无 DOM、无 react;传输口由调用方注入,便于单测)。
|
|
191
224
|
*
|
|
192
|
-
*
|
|
225
|
+
* 四条流程与更新包 README 的对应关系:
|
|
193
226
|
* - 检查更新:`updateCheck`(第 3 节第 3 步的 `host.call(UPD_CHECK, {})`);
|
|
194
227
|
* - 装上更新:`updateCheck` 拿凭证 → `updateInstall` 提交 → **按间隔轮询 `updateStatus`**
|
|
195
228
|
* 直到任务离开 `installing`/`verifying`(第 3 节第 3 步与第 9 节:安装是后台跑,
|
|
196
229
|
* 调用方不干等;凭证过期 `check-expired` 是正常错误码,重查一次再提交,见第 11 节);
|
|
197
|
-
* - 装上缺席的包:走总管自有的 `ilife-manager.install
|
|
230
|
+
* - 装上缺席的包:走总管自有的 `ilife-manager.install`(更新包的三个电话只管已装的包);
|
|
231
|
+
* - 总管自述版本:走总管自有的 `ilife-manager.version`(宿主读自己那份包描述文件,见 `manager-version.ts`)。
|
|
198
232
|
*/
|
|
199
233
|
/** 轮询上限:1 秒一次、最多 900 次(覆盖更新包 15 分钟的安装时限)。 */
|
|
200
234
|
const MAX_POLL_TRIES = 900;
|
|
@@ -253,6 +287,16 @@ window.__ModuleLoader__.load({
|
|
|
253
287
|
}
|
|
254
288
|
};
|
|
255
289
|
}
|
|
290
|
+
/** 取总管自述版本(票 #737):宿主读**自己这份已安装包**的 `package.json`,面板只渲染读到的值。
|
|
291
|
+
*
|
|
292
|
+
* 单次、无轮询;宿主读不到会回 `unknown`,传输层失败也回同一个字面量——面板照原样显示,
|
|
293
|
+
* 不假装知道版本(与卡路里 #130 的降级口径一致)。 */
|
|
294
|
+
async function loadManagerVersion(call) {
|
|
295
|
+
const out = await callManager(call, MANAGER_ACTIONS.version, {});
|
|
296
|
+
if (!out.ok) return VERSION_UNKNOWN;
|
|
297
|
+
const raw = out.value?.version;
|
|
298
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : VERSION_UNKNOWN;
|
|
299
|
+
}
|
|
256
300
|
/** 查一家:默认查新版(`updateCheck`,顺带回最新版本与凭证);给 `phoneName` 时查那个电话。 */
|
|
257
301
|
async function checkTarget(call, target, phoneName) {
|
|
258
302
|
const name = phoneName ?? target.phones?.check;
|
|
@@ -322,57 +366,136 @@ window.__ModuleLoader__.load({
|
|
|
322
366
|
if (target.panelRegistered === false) return "unregistered-product";
|
|
323
367
|
return "not-connected";
|
|
324
368
|
}
|
|
325
|
-
/**
|
|
369
|
+
/** 这一家是不是「装好了、只差重启」——判据是**事实**(磁盘版本 ≠ 正在跑的版本),不是原因码。
|
|
370
|
+
*
|
|
371
|
+
* 为什么不许按原因码判(票 #740 真机):同一种事实在更新核心里可能报成两种码——
|
|
372
|
+
* `pending-restart`,或者被上一次中断的任务盖成 `recovery-required`。按码判会把「该重启」
|
|
373
|
+
* 漏成一句「装不上」,用户看到的正是那种读不懂的屏。 */
|
|
374
|
+
function restartPendingOf(snapshot) {
|
|
375
|
+
const installed = snapshot.installedVersion;
|
|
376
|
+
return installed !== null && installed !== snapshot.runningVersion;
|
|
377
|
+
}
|
|
378
|
+
/** 待重启那一句(三段齐全:事实 → 后果 → 动作)。
|
|
379
|
+
*
|
|
380
|
+
* 措辞不写「新版」:磁盘上那份也可能比正在跑的**低**(手工降级),「新版」就不实了;
|
|
381
|
+
* 只照事实说「磁盘上装的是 X、正在运行的是 Y」。
|
|
382
|
+
* 动作只给「重启 DSH」:这一态下面板**做不了别的**——更新包的可装判据要求最新版严格高于
|
|
383
|
+
* **正在运行**的版本(`service.js:219`),而凭证只发在可装态(`service.js:256`:`receipt` 只在
|
|
384
|
+
* `canInstall` 时给),所以这一态里「重试安装」点了也是白点。 */
|
|
385
|
+
function restartLine(snapshot) {
|
|
386
|
+
const installed = snapshot.installedVersion ?? snapshot.job?.targetVersion ?? "新版本";
|
|
387
|
+
return "磁盘上装的是 " + String(installed) + ";正在运行的是 " + snapshot.runningVersion + "。重启 DSH(退出后重新打开)后生效。";
|
|
388
|
+
}
|
|
389
|
+
/** 「条件在别处(目录/清单/别处装的),改完回来重读一次」的原因码:面板能给的下一步就是「重新检查」。
|
|
390
|
+
*
|
|
391
|
+
* 为什么要逐码列(票 #740 第三轮的朗读表):`unknown-profile`/`registry-conflict`/`invalid-installation`/
|
|
392
|
+
* `source-install` 这四态原先一个动作都不给,屏上那句却写着「再点「检查更新」」——那颗按钮住在面板题头,
|
|
393
|
+
* 结果浮层一开(点「检查更新」就开)就被浮层盖住,够不着。跟两张「还没查过」的卡是同一类病。 */
|
|
394
|
+
const RECHECK_REASONS = /* @__PURE__ */ new Set([
|
|
395
|
+
"installation-changed",
|
|
396
|
+
"unknown-profile",
|
|
397
|
+
"registry-conflict",
|
|
398
|
+
"invalid-installation",
|
|
399
|
+
"source-install"
|
|
400
|
+
]);
|
|
401
|
+
/** 「这一步面板代劳不了、得照下面那条命令做」的原因码:这些话下面必须真有命令块。 */
|
|
402
|
+
const NEEDS_MANUAL = /* @__PURE__ */ new Set([
|
|
403
|
+
"source-install",
|
|
404
|
+
"invalid-installation",
|
|
405
|
+
"recovery-required"
|
|
406
|
+
]);
|
|
407
|
+
/** 拦截态各自给得出的下一步:面板能代劳的给按钮,代劳不了的给 null(那种走手工命令块)。 */
|
|
408
|
+
function actionForBlocked(reason) {
|
|
409
|
+
if (RECHECK_REASONS.has(reason)) return "recheck";
|
|
410
|
+
if (reason === "recovery-required") return "retry";
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
/** 这一行此刻该画哪颗按钮:结论自带的动作优先,**失败的行一律给「重新检查」**(票 #740 第三轮)。
|
|
414
|
+
*
|
|
415
|
+
* 失败的行(查不到最新版/检查过期/装失败)没有快照、也就没有结论,原先一颗按钮都不画:
|
|
416
|
+
* 屏上写着「再点「检查更新」」,那颗按钮却在浮层底下 ⇒ 无路可走。重新查一次对每一种失败都成立。 */
|
|
417
|
+
function cardActionOf(verdict, phase) {
|
|
418
|
+
if (verdict?.action) return verdict.action;
|
|
419
|
+
return phase === "failed" ? "recheck" : null;
|
|
420
|
+
}
|
|
421
|
+
/** 装成一家之后,其余各家的读数一律作废(票 #740):七家共用同一份使用范围清单,
|
|
422
|
+
* 刚那次安装已经改写了它,旧快照再拿去装必然被守卫拦下。这句话就是那条守卫的人话。 */
|
|
423
|
+
function staleVerdict() {
|
|
424
|
+
return {
|
|
425
|
+
kind: "blocked",
|
|
426
|
+
text: "刚在本面板装过别的插件,共用的安装清单变了,这一家要先重读一次。点「重新检查」,再点「装上更新」。",
|
|
427
|
+
action: "recheck"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/** 把除 `keepKey` 之外的行全标成过期(票 #740):装成一家 ⇒ 七家共用的安装清单被改写,
|
|
431
|
+
* 其余各家的旧快照再拿去装必然被守卫拦下。纯函数:状态源与单测共用同一份判据,
|
|
432
|
+
* 不关心行里还有什么(泛型只要求有 `stale` 这一格)。 */
|
|
433
|
+
function markStaleOthers(rows, keepKey) {
|
|
434
|
+
const next = {};
|
|
435
|
+
for (const [key, row] of Object.entries(rows)) next[key] = key === keepKey ? row : {
|
|
436
|
+
...row,
|
|
437
|
+
stale: true
|
|
438
|
+
};
|
|
439
|
+
return next;
|
|
440
|
+
}
|
|
441
|
+
/** 把快照翻成一行结论:还没查 / 已是最新 / 有新版 / 待重启 / 装不了(附人话原因与下一步)。 */
|
|
326
442
|
function verdictOf(target, snapshot) {
|
|
327
443
|
const absent = isAbsent(target);
|
|
328
444
|
const latest = snapshot.latestVersion;
|
|
329
445
|
if (absent && latest === null) return {
|
|
330
446
|
kind: "unknown",
|
|
331
|
-
text: "
|
|
332
|
-
action:
|
|
447
|
+
text: "未安装。点「重新检查」查最新版本,再点「装上」。",
|
|
448
|
+
action: "recheck"
|
|
333
449
|
};
|
|
334
450
|
if (absent) return {
|
|
335
451
|
kind: "update-available",
|
|
336
|
-
text: "未安装;最新版本 " + String(latest) + "
|
|
452
|
+
text: "未安装;最新版本 " + String(latest) + "。点「装上」安装。",
|
|
337
453
|
action: "install"
|
|
338
454
|
};
|
|
339
|
-
if (snapshot
|
|
455
|
+
if (restartPendingOf(snapshot)) return {
|
|
456
|
+
kind: "blocked",
|
|
457
|
+
text: restartLine(snapshot),
|
|
458
|
+
action: null
|
|
459
|
+
};
|
|
460
|
+
const installed = snapshot.installedVersion;
|
|
461
|
+
const hasNewer = latest !== null && latest !== installed;
|
|
462
|
+
if (snapshot.blockedReason !== null && hasNewer) {
|
|
340
463
|
const blocked = snapshot.blockedReason;
|
|
341
|
-
if (blocked === "pending-restart") return {
|
|
342
|
-
kind: "blocked",
|
|
343
|
-
text: reasonText(blocked),
|
|
344
|
-
action: null
|
|
345
|
-
};
|
|
346
464
|
return {
|
|
347
465
|
kind: "blocked",
|
|
348
466
|
text: reasonText(blocked),
|
|
349
|
-
action:
|
|
467
|
+
action: actionForBlocked(blocked),
|
|
468
|
+
manualHint: NEEDS_MANUAL.has(blocked)
|
|
350
469
|
};
|
|
351
470
|
}
|
|
352
471
|
if (latest === null) return {
|
|
353
472
|
kind: "unknown",
|
|
354
|
-
text: "
|
|
355
|
-
action:
|
|
473
|
+
text: "还没查过最新版本。点「重新检查」。",
|
|
474
|
+
action: "recheck"
|
|
356
475
|
};
|
|
357
476
|
if (snapshot.canInstall) return {
|
|
358
477
|
kind: "update-available",
|
|
359
|
-
text: "
|
|
478
|
+
text: "有新版本 " + String(latest) + "(正在运行 " + snapshot.runningVersion + ")。点「装上更新」。",
|
|
360
479
|
action: "update"
|
|
361
480
|
};
|
|
481
|
+
if (hasNewer) return {
|
|
482
|
+
kind: "update-available",
|
|
483
|
+
text: "有新版本 " + String(latest) + "(正在运行 " + snapshot.runningVersion + ")。点「重新检查」,再点「装上更新」。",
|
|
484
|
+
action: "recheck"
|
|
485
|
+
};
|
|
362
486
|
return {
|
|
363
487
|
kind: "up-to-date",
|
|
364
|
-
text: "
|
|
488
|
+
text: "已是最新版本 " + String(latest) + "。",
|
|
365
489
|
action: null
|
|
366
490
|
};
|
|
367
491
|
}
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
return "⚠️ 新版 " + installed + " 已装好,正在跑的还是 " + running + ",重启宿主后生效。";
|
|
492
|
+
/** 待重启横幅:**一行总账**,只点名哪几家要做「重启 DSH」这件事。
|
|
493
|
+
*
|
|
494
|
+
* 为什么不再逐家重复那句话(票 #740 对抗式审查第三轮的朗读表逮到):同一句「重启 DSH…后生效」
|
|
495
|
+
* 会同时出现在横幅和那家卡片上,一屏两遍。分工定死:**横幅答「谁要重启」,卡片答「为什么、怎么做」**。 */
|
|
496
|
+
function restartBannerText(titles) {
|
|
497
|
+
if (titles.length === 0) return null;
|
|
498
|
+
return "⚠️ 待重启 DSH(退出后重新打开):" + titles.join("、");
|
|
376
499
|
}
|
|
377
500
|
/** 面板展示的那条可复制命令(用户会照着敲的那一条)。
|
|
378
501
|
*
|
|
@@ -657,6 +780,13 @@ window.__ModuleLoader__.load({
|
|
|
657
780
|
default: return "idle";
|
|
658
781
|
}
|
|
659
782
|
}
|
|
783
|
+
/** 四个动作在屏上的名字:按钮文案与判据同源(票 #740)。 */
|
|
784
|
+
const ACTION_LABEL = {
|
|
785
|
+
install: "装上",
|
|
786
|
+
update: "装上更新",
|
|
787
|
+
retry: "重试安装",
|
|
788
|
+
recheck: "重新检查"
|
|
789
|
+
};
|
|
660
790
|
const IDLE$1 = {
|
|
661
791
|
phase: "idle",
|
|
662
792
|
outcome: null,
|
|
@@ -693,6 +823,10 @@ window.__ModuleLoader__.load({
|
|
|
693
823
|
[key]: next
|
|
694
824
|
}));
|
|
695
825
|
}, []);
|
|
826
|
+
/** 装成一家 ⇒ 其余各家的旧快照一律作废(票 #740):判据见 `markStaleOthers`。 */
|
|
827
|
+
const staleOthers = react.useCallback((keepKey) => {
|
|
828
|
+
setRows((previous) => markStaleOthers(previous, keepKey));
|
|
829
|
+
}, []);
|
|
696
830
|
return {
|
|
697
831
|
targets,
|
|
698
832
|
rows,
|
|
@@ -708,18 +842,21 @@ window.__ModuleLoader__.load({
|
|
|
708
842
|
for (const target of list) patch(target.key, {
|
|
709
843
|
phase: "checking",
|
|
710
844
|
outcome: rows[target.key]?.outcome ?? null,
|
|
711
|
-
failure: null
|
|
845
|
+
failure: null,
|
|
846
|
+
stale: false
|
|
712
847
|
});
|
|
713
848
|
Promise.all(list.map(async (target) => {
|
|
714
849
|
const result = await checkTarget(getCall(), target);
|
|
715
850
|
patch(target.key, result.ok ? {
|
|
716
851
|
phase: "ready",
|
|
717
852
|
outcome: result.value,
|
|
718
|
-
failure: null
|
|
853
|
+
failure: null,
|
|
854
|
+
stale: false
|
|
719
855
|
} : {
|
|
720
856
|
phase: "failed",
|
|
721
857
|
outcome: null,
|
|
722
|
-
failure: result
|
|
858
|
+
failure: result,
|
|
859
|
+
stale: false
|
|
723
860
|
});
|
|
724
861
|
})).finally(() => setChecking(false));
|
|
725
862
|
}, [
|
|
@@ -728,12 +865,36 @@ window.__ModuleLoader__.load({
|
|
|
728
865
|
rows,
|
|
729
866
|
targets
|
|
730
867
|
]),
|
|
731
|
-
act: react.useCallback((target) => {
|
|
868
|
+
act: react.useCallback((target, action) => {
|
|
732
869
|
const current = rows[target.key] ?? IDLE$1;
|
|
870
|
+
if (action === "recheck") {
|
|
871
|
+
patch(target.key, {
|
|
872
|
+
phase: "checking",
|
|
873
|
+
outcome: current.outcome,
|
|
874
|
+
failure: null,
|
|
875
|
+
stale: false
|
|
876
|
+
});
|
|
877
|
+
(async () => {
|
|
878
|
+
const checked = await checkTarget(getCall(), target);
|
|
879
|
+
patch(target.key, checked.ok ? {
|
|
880
|
+
phase: "ready",
|
|
881
|
+
outcome: checked.value,
|
|
882
|
+
failure: null,
|
|
883
|
+
stale: false
|
|
884
|
+
} : {
|
|
885
|
+
phase: "failed",
|
|
886
|
+
outcome: null,
|
|
887
|
+
failure: checked,
|
|
888
|
+
stale: false
|
|
889
|
+
});
|
|
890
|
+
})();
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
733
893
|
patch(target.key, {
|
|
734
894
|
...current,
|
|
735
895
|
phase: "installing",
|
|
736
|
-
failure: null
|
|
896
|
+
failure: null,
|
|
897
|
+
stale: false
|
|
737
898
|
});
|
|
738
899
|
(async () => {
|
|
739
900
|
const absent = isAbsent(target);
|
|
@@ -744,29 +905,36 @@ window.__ModuleLoader__.load({
|
|
|
744
905
|
patch(target.key, {
|
|
745
906
|
phase: "failed",
|
|
746
907
|
outcome: null,
|
|
747
|
-
failure: checked
|
|
908
|
+
failure: checked,
|
|
909
|
+
stale: false
|
|
748
910
|
});
|
|
749
911
|
return;
|
|
750
912
|
}
|
|
751
913
|
patch(target.key, {
|
|
752
914
|
phase: "installing",
|
|
753
915
|
outcome: checked.value,
|
|
754
|
-
failure: null
|
|
916
|
+
failure: null,
|
|
917
|
+
stale: false
|
|
755
918
|
});
|
|
756
919
|
version = checked.value.snapshot.latestVersion;
|
|
757
920
|
}
|
|
758
921
|
const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMs);
|
|
759
922
|
if (result.ok) {
|
|
923
|
+
const before = current.outcome?.snapshot.installedVersion ?? null;
|
|
760
924
|
const refreshed = await checkTarget(getCall(), target, target.phones?.status);
|
|
925
|
+
const after = refreshed.ok ? refreshed.value.snapshot.installedVersion ?? null : null;
|
|
761
926
|
patch(target.key, refreshed.ok ? {
|
|
762
927
|
phase: "ready",
|
|
763
928
|
outcome: refreshed.value,
|
|
764
|
-
failure: null
|
|
929
|
+
failure: null,
|
|
930
|
+
stale: false
|
|
765
931
|
} : {
|
|
766
932
|
phase: "failed",
|
|
767
933
|
outcome: null,
|
|
768
|
-
failure: refreshed
|
|
934
|
+
failure: refreshed,
|
|
935
|
+
stale: false
|
|
769
936
|
});
|
|
937
|
+
if (after !== null && after !== before) staleOthers(target.key);
|
|
770
938
|
await reload();
|
|
771
939
|
return;
|
|
772
940
|
}
|
|
@@ -776,7 +944,8 @@ window.__ModuleLoader__.load({
|
|
|
776
944
|
failure: {
|
|
777
945
|
...result,
|
|
778
946
|
manual: result.manual ?? current.outcome?.manual ?? null
|
|
779
|
-
}
|
|
947
|
+
},
|
|
948
|
+
stale: false
|
|
780
949
|
});
|
|
781
950
|
})();
|
|
782
951
|
}, [
|
|
@@ -784,7 +953,8 @@ window.__ModuleLoader__.load({
|
|
|
784
953
|
patch,
|
|
785
954
|
pollMs,
|
|
786
955
|
reload,
|
|
787
|
-
rows
|
|
956
|
+
rows,
|
|
957
|
+
staleOthers
|
|
788
958
|
])
|
|
789
959
|
};
|
|
790
960
|
}
|
|
@@ -801,20 +971,20 @@ window.__ModuleLoader__.load({
|
|
|
801
971
|
} : PANEL_STYLE.btn,
|
|
802
972
|
disabled,
|
|
803
973
|
onClick: checkAll,
|
|
804
|
-
title: "
|
|
974
|
+
title: "检查总管与六家插件的版本"
|
|
805
975
|
}, checking ? "检查中…" : "检查更新");
|
|
806
976
|
}
|
|
807
|
-
/**
|
|
977
|
+
/** 待重启横幅:一行总账,只点名哪几家要重启(理由与做法写在各家卡片上,见 `restartBannerText`)。 */
|
|
808
978
|
function RestartBanners(props) {
|
|
809
|
-
const
|
|
979
|
+
const titles = [];
|
|
810
980
|
for (const target of props.face.targets) {
|
|
811
981
|
const outcome = props.face.rows[target.key]?.outcome;
|
|
812
982
|
if (!outcome) continue;
|
|
813
|
-
|
|
814
|
-
if (text) lines.push(text);
|
|
983
|
+
if (restartPendingOf(outcome.snapshot)) titles.push(target.title);
|
|
815
984
|
}
|
|
816
|
-
|
|
817
|
-
|
|
985
|
+
const text = restartBannerText(titles);
|
|
986
|
+
if (text === null) return null;
|
|
987
|
+
return react.createElement("div", { style: PANEL_STYLE.banner }, text);
|
|
818
988
|
}
|
|
819
989
|
/** 七家结果:装不了的与待重启的常驻(横幅),详细结果放**居中浮层**(点「检查更新」开)。
|
|
820
990
|
*
|
|
@@ -837,10 +1007,11 @@ window.__ModuleLoader__.load({
|
|
|
837
1007
|
const body = shown.map((target) => {
|
|
838
1008
|
const row = rows[target.key];
|
|
839
1009
|
const snapshot = row.outcome?.snapshot ?? null;
|
|
840
|
-
const verdict = snapshot ? verdictOf(target, snapshot) : null;
|
|
1010
|
+
const verdict = row.stale === true ? staleVerdict() : snapshot ? verdictOf(target, snapshot) : null;
|
|
841
1011
|
const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
|
|
842
1012
|
const busy = row.phase === "installing";
|
|
843
|
-
const showManual = manual !== null && (row.phase === "failed" || verdict?.kind === "blocked");
|
|
1013
|
+
const showManual = manual !== null && (row.phase === "failed" || verdict?.manualHint === true || verdict?.kind === "blocked" && verdict.action === null);
|
|
1014
|
+
const buttonAction = cardActionOf(verdict, row.phase);
|
|
844
1015
|
const tone = rowToneOf(row, verdict);
|
|
845
1016
|
const paint = ROW_TONE[tone];
|
|
846
1017
|
/** 这一行「当前/最新」两个版本号:不等才算有新版,把「最新」那格上色(用户扫一眼先看有没有箭头)。 */
|
|
@@ -871,7 +1042,7 @@ window.__ModuleLoader__.load({
|
|
|
871
1042
|
} : {
|
|
872
1043
|
...PANEL_STYLE.verdictLine,
|
|
873
1044
|
color: "var(--dsw-alias-label-secondary, #9a9a9a)"
|
|
874
|
-
} }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")),
|
|
1045
|
+
} }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), buttonAction ? react.createElement("button", {
|
|
875
1046
|
type: "button",
|
|
876
1047
|
style: busy ? {
|
|
877
1048
|
...PANEL_STYLE.btnPrimary,
|
|
@@ -879,11 +1050,13 @@ window.__ModuleLoader__.load({
|
|
|
879
1050
|
cursor: "default"
|
|
880
1051
|
} : PANEL_STYLE.btnPrimary,
|
|
881
1052
|
disabled: busy,
|
|
882
|
-
onClick: () =>
|
|
883
|
-
|
|
1053
|
+
onClick: () => {
|
|
1054
|
+
props.face.act(target, buttonAction);
|
|
1055
|
+
}
|
|
1056
|
+
}, 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", {
|
|
884
1057
|
key: String(index),
|
|
885
1058
|
style: PANEL_STYLE.skill
|
|
886
|
-
}, line)), showManual ? react.createElement("code", { style: PANEL_STYLE.cmd }, manual) : null);
|
|
1059
|
+
}, line)), showManual ? react.createElement("div", { style: PANEL_STYLE.skill }, "在终端里执行这条命令可以手动完成这一步:") : null, showManual ? react.createElement("code", { style: PANEL_STYLE.cmd }, manual) : null);
|
|
887
1060
|
});
|
|
888
1061
|
return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), open ? react.createElement("div", {
|
|
889
1062
|
style: PANEL_STYLE.overlay,
|
|
@@ -905,7 +1078,7 @@ window.__ModuleLoader__.load({
|
|
|
905
1078
|
onClick: () => close(),
|
|
906
1079
|
title: "关闭",
|
|
907
1080
|
"aria-label": "关闭"
|
|
908
|
-
}, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "
|
|
1081
|
+
}, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "安装在宿主后台继续:关掉这块不会中断它。再点「检查更新」看最新进度。")))) : null);
|
|
909
1082
|
}
|
|
910
1083
|
/** 缺席卡:**按两处独立事实分三态**,每态只给能改变状态的动作。
|
|
911
1084
|
*
|
|
@@ -926,17 +1099,19 @@ window.__ModuleLoader__.load({
|
|
|
926
1099
|
const manual = (target ? manualForDisplay(target, row?.failure?.manual ?? row?.outcome?.manual ?? null) : null) ?? props.fallbackCommand;
|
|
927
1100
|
const installed = target ? target.installedVersion ?? target.runningVersion : null;
|
|
928
1101
|
const state = slotStateOf(target, props.inLedger);
|
|
1102
|
+
const blockedVerdict = target && row?.outcome && row.outcome.snapshot.blockedReason !== null ? verdictOf(target, row.outcome.snapshot) : null;
|
|
929
1103
|
let reason = row?.failure?.message ?? null;
|
|
930
|
-
if (
|
|
1104
|
+
if (reason === null && blockedVerdict) reason = blockedVerdict.text;
|
|
1105
|
+
const retryAction = row ? cardActionOf(blockedVerdict, row.phase) : null;
|
|
931
1106
|
let headline;
|
|
932
1107
|
switch (state) {
|
|
933
1108
|
case "absent":
|
|
934
1109
|
headline = target ? "未安装[" + target.packageName + "],装上后这个页签就能用了。" : "这个页签对应的插件还没装。";
|
|
935
1110
|
break;
|
|
936
1111
|
case "unregistered-product":
|
|
937
|
-
headline = target.packageName + " " + String(installed) + "
|
|
1112
|
+
headline = target.packageName + " " + String(installed) + " 已安装,但这一版的插件包里没有「爱生活页签」的注册代码:要等它发布带注册代码的新版本。重启和重装都不会改变这一点。";
|
|
938
1113
|
break;
|
|
939
|
-
default: headline = target.packageName + " " + String(installed) + "
|
|
1114
|
+
default: headline = target.packageName + " " + String(installed) + " 已安装,插件包里也有注册代码,但它的设置页没有加载到面板:重启 DSH 后再看这个页签;仍然这样,就在 DSH 日志里找它的报错。";
|
|
940
1115
|
}
|
|
941
1116
|
return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
|
|
942
1117
|
type: "button",
|
|
@@ -950,9 +1125,23 @@ window.__ModuleLoader__.load({
|
|
|
950
1125
|
},
|
|
951
1126
|
disabled: busy,
|
|
952
1127
|
onClick: () => {
|
|
953
|
-
if (target) props.face.act(target);
|
|
1128
|
+
if (target) props.face.act(target, "install");
|
|
954
1129
|
}
|
|
955
|
-
}, busy ? "正在装,请稍候…" : "装上") : null,
|
|
1130
|
+
}, busy ? "正在装,请稍候…" : "装上") : null, retryAction === "recheck" && target ? react.createElement("button", {
|
|
1131
|
+
type: "button",
|
|
1132
|
+
style: busy ? {
|
|
1133
|
+
...PANEL_STYLE.btn,
|
|
1134
|
+
marginTop: 8,
|
|
1135
|
+
marginLeft: 8,
|
|
1136
|
+
opacity: .55
|
|
1137
|
+
} : {
|
|
1138
|
+
...PANEL_STYLE.btn,
|
|
1139
|
+
marginTop: 8,
|
|
1140
|
+
marginLeft: 8
|
|
1141
|
+
},
|
|
1142
|
+
disabled: busy,
|
|
1143
|
+
onClick: () => props.face.act(target, "recheck")
|
|
1144
|
+
}, ACTION_LABEL.recheck) : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
|
|
956
1145
|
}
|
|
957
1146
|
//#endregion
|
|
958
1147
|
//#region src/health-contract.ts
|
|
@@ -998,20 +1187,38 @@ window.__ModuleLoader__.load({
|
|
|
998
1187
|
/** 面板到各家的配置体检:一次取数、六份报告(纯异步函数:无 DOM、无 react;传输口注入,便于单测)。
|
|
999
1188
|
*
|
|
1000
1189
|
* 三条纪律:
|
|
1001
|
-
* ·
|
|
1002
|
-
*
|
|
1190
|
+
* · **只走各家自己的电话**(`rpc.call(CARRIER_BASE, <电话名>, {method, payload}, …)`)——总管不认任何
|
|
1191
|
+
* 一家的端点、也不加任何单品的依赖;电话名与端点名都是各家契约件公开的事实。
|
|
1192
|
+
* ⚠ **载体形状**(#735 真机踩到):第一段必须是**载体基段** `/api`,第二段是那家的电话名
|
|
1193
|
+
* (=它注册在 `/api<通道>` 上的那一段),端点名写进载荷的 `method`。写错第一段就是 404,
|
|
1194
|
+
* 而 404 只落成「那一家取不到数」,屏上看起来像按钮坏了(先例:总管自己的电话在 #678 栽过同一处,
|
|
1195
|
+
* 见 `update-contract.ts` 的 `CARRIER_BASE`)。
|
|
1003
1196
|
* · **一次取数,六份报告**:总览那行与各家那张表读的是同一份结果(见 `health-panel.ts` 的单一状态源),
|
|
1004
1197
|
* 不许两处各算一遍。
|
|
1005
1198
|
* · 有界超时:一家不回不该拖死整屏(技能侧 spawn 上限 20 s,这里给 25 s)。
|
|
1006
1199
|
*/
|
|
1007
|
-
/**
|
|
1008
|
-
|
|
1200
|
+
/** 一家的电话名:通道名去掉前导斜杠那一段(各家宿主半注册在 `/api` + 通道名,信封 `method` 就是它)。 */
|
|
1201
|
+
function phoneOf(channel) {
|
|
1202
|
+
return channel.startsWith("/") ? channel.slice(1) : channel;
|
|
1203
|
+
}
|
|
1204
|
+
/** 整批取数的时限(毫秒),**不是**一家一份:六家的活是在**同一个宿主线程**上排队干的。
|
|
1205
|
+
*
|
|
1206
|
+
* 为什么不是「技能侧 spawn 上限 20 s + 一点余量」(票 #740 对抗式审查第二轮逮到):
|
|
1207
|
+
* 各家的宿主半用的是 `spawnSync`(各单品的 `src/bridge.ts` 里 `SPAWN_TIMEOUT_MS = 20_000`),
|
|
1208
|
+
* 它**阻塞**宿主的事件循环——六通电话同时打进来,真正干活的是一个接一个串起来的。
|
|
1209
|
+
* 于是「六家之和」才是这一批的真实上界:按一家 20 s 算最坏 120 s,常见情形(一家 1~3 s)约 10 s。
|
|
1210
|
+
* 60 s 取两者之间:给常见情形留足余量,又不至于让一次卡死把按钮按成两分钟不动。
|
|
1211
|
+
* 单家的错照样逐家报(那家页签里的体检表会写「体检超时(60s)…」)。 */
|
|
1212
|
+
const HEALTH_TIMEOUT_MS = 6e4;
|
|
1009
1213
|
function timeoutText() {
|
|
1010
1214
|
return "体检超时(" + String(Math.round(HEALTH_TIMEOUT_MS / 1e3)) + "s):技能没在时限内回话。";
|
|
1011
1215
|
}
|
|
1012
1216
|
async function fetchOne(call, row) {
|
|
1013
1217
|
try {
|
|
1014
|
-
const result = await call(row.channel,
|
|
1218
|
+
const result = await call(CARRIER_BASE, phoneOf(row.channel), {
|
|
1219
|
+
method: HEALTH_ENDPOINT,
|
|
1220
|
+
payload: {}
|
|
1221
|
+
}, AbortSignal.timeout(HEALTH_TIMEOUT_MS));
|
|
1015
1222
|
if (!result || typeof result !== "object" || typeof result.ok !== "boolean") return {
|
|
1016
1223
|
report: null,
|
|
1017
1224
|
error: "回执信封异常(非 ok 信封)。"
|
|
@@ -1118,6 +1325,17 @@ window.__ModuleLoader__.load({
|
|
|
1118
1325
|
} catch {}
|
|
1119
1326
|
return face;
|
|
1120
1327
|
}
|
|
1328
|
+
/** 汇总行该显示哪一句错(票 #735):取数过程本身的错优先;**一家报告都没回来**时,把第一条家错误顶上来。
|
|
1329
|
+
*
|
|
1330
|
+
* 为什么要有这一格:各家的错只画在那家页签里的体检表上,一行摘要看不见 ⇒ 用户点完只见「体检中…」闪一下、
|
|
1331
|
+
* 接着仍旧「还没体检」,会以为按钮坏了(真机上就是这么被误判的;那次六家全 404,汇总行一声不吭)。
|
|
1332
|
+
* 有报告回来时不顶:那时各家那张表里已经把错说清楚了,摘要行只管读数。 */
|
|
1333
|
+
function summaryErrorOf(face) {
|
|
1334
|
+
if (face.error !== null) return face.error;
|
|
1335
|
+
const rows = Object.values(face.rows);
|
|
1336
|
+
if (rows.some((row) => row.report !== null)) return null;
|
|
1337
|
+
return rows.find((row) => row.error !== null)?.error ?? null;
|
|
1338
|
+
}
|
|
1121
1339
|
//#endregion
|
|
1122
1340
|
//#region src/health-view.ts
|
|
1123
1341
|
/** 配置体检的两个画法:面板顶部那行总览(六家一盏灯)+ 各家那张表。
|
|
@@ -1339,15 +1557,26 @@ window.__ModuleLoader__.load({
|
|
|
1339
1557
|
}
|
|
1340
1558
|
/** 几截读数之间那个分隔符:不许染档位色(它不属于任何一档)。 */
|
|
1341
1559
|
const SEG_SEP = " · ";
|
|
1342
|
-
/** 汇总行那一句:绿要显眼(它是「正常」这件事本身),没通道的那几家不冒充绿。
|
|
1560
|
+
/** 汇总行那一句:绿要显眼(它是「正常」这件事本身),没通道的那几家不冒充绿。
|
|
1561
|
+
*
|
|
1562
|
+
* 「没有体检出口」与「还没跑过体检」是**两件事**(票 #735):前者那颗「体检一次」按下去不会有任何反应,
|
|
1563
|
+
* 必须自己说出来——两态共用一句「还没体检」时,用户与诊断都会被引到「点了没跑」上去。 */
|
|
1343
1564
|
function overallText(lights) {
|
|
1344
1565
|
const pending = lights.filter((light) => !light.hasChannel).length;
|
|
1345
|
-
const
|
|
1346
|
-
const tail = pending > 0 ? " · " + String(pending) + "
|
|
1347
|
-
if (
|
|
1566
|
+
const withChannel = lights.filter((light) => light.hasChannel);
|
|
1567
|
+
const tail = pending > 0 ? " · " + String(pending) + " 家没有体检出口" : "";
|
|
1568
|
+
if (withChannel.length === 0) {
|
|
1569
|
+
const what = lights.length === 0 ? "还没有任何一家登记体检出口" : String(lights.length) + " 家都没有体检出口(通道名没登记上)";
|
|
1570
|
+
return react.createElement("span", { style: {
|
|
1571
|
+
color: STATUS_TEXT.red,
|
|
1572
|
+
fontWeight: 700
|
|
1573
|
+
} }, "没有可体检的家:" + what);
|
|
1574
|
+
}
|
|
1575
|
+
const allGreen = withChannel.every((light) => light.status === "green");
|
|
1576
|
+
if (allGreen && pending === 0) return react.createElement("span", { style: {
|
|
1348
1577
|
color: STATUS_COLOR.green,
|
|
1349
1578
|
fontWeight: 700
|
|
1350
|
-
} }, "六家正常"
|
|
1579
|
+
} }, "六家正常");
|
|
1351
1580
|
const parts = [];
|
|
1352
1581
|
for (const status of ["red", "yellow"]) {
|
|
1353
1582
|
let count = 0;
|
|
@@ -1365,7 +1594,7 @@ window.__ModuleLoader__.load({
|
|
|
1365
1594
|
}
|
|
1366
1595
|
}, STATUS_LABEL[status] + " " + String(count)));
|
|
1367
1596
|
}
|
|
1368
|
-
if (parts.length === 0) return react.createElement("span", { style: { color: INK_DIM } }, "还没体检" + tail);
|
|
1597
|
+
if (parts.length === 0) return react.createElement("span", { style: { color: INK_DIM } }, (allGreen ? "有出口的都正常" : "还没体检") + tail);
|
|
1369
1598
|
parts.push(react.createElement("span", {
|
|
1370
1599
|
key: "todo",
|
|
1371
1600
|
style: { color: INK }
|
|
@@ -1409,11 +1638,10 @@ window.__ModuleLoader__.load({
|
|
|
1409
1638
|
* 与页签的分工:这行只答「一共几条要处理」;「哪几家、各几条」由页签的圆点与小数字答;
|
|
1410
1639
|
* 「具体哪一条坏了、去哪修」由那家页签里的 `HealthTable` 答。 */
|
|
1411
1640
|
function HealthSummaryLine(props) {
|
|
1412
|
-
const hasLights = props.lights.length > 0;
|
|
1413
1641
|
return react.createElement("div", {
|
|
1414
1642
|
style: HEALTH_STYLE.summaryRow,
|
|
1415
1643
|
"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) :
|
|
1644
|
+
}, 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
1645
|
type: "button",
|
|
1418
1646
|
style: props.running ? {
|
|
1419
1647
|
...HEALTH_STYLE.btn,
|
|
@@ -1533,6 +1761,9 @@ window.__ModuleLoader__.load({
|
|
|
1533
1761
|
* 文案与网址都取自 nav.ts 的两张静态表,本文件只画不算。
|
|
1534
1762
|
*/
|
|
1535
1763
|
const inject = ["slots", "connection"];
|
|
1764
|
+
/** 包名 → 本家那条客户端通道(票 #735)。取值面是导航表那份镜像,不再从页签槽账本的自定义选项里读:
|
|
1765
|
+
* 装机槽位面不透传自定义键,读了恒是空串(详见 `nav.ts` 上 `ManagerTab.channel` 的注释)。 */
|
|
1766
|
+
const CHANNEL_BY_PLUGIN = new Map(MANAGER_TABS.map((tab) => [tab.plugin, tab.channel]));
|
|
1536
1767
|
/** 总管视觉(内联 style;颜色走 DSH 主题别名,深浅主题自适应,写死值只做回退;与技能面板同语言)。 */
|
|
1537
1768
|
const S = {
|
|
1538
1769
|
headRow: {
|
|
@@ -1710,6 +1941,23 @@ window.__ModuleLoader__.load({
|
|
|
1710
1941
|
style: S.moreRow
|
|
1711
1942
|
}, 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
1943
|
}
|
|
1944
|
+
/** 总管自述版本(票 #737):**不手写**,问宿主——宿主读自己这份已安装包的 `package.json`。
|
|
1945
|
+
*
|
|
1946
|
+
* 单次取数(`loadManagerVersion`),无轮询;初值 `unknown`,读不到也停在 `unknown`。
|
|
1947
|
+
* 与卡路里 #130 同一条路:面板是浏览器产物、禁 node 内建,读盘只许在宿主半。 */
|
|
1948
|
+
function useManagerVersion(getCall) {
|
|
1949
|
+
const [version, setVersion] = react.useState(VERSION_UNKNOWN);
|
|
1950
|
+
react.useEffect(() => {
|
|
1951
|
+
let alive = true;
|
|
1952
|
+
loadManagerVersion(getCall()).then((next) => {
|
|
1953
|
+
if (alive) setVersion(next);
|
|
1954
|
+
});
|
|
1955
|
+
return () => {
|
|
1956
|
+
alive = false;
|
|
1957
|
+
};
|
|
1958
|
+
}, [getCall]);
|
|
1959
|
+
return version;
|
|
1960
|
+
}
|
|
1713
1961
|
/** 爱生活面板:总设置区 + 检查更新(七家)+ 爱生活页签条(slot 驱动)+ 技能设置页投影/缺席卡。 */
|
|
1714
1962
|
function LifePackSection(props) {
|
|
1715
1963
|
const tabsId = react.useId();
|
|
@@ -1717,6 +1965,7 @@ window.__ModuleLoader__.load({
|
|
|
1717
1965
|
const rows = props.useTabs((value) => value);
|
|
1718
1966
|
const present = new Set(rows.map((r) => r.id));
|
|
1719
1967
|
const face = useUpdateRows(props.getCall);
|
|
1968
|
+
const managerVersion = useManagerVersion(props.getCall);
|
|
1720
1969
|
const health = useHealthPanel(() => props.getCall(), react.useMemo(() => rows.filter((row) => row.channel.length > 0).map((row) => ({
|
|
1721
1970
|
id: row.id,
|
|
1722
1971
|
channel: row.channel
|
|
@@ -1766,10 +2015,10 @@ window.__ModuleLoader__.load({
|
|
|
1766
2015
|
setActiveId(next.plugin);
|
|
1767
2016
|
tabRefs.current[nextIndex]?.focus();
|
|
1768
2017
|
}
|
|
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, "
|
|
2018
|
+
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
2019
|
lights,
|
|
1771
2020
|
running: health.running,
|
|
1772
|
-
error: health
|
|
2021
|
+
error: summaryErrorOf(health),
|
|
1773
2022
|
onRun: health.run
|
|
1774
2023
|
}), react.createElement("div", {
|
|
1775
2024
|
role: "tablist",
|
|
@@ -1853,7 +2102,7 @@ window.__ModuleLoader__.load({
|
|
|
1853
2102
|
id: entry.options.id ?? "",
|
|
1854
2103
|
order: entry.options.order ?? 0,
|
|
1855
2104
|
label: resolveLabel(entry.options.label),
|
|
1856
|
-
channel: entry.options.
|
|
2105
|
+
channel: CHANNEL_BY_PLUGIN.get(entry.options.id ?? "") ?? ""
|
|
1857
2106
|
})).sort((a, b) => a.order - b.order);
|
|
1858
2107
|
}
|
|
1859
2108
|
return tabs;
|