dsh-life-pack 0.2.2 → 0.2.4
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 +543 -14
- package/dist/client.js.map +1 -1
- package/dist/dsh-ctx.d.ts +10 -1
- package/dist/dsh-ctx.d.ts.map +1 -1
- package/dist/health-contract.d.ts +58 -0
- package/dist/health-contract.d.ts.map +1 -0
- package/dist/health-contract.js +64 -0
- package/dist/health-contract.js.map +1 -0
- package/dist/health-fetch.d.ts +36 -0
- package/dist/health-fetch.d.ts.map +1 -0
- package/dist/health-fetch.js +49 -0
- package/dist/health-fetch.js.map +1 -0
- package/dist/health-panel.d.ts +25 -0
- package/dist/health-panel.d.ts.map +1 -0
- package/dist/health-panel.js +63 -0
- package/dist/health-panel.js.map +1 -0
- package/dist/health-view.d.ts +54 -0
- package/dist/health-view.d.ts.map +1 -0
- package/dist/health-view.js +193 -0
- package/dist/health-view.js.map +1 -0
- package/dist/nav.d.ts +1 -1
- package/dist/nav.js +1 -1
- package/dist/update-client.d.ts.map +1 -1
- package/dist/update-client.js +2 -1
- package/dist/update-client.js.map +1 -1
- package/dist/update-contract.d.ts +4 -2
- package/dist/update-contract.d.ts.map +1 -1
- package/dist/update-contract.js +4 -2
- package/dist/update-contract.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -134,11 +134,13 @@ window.__ModuleLoader__.load({
|
|
|
134
134
|
//#region src/update-contract.ts
|
|
135
135
|
/** 载体:宿主注册 `connection.fetch.register` 的路径 / 面板 `connection.rpc.call` 的两段参数。 */
|
|
136
136
|
const MANAGER_RPC = {
|
|
137
|
+
/** 载体基段(`connection.rpc.call` 的**第一段**参数)。传错第一段就是 404:真机上踩过一次。 */
|
|
138
|
+
base: "/api",
|
|
137
139
|
/** 单段通道名,须 match `/^\/[A-Za-z0-9._~-]+$/`(cookbook §6)。 */
|
|
138
140
|
channel: "/ilife-manager",
|
|
139
|
-
/**
|
|
141
|
+
/** 面板侧第二段参数(等于通道名去掉前导斜杠那一段)。 */
|
|
140
142
|
endpoint: "ilife-manager",
|
|
141
|
-
/**
|
|
143
|
+
/** 宿主侧注册的完整路径(= `base` + `channel`)。 */
|
|
142
144
|
path: "/api/ilife-manager"
|
|
143
145
|
};
|
|
144
146
|
/** 总管自有电话名:更新包的三个动作名是冻结集合(`config.ts:14`),这里另起名字不撞车。 */
|
|
@@ -216,7 +218,7 @@ window.__ModuleLoader__.load({
|
|
|
216
218
|
if (typeof call !== "function") return failure("internal", { detail: "宿主连接缺席:connection.rpc.call 不可用" });
|
|
217
219
|
let result;
|
|
218
220
|
try {
|
|
219
|
-
result = await call(MANAGER_RPC.
|
|
221
|
+
result = await call(MANAGER_RPC.base, MANAGER_RPC.endpoint, {
|
|
220
222
|
method,
|
|
221
223
|
payload
|
|
222
224
|
});
|
|
@@ -497,9 +499,58 @@ window.__ModuleLoader__.load({
|
|
|
497
499
|
color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
|
|
498
500
|
fontSize: 12,
|
|
499
501
|
marginTop: 4
|
|
502
|
+
},
|
|
503
|
+
overlay: {
|
|
504
|
+
position: "fixed",
|
|
505
|
+
inset: 0,
|
|
506
|
+
zIndex: 40,
|
|
507
|
+
display: "flex",
|
|
508
|
+
alignItems: "center",
|
|
509
|
+
justifyContent: "center",
|
|
510
|
+
padding: 16,
|
|
511
|
+
background: "rgba(0,0,0,.45)"
|
|
512
|
+
},
|
|
513
|
+
dialog: {
|
|
514
|
+
width: "100%",
|
|
515
|
+
maxWidth: 560,
|
|
516
|
+
maxHeight: "80vh",
|
|
517
|
+
overflow: "auto",
|
|
518
|
+
padding: "12px 14px",
|
|
519
|
+
borderRadius: 12,
|
|
520
|
+
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.4))",
|
|
521
|
+
background: "var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #222))",
|
|
522
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
523
|
+
boxShadow: "0 12px 32px rgba(0,0,0,.35)"
|
|
524
|
+
},
|
|
525
|
+
dialogHead: {
|
|
526
|
+
display: "flex",
|
|
527
|
+
alignItems: "center",
|
|
528
|
+
justifyContent: "space-between",
|
|
529
|
+
gap: 8,
|
|
530
|
+
marginBottom: 8
|
|
531
|
+
},
|
|
532
|
+
dialogTitle: {
|
|
533
|
+
fontSize: 14,
|
|
534
|
+
fontWeight: 700
|
|
535
|
+
},
|
|
536
|
+
close: {
|
|
537
|
+
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
|
|
538
|
+
background: "transparent",
|
|
539
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
540
|
+
borderRadius: 8,
|
|
541
|
+
width: 26,
|
|
542
|
+
height: 26,
|
|
543
|
+
lineHeight: 1,
|
|
544
|
+
fontSize: 14,
|
|
545
|
+
cursor: "pointer"
|
|
546
|
+
},
|
|
547
|
+
dialogNote: {
|
|
548
|
+
color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
|
|
549
|
+
fontSize: 12,
|
|
550
|
+
marginTop: 4
|
|
500
551
|
}
|
|
501
552
|
};
|
|
502
|
-
const IDLE = {
|
|
553
|
+
const IDLE$1 = {
|
|
503
554
|
phase: "idle",
|
|
504
555
|
outcome: null,
|
|
505
556
|
failure: null
|
|
@@ -511,6 +562,7 @@ window.__ModuleLoader__.load({
|
|
|
511
562
|
const [pollMs, setPollMs] = react.useState(1e3);
|
|
512
563
|
const [loadError, setLoadError] = react.useState(null);
|
|
513
564
|
const [checking, setChecking] = react.useState(false);
|
|
565
|
+
const [open, setOpen] = react.useState(false);
|
|
514
566
|
react.useEffect(() => {
|
|
515
567
|
let alive = true;
|
|
516
568
|
(async () => {
|
|
@@ -537,8 +589,11 @@ window.__ModuleLoader__.load({
|
|
|
537
589
|
pollMs,
|
|
538
590
|
loadError,
|
|
539
591
|
checking,
|
|
592
|
+
open,
|
|
593
|
+
close: () => setOpen(false),
|
|
540
594
|
checkAll: react.useCallback(() => {
|
|
541
595
|
setChecking(true);
|
|
596
|
+
setOpen(true);
|
|
542
597
|
const list = targets;
|
|
543
598
|
for (const target of list) patch(target.key, {
|
|
544
599
|
phase: "checking",
|
|
@@ -564,7 +619,7 @@ window.__ModuleLoader__.load({
|
|
|
564
619
|
targets
|
|
565
620
|
]),
|
|
566
621
|
act: react.useCallback((target) => {
|
|
567
|
-
const current = rows[target.key] ?? IDLE;
|
|
622
|
+
const current = rows[target.key] ?? IDLE$1;
|
|
568
623
|
patch(target.key, {
|
|
569
624
|
...current,
|
|
570
625
|
phase: "installing",
|
|
@@ -649,19 +704,31 @@ window.__ModuleLoader__.load({
|
|
|
649
704
|
if (lines.length === 0) return null;
|
|
650
705
|
return react.createElement("div", { style: PANEL_STYLE.banner }, lines.map((line, index) => react.createElement("div", { key: String(index) }, line)));
|
|
651
706
|
}
|
|
652
|
-
/**
|
|
707
|
+
/** 七家结果:装不了的与待重启的常驻(横幅),详细结果放**居中浮层**(点「检查更新」开)。
|
|
708
|
+
*
|
|
709
|
+
* 为什么是浮层:内联展开会把面板本体撑长、且它自身没有关闭控件(真机第一次做成内联后用户反馈
|
|
710
|
+
* 「打开就无法收回了」)。参照实现也是浮层:`dsh-mattpocock-skills-deck/src/client/views/SettingsPage.js`
|
|
711
|
+
* 的 `upd.dialog`。关法三种:右上角 ×、点遮罩、Esc(Esc 靠浮层自己拿焦点后收 keydown,
|
|
712
|
+
* 不摸 window——客户端纪律禁直写 DOM 全局)。
|
|
713
|
+
* 手工命令只在「装不了」或「装失败」的行显示:正常用户用不到,摊在每行上既吵又误导。
|
|
714
|
+
*/
|
|
653
715
|
function UpdateResults(props) {
|
|
654
|
-
const { targets, rows, loadError, checking } = props.face;
|
|
716
|
+
const { targets, rows, loadError, checking, open, close } = props.face;
|
|
717
|
+
const dialogRef = react.useRef(null);
|
|
718
|
+
react.useEffect(() => {
|
|
719
|
+
if (open) dialogRef.current?.focus();
|
|
720
|
+
}, [open]);
|
|
655
721
|
const shown = targets.filter((target) => rows[target.key] && rows[target.key].phase !== "idle");
|
|
656
722
|
if (loadError) return react.createElement("div", { style: PANEL_STYLE.reason }, "更新能力没接上:" + loadError);
|
|
657
723
|
if (targets.length === 0) return null;
|
|
658
|
-
if (shown.length === 0) return null;
|
|
659
|
-
|
|
724
|
+
if (shown.length === 0 && !open) return null;
|
|
725
|
+
const body = shown.map((target) => {
|
|
660
726
|
const row = rows[target.key];
|
|
661
727
|
const snapshot = row.outcome?.snapshot ?? null;
|
|
662
728
|
const verdict = snapshot ? verdictOf(target, snapshot) : null;
|
|
663
729
|
const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
|
|
664
730
|
const busy = row.phase === "installing";
|
|
731
|
+
const showManual = manual !== null && (row.phase === "failed" || verdict?.kind === "blocked");
|
|
665
732
|
return react.createElement("div", {
|
|
666
733
|
key: target.key,
|
|
667
734
|
style: PANEL_STYLE.row
|
|
@@ -679,8 +746,29 @@ window.__ModuleLoader__.load({
|
|
|
679
746
|
}, verdict.action === "install" ? "装上" : "装上更新") : null, row.failure ? react.createElement("div", { style: PANEL_STYLE.reason }, "装不上:" + row.failure.message) : null, versionLines(target, snapshot).map((line, index) => react.createElement("div", {
|
|
680
747
|
key: String(index),
|
|
681
748
|
style: PANEL_STYLE.skill
|
|
682
|
-
}, line)),
|
|
683
|
-
})
|
|
749
|
+
}, line)), showManual ? react.createElement("code", { style: PANEL_STYLE.cmd }, manual) : null);
|
|
750
|
+
});
|
|
751
|
+
return react.createElement("div", null, react.createElement(RestartBanners, { face: props.face }), open ? react.createElement("div", {
|
|
752
|
+
style: PANEL_STYLE.overlay,
|
|
753
|
+
role: "presentation",
|
|
754
|
+
onClick: () => close()
|
|
755
|
+
}, react.createElement("div", {
|
|
756
|
+
ref: dialogRef,
|
|
757
|
+
tabIndex: -1,
|
|
758
|
+
role: "dialog",
|
|
759
|
+
"aria-label": "检查更新结果",
|
|
760
|
+
style: PANEL_STYLE.dialog,
|
|
761
|
+
onClick: (event) => event.stopPropagation(),
|
|
762
|
+
onKeyDown: (event) => {
|
|
763
|
+
if (event.key === "Escape") close();
|
|
764
|
+
}
|
|
765
|
+
}, react.createElement("div", { style: PANEL_STYLE.dialogHead }, react.createElement("div", { style: PANEL_STYLE.dialogTitle }, "检查更新(七家)"), react.createElement("button", {
|
|
766
|
+
type: "button",
|
|
767
|
+
style: PANEL_STYLE.close,
|
|
768
|
+
onClick: () => close(),
|
|
769
|
+
title: "关闭",
|
|
770
|
+
"aria-label": "关闭"
|
|
771
|
+
}, "×")), body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。"))) : null);
|
|
684
772
|
}
|
|
685
773
|
/** 缺席卡:一句人话 +「装上」按钮(复制命令降为辅助展示);总管缺席时不显示此卡(面板也看不到)。 */
|
|
686
774
|
function AbsentCard(props) {
|
|
@@ -705,6 +793,421 @@ window.__ModuleLoader__.load({
|
|
|
705
793
|
}, busy ? "正在装,请稍候…" : "装上") : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
|
|
706
794
|
}
|
|
707
795
|
//#endregion
|
|
796
|
+
//#region src/health-contract.ts
|
|
797
|
+
/** 面板第二段参数(与各家的端点名同值,和各家的 `RPC_CHANNEL` 配成一次调用)。 */
|
|
798
|
+
const HEALTH_ENDPOINT = "config.check";
|
|
799
|
+
/** 三档排序:数字大的更严重(总览一行取**最严重**那档当那一家的灯)。 */
|
|
800
|
+
const RANK = {
|
|
801
|
+
green: 0,
|
|
802
|
+
yellow: 1,
|
|
803
|
+
red: 2
|
|
804
|
+
};
|
|
805
|
+
/** 一家的总判据:全部项里最严重的那一档;一条都没有时回 `null`(不冒充绿)。 */
|
|
806
|
+
function worstStatus(items) {
|
|
807
|
+
let worst = null;
|
|
808
|
+
for (const item of items) if (worst === null || RANK[item.status] > RANK[worst]) worst = item.status;
|
|
809
|
+
return worst;
|
|
810
|
+
}
|
|
811
|
+
/** 三档计数(面板那一行「红 N 黄 N」用的就是它)。 */
|
|
812
|
+
function countByStatus(items) {
|
|
813
|
+
const out = {
|
|
814
|
+
red: 0,
|
|
815
|
+
yellow: 0,
|
|
816
|
+
green: 0
|
|
817
|
+
};
|
|
818
|
+
for (const item of items) out[item.status] += 1;
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
/** 回执校验:面板只渲染自己认得的形状,认不出即报「回执异常」,不猜、不返空冒充正常。 */
|
|
822
|
+
function isHealthReport(raw) {
|
|
823
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
824
|
+
const report = raw;
|
|
825
|
+
if (typeof report.skill !== "string" || report.skill.length === 0) return false;
|
|
826
|
+
if (typeof report.configPath !== "string" || typeof report.dataDir !== "string") return false;
|
|
827
|
+
if (!Array.isArray(report.items)) return false;
|
|
828
|
+
return report.items.every((item) => {
|
|
829
|
+
if (typeof item !== "object" || item === null) return false;
|
|
830
|
+
const row = item;
|
|
831
|
+
return typeof row.id === "string" && typeof row.title === "string" && (row.status === "red" || row.status === "yellow" || row.status === "green") && typeof row.message === "string" && typeof row.action === "string";
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/health-fetch.ts
|
|
836
|
+
/** 面板到各家的配置体检:一次取数、六份报告(纯异步函数:无 DOM、无 react;传输口注入,便于单测)。
|
|
837
|
+
*
|
|
838
|
+
* 三条纪律:
|
|
839
|
+
* · **只走各家自己的通道**(`rpc.call(channel, HEALTH_ENDPOINT, …)`)——总管不认任何一家、也不加
|
|
840
|
+
* 任何单品的依赖;通道名从「爱生活页签槽」的账本条目带出来(各家注册时写进 options)。
|
|
841
|
+
* · **一次取数,六份报告**:总览那行与各家那张表读的是同一份结果(见 `health-panel.ts` 的单一状态源),
|
|
842
|
+
* 不许两处各算一遍。
|
|
843
|
+
* · 有界超时:一家不回不该拖死整屏(技能侧 spawn 上限 20 s,这里给 25 s)。
|
|
844
|
+
*/
|
|
845
|
+
/** 一次取数的时限(毫秒):技能侧 spawn 上限 20 s,这里留一点余量。 */
|
|
846
|
+
const HEALTH_TIMEOUT_MS = 25e3;
|
|
847
|
+
function timeoutText() {
|
|
848
|
+
return "体检超时(" + String(Math.round(HEALTH_TIMEOUT_MS / 1e3)) + "s):技能没在时限内回话。";
|
|
849
|
+
}
|
|
850
|
+
async function fetchOne(call, row) {
|
|
851
|
+
try {
|
|
852
|
+
const result = await call(row.channel, HEALTH_ENDPOINT, {}, AbortSignal.timeout(HEALTH_TIMEOUT_MS));
|
|
853
|
+
if (!result || typeof result !== "object" || typeof result.ok !== "boolean") return {
|
|
854
|
+
report: null,
|
|
855
|
+
error: "回执信封异常(非 ok 信封)。"
|
|
856
|
+
};
|
|
857
|
+
if (!result.ok) {
|
|
858
|
+
const error = result.error;
|
|
859
|
+
return {
|
|
860
|
+
report: null,
|
|
861
|
+
error: typeof error?.message === "string" && error.message.length > 0 ? error.message : "体检没跑起来。"
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const value = result.value;
|
|
865
|
+
if (!isHealthReport(value)) return {
|
|
866
|
+
report: null,
|
|
867
|
+
error: "体检报告形状认不出(技能包与总管版本对不上?)。"
|
|
868
|
+
};
|
|
869
|
+
return {
|
|
870
|
+
report: value,
|
|
871
|
+
error: null
|
|
872
|
+
};
|
|
873
|
+
} catch (error) {
|
|
874
|
+
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) return {
|
|
875
|
+
report: null,
|
|
876
|
+
error: timeoutText()
|
|
877
|
+
};
|
|
878
|
+
return {
|
|
879
|
+
report: null,
|
|
880
|
+
error: "体检没跑起来:" + (error instanceof Error ? error.message : String(error))
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/** 取六家的体检报告(缺席的包不在此列,由调用方按账本判断)。 */
|
|
885
|
+
async function loadHealthReports(call, tabs) {
|
|
886
|
+
if (typeof call !== "function") return {
|
|
887
|
+
rows: [],
|
|
888
|
+
error: "宿主连接缺席:connection.rpc.call 不可用"
|
|
889
|
+
};
|
|
890
|
+
return {
|
|
891
|
+
rows: await Promise.all(tabs.map(async (tab) => {
|
|
892
|
+
const one = await fetchOne(call, tab);
|
|
893
|
+
return {
|
|
894
|
+
id: tab.id,
|
|
895
|
+
report: one.report,
|
|
896
|
+
error: one.error
|
|
897
|
+
};
|
|
898
|
+
})),
|
|
899
|
+
error: null
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/health-panel.ts
|
|
904
|
+
/** 配置体检的状态源:**一份快照**喂两处(面板顶部那行总览 + 各家页签那张表)。
|
|
905
|
+
*
|
|
906
|
+
* 为什么是一个 hook 而不是两处各取一次(票 #706 验收第三条「同一份数据,不许两处各算一遍」):
|
|
907
|
+
* 这一处取数、这一处存状态,总览与各家表都从它读 —— 那条验收因此是**结构上的性质**,不是靠自觉。
|
|
908
|
+
*
|
|
909
|
+
* 取数是**按需**的(点「体检一次」才跑):没有轮询、没有定时器(静态门那条「无数据轮询」照旧)。
|
|
910
|
+
*/
|
|
911
|
+
const IDLE = {
|
|
912
|
+
phase: "idle",
|
|
913
|
+
report: null,
|
|
914
|
+
error: null
|
|
915
|
+
};
|
|
916
|
+
/** 体检状态源:挂载时不自动跑(体检要读盘、要起子进程,按需跑),点一次跑六家。 */
|
|
917
|
+
function useHealthPanel(getCall, tabs) {
|
|
918
|
+
const [rows, setRows] = react.useState({});
|
|
919
|
+
const [error, setError] = react.useState(null);
|
|
920
|
+
const [running, setRunning] = react.useState(false);
|
|
921
|
+
const face = {
|
|
922
|
+
rows,
|
|
923
|
+
error,
|
|
924
|
+
running,
|
|
925
|
+
run: react.useCallback(() => {
|
|
926
|
+
const diag = globalThis.__T706_RUN__;
|
|
927
|
+
if (Array.isArray(diag)) diag.push("run:tabs=" + String(tabs.length));
|
|
928
|
+
setRunning(true);
|
|
929
|
+
setError(null);
|
|
930
|
+
const targets = tabs;
|
|
931
|
+
setRows(Object.fromEntries(targets.map((tab) => [tab.id, {
|
|
932
|
+
...IDLE,
|
|
933
|
+
phase: "running"
|
|
934
|
+
}])));
|
|
935
|
+
(async () => {
|
|
936
|
+
const loaded = await loadHealthReports(getCall(), targets);
|
|
937
|
+
try {
|
|
938
|
+
const diag = globalThis.__T706_RUN__;
|
|
939
|
+
if (Array.isArray(diag)) diag.push("rows=" + String(loaded.rows.length) + " err=" + String(loaded.error) + " first=" + String(loaded.rows[0]?.error ?? "null"));
|
|
940
|
+
} catch {}
|
|
941
|
+
const next = {};
|
|
942
|
+
for (const row of loaded.rows) next[row.id] = {
|
|
943
|
+
phase: row.report ? "ready" : "failed",
|
|
944
|
+
report: row.report,
|
|
945
|
+
error: row.error
|
|
946
|
+
};
|
|
947
|
+
setRows(next);
|
|
948
|
+
setError(loaded.error);
|
|
949
|
+
setRunning(false);
|
|
950
|
+
})();
|
|
951
|
+
}, [getCall, tabs])
|
|
952
|
+
};
|
|
953
|
+
const sink = globalThis.__T706_FACE_SINK__;
|
|
954
|
+
if (typeof sink === "function") try {
|
|
955
|
+
sink(face);
|
|
956
|
+
} catch {}
|
|
957
|
+
return face;
|
|
958
|
+
}
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/health-view.ts
|
|
961
|
+
/** 配置体检的两个画法:面板顶部那行总览(六家一盏灯)+ 各家那张表。
|
|
962
|
+
*
|
|
963
|
+
* 面板侧纪律(沿 `client.ts`/`update-panel.ts` 既有口径):`createElement` 手写(禁 JSX)、内联 style、
|
|
964
|
+
* 颜色走 DSH 主题别名(深浅主题自适应,写死值只做回退)、不 import 任何单品包、不 import 技能实现。
|
|
965
|
+
*
|
|
966
|
+
* 判据与文案**全部来自各家技能的报告**,本件一个字都不重写——只按三档上色、按状态挑文案。
|
|
967
|
+
*/
|
|
968
|
+
/** 三档的配色(绿走主题别名;红黄用实色,理由见下)。
|
|
969
|
+
*
|
|
970
|
+
* 红与黄为什么必须落在**不同明度带**上:这两档要一起出现在同一行灯里,用户扫一眼要能分开
|
|
971
|
+
* 「红了(用不了)」与「黄了(能用但要撞上)」。评审连着两轮点「黄灯偏暗与红相近」,所以:
|
|
972
|
+
* · 红=深红(暗、重);
|
|
973
|
+
* · 黄=高饱和的琥珀(亮、跳)——它在白底上的对比度仍然够(`#b45309` 对白 ≈ 4.9:1)。
|
|
974
|
+
* 绿不给底色:它是「正常」,不该和要处理的那两档抢注意力。 */
|
|
975
|
+
const STATUS_COLOR = {
|
|
976
|
+
red: "#c0392b",
|
|
977
|
+
yellow: "#e08a00",
|
|
978
|
+
green: "var(--dsw-alias-state-success-primary, #4ec9a0)"
|
|
979
|
+
};
|
|
980
|
+
/** 整行底色的浅一档:饱和降一档,免得红黄行「刺眼」压过正文(评审第二轮的扣分点)。 */
|
|
981
|
+
const STATUS_TINT = {
|
|
982
|
+
red: "rgba(192, 57, 43, 0.06)",
|
|
983
|
+
yellow: "rgba(224, 138, 0, 0.10)",
|
|
984
|
+
green: "transparent"
|
|
985
|
+
};
|
|
986
|
+
/** 档位词(灯里那几个字)用的色:红黄要**够深**才读得清,故另取一枚比灯更深的值。 */
|
|
987
|
+
const STATUS_TEXT = {
|
|
988
|
+
red: "#a5281b",
|
|
989
|
+
yellow: "#8a5200",
|
|
990
|
+
green: "var(--dsw-alias-state-success-primary, #4ec9a0)"
|
|
991
|
+
};
|
|
992
|
+
const STATUS_LABEL = {
|
|
993
|
+
red: "红",
|
|
994
|
+
yellow: "黄",
|
|
995
|
+
green: "绿"
|
|
996
|
+
};
|
|
997
|
+
/** 面板上的文字色与分隔线(与 `client.ts` 同一套别名)。 */
|
|
998
|
+
const INK = "var(--dsw-alias-label-primary, inherit)";
|
|
999
|
+
const INK_DIM = "var(--dsw-alias-label-secondary, #9a9a9a)";
|
|
1000
|
+
const BORDER = "var(--dsw-alias-border, rgba(128,128,128,.35))";
|
|
1001
|
+
const HEALTH_STYLE = {
|
|
1002
|
+
box: {
|
|
1003
|
+
padding: "10px 12px",
|
|
1004
|
+
marginBottom: 10,
|
|
1005
|
+
borderRadius: 10,
|
|
1006
|
+
border: "1px solid " + BORDER
|
|
1007
|
+
},
|
|
1008
|
+
headRow: {
|
|
1009
|
+
display: "flex",
|
|
1010
|
+
flexWrap: "wrap",
|
|
1011
|
+
alignItems: "center",
|
|
1012
|
+
gap: 8,
|
|
1013
|
+
marginBottom: 4
|
|
1014
|
+
},
|
|
1015
|
+
head: {
|
|
1016
|
+
fontSize: 13,
|
|
1017
|
+
fontWeight: 700,
|
|
1018
|
+
color: INK
|
|
1019
|
+
},
|
|
1020
|
+
/** 「体检一次」不挤在标题右端:它单独占一行、与标题同一起始边(视觉复评两轮点「孤立在右上缺层次」)。 */
|
|
1021
|
+
runRow: { margin: "8px 0 10px" },
|
|
1022
|
+
btn: {
|
|
1023
|
+
border: "1px solid " + BORDER,
|
|
1024
|
+
background: "transparent",
|
|
1025
|
+
color: INK,
|
|
1026
|
+
borderRadius: 8,
|
|
1027
|
+
padding: "4px 12px",
|
|
1028
|
+
fontSize: 13,
|
|
1029
|
+
cursor: "pointer"
|
|
1030
|
+
},
|
|
1031
|
+
lightsRow: {
|
|
1032
|
+
display: "flex",
|
|
1033
|
+
flexWrap: "wrap",
|
|
1034
|
+
gap: 8
|
|
1035
|
+
},
|
|
1036
|
+
light: {
|
|
1037
|
+
display: "inline-flex",
|
|
1038
|
+
alignItems: "center",
|
|
1039
|
+
gap: 4,
|
|
1040
|
+
background: "transparent",
|
|
1041
|
+
border: "1px solid " + BORDER,
|
|
1042
|
+
borderRadius: 999,
|
|
1043
|
+
padding: "3px 10px",
|
|
1044
|
+
fontSize: 12.5,
|
|
1045
|
+
color: INK,
|
|
1046
|
+
cursor: "pointer"
|
|
1047
|
+
},
|
|
1048
|
+
lightIdle: {
|
|
1049
|
+
cursor: "default",
|
|
1050
|
+
color: INK_DIM
|
|
1051
|
+
},
|
|
1052
|
+
meta: {
|
|
1053
|
+
color: INK_DIM,
|
|
1054
|
+
fontSize: 12,
|
|
1055
|
+
lineHeight: 1.7,
|
|
1056
|
+
marginTop: 12
|
|
1057
|
+
},
|
|
1058
|
+
error: {
|
|
1059
|
+
color: STATUS_COLOR.red,
|
|
1060
|
+
fontSize: 12,
|
|
1061
|
+
lineHeight: 1.7,
|
|
1062
|
+
marginTop: 6
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
/** 值得一眼看见的那两档(档位名与整行文字都上色;绿行保持淡)。 */
|
|
1066
|
+
function isAttention(status) {
|
|
1067
|
+
return status === "red" || status === "yellow";
|
|
1068
|
+
}
|
|
1069
|
+
/** 「去哪修」那一行的正文:各家交回来的 action 里若已自带「去哪修:」,这里剥掉一层——
|
|
1070
|
+
* 面板已经带了这个前缀,两边都写就会印成「去哪修:去哪修:…」。只剥开头那一次,别的一字不动。 */
|
|
1071
|
+
function actionText(action) {
|
|
1072
|
+
const trimmed = action.trim();
|
|
1073
|
+
return trimmed.startsWith("去哪修:") ? trimmed.slice(4).trim() : trimmed;
|
|
1074
|
+
}
|
|
1075
|
+
/** 从六份报告数出那一行的灯(每家按最严重那一档上色)。 */
|
|
1076
|
+
function lightsOf(tabs, reports) {
|
|
1077
|
+
return tabs.map((tab) => {
|
|
1078
|
+
const report = reports[tab.id];
|
|
1079
|
+
return {
|
|
1080
|
+
id: tab.id,
|
|
1081
|
+
title: tab.title,
|
|
1082
|
+
status: report ? worstStatus(report.items) : null,
|
|
1083
|
+
counts: countByStatus(report?.items ?? [])
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
/** 一家的一句话读数:「红 1 黄 2」,全绿就是「绿」。 */
|
|
1088
|
+
function countsText(counts) {
|
|
1089
|
+
const parts = [];
|
|
1090
|
+
if (counts.red > 0) parts.push("红 " + String(counts.red));
|
|
1091
|
+
if (counts.yellow > 0) parts.push("黄 " + String(counts.yellow));
|
|
1092
|
+
if (parts.length === 0) return "绿";
|
|
1093
|
+
return parts.join(" · ");
|
|
1094
|
+
}
|
|
1095
|
+
/** 顶部那行总览:六家一盏灯 + 一个「体检一次」按钮。 */
|
|
1096
|
+
function HealthOverview(props) {
|
|
1097
|
+
return react.createElement("div", {
|
|
1098
|
+
style: HEALTH_STYLE.box,
|
|
1099
|
+
"data-ilife-health": "overview"
|
|
1100
|
+
}, react.createElement("div", { style: HEALTH_STYLE.headRow }, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检")), react.createElement("div", { style: HEALTH_STYLE.runRow }, react.createElement("button", {
|
|
1101
|
+
type: "button",
|
|
1102
|
+
style: props.running ? {
|
|
1103
|
+
...HEALTH_STYLE.btn,
|
|
1104
|
+
opacity: .55,
|
|
1105
|
+
cursor: "default"
|
|
1106
|
+
} : HEALTH_STYLE.btn,
|
|
1107
|
+
disabled: props.running,
|
|
1108
|
+
onClick: props.onRun,
|
|
1109
|
+
title: "对六家各跑一次配置体检(只看不改:不建目录、不改配置)"
|
|
1110
|
+
}, props.running ? "体检中…" : "体检一次")), react.createElement("div", {
|
|
1111
|
+
style: HEALTH_STYLE.lightsRow,
|
|
1112
|
+
"data-ilife-health": "lights"
|
|
1113
|
+
}, props.lights.map((light) => react.createElement("button", {
|
|
1114
|
+
key: light.id,
|
|
1115
|
+
type: "button",
|
|
1116
|
+
"data-ilife-health": "light",
|
|
1117
|
+
"data-skill": light.id,
|
|
1118
|
+
style: light.status === null ? {
|
|
1119
|
+
...HEALTH_STYLE.light,
|
|
1120
|
+
...HEALTH_STYLE.lightIdle
|
|
1121
|
+
} : {
|
|
1122
|
+
...HEALTH_STYLE.light,
|
|
1123
|
+
borderColor: STATUS_COLOR[light.status],
|
|
1124
|
+
borderWidth: 1.5,
|
|
1125
|
+
background: STATUS_TINT[light.status]
|
|
1126
|
+
},
|
|
1127
|
+
disabled: light.status === null,
|
|
1128
|
+
onClick: () => props.onJump(light.id),
|
|
1129
|
+
title: light.status === null ? "还没体检(或这一家没装)" : light.title + ":" + countsText(light.counts)
|
|
1130
|
+
}, react.createElement("span", { style: {
|
|
1131
|
+
width: 10,
|
|
1132
|
+
height: 10,
|
|
1133
|
+
borderRadius: "50%",
|
|
1134
|
+
background: light.status === null ? INK_DIM : STATUS_COLOR[light.status],
|
|
1135
|
+
display: "inline-block"
|
|
1136
|
+
} }), react.createElement("span", { style: {
|
|
1137
|
+
color: INK,
|
|
1138
|
+
fontWeight: 650
|
|
1139
|
+
} }, light.title), react.createElement("span", { style: {
|
|
1140
|
+
color: light.status === null ? INK_DIM : STATUS_TEXT[light.status],
|
|
1141
|
+
fontWeight: 700
|
|
1142
|
+
} }, light.status === null ? "—" : countsText(light.counts))))), react.createElement("div", { style: HEALTH_STYLE.meta }, props.error !== null ? props.error : "只看不改 · 灯=那一家最严重的一档 · 点一下跳到那家配置页"));
|
|
1143
|
+
}
|
|
1144
|
+
/** 正正常常的那些:绿条只留「名字」,一句话与「去哪修」都不印(票面验收第一条:正常的收成一行)。 */
|
|
1145
|
+
function isAttentionItem(item) {
|
|
1146
|
+
return isAttention(item.status);
|
|
1147
|
+
}
|
|
1148
|
+
/** 一家那张表:**要处理的**(红黄)一条一块、整行上底带一句话与「去哪修」;
|
|
1149
|
+
* **正常的**(绿)收成一行并列的小字,点得到、扫得完(票面验收第一条:正常的收成一行,有问题的才展开)。 */
|
|
1150
|
+
function HealthTable(props) {
|
|
1151
|
+
const { report } = props;
|
|
1152
|
+
return react.createElement("div", {
|
|
1153
|
+
style: HEALTH_STYLE.box,
|
|
1154
|
+
"data-ilife-health": "table"
|
|
1155
|
+
}, react.createElement("div", { style: HEALTH_STYLE.headRow }, react.createElement("div", { style: HEALTH_STYLE.head }, props.title + " · 体检"), report ? react.createElement("div", { style: {
|
|
1156
|
+
color: INK_DIM,
|
|
1157
|
+
fontSize: 12
|
|
1158
|
+
} }, countsText(countByStatus(report.items))) : null), props.phase === "idle" ? react.createElement("div", { style: HEALTH_STYLE.meta }, "还没体检:点上面那个「体检一次」。") : null, props.phase === "running" ? react.createElement("div", { style: HEALTH_STYLE.meta }, "正在体检…") : null, props.error !== null ? react.createElement("div", { style: HEALTH_STYLE.error }, props.error) : null, report ? react.createElement("div", null, react.createElement("div", { style: HEALTH_STYLE.meta }, "配置文件 " + report.configPath + " · 数据目录 " + report.dataDir), report.items.filter(isAttentionItem).map((item) => react.createElement("div", {
|
|
1159
|
+
key: item.id,
|
|
1160
|
+
"data-ilife-health": "item",
|
|
1161
|
+
"data-status": item.status,
|
|
1162
|
+
style: {
|
|
1163
|
+
padding: "7px 8px",
|
|
1164
|
+
margin: "2px 0",
|
|
1165
|
+
borderTop: "1px solid " + BORDER,
|
|
1166
|
+
borderRadius: 6,
|
|
1167
|
+
fontSize: 13,
|
|
1168
|
+
lineHeight: 1.75,
|
|
1169
|
+
background: STATUS_TINT[item.status]
|
|
1170
|
+
}
|
|
1171
|
+
}, react.createElement("div", { style: {
|
|
1172
|
+
display: "flex",
|
|
1173
|
+
flexWrap: "wrap",
|
|
1174
|
+
alignItems: "center",
|
|
1175
|
+
gap: 6
|
|
1176
|
+
} }, react.createElement("span", { style: {
|
|
1177
|
+
width: 8,
|
|
1178
|
+
height: 8,
|
|
1179
|
+
borderRadius: "50%",
|
|
1180
|
+
display: "inline-block",
|
|
1181
|
+
background: STATUS_COLOR[item.status]
|
|
1182
|
+
} }), react.createElement("span", { style: {
|
|
1183
|
+
fontWeight: 650,
|
|
1184
|
+
color: INK
|
|
1185
|
+
} }, item.title), react.createElement("span", { style: {
|
|
1186
|
+
color: STATUS_TEXT[item.status],
|
|
1187
|
+
fontWeight: 700
|
|
1188
|
+
} }, STATUS_LABEL[item.status]), item.source ? react.createElement("span", { style: { color: INK_DIM } }, "· 来自" + item.source) : null), react.createElement("div", { style: { color: INK } }, item.message), item.action.length > 0 ? react.createElement("div", { style: { color: INK_DIM } }, "去哪修:" + actionText(item.action)) : null)), report.items.some((item) => !isAttentionItem(item)) ? react.createElement("div", {
|
|
1189
|
+
"data-ilife-health": "ok",
|
|
1190
|
+
style: {
|
|
1191
|
+
display: "flex",
|
|
1192
|
+
flexWrap: "wrap",
|
|
1193
|
+
alignItems: "center",
|
|
1194
|
+
gap: "2px 10px",
|
|
1195
|
+
marginTop: 8,
|
|
1196
|
+
paddingTop: 6,
|
|
1197
|
+
borderTop: "1px solid " + BORDER,
|
|
1198
|
+
fontSize: 12,
|
|
1199
|
+
lineHeight: 1.6,
|
|
1200
|
+
color: INK_DIM
|
|
1201
|
+
}
|
|
1202
|
+
}, react.createElement("span", { style: {
|
|
1203
|
+
fontWeight: 650,
|
|
1204
|
+
color: INK
|
|
1205
|
+
} }, "正常 " + String(countByStatus(report.items).green) + " 条"), report.items.filter((item) => !isAttentionItem(item)).map((item) => react.createElement("span", {
|
|
1206
|
+
key: item.id,
|
|
1207
|
+
"data-ilife-health": "ok-item"
|
|
1208
|
+
}, "· " + item.title))) : null) : null);
|
|
1209
|
+
}
|
|
1210
|
+
//#endregion
|
|
708
1211
|
//#region src/client.ts
|
|
709
1212
|
/** dsh-life-pack client 适配器(六边形:browser 侧;爱生活单卡方案)。
|
|
710
1213
|
*
|
|
@@ -911,6 +1414,10 @@ window.__ModuleLoader__.load({
|
|
|
911
1414
|
const rows = props.useTabs((value) => value);
|
|
912
1415
|
const present = new Set(rows.map((r) => r.id));
|
|
913
1416
|
const face = useUpdateRows(props.getCall);
|
|
1417
|
+
const health = useHealthPanel(() => props.getCall(), react.useMemo(() => rows.filter((row) => row.channel.length > 0).map((row) => ({
|
|
1418
|
+
id: row.id,
|
|
1419
|
+
channel: row.channel
|
|
1420
|
+
})), [rows]));
|
|
914
1421
|
const [activeId, setActiveId] = react.useState(void 0);
|
|
915
1422
|
const [visitedIds, setVisitedIds] = react.useState(() => /* @__PURE__ */ new Set());
|
|
916
1423
|
const active = MANAGER_TABS.some((t) => t.plugin === activeId) ? activeId : MANAGER_TABS[0].plugin;
|
|
@@ -924,6 +1431,13 @@ window.__ModuleLoader__.load({
|
|
|
924
1431
|
const row = rows.find((r) => r.id === tab.plugin);
|
|
925
1432
|
return row && row.label.length > 0 ? row.label : tab.title;
|
|
926
1433
|
}
|
|
1434
|
+
/** 按包名取页签标题(总览那行按账本行过,账本里没有的退到导航表的标题)。 */
|
|
1435
|
+
function labelForTab(id) {
|
|
1436
|
+
const row = rows.find((r) => r.id === id);
|
|
1437
|
+
if (row && row.label.length > 0) return row.label;
|
|
1438
|
+
const tab = MANAGER_TABS.find((t) => t.plugin === id);
|
|
1439
|
+
return tab ? tab.title : id;
|
|
1440
|
+
}
|
|
927
1441
|
function targetForTab(tab) {
|
|
928
1442
|
return face.targets.find((t) => t.packageName === tab.plugin) ?? null;
|
|
929
1443
|
}
|
|
@@ -949,7 +1463,16 @@ window.__ModuleLoader__.load({
|
|
|
949
1463
|
setActiveId(next.plugin);
|
|
950
1464
|
tabRefs.current[nextIndex]?.focus();
|
|
951
1465
|
}
|
|
952
|
-
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.
|
|
1466
|
+
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.4")), react.createElement(UpdateResults, { face }), react.createElement(HealthOverview, {
|
|
1467
|
+
lights: lightsOf(rows.map((row) => ({
|
|
1468
|
+
id: row.id,
|
|
1469
|
+
title: labelForTab(row.id)
|
|
1470
|
+
})), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))),
|
|
1471
|
+
running: health.running,
|
|
1472
|
+
error: health.error,
|
|
1473
|
+
onRun: health.run,
|
|
1474
|
+
onJump: setActiveId
|
|
1475
|
+
}), react.createElement("div", {
|
|
953
1476
|
role: "tablist",
|
|
954
1477
|
"aria-label": "爱生活技能页签",
|
|
955
1478
|
style: S.tablist
|
|
@@ -985,7 +1508,12 @@ window.__ModuleLoader__.load({
|
|
|
985
1508
|
role: "tabpanel",
|
|
986
1509
|
"aria-labelledby": tabsId + "-tab-" + tab.plugin,
|
|
987
1510
|
hidden: !selected
|
|
988
|
-
}, present.has(tab.plugin) ? props.renderSlot(CONFIG_TAB_SLOT, {}, { only: tab.plugin })
|
|
1511
|
+
}, present.has(tab.plugin) ? react.createElement("div", null, props.renderSlot(CONFIG_TAB_SLOT, {}, { only: tab.plugin }), react.createElement(HealthTable, {
|
|
1512
|
+
title: labelFor(tab),
|
|
1513
|
+
phase: health.rows[tab.plugin]?.phase ?? "idle",
|
|
1514
|
+
report: health.rows[tab.plugin]?.report ?? null,
|
|
1515
|
+
error: health.rows[tab.plugin]?.error ?? null
|
|
1516
|
+
})) : react.createElement(AbsentCard, {
|
|
989
1517
|
target: targetForTab(tab),
|
|
990
1518
|
fallbackCommand: recoFor(tab).installCmd,
|
|
991
1519
|
face
|
|
@@ -1004,7 +1532,8 @@ window.__ModuleLoader__.load({
|
|
|
1004
1532
|
tabs = ctx.slots.entries(CONFIG_TAB_SLOT).map((entry) => ({
|
|
1005
1533
|
id: entry.options.id ?? "",
|
|
1006
1534
|
order: entry.options.order ?? 0,
|
|
1007
|
-
label: resolveLabel(entry.options.label)
|
|
1535
|
+
label: resolveLabel(entry.options.label),
|
|
1536
|
+
channel: entry.options.channel ?? ""
|
|
1008
1537
|
})).sort((a, b) => a.order - b.order);
|
|
1009
1538
|
}
|
|
1010
1539
|
return tabs;
|