dsh-life-pack 0.3.0 → 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 +234 -73
- package/dist/client.js.map +1 -1
- package/dist/health-fetch.d.ts +9 -2
- package/dist/health-fetch.d.ts.map +1 -1
- package/dist/health-fetch.js +9 -2
- package/dist/health-fetch.js.map +1 -1
- package/dist/update-contract.d.ts.map +1 -1
- package/dist/update-contract.js +29 -18
- 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-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
|
@@ -186,28 +186,37 @@ window.__ModuleLoader__.load({
|
|
|
186
186
|
* 一处定义、两侧引用:宿主半 `manager-version.ts` 读失败时回它,面板半 `update-client.ts`
|
|
187
187
|
* 归一化时也认它——面板侧不许 import 宿主半(会把 node 内建带进浏览器束)。 */
|
|
188
188
|
const VERSION_UNKNOWN = "unknown";
|
|
189
|
-
/** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。
|
|
189
|
+
/** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。
|
|
190
|
+
*
|
|
191
|
+
* 每条都要三段齐全(票 #740 的文案判据):**事实**(哪里对不上)→ **后果**(这家现在能不能装)
|
|
192
|
+
* → **动作**(用户做得到、且面板上真有的那一步;面板代劳不了的走「下面这条命令」)。
|
|
193
|
+
* 禁用口语与自造词:「半截任务」「电话没接上」「回执异常」「还出现就重装」「查宿主日志」一律不写。
|
|
194
|
+
*
|
|
195
|
+
* 点名的按钮必须是**这一行上真够得着**的那颗(票 #740 第三轮):结果浮层一开就盖住题头的
|
|
196
|
+
* 「检查更新」,所以「改完再读一次」这类下一步一律写「重新检查」(画在那一行上);
|
|
197
|
+
* 只有发生在**重启之后**的下一步才写「检查更新」。反过来,凡是写着「下面这条命令」的那几句,
|
|
198
|
+
* 那一行的命令块必须真出来(判据 `Verdict.manualHint`)——两处都由门禁守着。 */
|
|
190
199
|
const REASON_TEXT = {
|
|
191
|
-
"unknown-profile": "
|
|
192
|
-
"source-install": "
|
|
193
|
-
"invalid-installation": "
|
|
194
|
-
"installation-changed": "
|
|
195
|
-
"pending-restart": "
|
|
196
|
-
"registry-conflict": "
|
|
197
|
-
"incompatible-node": "
|
|
198
|
-
"recovery-required": "
|
|
199
|
-
"check-failed": "
|
|
200
|
-
"invalid-release": "
|
|
201
|
-
"check-expired": "
|
|
202
|
-
"update-busy": "
|
|
203
|
-
"install-failed": "
|
|
204
|
-
"manager-unreachable": "
|
|
205
|
-
"bad-request": "
|
|
206
|
-
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。"
|
|
207
216
|
};
|
|
208
217
|
/** 原因码 → 人话(认不出的码不猜、原样回码并说明这是未知原因)。 */
|
|
209
218
|
function reasonText(code) {
|
|
210
|
-
return REASON_TEXT[code] ?? "
|
|
219
|
+
return REASON_TEXT[code] ?? "面板不认识这个原因码(" + code + ")。请在 DSH 日志里找同一处报错,报到本仓 issue。";
|
|
211
220
|
}
|
|
212
221
|
//#endregion
|
|
213
222
|
//#region src/update-client.ts
|
|
@@ -357,57 +366,136 @@ window.__ModuleLoader__.load({
|
|
|
357
366
|
if (target.panelRegistered === false) return "unregistered-product";
|
|
358
367
|
return "not-connected";
|
|
359
368
|
}
|
|
360
|
-
/**
|
|
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
|
+
/** 把快照翻成一行结论:还没查 / 已是最新 / 有新版 / 待重启 / 装不了(附人话原因与下一步)。 */
|
|
361
442
|
function verdictOf(target, snapshot) {
|
|
362
443
|
const absent = isAbsent(target);
|
|
363
444
|
const latest = snapshot.latestVersion;
|
|
364
445
|
if (absent && latest === null) return {
|
|
365
446
|
kind: "unknown",
|
|
366
|
-
text: "
|
|
367
|
-
action:
|
|
447
|
+
text: "未安装。点「重新检查」查最新版本,再点「装上」。",
|
|
448
|
+
action: "recheck"
|
|
368
449
|
};
|
|
369
450
|
if (absent) return {
|
|
370
451
|
kind: "update-available",
|
|
371
|
-
text: "未安装;最新版本 " + String(latest) + "
|
|
452
|
+
text: "未安装;最新版本 " + String(latest) + "。点「装上」安装。",
|
|
372
453
|
action: "install"
|
|
373
454
|
};
|
|
374
|
-
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) {
|
|
375
463
|
const blocked = snapshot.blockedReason;
|
|
376
|
-
if (blocked === "pending-restart") return {
|
|
377
|
-
kind: "blocked",
|
|
378
|
-
text: reasonText(blocked),
|
|
379
|
-
action: null
|
|
380
|
-
};
|
|
381
464
|
return {
|
|
382
465
|
kind: "blocked",
|
|
383
466
|
text: reasonText(blocked),
|
|
384
|
-
action:
|
|
467
|
+
action: actionForBlocked(blocked),
|
|
468
|
+
manualHint: NEEDS_MANUAL.has(blocked)
|
|
385
469
|
};
|
|
386
470
|
}
|
|
387
471
|
if (latest === null) return {
|
|
388
472
|
kind: "unknown",
|
|
389
|
-
text: "
|
|
390
|
-
action:
|
|
473
|
+
text: "还没查过最新版本。点「重新检查」。",
|
|
474
|
+
action: "recheck"
|
|
391
475
|
};
|
|
392
476
|
if (snapshot.canInstall) return {
|
|
393
477
|
kind: "update-available",
|
|
394
|
-
text: "
|
|
478
|
+
text: "有新版本 " + String(latest) + "(正在运行 " + snapshot.runningVersion + ")。点「装上更新」。",
|
|
395
479
|
action: "update"
|
|
396
480
|
};
|
|
481
|
+
if (hasNewer) return {
|
|
482
|
+
kind: "update-available",
|
|
483
|
+
text: "有新版本 " + String(latest) + "(正在运行 " + snapshot.runningVersion + ")。点「重新检查」,再点「装上更新」。",
|
|
484
|
+
action: "recheck"
|
|
485
|
+
};
|
|
397
486
|
return {
|
|
398
487
|
kind: "up-to-date",
|
|
399
|
-
text: "
|
|
488
|
+
text: "已是最新版本 " + String(latest) + "。",
|
|
400
489
|
action: null
|
|
401
490
|
};
|
|
402
491
|
}
|
|
403
|
-
/**
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
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("、");
|
|
411
499
|
}
|
|
412
500
|
/** 面板展示的那条可复制命令(用户会照着敲的那一条)。
|
|
413
501
|
*
|
|
@@ -692,6 +780,13 @@ window.__ModuleLoader__.load({
|
|
|
692
780
|
default: return "idle";
|
|
693
781
|
}
|
|
694
782
|
}
|
|
783
|
+
/** 四个动作在屏上的名字:按钮文案与判据同源(票 #740)。 */
|
|
784
|
+
const ACTION_LABEL = {
|
|
785
|
+
install: "装上",
|
|
786
|
+
update: "装上更新",
|
|
787
|
+
retry: "重试安装",
|
|
788
|
+
recheck: "重新检查"
|
|
789
|
+
};
|
|
695
790
|
const IDLE$1 = {
|
|
696
791
|
phase: "idle",
|
|
697
792
|
outcome: null,
|
|
@@ -728,6 +823,10 @@ window.__ModuleLoader__.load({
|
|
|
728
823
|
[key]: next
|
|
729
824
|
}));
|
|
730
825
|
}, []);
|
|
826
|
+
/** 装成一家 ⇒ 其余各家的旧快照一律作废(票 #740):判据见 `markStaleOthers`。 */
|
|
827
|
+
const staleOthers = react.useCallback((keepKey) => {
|
|
828
|
+
setRows((previous) => markStaleOthers(previous, keepKey));
|
|
829
|
+
}, []);
|
|
731
830
|
return {
|
|
732
831
|
targets,
|
|
733
832
|
rows,
|
|
@@ -743,18 +842,21 @@ window.__ModuleLoader__.load({
|
|
|
743
842
|
for (const target of list) patch(target.key, {
|
|
744
843
|
phase: "checking",
|
|
745
844
|
outcome: rows[target.key]?.outcome ?? null,
|
|
746
|
-
failure: null
|
|
845
|
+
failure: null,
|
|
846
|
+
stale: false
|
|
747
847
|
});
|
|
748
848
|
Promise.all(list.map(async (target) => {
|
|
749
849
|
const result = await checkTarget(getCall(), target);
|
|
750
850
|
patch(target.key, result.ok ? {
|
|
751
851
|
phase: "ready",
|
|
752
852
|
outcome: result.value,
|
|
753
|
-
failure: null
|
|
853
|
+
failure: null,
|
|
854
|
+
stale: false
|
|
754
855
|
} : {
|
|
755
856
|
phase: "failed",
|
|
756
857
|
outcome: null,
|
|
757
|
-
failure: result
|
|
858
|
+
failure: result,
|
|
859
|
+
stale: false
|
|
758
860
|
});
|
|
759
861
|
})).finally(() => setChecking(false));
|
|
760
862
|
}, [
|
|
@@ -763,12 +865,36 @@ window.__ModuleLoader__.load({
|
|
|
763
865
|
rows,
|
|
764
866
|
targets
|
|
765
867
|
]),
|
|
766
|
-
act: react.useCallback((target) => {
|
|
868
|
+
act: react.useCallback((target, action) => {
|
|
767
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
|
+
}
|
|
768
893
|
patch(target.key, {
|
|
769
894
|
...current,
|
|
770
895
|
phase: "installing",
|
|
771
|
-
failure: null
|
|
896
|
+
failure: null,
|
|
897
|
+
stale: false
|
|
772
898
|
});
|
|
773
899
|
(async () => {
|
|
774
900
|
const absent = isAbsent(target);
|
|
@@ -779,29 +905,36 @@ window.__ModuleLoader__.load({
|
|
|
779
905
|
patch(target.key, {
|
|
780
906
|
phase: "failed",
|
|
781
907
|
outcome: null,
|
|
782
|
-
failure: checked
|
|
908
|
+
failure: checked,
|
|
909
|
+
stale: false
|
|
783
910
|
});
|
|
784
911
|
return;
|
|
785
912
|
}
|
|
786
913
|
patch(target.key, {
|
|
787
914
|
phase: "installing",
|
|
788
915
|
outcome: checked.value,
|
|
789
|
-
failure: null
|
|
916
|
+
failure: null,
|
|
917
|
+
stale: false
|
|
790
918
|
});
|
|
791
919
|
version = checked.value.snapshot.latestVersion;
|
|
792
920
|
}
|
|
793
921
|
const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMs);
|
|
794
922
|
if (result.ok) {
|
|
923
|
+
const before = current.outcome?.snapshot.installedVersion ?? null;
|
|
795
924
|
const refreshed = await checkTarget(getCall(), target, target.phones?.status);
|
|
925
|
+
const after = refreshed.ok ? refreshed.value.snapshot.installedVersion ?? null : null;
|
|
796
926
|
patch(target.key, refreshed.ok ? {
|
|
797
927
|
phase: "ready",
|
|
798
928
|
outcome: refreshed.value,
|
|
799
|
-
failure: null
|
|
929
|
+
failure: null,
|
|
930
|
+
stale: false
|
|
800
931
|
} : {
|
|
801
932
|
phase: "failed",
|
|
802
933
|
outcome: null,
|
|
803
|
-
failure: refreshed
|
|
934
|
+
failure: refreshed,
|
|
935
|
+
stale: false
|
|
804
936
|
});
|
|
937
|
+
if (after !== null && after !== before) staleOthers(target.key);
|
|
805
938
|
await reload();
|
|
806
939
|
return;
|
|
807
940
|
}
|
|
@@ -811,7 +944,8 @@ window.__ModuleLoader__.load({
|
|
|
811
944
|
failure: {
|
|
812
945
|
...result,
|
|
813
946
|
manual: result.manual ?? current.outcome?.manual ?? null
|
|
814
|
-
}
|
|
947
|
+
},
|
|
948
|
+
stale: false
|
|
815
949
|
});
|
|
816
950
|
})();
|
|
817
951
|
}, [
|
|
@@ -819,7 +953,8 @@ window.__ModuleLoader__.load({
|
|
|
819
953
|
patch,
|
|
820
954
|
pollMs,
|
|
821
955
|
reload,
|
|
822
|
-
rows
|
|
956
|
+
rows,
|
|
957
|
+
staleOthers
|
|
823
958
|
])
|
|
824
959
|
};
|
|
825
960
|
}
|
|
@@ -836,20 +971,20 @@ window.__ModuleLoader__.load({
|
|
|
836
971
|
} : PANEL_STYLE.btn,
|
|
837
972
|
disabled,
|
|
838
973
|
onClick: checkAll,
|
|
839
|
-
title: "
|
|
974
|
+
title: "检查总管与六家插件的版本"
|
|
840
975
|
}, checking ? "检查中…" : "检查更新");
|
|
841
976
|
}
|
|
842
|
-
/**
|
|
977
|
+
/** 待重启横幅:一行总账,只点名哪几家要重启(理由与做法写在各家卡片上,见 `restartBannerText`)。 */
|
|
843
978
|
function RestartBanners(props) {
|
|
844
|
-
const
|
|
979
|
+
const titles = [];
|
|
845
980
|
for (const target of props.face.targets) {
|
|
846
981
|
const outcome = props.face.rows[target.key]?.outcome;
|
|
847
982
|
if (!outcome) continue;
|
|
848
|
-
|
|
849
|
-
if (text) lines.push(text);
|
|
983
|
+
if (restartPendingOf(outcome.snapshot)) titles.push(target.title);
|
|
850
984
|
}
|
|
851
|
-
|
|
852
|
-
|
|
985
|
+
const text = restartBannerText(titles);
|
|
986
|
+
if (text === null) return null;
|
|
987
|
+
return react.createElement("div", { style: PANEL_STYLE.banner }, text);
|
|
853
988
|
}
|
|
854
989
|
/** 七家结果:装不了的与待重启的常驻(横幅),详细结果放**居中浮层**(点「检查更新」开)。
|
|
855
990
|
*
|
|
@@ -872,10 +1007,11 @@ window.__ModuleLoader__.load({
|
|
|
872
1007
|
const body = shown.map((target) => {
|
|
873
1008
|
const row = rows[target.key];
|
|
874
1009
|
const snapshot = row.outcome?.snapshot ?? null;
|
|
875
|
-
const verdict = snapshot ? verdictOf(target, snapshot) : null;
|
|
1010
|
+
const verdict = row.stale === true ? staleVerdict() : snapshot ? verdictOf(target, snapshot) : null;
|
|
876
1011
|
const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
|
|
877
1012
|
const busy = row.phase === "installing";
|
|
878
|
-
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);
|
|
879
1015
|
const tone = rowToneOf(row, verdict);
|
|
880
1016
|
const paint = ROW_TONE[tone];
|
|
881
1017
|
/** 这一行「当前/最新」两个版本号:不等才算有新版,把「最新」那格上色(用户扫一眼先看有没有箭头)。 */
|
|
@@ -906,7 +1042,7 @@ window.__ModuleLoader__.load({
|
|
|
906
1042
|
} : {
|
|
907
1043
|
...PANEL_STYLE.verdictLine,
|
|
908
1044
|
color: "var(--dsw-alias-label-secondary, #9a9a9a)"
|
|
909
|
-
} }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")),
|
|
1045
|
+
} }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), buttonAction ? react.createElement("button", {
|
|
910
1046
|
type: "button",
|
|
911
1047
|
style: busy ? {
|
|
912
1048
|
...PANEL_STYLE.btnPrimary,
|
|
@@ -914,11 +1050,13 @@ window.__ModuleLoader__.load({
|
|
|
914
1050
|
cursor: "default"
|
|
915
1051
|
} : PANEL_STYLE.btnPrimary,
|
|
916
1052
|
disabled: busy,
|
|
917
|
-
onClick: () =>
|
|
918
|
-
|
|
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", {
|
|
919
1057
|
key: String(index),
|
|
920
1058
|
style: PANEL_STYLE.skill
|
|
921
|
-
}, 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);
|
|
922
1060
|
});
|
|
923
1061
|
return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), open ? react.createElement("div", {
|
|
924
1062
|
style: PANEL_STYLE.overlay,
|
|
@@ -940,7 +1078,7 @@ window.__ModuleLoader__.load({
|
|
|
940
1078
|
onClick: () => close(),
|
|
941
1079
|
title: "关闭",
|
|
942
1080
|
"aria-label": "关闭"
|
|
943
|
-
}, "×")), 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);
|
|
944
1082
|
}
|
|
945
1083
|
/** 缺席卡:**按两处独立事实分三态**,每态只给能改变状态的动作。
|
|
946
1084
|
*
|
|
@@ -961,17 +1099,19 @@ window.__ModuleLoader__.load({
|
|
|
961
1099
|
const manual = (target ? manualForDisplay(target, row?.failure?.manual ?? row?.outcome?.manual ?? null) : null) ?? props.fallbackCommand;
|
|
962
1100
|
const installed = target ? target.installedVersion ?? target.runningVersion : null;
|
|
963
1101
|
const state = slotStateOf(target, props.inLedger);
|
|
1102
|
+
const blockedVerdict = target && row?.outcome && row.outcome.snapshot.blockedReason !== null ? verdictOf(target, row.outcome.snapshot) : null;
|
|
964
1103
|
let reason = row?.failure?.message ?? null;
|
|
965
|
-
if (
|
|
1104
|
+
if (reason === null && blockedVerdict) reason = blockedVerdict.text;
|
|
1105
|
+
const retryAction = row ? cardActionOf(blockedVerdict, row.phase) : null;
|
|
966
1106
|
let headline;
|
|
967
1107
|
switch (state) {
|
|
968
1108
|
case "absent":
|
|
969
1109
|
headline = target ? "未安装[" + target.packageName + "],装上后这个页签就能用了。" : "这个页签对应的插件还没装。";
|
|
970
1110
|
break;
|
|
971
1111
|
case "unregistered-product":
|
|
972
|
-
headline = target.packageName + " " + String(installed) + "
|
|
1112
|
+
headline = target.packageName + " " + String(installed) + " 已安装,但这一版的插件包里没有「爱生活页签」的注册代码:要等它发布带注册代码的新版本。重启和重装都不会改变这一点。";
|
|
973
1113
|
break;
|
|
974
|
-
default: headline = target.packageName + " " + String(installed) + "
|
|
1114
|
+
default: headline = target.packageName + " " + String(installed) + " 已安装,插件包里也有注册代码,但它的设置页没有加载到面板:重启 DSH 后再看这个页签;仍然这样,就在 DSH 日志里找它的报错。";
|
|
975
1115
|
}
|
|
976
1116
|
return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
|
|
977
1117
|
type: "button",
|
|
@@ -985,9 +1125,23 @@ window.__ModuleLoader__.load({
|
|
|
985
1125
|
},
|
|
986
1126
|
disabled: busy,
|
|
987
1127
|
onClick: () => {
|
|
988
|
-
if (target) props.face.act(target);
|
|
1128
|
+
if (target) props.face.act(target, "install");
|
|
989
1129
|
}
|
|
990
|
-
}, 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));
|
|
991
1145
|
}
|
|
992
1146
|
//#endregion
|
|
993
1147
|
//#region src/health-contract.ts
|
|
@@ -1047,8 +1201,15 @@ window.__ModuleLoader__.load({
|
|
|
1047
1201
|
function phoneOf(channel) {
|
|
1048
1202
|
return channel.startsWith("/") ? channel.slice(1) : channel;
|
|
1049
1203
|
}
|
|
1050
|
-
/**
|
|
1051
|
-
|
|
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;
|
|
1052
1213
|
function timeoutText() {
|
|
1053
1214
|
return "体检超时(" + String(Math.round(HEALTH_TIMEOUT_MS / 1e3)) + "s):技能没在时限内回话。";
|
|
1054
1215
|
}
|