@noob-stupid/dsh-plugin-console 0.5.97 → 0.5.99
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/lib/client.js +368 -6
- package/lib/server/domain/compat-pending-records.js +355 -0
- package/lib/server/domain/compat-scan.js +106 -0
- package/lib/server/domain/framework-cleanup.js +63 -28
- package/lib/server/domain/framework-stale-repair.js +23 -8
- package/lib/server/domain/framework-stale-restore.js +215 -0
- package/lib/server/infra/paths.js +32 -3
- package/lib/server/routes/compat-scan.js +123 -0
- package/lib/server/routes/compat.js +90 -1
- package/lib/server/routes/index.js +19 -2
- package/lib/server/routes/state.js +31 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
2
|
id: "@noob-stupid/dsh-plugin-console",
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
var module = { exports: {} };
|
|
@@ -387,6 +387,47 @@
|
|
|
387
387
|
gateRowFromUpgrade: "来源:框架升级预扫",
|
|
388
388
|
gateRowAt: " · {t}",
|
|
389
389
|
gateRowAdoptable: " · 已通过源码扫描,可解锁",
|
|
390
|
+
// ★ 0.5.98 加法(用户 2026-10-28):适配门**记录**的如实标注 + 删除入口(两段式)。
|
|
391
|
+
// 现场:3 条 pending 记录的补丁行早已不存在、moduleName 也是 null —— 面板必须如实说出来。
|
|
392
|
+
gateRowModule: "模块:",
|
|
393
|
+
gateRowMissing: "该行已不存在",
|
|
394
|
+
gateStaleLine: "另有 {n} 条记录的补丁行已不存在(永远等不到适配,可以清掉)",
|
|
395
|
+
gateForgetRow: "删除记录",
|
|
396
|
+
gateForgetRowTitle: "只删这条记录:不动插件、不动补丁文件(点一下先确认)",
|
|
397
|
+
gateForgetStale: "清理无效记录",
|
|
398
|
+
gateForgetStaleTitle: "只清掉补丁行已不存在的记录:不动插件、不动补丁文件(点一下先确认)",
|
|
399
|
+
gateForgetArmRow: "将删除记录 {id}(模块:{name})。",
|
|
400
|
+
gateForgetArmStale: "将清理 {n} 条失效记录(补丁行已不存在的那几条)。真实待适配与历史记录都不会被删。",
|
|
401
|
+
gateForgetSafety: "只删 compat-pending.json 里的记录:不动插件、不动补丁文件,写盘前先备份。",
|
|
402
|
+
gateForgetConfirm: "确认删除",
|
|
403
|
+
gateForgetCancel: "取消",
|
|
404
|
+
gateForgetDoneRow: "已删除记录 {id}(插件与补丁一个字都没动)",
|
|
405
|
+
gateForgetDoneStale: "已清理 {n} 条失效记录(插件与补丁一个字都没动)",
|
|
406
|
+
gateForgetNone: "没有可删除的记录(文件未改动)",
|
|
407
|
+
gateForgetFailed: "删除失败:",
|
|
408
|
+
// ★ 0.5.100 加法(用户 2026-11-02):「按需检查」—— 预检只在升级前跑,平时门控是空的,
|
|
409
|
+
// 0 条会被误读成"全部兼容"。这里给一个"现在查一次"的入口 + 空态说明 + 上次检查摘要。
|
|
410
|
+
gateScanBtn: "检查当前是否适配",
|
|
411
|
+
gateScanBusy: "正在检查…",
|
|
412
|
+
gateScanTitle: "对当前补丁里的行跑一次适配判定:只记录结论,不禁用任何行",
|
|
413
|
+
gateScanNoTargets: "当前没有可检查的行(补丁里没有可判定的插件行),所以不显示检查按钮",
|
|
414
|
+
gateScanDone: "检查 {n} 行:通过 {p} / 不适配 {f} / 无法判定 {u}",
|
|
415
|
+
gateScanFailed: "检查失败:",
|
|
416
|
+
gateNoRecordsHint: "本次没有裁决记录(预检只在升级前运行,可点上面按钮检查)",
|
|
417
|
+
gateLastScan: "上次检查({at}):{n} 行 · 通过 {p} / 不适配 {f} / 无法判定 {u}",
|
|
418
|
+
// ★ 0.5.101 加法(用户 2026-11-02):**加载失败的行**(运行期挂载失败)分区 + 两段式停用。
|
|
419
|
+
// 与"适配裁决"分开:这类失败不经过预扫/隔离链,**不写 compat-pending.json**。
|
|
420
|
+
gateLoadFailedTitle: "加载失败的行({n} 行 · 运行期挂载失败)",
|
|
421
|
+
gateLoadFailedHint: "这些行是**运行期加载失败**(例:某个依赖包解析不到),不是适配裁决,也不会写进适配记录;可在此停用它们。",
|
|
422
|
+
gateLoadDisable: "停用",
|
|
423
|
+
gateLoadDisableTitle: "停用这一行(会往补丁里写一条 disabled 块;点一下先确认)",
|
|
424
|
+
gateLoadArmRow: "将停用 {id}({name})。",
|
|
425
|
+
gateLoadSafety: "这会往 profile 补丁里写一条 disabled 块(可随时在列表里重新启用);不会卸载包、不会动其它行。",
|
|
426
|
+
gateLoadConfirm: "确认停用",
|
|
427
|
+
gateLoadCancel: "取消",
|
|
428
|
+
gateLoadFailedDone: "已请求停用 {id}(补丁已更新,HMR 应用后列表会随之变化)",
|
|
429
|
+
gateLoadFailedGone: "该行已不在运行时列表里(可能已被删除),本动作没有发请求",
|
|
430
|
+
gateLoadFailedFailed: "停用失败:",
|
|
390
431
|
// ★ 0.5.x 加法(用户 2026-10-18):「安全启动」在界面上的入口 —— 门控面板里的一个分区。
|
|
391
432
|
// 用户原话「现在这能力藏得太深,只有命令行能用」;这里只做展示 + 两段式上膛,
|
|
392
433
|
// 判据与动作白名单全在服务端 POST /plugin-console/safe-boot。
|
|
@@ -1163,6 +1204,44 @@
|
|
|
1163
1204
|
gateRowReason: "Reason: {reason}",
|
|
1164
1205
|
gateRowFromBoot: "Source: boot-failure quarantine",
|
|
1165
1206
|
gateRowFromUpgrade: "Source: framework upgrade pre-scan",
|
|
1207
|
+
// ★ 0.5.98 additive: honest labelling of adaptation-gate **records** + a delete entry (two-step).
|
|
1208
|
+
gateRowModule: "Module: ",
|
|
1209
|
+
gateRowMissing: "this row no longer exists",
|
|
1210
|
+
gateStaleLine: "{n} more record(s) point to rows that no longer exist (they can never be adapted — safe to clean up)",
|
|
1211
|
+
gateForgetRow: "Delete record",
|
|
1212
|
+
gateForgetRowTitle: "Deletes this record only: no plugin and no patch file is touched (click once to confirm)",
|
|
1213
|
+
gateForgetStale: "Clean up invalid records",
|
|
1214
|
+
gateForgetStaleTitle: "Only removes records whose patch row no longer exists: no plugin and no patch file is touched (click once to confirm)",
|
|
1215
|
+
gateForgetArmRow: "Record {id} (module: {name}) will be deleted.",
|
|
1216
|
+
gateForgetArmStale: "{n} invalid record(s) (their patch rows no longer exist) will be removed. Real pending rows and history stay untouched.",
|
|
1217
|
+
gateForgetSafety: "Records in compat-pending.json only: no plugin and no patch file is touched, and a backup is written first.",
|
|
1218
|
+
gateForgetConfirm: "Confirm delete",
|
|
1219
|
+
gateForgetCancel: "Cancel",
|
|
1220
|
+
gateForgetDoneRow: "Deleted record {id} (plugins and patch file untouched)",
|
|
1221
|
+
gateForgetDoneStale: "Removed {n} invalid record(s) (plugins and patch file untouched)",
|
|
1222
|
+
gateForgetNone: "Nothing to delete (file unchanged)",
|
|
1223
|
+
gateForgetFailed: "Delete failed: ",
|
|
1224
|
+
// ★ 0.5.100 additive (user 2026-11-02): on-demand adaptation check in the gating panel.
|
|
1225
|
+
gateScanBtn: "Check current compatibility",
|
|
1226
|
+
gateScanBusy: "Checking…",
|
|
1227
|
+
gateScanTitle: "Runs the adaptation verdict over the rows in the current patch: records conclusions only, disables nothing",
|
|
1228
|
+
gateScanNoTargets: "No rows to check (the patch has no judgeable plugin rows), so the check button is not shown",
|
|
1229
|
+
gateScanDone: "Checked {n} row(s): pass {p} / incompatible {f} / undecidable {u}",
|
|
1230
|
+
gateScanFailed: "Check failed: ",
|
|
1231
|
+
gateNoRecordsHint: "No verdict records yet (the pre-scan only runs before an upgrade — use the button above to check now)",
|
|
1232
|
+
gateLastScan: "Last check ({at}): {n} row(s) · pass {p} / incompatible {f} / undecidable {u}",
|
|
1233
|
+
// ★ 0.5.101 additive (user 2026-11-02): rows that FAILED TO LOAD at runtime.
|
|
1234
|
+
gateLoadFailedTitle: "Rows that failed to load ({n} · runtime mount failure)",
|
|
1235
|
+
gateLoadFailedHint: "These rows failed to load at runtime (e.g. a dependency cannot be resolved). This is not an adaptation verdict and is never written to the adaptation records; you can disable them here.",
|
|
1236
|
+
gateLoadDisable: "Disable",
|
|
1237
|
+
gateLoadDisableTitle: "Disable this row (writes a disabled block into the patch; click once to confirm)",
|
|
1238
|
+
gateLoadArmRow: "Row {id} ({name}) will be disabled.",
|
|
1239
|
+
gateLoadSafety: "This writes one disabled block into the profile patch (you can re-enable it from the list at any time); it does not uninstall the package and does not touch other rows.",
|
|
1240
|
+
gateLoadConfirm: "Confirm disable",
|
|
1241
|
+
gateLoadCancel: "Cancel",
|
|
1242
|
+
gateLoadFailedDone: "Disable requested for {id} (the patch is updated; the list follows once HMR applies)",
|
|
1243
|
+
gateLoadFailedGone: "This row is no longer in the runtime list (it may have been removed), so no request was sent",
|
|
1244
|
+
gateLoadFailedFailed: "Disable failed: ",
|
|
1166
1245
|
gateRowAt: " · {t}",
|
|
1167
1246
|
gateRowAdoptable: " · passed the source scan, can be unlocked",
|
|
1168
1247
|
safeBootSectionTitle: "Safe boot",
|
|
@@ -5776,12 +5855,122 @@ const RESTART_FAILED_NOTE = "重启请求失败";
|
|
|
5776
5855
|
const gateReady = state !== null && state.status === "ready";
|
|
5777
5856
|
const gateAutoDisable = !gateReady || (state.data.compatGate ?? {}).autoDisable !== false;
|
|
5778
5857
|
const gateAutoDetect = !gateReady || (state.data.compatGate ?? {}).autoDetect !== false;
|
|
5779
|
-
|
|
5858
|
+
// ★ 0.5.98 改错(用户 2026-10-28 实测):计数只数**真待适配**的 —— `status=pending`
|
|
5859
|
+
// **且**该行此刻仍在补丁里。旧口径取 `compatPending.pending.length`,把两种"不是待办"的
|
|
5860
|
+
// 记录算了进去:① 已经 adopted 的历史(服务端本来就滤掉了,但语义上也不该数);
|
|
5861
|
+
// ② **行早已不存在的 pending**(真机现场 3 条,moduleName 也是 null)——
|
|
5862
|
+
// 于是面板一直挂着"有 N 行待适配",而实际一行都没有。
|
|
5863
|
+
// 判据的唯一真源在服务端(domain/compat-pending-records.js#isActionablePending,
|
|
5864
|
+
// /state 下发 actionableCount / staleCount);拿不到这两个字段时(老服务端),
|
|
5865
|
+
// 这里按**同一个判据**用 state.data.patch 的既有行清单(disables/forced/inserts,
|
|
5866
|
+
// 就是补丁解析的产物)回退算,**绝不退回数组长度**。
|
|
5867
|
+
const gatePatchRowIds = new Set([
|
|
5868
|
+
...((state !== null && state.data && state.data.patch && Array.isArray(state.data.patch.disables)) ? state.data.patch.disables : []),
|
|
5869
|
+
...((state !== null && state.data && state.data.patch && Array.isArray(state.data.patch.forced)) ? state.data.patch.forced : []),
|
|
5870
|
+
...((state !== null && state.data && state.data.patch && Array.isArray(state.data.patch.inserts)) ? state.data.patch.inserts : []),
|
|
5871
|
+
]);
|
|
5872
|
+
const gatePendingRaw = gateReady ? (state.data.compatPending?.pending ?? []) : [];
|
|
5873
|
+
/** 一条记录"行还在不在"(服务端 rowMissing 优先;老服务端按本地行清单算 —— 同一判据)。 */
|
|
5874
|
+
const gateRowMissing = (r) => (r !== null && r !== undefined && typeof r.rowMissing === "boolean"
|
|
5875
|
+
? r.rowMissing
|
|
5876
|
+
: !(r !== null && r !== undefined && typeof r.rowId === "string" && gatePatchRowIds.has(r.rowId)));
|
|
5877
|
+
/** 一条记录"是不是真待适配"(服务端 actionable 优先;老服务端按同一判据回退)。 */
|
|
5878
|
+
const gateRowActionable = (r) => (r !== null && r !== undefined && typeof r.actionable === "boolean"
|
|
5879
|
+
? r.actionable
|
|
5880
|
+
: ((r?.status ?? "pending") === "pending" && !gateRowMissing(r)));
|
|
5881
|
+
const gateActionableRows = gatePendingRaw.filter((p) => (p?.status ?? "pending") === "pending" && !gateRowMissing(p));
|
|
5882
|
+
const gateStaleRows = gatePendingRaw.filter((p) => (p?.status ?? "pending") === "pending" && gateRowMissing(p));
|
|
5883
|
+
const gatePendingCount = gateReady
|
|
5884
|
+
? (Number.isFinite(state.data.compatPending?.actionableCount) ? state.data.compatPending.actionableCount : gateActionableRows.length)
|
|
5885
|
+
: 0;
|
|
5886
|
+
const gateStaleCount = gateReady
|
|
5887
|
+
? (Number.isFinite(state.data.compatPending?.staleCount) ? state.data.compatPending.staleCount : gateStaleRows.length)
|
|
5888
|
+
: 0;
|
|
5780
5889
|
// 门控明细清单(用户 2026-09-24 要求:面板要显示"是哪些插件",不只是数量)
|
|
5781
5890
|
const [gateShowAll, setGateShowAll] = react.useState(false);
|
|
5782
5891
|
const [compatNoticeClosed, setCompatNoticeClosed] = react.useState(false);
|
|
5783
5892
|
const gateRows = gateReady ? (state.data.gating ?? null) : null;
|
|
5784
5893
|
const gateTime = (ms) => (typeof ms === "number" && ms > 0 ? new Date(ms).toLocaleString() : "—");
|
|
5894
|
+
// ★ 0.5.98 加法(用户 2026-10-28 明确要求):**删记录**的两个入口(单条 / 批量清理失效)。
|
|
5895
|
+
// 三条硬约束:① 只删记录 —— 不动插件、不动补丁文件(服务端只改 compat-pending.json);
|
|
5896
|
+
// ② **两段式** —— 第一下只上膛(零请求,并把"要删什么、不影响什么"写清楚),确认后才真发;
|
|
5897
|
+
// ③ 失败一律把服务端原文摆出来(不静默、不谎报成功)。
|
|
5898
|
+
const [gateForgetArmed, setGateForgetArmed] = react.useState(null); // { kind: "row", rowId, moduleName } | { kind: "stale" }
|
|
5899
|
+
const [gateForgetBusy, setGateForgetBusy] = react.useState(false);
|
|
5900
|
+
const [gateForgetNote, setGateForgetNote] = react.useState(null); // { text, level }
|
|
5901
|
+
// ★ 0.5.100 加法:「按需检查」的按钮状态与结果反馈(用户 2026-11-02「平时不预检」)。
|
|
5902
|
+
// `gateScanTargets` = 当前**可检查**的行数(有 rowId 且不是控制台自己):为 0 时不摆按钮。
|
|
5903
|
+
const gateScanTargets = gateReady && Array.isArray(state.data.entries)
|
|
5904
|
+
? state.data.entries.filter((e) => typeof e?.rowId === "string" && e.rowId !== "" && e.rowId !== "plugin-console").length
|
|
5905
|
+
: 0;
|
|
5906
|
+
const gateLastScan = gateReady && state.data.compatPending !== null && state.data.compatPending !== undefined
|
|
5907
|
+
? (state.data.compatPending.lastScan ?? null)
|
|
5908
|
+
: null;
|
|
5909
|
+
const [gateScanBusy, setGateScanBusy] = react.useState(false);
|
|
5910
|
+
const [gateScanNote, setGateScanNote] = react.useState(null); // { text, level }
|
|
5911
|
+
/** 一键「检查当前是否适配」:服务端只记录结论、绝不禁用任何行;成功/失败都要有明确反馈。 */
|
|
5912
|
+
const gateScanSend = () => {
|
|
5913
|
+
if (gateScanBusy) return;
|
|
5914
|
+
setGateScanBusy(true);
|
|
5915
|
+
setGateScanNote(null);
|
|
5916
|
+
call("/plugin-console/compat-gate", { action: "scan-current" }).then(
|
|
5917
|
+
(data) => {
|
|
5918
|
+
const n = Number(data?.checked ?? 0);
|
|
5919
|
+
const p = Number(data?.pass ?? 0);
|
|
5920
|
+
const f = Number(data?.fail ?? 0);
|
|
5921
|
+
const u = Number(data?.unknown ?? 0);
|
|
5922
|
+
setGateScanNote({ text: t("gateScanDone").replace("{n}", String(n)).replace("{p}", String(p)).replace("{f}", String(f)).replace("{u}", String(u)), level: null });
|
|
5923
|
+
refresh(); // 结论落盘了 ⇒ 重拉 /state(列表、计数、"上次检查"一起变)
|
|
5924
|
+
},
|
|
5925
|
+
(error) => setGateScanNote({ text: t("gateScanFailed") + friendlyGithubError(error).message, level: "error" }),
|
|
5926
|
+
).finally(() => setGateScanBusy(false));
|
|
5927
|
+
};
|
|
5928
|
+
// ★ 0.5.101 加法(用户 2026-11-02):**运行期加载失败**的行(`fiberPhase === 'failed'`)。
|
|
5929
|
+
// 真机:`experimental-inspector` 因 `@deepseek-ai/cordis` 解析失败挂载失败,门控完全看不到
|
|
5930
|
+
// —— 那条失败不经过预扫/隔离链。这里只做**视图**(服务端 gating.loadFailed;**从不**写
|
|
5931
|
+
// compat-pending.json,那是适配裁决记录);停用仍是用户的动作(两段式,落到既有 /toggle)。
|
|
5932
|
+
const gateLoadFailed = gateReady && gateRows !== null && Array.isArray(gateRows.loadFailed) ? gateRows.loadFailed : [];
|
|
5933
|
+
const [gateLoadArmed, setGateLoadArmed] = react.useState(null); // { rowId, name }
|
|
5934
|
+
const [gateLoadNote, setGateLoadNote] = react.useState(null); // { text, level }
|
|
5935
|
+
const gateLoadEntry = (rowId) => (gateReady && Array.isArray(state.data.entries)
|
|
5936
|
+
? (state.data.entries.find((e) => e.rowId === rowId) ?? null)
|
|
5937
|
+
: null);
|
|
5938
|
+
/** 两段式第一段:只上膛(零请求)。 */
|
|
5939
|
+
const gateLoadArm = (next) => { setGateLoadNote(null); setGateLoadArmed(next); };
|
|
5940
|
+
/** 确认后才真发:走**既有** /toggle(enabled:false),成功后刷新。 */
|
|
5941
|
+
const gateLoadDisable = (armed) => {
|
|
5942
|
+
const entry = armed === null ? null : gateLoadEntry(armed.rowId);
|
|
5943
|
+
setGateLoadArmed(null);
|
|
5944
|
+
setGateLoadNote(null);
|
|
5945
|
+
if (entry === null) { setGateLoadNote({ text: t("gateLoadFailedFailed") + t("gateLoadFailedGone"), level: "error" }); return; }
|
|
5946
|
+
call("/plugin-console/toggle", { entryId: entry.entryId, enabled: false }).then(
|
|
5947
|
+
() => { setGateLoadNote({ text: t("gateLoadFailedDone").replace("{id}", String(armed.rowId)), level: null }); refresh(); },
|
|
5948
|
+
(error) => setGateLoadNote({ text: t("gateLoadFailedFailed") + friendlyGithubError(error).message, level: "error" }),
|
|
5949
|
+
);
|
|
5950
|
+
};
|
|
5951
|
+
/** 两段式的第一段:只上膛,一个请求都不发(换目标 = 换载荷,旧载荷不会跟着发出去)。 */
|
|
5952
|
+
const gateForgetArm = (next) => { setGateForgetNote(null); setGateForgetArmed(next); };
|
|
5953
|
+
/** 唯一出口:真发删除请求(成功后刷新 /state,列表与计数一起变)。 */
|
|
5954
|
+
const gateForgetSend = (armed) => {
|
|
5955
|
+
if (armed === null || gateForgetBusy) return;
|
|
5956
|
+
setGateForgetArmed(null); // 一旦真发请求,上膛状态作废(绝不留"看着像待确认"的残影)
|
|
5957
|
+
setGateForgetBusy(true);
|
|
5958
|
+
setGateForgetNote(null);
|
|
5959
|
+
const body = armed.kind === "stale" ? { action: "forget-stale" } : { action: "forget", rowId: String(armed.rowId) };
|
|
5960
|
+
call("/plugin-console/compat-gate", body).then(
|
|
5961
|
+
(data) => {
|
|
5962
|
+
const removed = Array.isArray(data?.removed) ? data.removed : [];
|
|
5963
|
+
const text = data !== null && data !== undefined && data.changed === false
|
|
5964
|
+
? t("gateForgetNone")
|
|
5965
|
+
: (armed.kind === "stale"
|
|
5966
|
+
? t("gateForgetDoneStale").replace("{n}", String(removed.length))
|
|
5967
|
+
: t("gateForgetDoneRow").replace("{id}", String(armed.rowId)));
|
|
5968
|
+
setGateForgetNote({ text, level: null });
|
|
5969
|
+
refresh(); // 删完必须重拉 /state(计数与列表都跟着变,不留旧数字)
|
|
5970
|
+
},
|
|
5971
|
+
(error) => setGateForgetNote({ text: t("gateForgetFailed") + friendlyGithubError(error).message, level: "error" }),
|
|
5972
|
+
).finally(() => setGateForgetBusy(false));
|
|
5973
|
+
};
|
|
5785
5974
|
// ══ 安全启动(门控面板里的一个分区)══════════════════════════════════════════════
|
|
5786
5975
|
// 用户 2026-10-18 原话:「现在这能力藏得太深,只有命令行能用」。后端 POST /safe-boot 早就有了
|
|
5787
5976
|
// (lib/server/routes/safe-boot.js;动作白名单 list / restore-last-good / disable-suspects /
|
|
@@ -6711,20 +6900,77 @@ const RESTART_FAILED_NOTE = "重启请求失败";
|
|
|
6711
6900
|
onClick: () => setCompatGate({ autoDetect: !gateAutoDetect }),
|
|
6712
6901
|
}, el("div", { className: styles.pcSwitchKnob }))),
|
|
6713
6902
|
el("p", { className: styles.message }, t("compatGateHint")),
|
|
6714
|
-
el("p", { className: styles.status, "data-pending": gatePendingCount > 0 ? "true" : undefined },
|
|
6903
|
+
el("p", { className: styles.status, "data-pending": gatePendingCount > 0 ? "true" : undefined, "data-gate-pending": String(gatePendingCount), "data-gate-stale": String(gateStaleCount) },
|
|
6715
6904
|
gatePendingCount > 0 ? t("gatePendingInfo").replace("{n}", String(gatePendingCount)) : t("gatePendingNone")),
|
|
6905
|
+
// ★ 0.5.100 加法(用户 2026-11-02「我们一般情况下都不预检吧」):
|
|
6906
|
+
// 预检只在**升级前**跑,所以平时门控是空的 —— 而 0 条会被误读成"全部兼容"。
|
|
6907
|
+
// 这里给两件事:① 一个「现在查一次」的按钮(判据复用服务端框架升级预扫那条链,
|
|
6908
|
+
// **只记录结论、不禁用任何行**);② 没有裁决记录时的说明 + 上次检查的摘要。
|
|
6909
|
+
// 零行可查时**不摆按钮**,并说明为什么(不许出现点了必然没反应的按钮)。
|
|
6910
|
+
gateScanTargets === 0
|
|
6911
|
+
? el("p", { className: styles.status, "data-gate-scan-none": "true" }, t("gateScanNoTargets"))
|
|
6912
|
+
: el("div", { style: { flexDirection: "row", gap: 8, display: "flex", alignItems: "center" } },
|
|
6913
|
+
el("button", {
|
|
6914
|
+
type: "button",
|
|
6915
|
+
className: styles.toggle,
|
|
6916
|
+
"aria-label": "gateScanCurrent",
|
|
6917
|
+
disabled: gateScanBusy,
|
|
6918
|
+
title: t("gateScanTitle"),
|
|
6919
|
+
onClick: () => { gateScanSend(); },
|
|
6920
|
+
}, gateScanBusy ? t("gateScanBusy") : t("gateScanBtn"))),
|
|
6921
|
+
gateScanNote === null
|
|
6922
|
+
? null
|
|
6923
|
+
: el("p", {
|
|
6924
|
+
className: styles.message,
|
|
6925
|
+
"data-gate-scan-note": "true",
|
|
6926
|
+
"data-error": gateScanNote.level === "error" ? "true" : undefined,
|
|
6927
|
+
style: gateScanNote.level === "error" ? undefined : { color: "var(--dsw-alias-state-success-primary)" },
|
|
6928
|
+
}, String(gateScanNote.text ?? "—")),
|
|
6929
|
+
gateLastScan === null
|
|
6930
|
+
? null
|
|
6931
|
+
: el("p", { className: styles.status, "data-gate-lastscan": "true" },
|
|
6932
|
+
t("gateLastScan")
|
|
6933
|
+
.replace("{at}", gateTime(gateLastScan.at))
|
|
6934
|
+
.replace("{n}", String(gateLastScan.checked ?? 0))
|
|
6935
|
+
.replace("{p}", String(gateLastScan.pass ?? 0))
|
|
6936
|
+
.replace("{f}", String(gateLastScan.fail ?? 0))
|
|
6937
|
+
.replace("{u}", String(gateLastScan.unknown ?? 0))),
|
|
6938
|
+
// 门控为空时的明确文案:不让 0 条看起来像"全绿"(用户 2026-11-02 点名)
|
|
6939
|
+
gateRows === null || gateRows.pendingCount + gateRows.adoptedCount === 0
|
|
6940
|
+
? el("p", { className: styles.status, "data-gate-empty-hint": "true" }, t("gateNoRecordsHint"))
|
|
6941
|
+
: null,
|
|
6716
6942
|
// 明细清单(2026-09-24 用户要求:面板要能看到是哪些插件,有列表数据)
|
|
6717
6943
|
gateRows !== null && gateRows.pendingCount + gateRows.adoptedCount > 0
|
|
6718
6944
|
? el("div", { style: { flexDirection: "column", gap: 6, display: "flex" } },
|
|
6719
|
-
|
|
6945
|
+
// ★ 0.5.98 改错:这里的数字改成"真待适配"的条数(与服务端 actionableCount 同一判据)。
|
|
6946
|
+
// 下面仍列出**全部** pending 记录(含失效的),失效的逐条标注「该行已不存在」——
|
|
6947
|
+
// 藏着不显示才是骗人,标出来才是如实。
|
|
6948
|
+
el("strong", { className: styles.name }, t("gateListPending").replace("{n}", String(gateRows.pending.filter(gateRowActionable).length))),
|
|
6720
6949
|
gateRows.pending.length === 0
|
|
6721
6950
|
? el("p", { className: styles.status }, t("gatePendingNone"))
|
|
6722
6951
|
: null,
|
|
6723
6952
|
(gateShowAll ? gateRows.pending : gateRows.pending.slice(0, 8)).map((r) => el("div", {
|
|
6724
6953
|
key: "p-" + String(r.rowId),
|
|
6725
6954
|
style: { flexDirection: "column", gap: 2, display: "flex", padding: "4px 0" },
|
|
6955
|
+
"data-gate-row": String(r.rowId),
|
|
6956
|
+
"data-row-missing": gateRowMissing(r) ? "true" : "false",
|
|
6726
6957
|
},
|
|
6727
6958
|
el("span", { className: styles.name }, (r.enabled === false ? "⛔ " : "• ") + String(r.name) + (r.version ? " v" + r.version : "")),
|
|
6959
|
+
// ★ 0.5.98 加法:模块名单独一行 —— 记录里没有 moduleName 就如实显示「—」,**绝不编造**。
|
|
6960
|
+
// 行已不在补丁里的,紧跟一个标记;删除入口就在这一行(两段式)。
|
|
6961
|
+
el("span", { className: styles.status, "data-gate-module": "true" },
|
|
6962
|
+
t("gateRowModule") + " " + (typeof r.moduleName === "string" && r.moduleName !== "" ? r.moduleName : "—"),
|
|
6963
|
+
gateRowMissing(r) ? " · " + t("gateRowMissing") : "",
|
|
6964
|
+
el("button", {
|
|
6965
|
+
type: "button",
|
|
6966
|
+
className: styles.toggle,
|
|
6967
|
+
style: { marginLeft: 8, fontSize: 11, padding: "1px 6px" },
|
|
6968
|
+
"aria-label": "gateRowForget",
|
|
6969
|
+
"data-gate-forget-row": String(r.rowId),
|
|
6970
|
+
disabled: gateForgetBusy,
|
|
6971
|
+
title: t("gateForgetRowTitle"),
|
|
6972
|
+
onClick: () => gateForgetArm({ kind: "row", rowId: String(r.rowId), moduleName: typeof r.moduleName === "string" ? r.moduleName : null }),
|
|
6973
|
+
}, t("gateForgetRow"))),
|
|
6728
6974
|
el("span", { className: styles.status }, t("gateRowReason").replace("{reason}", String(r.note ?? "—"))),
|
|
6729
6975
|
el("span", { className: styles.status },
|
|
6730
6976
|
(r.source === "boot-quarantine" ? t("gateRowFromBoot") : t("gateRowFromUpgrade"))
|
|
@@ -6734,6 +6980,21 @@ const RESTART_FAILED_NOTE = "重启请求失败";
|
|
|
6734
6980
|
? el("button", { type: "button", className: styles.toggle, onClick: () => setGateShowAll((v) => !v) },
|
|
6735
6981
|
gateShowAll ? t("gateListFold") : t("gateListMore").replace("{n}", String(gateRows.pendingCount)))
|
|
6736
6982
|
: null,
|
|
6983
|
+
// ★ 0.5.98 加法:失效记录摘要 + 「清理无效记录」(**只在真有可清理的**时才摆按钮 ——
|
|
6984
|
+
// 零失效时连按钮都不出现,绝不摆一个点了必然没反应的按钮)。
|
|
6985
|
+
gateStaleCount > 0
|
|
6986
|
+
? el("div", { "data-gate-stale-block": "true", style: { flexDirection: "column", gap: 4, display: "flex", paddingTop: 4 } },
|
|
6987
|
+
el("p", { className: styles.status, "data-gate-stale-line": "true" },
|
|
6988
|
+
t("gateStaleLine").replace("{n}", String(gateStaleCount))),
|
|
6989
|
+
el("button", {
|
|
6990
|
+
type: "button",
|
|
6991
|
+
className: styles.toggle,
|
|
6992
|
+
"aria-label": "gateForgetStale",
|
|
6993
|
+
disabled: gateForgetBusy,
|
|
6994
|
+
title: t("gateForgetStaleTitle"),
|
|
6995
|
+
onClick: () => gateForgetArm({ kind: "stale" }),
|
|
6996
|
+
}, t("gateForgetStale")))
|
|
6997
|
+
: null,
|
|
6737
6998
|
gateRows.adoptedCount > 0
|
|
6738
6999
|
? el("div", { style: { flexDirection: "column", gap: 2, display: "flex" } },
|
|
6739
7000
|
// ★ 0.5.80:默认折叠,只留一行摘要(点开才列明细)—— 历史不该把当前态挤下去
|
|
@@ -6745,10 +7006,111 @@ const RESTART_FAILED_NOTE = "重启请求失败";
|
|
|
6745
7006
|
+ " · " + t("gateListAdopted").replace("{n}", String(gateRows.adoptedCount))),
|
|
6746
7007
|
gateHistoryOpen
|
|
6747
7008
|
? gateRows.adopted.map((r) => el("span", { key: "a-" + String(r.rowId), className: styles.status },
|
|
6748
|
-
"✓ " + String(r.name) + (r.version ? " v" + r.version : "") + (r.adoptedAt ? " · " + gateTime(r.adoptedAt) : "")
|
|
7009
|
+
"✓ " + String(r.name) + (r.version ? " v" + r.version : "") + (r.adoptedAt ? " · " + gateTime(r.adoptedAt) : ""),
|
|
7010
|
+
// ★ 0.5.98:历史记录也能逐条删(同样是两段式)—— 每条记录都有删除入口
|
|
7011
|
+
el("button", {
|
|
7012
|
+
type: "button",
|
|
7013
|
+
className: styles.toggle,
|
|
7014
|
+
style: { marginLeft: 6, fontSize: 11, padding: "1px 6px" },
|
|
7015
|
+
"aria-label": "gateRowForget",
|
|
7016
|
+
"data-gate-forget-row": String(r.rowId),
|
|
7017
|
+
disabled: gateForgetBusy,
|
|
7018
|
+
title: t("gateForgetRowTitle"),
|
|
7019
|
+
onClick: () => gateForgetArm({ kind: "row", rowId: String(r.rowId), moduleName: typeof r.moduleName === "string" ? r.moduleName : null }),
|
|
7020
|
+
}, t("gateForgetRow"))))
|
|
6749
7021
|
: null)
|
|
6750
7022
|
: null)
|
|
6751
|
-
: null
|
|
7023
|
+
: null,
|
|
7024
|
+
// ★ 0.5.101 加法(用户 2026-11-02):**加载失败的行**分区。
|
|
7025
|
+
// 与"适配裁决"是两件事:这些行是**运行期挂载失败**(例:某个依赖包解析不到),
|
|
7026
|
+
// 不经过预扫/隔离链,所以也**不会写进 compat-pending.json**;这里给的是视图 +
|
|
7027
|
+
// 「停用」(两段式,落到既有 /toggle,真的写补丁 disabled 块)。
|
|
7028
|
+
// 零失败行时整个分区不出现(不摆空壳)。
|
|
7029
|
+
gateLoadFailed.length === 0
|
|
7030
|
+
? null
|
|
7031
|
+
: el("div", {
|
|
7032
|
+
"data-gate-loadfailed": "true",
|
|
7033
|
+
style: { flexDirection: "column", gap: 6, display: "flex", marginTop: 4, paddingTop: 8, borderTop: "1px solid var(--dsw-alias-border-l2)" },
|
|
7034
|
+
},
|
|
7035
|
+
el("strong", { className: styles.name }, t("gateLoadFailedTitle").replace("{n}", String(gateLoadFailed.length))),
|
|
7036
|
+
el("p", { className: styles.status }, t("gateLoadFailedHint")),
|
|
7037
|
+
gateLoadFailed.map((r) => el("div", {
|
|
7038
|
+
key: "lf-" + String(r.rowId),
|
|
7039
|
+
"data-gate-loadfailed-row": String(r.rowId),
|
|
7040
|
+
style: { flexDirection: "column", gap: 2, display: "flex", padding: "4px 0" },
|
|
7041
|
+
},
|
|
7042
|
+
el("span", { className: styles.name }, "⛔ " + String(r.moduleName ?? r.rowId) + (r.version ? " v" + r.version : "")),
|
|
7043
|
+
el("span", { className: styles.status, "data-gate-loadfailed-module": "true" },
|
|
7044
|
+
t("gateRowModule") + " " + (typeof r.moduleName === "string" && r.moduleName !== "" ? r.moduleName : "—")
|
|
7045
|
+
+ " · " + String(r.fiberPhase ?? "failed"),
|
|
7046
|
+
el("button", {
|
|
7047
|
+
type: "button",
|
|
7048
|
+
className: styles.toggle,
|
|
7049
|
+
style: { marginLeft: 8, fontSize: 11, padding: "1px 6px" },
|
|
7050
|
+
"aria-label": "gateLoadDisable",
|
|
7051
|
+
"data-gate-load-disable": String(r.rowId),
|
|
7052
|
+
title: t("gateLoadDisableTitle"),
|
|
7053
|
+
onClick: () => gateLoadArm({ rowId: String(r.rowId), name: typeof r.moduleName === "string" ? r.moduleName : null }),
|
|
7054
|
+
}, t("gateLoadDisable"))))),
|
|
7055
|
+
gateLoadArmed === null
|
|
7056
|
+
? null
|
|
7057
|
+
: el("div", { "data-gate-load-armed": "true", style: { flexDirection: "column", gap: 6, display: "flex", borderLeft: "2px solid var(--dsw-alias-state-warning-primary)", paddingLeft: 8 } },
|
|
7058
|
+
el("p", { className: styles.message, "data-warn": "true", "data-gate-load-text": "true" },
|
|
7059
|
+
t("gateLoadArmRow").replace("{id}", String(gateLoadArmed.rowId)).replace("{name}", String(gateLoadArmed.name ?? "—"))),
|
|
7060
|
+
el("p", { className: styles.status }, t("gateLoadSafety")),
|
|
7061
|
+
el("div", { style: { flexDirection: "row", gap: 8, display: "flex" } },
|
|
7062
|
+
el("button", {
|
|
7063
|
+
type: "button",
|
|
7064
|
+
className: styles.toggle,
|
|
7065
|
+
"aria-label": "gateLoadConfirm",
|
|
7066
|
+
onClick: () => { gateLoadDisable(gateLoadArmed); },
|
|
7067
|
+
}, t("gateLoadConfirm")),
|
|
7068
|
+
el("button", {
|
|
7069
|
+
type: "button",
|
|
7070
|
+
className: styles.toggle,
|
|
7071
|
+
"aria-label": "gateLoadCancel",
|
|
7072
|
+
onClick: () => { setGateLoadArmed(null); setGateLoadNote(null); },
|
|
7073
|
+
}, t("gateLoadCancel")))),
|
|
7074
|
+
gateLoadNote === null
|
|
7075
|
+
? null
|
|
7076
|
+
: el("p", {
|
|
7077
|
+
className: styles.message,
|
|
7078
|
+
"data-gate-load-note": "true",
|
|
7079
|
+
"data-error": gateLoadNote.level === "error" ? "true" : undefined,
|
|
7080
|
+
style: gateLoadNote.level === "error" ? undefined : { color: "var(--dsw-alias-state-success-primary)" },
|
|
7081
|
+
}, String(gateLoadNote.text ?? "—"))),
|
|
7082
|
+
// ★ 0.5.98 加法:删除的**两段式**确认区 + 结果反馈(都留在面板上,不弹系统对话框)。
|
|
7083
|
+
// 上膛时把"要删什么、不会动什么"写清楚:只删记录,不动插件、不动补丁文件。
|
|
7084
|
+
gateForgetArmed === null
|
|
7085
|
+
? null
|
|
7086
|
+
: el("div", { "data-gate-forget-armed": "true", style: { flexDirection: "column", gap: 6, display: "flex", borderLeft: "2px solid var(--dsw-alias-state-warning-primary)", paddingLeft: 8 } },
|
|
7087
|
+
el("p", { className: styles.message, "data-warn": "true", "data-gate-forget-text": "true" },
|
|
7088
|
+
gateForgetArmed.kind === "stale"
|
|
7089
|
+
? t("gateForgetArmStale").replace("{n}", String(gateStaleCount))
|
|
7090
|
+
: t("gateForgetArmRow").replace("{id}", String(gateForgetArmed.rowId)).replace("{name}", String(gateForgetArmed.moduleName ?? "—"))),
|
|
7091
|
+
el("p", { className: styles.status }, t("gateForgetSafety")),
|
|
7092
|
+
el("div", { style: { flexDirection: "row", gap: 8, display: "flex" } },
|
|
7093
|
+
el("button", {
|
|
7094
|
+
type: "button",
|
|
7095
|
+
className: styles.toggle,
|
|
7096
|
+
"aria-label": "gateForgetConfirm",
|
|
7097
|
+
disabled: gateForgetBusy,
|
|
7098
|
+
onClick: () => { gateForgetSend(gateForgetArmed); },
|
|
7099
|
+
}, t("gateForgetConfirm")),
|
|
7100
|
+
el("button", {
|
|
7101
|
+
type: "button",
|
|
7102
|
+
className: styles.toggle,
|
|
7103
|
+
"aria-label": "gateForgetCancel",
|
|
7104
|
+
onClick: () => { setGateForgetArmed(null); setGateForgetNote(null); },
|
|
7105
|
+
}, t("gateForgetCancel")))),
|
|
7106
|
+
gateForgetNote === null
|
|
7107
|
+
? null
|
|
7108
|
+
: el("p", {
|
|
7109
|
+
className: styles.message,
|
|
7110
|
+
"data-gate-forget-note": "true",
|
|
7111
|
+
"data-error": gateForgetNote.level === "error" ? "true" : undefined,
|
|
7112
|
+
style: gateForgetNote.level === "error" ? undefined : { color: "var(--dsw-alias-state-success-primary)" },
|
|
7113
|
+
}, String(gateForgetNote.text ?? "—"))),
|
|
6752
7114
|
// ★ 0.5.x 加法(用户 2026-10-18):「安全启动」分区 —— 挂在门控面板里(不新增工具栏按钮)。
|
|
6753
7115
|
// 默认收起,展开那一下才读一次快照列表;四个动作见 safeBootPanel(list / restore-last-good /
|
|
6754
7116
|
// disable-suspects / mark-good)。它排在门控正文**之后**,Star 行仍在最后(贴底那条不变)。
|