@noob-stupid/dsh-plugin-console 0.5.71 → 0.5.85

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 CHANGED
@@ -111,6 +111,20 @@ window.__ModuleLoader__.load({
111
111
  safetyLockRepairPartial: "重建后仍有差距,见下方问题",
112
112
  safetyLockNoRebuild: "当前不需要重建",
113
113
  safetyLockCheckBtn: "体检",
114
+ // 0.5.84 加法(用户第 7 项):磁盘体检 / 两步清理
115
+ diskTitle: "磁盘占用(plugin-console)",
116
+ diskDesc: "只读体检:这个目录谁占了多少、哪些能安全清理、哪些绝不能动(当前回滚点指向的备份一律不许删)",
117
+ diskCheckBtn: "磁盘体检",
118
+ diskSummary: "共 {mb} MB({n} 项)· 可直接清理约 {safe} MB · 可清理(含取舍)约 {reclaim} MB",
119
+ diskSafeHint: "只清理「安全」级(日志 / 旧残留 / 状态 .bak):不影响任何功能",
120
+ diskConfirmHint: "确认清理:这一步才会真的删(服务端只删体检白名单里的条目)",
121
+ diskCleanupBtn: "清理安全项",
122
+ diskConfirmBtn: "确认清理(约 {mb} MB)",
123
+ diskPlanReady: "将清理 {n} 项、释放约 {mb} MB —— 确认后才会真的删",
124
+ diskConfirmBtn2: "确认清理",
125
+ diskDone: "已清理",
126
+ diskFailed: "磁盘体检失败",
127
+ diskNoCandidate: "没有可安全清理的条目(大头通常是当前回滚点指向的框架备份,不能删)",
114
128
  safetyLockRepairBtn: "重建 lock(显式)",
115
129
  // 2026-09-27 加法:安装结果旁的「建议动作」执行框(短句进面板,长解释进悬浮 title)
116
130
  actionBoxTitle: "建议动作",
@@ -127,6 +141,12 @@ window.__ModuleLoader__.load({
127
141
  linksToggleBtn: "开发式链接(link)明细",
128
142
  linksToggleSummary: "开发式链接(link):共 {n} 个,自足 {ready} 个",
129
143
  linksToggleTitle: "展开/收起 link 明细(默认收起)",
144
+ // 0.5.84 加法(用户第 6 项):功能包工具栏里的 link 入口 + 它打开的那块面板
145
+ linksBtn: "开发式链接",
146
+ linksPanelTitle: "开发式链接(link:)—— 直接展示 /plugin-console/links 的只读清单:指向哪、目标还在不在、依赖/peer 齐不齐、垫片在不在",
147
+ linksPanelLoading: "正在读取…",
148
+ linksPanelReload: "重新读取",
149
+ linksPanelEmpty: "没有 link: 钉住的插件(这份清单只列开发式链接)",
130
150
  devRootsTitle: "开发区根(只有这些根里的目录能钉住)",
131
151
  devRootsNone: "(未登记)",
132
152
  devRootAddPlaceholder: "注册新的开发区根(绝对路径,例如 D:\\my-plugins)",
@@ -251,6 +271,8 @@ window.__ModuleLoader__.load({
251
271
  gatePendingNone: "当前没有待适配的行",
252
272
  gateListPending: "待适配明细({n} 行)",
253
273
  gateListAdopted: "已适配解锁({n} 行,历史上被拦过、现已放行)",
274
+ gateHistoryShow: "展开查看 {n} 条历史",
275
+ gateHistoryHide: "收起历史",
254
276
  gateListMore: "展开全部 {n} 行",
255
277
  gateListFold: "收起列表",
256
278
  gateRowReason: "原因:{reason}",
@@ -280,6 +302,11 @@ window.__ModuleLoader__.load({
280
302
  migrateBtn: "迁移并适配",
281
303
  aiEmpowerFrameworkCheck: "框架适配检测",
282
304
  userForcedTag: "补丁强制启用",
305
+ // ★ 0.5.76(用户 2026-10-18):禁用**出处**要写明,别只给一句"已停用"
306
+ disabledByBundleTag: "插件默认禁用",
307
+ forcedNotLoadedTag: "强制启用未生效",
308
+ revokeForceLabel: "撤销该强制启用",
309
+ revokeForceHint: "这条 disabled: false 覆盖没有让插件加载起来 —— 撤掉它,界面不再显示「强制启用」的假象(不会改动清单)",
283
310
  protectedTag: "受保护",
284
311
  unobserved: "未挂载",
285
312
  pending: "等待依赖",
@@ -312,6 +339,8 @@ window.__ModuleLoader__.load({
312
339
  // 0.5.38 加法:三处没齐备(清单 / lock / 挂载行)时**绝不**说"已安装并启用"——
313
340
  // 那种状态下任何一次 pnpm 操作或重启都会把它清掉(真机 dshmarket 现场)。
314
341
  installedNotPersisted: "⚠ 已装上但未持久化(重启/重装会消失)",
342
+ // 0.5.84 判据降噪:只是「口径不一致」时换这句 —— 不再拿"会消失"吓人
343
+ installedNotPersistedWarn: "已装上;清单 / lock 口径还没对齐(不影响重启后加载,可顺手钉一下)",
315
344
  installNote: "HMR 正在生效;若插件带界面,请刷新页面",
316
345
  bundleNote: "该包是 bundle 层插件(如皮肤合集),重启 DSH 服务后生效",
317
346
  // ── 安装诊断短句(2026-09-26 加法):面板只放短句;解释性长文留在服务端字段里,
@@ -319,6 +348,8 @@ window.__ModuleLoader__.load({
319
348
  diagCardTitle: "安装诊断",
320
349
  // 0.5.38 加法:持久化判据(服务端 domain/persist.js)——三处任一没就位就出这张卡
321
350
  diagNotPersisted: "未持久化:清单 / lock / 挂载行有缺口",
351
+ // 0.5.84 判据降噪:降级后的短句(可折叠提示,不再报红)
352
+ diagPersistDowngraded: "口径不一致(不影响重启后加载)",
322
353
  // 分隔符也进字典:英文界面用半角标点,避免中文全角标点混进英文句子
323
354
  diagColon: ":",
324
355
  diagSemi: ";",
@@ -414,6 +445,7 @@ window.__ModuleLoader__.load({
414
445
  stageConfiguring: "写入启用配置",
415
446
  stageRepairing: "本地 AI 接管安装中…",
416
447
  restartService: "重启服务(官方路径)",
448
+ restartHostedNotice: "本实例由桌面端外壳托管 —— 请在桌面端内重启:完全退出 DeepSeek Harness 再打开(控制台不自行 kill / spawn,以免与宿主争端口)。",
417
449
  restarting: "正在按官方路径处理重启请求…",
418
450
  autoReload: "页面即将自动刷新…",
419
451
  aiRepaired: "本地 AI 已接管并完成修复,请刷新页面查看",
@@ -725,6 +757,19 @@ window.__ModuleLoader__.load({
725
757
  safetyLockRepairPartial: "Still out of sync after the rebuild — see the problems below",
726
758
  safetyLockNoRebuild: "No rebuild needed right now",
727
759
  safetyLockCheckBtn: "Check",
760
+ diskTitle: "Disk usage (plugin-console)",
761
+ diskDesc: "Read-only audit: what fills this directory, what can be cleaned safely, and what must never be touched (the backup the current rollback point refers to is off limits)",
762
+ diskCheckBtn: "Audit disk",
763
+ diskSummary: "{mb} MB total ({n} entries) - about {safe} MB safely cleanable, about {reclaim} MB cleanable with a tradeoff",
764
+ diskSafeHint: "Clean only the safe tier (logs / old residue / state .bak files): no functionality is affected",
765
+ diskConfirmHint: "Confirm cleanup: this is the step that actually deletes (the server only deletes entries from the audit whitelist)",
766
+ diskCleanupBtn: "Clean safe items",
767
+ diskConfirmBtn: "Confirm cleanup (~{mb} MB)",
768
+ diskPlanReady: "Will remove {n} items, about {mb} MB freed - nothing is deleted until you confirm",
769
+ diskConfirmBtn2: "Confirm cleanup",
770
+ diskDone: "Cleaned",
771
+ diskFailed: "Disk audit failed",
772
+ diskNoCandidate: "Nothing is safely cleanable (the largest item is usually the framework backup the current rollback point refers to, which must not be deleted)",
728
773
  safetyLockRepairBtn: "Rebuild lock (explicit)",
729
774
  actionBoxTitle: "Suggested action",
730
775
  actionPinLabel: "Pin this dependency (record as link:)",
@@ -740,6 +785,11 @@ window.__ModuleLoader__.load({
740
785
  linksToggleBtn: "Dev link (link:) details",
741
786
  linksToggleSummary: "Dev links (link:): {n} total, {ready} self-sufficient",
742
787
  linksToggleTitle: "Expand/collapse link details (collapsed by default)",
788
+ linksBtn: "Dev links",
789
+ linksPanelTitle: "Dev links (link:) - the read-only list from /plugin-console/links: where it points, whether the target still exists, whether deps/peers are complete, and whether shims are in place",
790
+ linksPanelLoading: "Reading...",
791
+ linksPanelReload: "Reload",
792
+ linksPanelEmpty: "No plugin is pinned with link: (this list only covers dev links)",
743
793
  devRootsTitle: "Dev roots (only directories inside these can be pinned)",
744
794
  devRootsNone: "(none registered)",
745
795
  devRootAddPlaceholder: "Register a dev root (absolute path, e.g. D:\\my-plugins)",
@@ -891,6 +941,10 @@ window.__ModuleLoader__.load({
891
941
  migrateBtn: "Migrate & adapt",
892
942
  aiEmpowerFrameworkCheck: "Framework compat check",
893
943
  userForcedTag: "Forced on by patch",
944
+ disabledByBundleTag: "Disabled by plugin default",
945
+ forcedNotLoadedTag: "Force-enable had no effect",
946
+ revokeForceLabel: "Revoke this force-enable",
947
+ revokeForceHint: "This disabled: false override did not make the plugin load; revoking it stops the false forced-on badge (the manifest is untouched)",
894
948
  protectedTag: "Protected",
895
949
  unobserved: "Not mounted",
896
950
  pending: "Waiting for dependencies",
@@ -921,12 +975,14 @@ window.__ModuleLoader__.load({
921
975
  recentFailures: "Recent install failures",
922
976
  installed: "Installed and enabled",
923
977
  installedNotPersisted: "⚠ Installed but NOT persisted (it disappears on restart/reinstall)",
978
+ installedNotPersistedWarn: "Installed; manifest / lock are not fully aligned (this does NOT affect loading after a restart - pin it whenever convenient)",
924
979
  installNote: "HMR is applying; refresh the page if the plugin ships UI",
925
980
  bundleNote: "This package is a bundle-layer plugin (e.g. skin packs); restart the DSH service to activate it",
926
981
  // ── install-diagnosis one-liners (2026-09-26, additive): the panel shows short sentences only;
927
982
  // the long server-side explanations go to the title tooltip, never into the panel body ──
928
983
  diagCardTitle: "Install diagnosis",
929
984
  diagNotPersisted: "Not persisted: a gap remains in manifest / lock / mount row",
985
+ diagPersistDowngraded: "Not aligned (this does not affect loading after a restart)",
930
986
  // separators live in the dictionary too: English uses half-width punctuation
931
987
  diagColon: ": ",
932
988
  diagSemi: "; ",
@@ -1018,6 +1074,7 @@ window.__ModuleLoader__.load({
1018
1074
  stageConfiguring: "Writing enable config",
1019
1075
  stageRepairing: "Local AI is taking over the install…",
1020
1076
  restartService: "Restart service (official path)",
1077
+ restartHostedNotice: "This instance is hosted by the desktop shell - restart it there by fully quitting and reopening DeepSeek Harness.",
1021
1078
  restarting: "Handling the restart request via the official path…",
1022
1079
  autoReload: "Page will auto-reload…",
1023
1080
  aiRepaired: "Local AI took over and finished the repair; refresh the page",
@@ -2302,11 +2359,40 @@ function healthSignature(health) {
2302
2359
  });
2303
2360
  // 门控面板(用户要求:总开关收进「功能包」里的 [门控] 按钮,不再占已安装列表的表头)
2304
2361
  const [gateOpen, setGateOpen] = react.useState(false);
2362
+ // ★ 0.5.84 加法(用户第 6 项):功能包工具栏里的 **link 入口**。
2363
+ // 过去开发式链接(link:)清单只长在「升级安全」面板**内部**的折叠块里 —— 得先找到那个入口、
2364
+ // 再展开才看得到;用户要的是从工具栏直接跳到那份数据。数据源与判据与安全面板**完全同一份**
2365
+ // (`GET /plugin-console/links`,纯只读,判据全在 domain/link-inventory.js);
2366
+ // 这里只负责如实展示,不推断、不美化,也不自己算"自足/缺什么"。
2367
+ // 安全面板里那块折叠展示**保留不动**(老入口照旧),这是加法。
2368
+ const [linksPanelOpen, setLinksPanelOpen] = react.useState(false);
2369
+ const [linksPanelData, setLinksPanelData] = react.useState(null);
2370
+ const [linksPanelBusy, setLinksPanelBusy] = react.useState(false);
2371
+ const linksPanelLoad = async () => {
2372
+ if (linksPanelBusy) return;
2373
+ setLinksPanelBusy(true);
2374
+ try {
2375
+ const data = await call("/plugin-console/links");
2376
+ if (data !== null) setLinksPanelData(data);
2377
+ } catch (error) {
2378
+ // 拿不到就如实说(不静默成"没有 link 依赖")
2379
+ setLinksPanelData({ ok: false, items: [], count: 0, readyCount: 0, error: (error && error.message) || t("safetySwitchFailed") });
2380
+ } finally { setLinksPanelBusy(false); }
2381
+ };
2382
+ // ★ 0.5.80:历史解锁记录默认折叠(用户:新的应覆盖旧的,别一直铺满)
2383
+ const [gateHistoryOpen, setGateHistoryOpen] = react.useState(false);
2305
2384
  // 功能包位置:长按拖动,持久化 localStorage(每次打开沿用上次位置)
2306
2385
  const [toolbarPos, setToolbarPos] = react.useState(() => {
2307
2386
  try {
2308
2387
  const v = JSON.parse(localStorage.getItem("pc-toolbar-pos") || "null");
2309
- if (v && Number.isFinite(v.left) && Number.isFinite(v.top)) return { left: v.left, top: v.top };
2388
+ if (v && Number.isFinite(v.left) && Number.isFinite(v.top)) {
2389
+ // ★ 用户 2026-10-18:存下来的位置**必须钳进可视区**,否则拖到标题栏/屏幕外就再也拿不回来。
2390
+ // 上边界 64px 是标题栏与窗口按钮的安全区(原来只钳到 8px,正好落在标题栏里)。
2391
+ const SAFE_TOP = 64;
2392
+ const maxLeft = Math.max(8, (window.innerWidth || 1280) - 120);
2393
+ const maxTop = Math.max(SAFE_TOP, (window.innerHeight || 800) - 80);
2394
+ return { left: Math.min(Math.max(8, v.left), maxLeft), top: Math.min(Math.max(SAFE_TOP, v.top), maxTop) };
2395
+ }
2310
2396
  } catch {}
2311
2397
  return null;
2312
2398
  });
@@ -2929,6 +3015,15 @@ function healthSignature(health) {
2929
3015
  (error) => setDetails({ entryId: entry.entryId, status: "error", error }),
2930
3016
  );
2931
3017
  };
3018
+ // ★ 0.5.76:撤销"强制启用"覆盖(补丁里的 disabled: false)。只对确实 forced 的行生效,服务端会校验。
3019
+ const revokeForce = (entry) => {
3020
+ if (busy !== null) return;
3021
+ setBusy(entry.entryId);
3022
+ call("/plugin-console/revoke-force", { entryId: entry.entryId }).then(
3023
+ (data) => { setMessage(data?.note ?? t("revokeForceLabel")); refresh(); },
3024
+ (error) => { setMessage(friendlyGithubError(error).message); },
3025
+ ).finally(() => setBusy(null));
3026
+ };
2932
3027
  const checkUpdate = (entry) => {
2933
3028
  const pkgName = entry.moduleName;
2934
3029
  setUpdateCheck({ status: "checking" });
@@ -3448,7 +3543,13 @@ function healthSignature(health) {
3448
3543
  // 0.5.38 加法(用户诉求「下载下来就不用 lock、重启也不会消失」):
3449
3544
  // 三处没齐备时**绝不报"已安装并启用"** —— 说清它还缺什么、后果是什么,
3450
3545
  // 并把服务端下发的「一键钉住」执行框带出来(下面的 suggestedAction 分支)。
3451
- setMessage(t("installedNotPersisted") + ":" + data.packageName + (typeof data.persistNote === "string" && data.persistNote !== "" ? "。" + data.persistNote : "") + diagMessage(data));
3546
+ // 0.5.84 降噪:只有「会丢插件」那一档才用"重启/重装会消失"这句;只是口径
3547
+ // 不一致(lock 形态、清单与磁盘版本)时用软一点的措辞,原因照样在诊断行里。
3548
+ const sev = data.persist !== null && data.persist !== undefined && typeof data.persist.severity === "object" && data.persist.severity !== null ? data.persist.severity : null;
3549
+ const criticalGap = sev === null ? true : sev.level === "critical";
3550
+ const head = criticalGap ? t("installedNotPersisted") : t("installedNotPersistedWarn");
3551
+ const detail = typeof data.persistNote === "string" && data.persistNote !== "" ? data.persistNote : (typeof sev?.note === "string" ? sev.note : "");
3552
+ setMessage(head + ":" + data.packageName + (detail !== "" ? "。" + detail : "") + diagMessage(data));
3452
3553
  setReloadHint(true);
3453
3554
  } else {
3454
3555
  setMessage(t("installed") + ":" + data.packageName + "(" + (data.entryId ?? "") + ")。" + (data.curlNote ?? (data.bundle ? t("bundleNote") : t("installNote"))) + (data.bundleNote ? "。" + data.bundleNote : "") + (data.lockNote ? "。⚠️ " + data.lockNote : "") + (data.depNote ? "。" + data.depNote : "") + diagMessage(data));
@@ -3599,15 +3700,27 @@ function healthSignature(health) {
3599
3700
  title: channelNotes.join("\n"),
3600
3701
  });
3601
3702
  }
3602
- // ⑤ 持久化判据(0.5.38 加法,服务端 domain/persist.js:{ missing, actual, note }):
3603
- // 三处(清单 spec / pnpm-lock.yaml 条目 / 挂载行)任一没就位 → error 级诊断行,
3703
+ // ⑤ 持久化判据(0.5.38 加法,服务端 domain/persist.js:{ missing, actual, note, severity }):
3704
+ // 三处(清单 spec / pnpm-lock.yaml 条目 / 挂载行)任一没就位 → 诊断行,
3604
3705
  // 完整缺口清单挂 title。**装成功但没钉住**的作业从此不会只显示"已安装并启用"。
3706
+ // ★ 0.5.84 判据降噪(用户 2026-10 反馈:红字太多,真要紧的那条被淹了):
3707
+ // 服务端 persistSeverity() 已把每个缺口分成「会丢插件 / 只是口径不一致」两档,
3708
+ // 这里**只认那一份结论**,不自己重判:
3709
+ // · critical(清单里没有它 / 重启后挂不上)→ error 级(红),并给完整缺口清单;
3710
+ // · warn(lock 形态/目标、清单形态与磁盘版本不一致)→ warn 级(可折叠提示)+
3711
+ // 明说"不影响重启后加载",不再拿红字吓人。
3712
+ // 老服务端没有 severity 字段 → 按 critical 处理(保持 0.5.38 以来的既有行为,不静默降级)。
3605
3713
  if (view.status === "done" && view.persisted === false) {
3714
+ const severity = view.persist !== null && view.persist !== undefined && typeof view.persist.severity === "object" && view.persist.severity !== null ? view.persist.severity : null;
3715
+ const critical = severity === null ? true : severity.level === "critical";
3716
+ const missingText = Array.isArray(view.persist?.missing) && view.persist.missing.length > 0 ? t("diagColon") + view.persist.missing.join(" / ") : "";
3717
+ const longText = typeof view.persistNote === "string" && view.persistNote !== "" ? view.persistNote
3718
+ : (typeof view.persist?.note === "string" ? view.persist.note : null);
3606
3719
  lines.push({
3607
3720
  key: "persist",
3608
- text: t("diagNotPersisted") + (Array.isArray(view.persist?.missing) && view.persist.missing.length > 0 ? t("diagColon") + view.persist.missing.join(" / ") : ""),
3609
- level: "error",
3610
- title: typeof view.persistNote === "string" && view.persistNote !== "" ? view.persistNote : (typeof view.persist?.note === "string" ? view.persist.note : null),
3721
+ text: critical ? t("diagNotPersisted") + missingText : t("diagPersistDowngraded") + missingText,
3722
+ level: critical ? "error" : "warn",
3723
+ title: !critical && typeof severity?.note === "string" && severity.note !== "" ? severity.note : longText,
3611
3724
  });
3612
3725
  }
3613
3726
  return lines;
@@ -3796,7 +3909,7 @@ function healthSignature(health) {
3796
3909
  };
3797
3910
  // subdir 用于「从这个子目录安装」(0.5.47 加法):服务端按它去读 <subdir>/package.json、
3798
3911
  // 稀疏取该子目录源码、再按既有本地目录来源(link:)装。为空时请求体形状与改动前逐字相同。
3799
- const startJob = (repo, packageName, source = "github", kind = "plugin", update = false, subdir = null) => {
3912
+ const startJob = (repo, packageName, source = "github", kind = "plugin", update = false, subdir = null, version = null) => {
3800
3913
  // 防并发(与 addLocal 一致):已有安装/更新进行中时忽略新点击(更新按钮此前可绕过,
3801
3914
  // 反复点击会产生几十个并发任务装同一插件)
3802
3915
  // 只拦**同一个目标**:不同插件可以并行更新(服务端是作业模型,天然支持)
@@ -3811,6 +3924,9 @@ function healthSignature(health) {
3811
3924
  setRestartHint(false);
3812
3925
  setMessage(null);
3813
3926
  const body = { repo: repo ?? "", packageName, source, kind, update };
3927
+ // ★ 0.5.81(用户 2026-10-18):目标版本必须传下去 —— 不带版本时 pnpm 按 latest 把旧版本原地重装,
3928
+ // 表现就是"更新了、刷新后还是老版本"。null/空串一律不带(行为与改动前逐字相同)。
3929
+ if (typeof version === "string" && version !== "") body.version = version;
3814
3930
  if (subdir !== null && subdir !== undefined && String(subdir) !== "") body.subdir = String(subdir);
3815
3931
  call("/plugin-console/install", body).then(
3816
3932
  (data) => {
@@ -4096,7 +4212,8 @@ function healthSignature(health) {
4096
4212
  type: "button",
4097
4213
  className: styles.toggle,
4098
4214
  disabled: installing === entry.moduleName,
4099
- onClick: () => startJob("", entry.moduleName, "github", "plugin", true),
4215
+ // ★ 0.5.81:把服务端算出的目标版本一起传下去(传 null 时行为与改动前逐字相同)
4216
+ onClick: () => startJob("", entry.moduleName, "github", "plugin", true, null, updateCheck.target ?? updateCheck.latest ?? null),
4100
4217
  }, installing === entry.moduleName ? t("installing") : (entry.pendingCompat === true ? t("updateAndAdapt") : t("updateNow")))),
4101
4218
  Array.isArray(updateCheck.depsOutdated) && updateCheck.depsOutdated.length > 0
4102
4219
  ? el("p", { className: styles.status, "data-error": "true" },
@@ -4135,6 +4252,10 @@ function healthSignature(health) {
4135
4252
  entry.protected ? el("span", { className: styles.tag, "data-user": "true" }, t("protectedTag")) : null,
4136
4253
  entry.userDisabled ? el("span", { className: styles.tag, "data-user": "true" }, t("userDisabledTag")) : null,
4137
4254
  entry.userForced ? el("span", { className: styles.tag, "data-user": "true" }, t("userForcedTag")) : null,
4255
+ // ★ 0.5.76:被**别处**(插件自身补丁/框架默认)禁用的行 —— 这就是"补丁强制启用"的来由
4256
+ entry.disableSource === "bundle" ? el("span", { className: styles.tag, "data-user": "true", "data-source": "bundle" }, t("disabledByBundleTag")) : null,
4257
+ // ★ 0.5.76:补丁写了强制开启、运行时却仍没起来 ⇒ 这条覆盖是**无效的**,必须说出来
4258
+ entry.forcedNotLoaded === true ? el("span", { className: styles.tag, "data-user": "true", "data-source": "ineffective" }, t("forcedNotLoadedTag")) : null,
4138
4259
  ),
4139
4260
  el("div", { className: styles.meta },
4140
4261
  el("span", { className: styles.phase, "data-phase": entry.fiberPhase ?? "unobserved" },
@@ -4164,11 +4285,19 @@ function healthSignature(health) {
4164
4285
  el("button", {
4165
4286
  type: "button",
4166
4287
  className: styles.toggle,
4167
- disabled: !entry.toggleable || busy === entry.entryId || entry.pendingCompat === true,
4288
+ // ★ 强制启用且未运行 ⇒ 这个开关本身是空操作(patch.js#enableEntry 直接 changed:false),藏起来
4289
+ disabled: !entry.toggleable || busy === entry.entryId, // ★ 待适配不再禁用启用(用户 2026-10-18):启用=确认启用,服务端会把它标为已采纳
4168
4290
  // 0.5.34:结论已被当前判据撤回的行不再显示「待适配」(旧判据的误报不许当事实展示)
4169
4291
  title: entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedHint") : t("pendingCompatHint")) : undefined,
4170
4292
  onClick: () => toggle(entry, !entry.enabled),
4171
4293
  }, busy === entry.entryId ? t("loadingPhase") : (entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedTag") : t("pendingCompatTag")) : t(entry.enabled ? "off" : "on"))),
4294
+ // ★ 0.5.76:补丁写了强制开启但运行时没起来 ⇒ 给一个撤销覆盖的入口
4295
+ entry.userForced === true ? el("button", {
4296
+ type: "button",
4297
+ className: styles.toggle,
4298
+ title: t("revokeForceHint"),
4299
+ onClick: () => revokeForce(entry),
4300
+ }, t("revokeForceLabel")) : null,
4172
4301
  el("button", {
4173
4302
  type: "button",
4174
4303
  className: styles.toggle,
@@ -4888,7 +5017,44 @@ function healthSignature(health) {
4888
5017
  el("button", { type: "button", className: styles.toggle, onClick: fwStatusRefresh }, t("fwPanelRefreshStatus")),
4889
5018
  el("button", { type: "button", className: styles.toggle, disabled: fwCheckBusy, onClick: () => fwCheckRefresh(true) }, fwCheckBusy ? t("fwPanelChecking") : t("fwPanelRecheck"))),
4890
5019
  el("div", { className: styles.rowTop },
4891
- el("button", { type: "button", className: styles.toggle, onClick: () => { restartServiceViaServer(setMessage); } }, t("restartService")))))
5020
+ hostedByShell
5021
+ ? el("p", { className: styles.status, "data-hosted": "true" }, t("restartHostedNotice"))
5022
+ : el("button", { type: "button", className: styles.toggle, onClick: () => { restartServiceViaServer(setMessage); } }, t("restartService")))))
5023
+ : null,
5024
+ // ★ 0.5.84(用户第 6 项):功能包 → [开发式链接] 的面板:直接展示 /plugin-console/links
5025
+ // 的同一份数据(与「升级安全」里那块折叠展示同源)。判据来自服务端,这里只如实展示:
5026
+ // 每行 = ✓/• + 包名 + 版本 + 指向 + 缺什么(悬停看完整原因)。
5027
+ linksPanelOpen
5028
+ ? el("div", { className: styles.modalBackdrop, onClick: (event) => { if (event.target === event.currentTarget) setLinksPanelOpen(false); } },
5029
+ el("div", { className: styles.modalCard, style: { maxWidth: 620 } },
5030
+ el("div", { className: styles.rowTop, style: { justifyContent: "space-between" } },
5031
+ el("strong", { className: styles.name }, t("linksBtn")),
5032
+ el("button", { type: "button", className: styles.trashBtn, style: { fontSize: 14, padding: "4px 8px" }, title: t("closeModal"), "aria-label": t("closeModal"), onClick: () => setLinksPanelOpen(false) }, "✕")),
5033
+ el("p", { className: styles.message }, t("linksPanelTitle")),
5034
+ linksPanelData === null
5035
+ ? el("p", { className: styles.status }, t("linksPanelLoading"))
5036
+ : (linksPanelData.ok === false
5037
+ ? el("p", { className: styles.message, "data-warn": "true" }, t("linksFailed") + ":" + String(linksPanelData.error ?? "-"))
5038
+ : el("div", { style: { flexDirection: "column", gap: 4, display: "flex", maxHeight: "52vh", overflow: "auto" } },
5039
+ // ⚠️ 这里**不能**用 fill():它是 PluginSafetyTab 内部的局部助手,
5040
+ // 主面板里出现 fill( 就是 0.5.59 那次「整块空白」事故的根因
5041
+ // (tests/test-ui-contract.mjs ⑨ 与 test-client-scope.mjs ★★ 都把这条钉死了)。
5042
+ // 这里用最朴素的字符串替换,语义与 fill 的两个占位符一致。
5043
+ el("div", { style: { fontSize: 12, fontWeight: 500 } },
5044
+ t("linksTitle").replace("{n}", String(linksPanelData.count ?? 0)).replace("{ready}", String(linksPanelData.readyCount ?? 0))),
5045
+ (Array.isArray(linksPanelData.items) ? linksPanelData.items : []).length === 0
5046
+ ? el("p", { className: styles.status }, t("linksPanelEmpty"))
5047
+ : null,
5048
+ (Array.isArray(linksPanelData.items) ? linksPanelData.items : []).map((x) => el("div", { key: "lp-" + x.packageName, title: x.note ?? undefined, style: { fontSize: 12, opacity: .9, wordBreak: "break-all" } },
5049
+ (x.ready === true ? "✓ " : "• ") + x.packageName + (x.version ? " v" + x.version : "")
5050
+ + (x.target ? " → " + x.target : "")
5051
+ + (Array.isArray(x.missingDeps) && x.missingDeps.length > 0 ? " · 缺依赖 " + x.missingDeps.length : "")
5052
+ + (Array.isArray(x.missingPeers) && x.missingPeers.length > 0 ? " · 缺 peer " + x.missingPeers.length : "")
5053
+ + (x.foreignPeers !== null && x.foreignPeers !== undefined && Array.isArray(x.foreignPeers) && x.foreignPeers.length > 0 ? " · 异副本 " + x.foreignPeers.length : "")
5054
+ + (x.shims !== null && x.shims !== undefined ? " · 垫片 " + String(x.shims.count ?? 0) : "")
5055
+ + (x.persisted !== null && x.persisted !== undefined && x.persisted.ok === false ? " · 未持久化" : ""))))),
5056
+ el("div", { className: styles.rowTop, style: { justifyContent: "flex-end", gap: 8, marginTop: 10 } },
5057
+ el("button", { type: "button", className: styles.toolItem, "data-busy": linksPanelBusy ? "true" : "false", onClick: linksPanelLoad }, linksPanelBusy ? t("linksPanelLoading") : t("linksPanelReload")))))
4892
5058
  : null,
4893
5059
  gateOpen
4894
5060
  ? el("div", { className: styles.modalBackdrop, onClick: (event) => { if (event.target === event.currentTarget) setGateOpen(false); } },
@@ -4922,9 +5088,7 @@ function healthSignature(health) {
4922
5088
  el("p", { className: styles.message }, t("compatGateHint")),
4923
5089
  el("p", { className: styles.status, "data-pending": gatePendingCount > 0 ? "true" : undefined },
4924
5090
  gatePendingCount > 0 ? t("gatePendingInfo").replace("{n}", String(gatePendingCount)) : t("gatePendingNone")),
4925
- el("p", { className: styles.status, "data-pending": gatePendingCount > 0 ? "true" : undefined },
4926
- gatePendingCount > 0 ? t("gatePendingInfo").replace("{n}", String(gatePendingCount)) : t("gatePendingNone")),
4927
- // 明细清单(2026-09-24 用户要求:面板要能看到是哪些插件,有列表数据)
5091
+ // 明细清单(2026-09-24 用户要求:面板要能看到是哪些插件,有列表数据)
4928
5092
  gateRows !== null && gateRows.pendingCount + gateRows.adoptedCount > 0
4929
5093
  ? el("div", { style: { flexDirection: "column", gap: 6, display: "flex" } },
4930
5094
  el("strong", { className: styles.name }, t("gateListPending").replace("{n}", String(gateRows.pendingCount))),
@@ -4947,9 +5111,17 @@ function healthSignature(health) {
4947
5111
  : null,
4948
5112
  gateRows.adoptedCount > 0
4949
5113
  ? el("div", { style: { flexDirection: "column", gap: 2, display: "flex" } },
4950
- el("strong", { className: styles.name }, t("gateListAdopted").replace("{n}", String(gateRows.adoptedCount))),
4951
- gateRows.adopted.slice(0, 8).map((r) => el("span", { key: "a-" + String(r.rowId), className: styles.status },
4952
- "✓ " + String(r.name) + (r.version ? " v" + r.version : "") + (r.adoptedAt ? " · " + gateTime(r.adoptedAt) : ""))))
5114
+ // ★ 0.5.80:默认折叠,只留一行摘要(点开才列明细)—— 历史不该把当前态挤下去
5115
+ el("button", {
5116
+ type: "button",
5117
+ className: styles.toggle,
5118
+ onClick: () => setGateHistoryOpen((v) => !v),
5119
+ }, (gateHistoryOpen ? t("gateHistoryHide") : t("gateHistoryShow").replace("{n}", String(gateRows.adoptedCount)))
5120
+ + " · " + t("gateListAdopted").replace("{n}", String(gateRows.adoptedCount))),
5121
+ gateHistoryOpen
5122
+ ? gateRows.adopted.map((r) => el("span", { key: "a-" + String(r.rowId), className: styles.status },
5123
+ "✓ " + String(r.name) + (r.version ? " v" + r.version : "") + (r.adoptedAt ? " · " + gateTime(r.adoptedAt) : "")))
5124
+ : null)
4953
5125
  : null)
4954
5126
  : null),
4955
5127
  // 0.5.42 加法(用户点名):右侧贴底的 Star 提示 —— 只加这一行 UI,
@@ -5339,7 +5511,7 @@ function healthSignature(health) {
5339
5511
  const bw = e.currentTarget.offsetWidth || 96;
5340
5512
  const bh = e.currentTarget.offsetHeight || 30;
5341
5513
  const left = Math.max(8, Math.min(w - bw - 8, e.clientX - dragRef.current.grabX));
5342
- const top = Math.max(8, Math.min(h - bh - 8, e.clientY - dragRef.current.grabY));
5514
+ const top = Math.max(64, Math.min(h - bh - 8, e.clientY - dragRef.current.grabY)); // ★ 64 = 标题栏安全区
5343
5515
  setDragPos({ left, top });
5344
5516
  },
5345
5517
  onPointerUp: () => {
@@ -5372,13 +5544,14 @@ function healthSignature(health) {
5372
5544
  }, t("toolbarLabel") + (toolbarOpen ? " ▾" : " ▸")),
5373
5545
  toolbarOpen
5374
5546
  ? el("div", { className: styles.toolbarGroup, style: (dragPos ?? toolbarPos) !== null ? { left: (dragPos ?? toolbarPos).left, top: (dragPos ?? toolbarPos).top + 36, right: "auto" } : undefined },
5375
- el("button", {
5376
- type: "button",
5377
- className: styles.toolItem,
5378
- title: t("restartService"),
5379
- onClick: () => { restartServiceViaServer(setMessage); },
5380
- }, t("restartService")),
5381
- el("button", {
5547
+
5548
+ hostedByShell ? null : el("button", { // ★ 0.5.84:桌面端(shell 托管)不显示;web 端保留且可用
5549
+ type: "button",
5550
+ className: styles.toolItem,
5551
+ title: t("restartService"),
5552
+ onClick: () => { restartServiceViaServer(setMessage); },
5553
+ }, t("restartService")),
5554
+ el("button", {
5382
5555
  type: "button",
5383
5556
  className: styles.toolItem,
5384
5557
  "data-active": repoLandOn ? "true" : "false",
@@ -5414,6 +5587,16 @@ function healthSignature(health) {
5414
5587
  "aria-label": t("sourcesBtn"),
5415
5588
  onClick: openSources,
5416
5589
  }, t("sourcesBtn")),
5590
+ el("button", {
5591
+ type: "button",
5592
+ className: styles.toolItem,
5593
+ "data-active": linksPanelOpen ? "true" : "false",
5594
+ "data-busy": linksPanelBusy ? "true" : "false",
5595
+ title: t("linksPanelTitle"),
5596
+ "aria-label": t("linksBtn"),
5597
+ // ★ 0.5.84(用户第 6 项):打开时拉一次真实清单(与安全面板同一个只读接口)
5598
+ onClick: () => setLinksPanelOpen((v) => { const next = !v; if (next) linksPanelLoad(); return next; }),
5599
+ }, t("linksBtn") + (linksPanelData !== null && Array.isArray(linksPanelData.items) ? " " + linksPanelData.items.length : "")),
5417
5600
  el("button", {
5418
5601
  type: "button",
5419
5602
  className: styles.toolItem,
@@ -6247,6 +6430,45 @@ function healthSignature(health) {
6247
6430
  const [pinInput, setPinInput] = react.useState("");
6248
6431
  const [pinNote, setPinNote] = react.useState(null);
6249
6432
  const [allowAgent, setAllowAgent] = react.useState(false);
6433
+ // ★ 0.5.84 加法(用户第 7 项):磁盘体检 / 清理的状态与两个动作。
6434
+ // 判据全在服务端(domain/disk-audit.js):面板只展示服务端给的 level 与体积,
6435
+ // 并且**必须**经过「先要计划(不带 confirm)→ 再确认(confirm:true)」两步。
6436
+ const [diskAudit, setDiskAudit] = react.useState(null);
6437
+ const [diskPlan, setDiskPlan] = react.useState(null);
6438
+ const [diskBusy, setDiskBusy] = react.useState(false);
6439
+ const [diskNote, setDiskNote] = react.useState(null);
6440
+ const diskCheck = async () => {
6441
+ if (diskBusy) return;
6442
+ setDiskBusy(true); setDiskNote(null); setDiskPlan(null);
6443
+ try {
6444
+ const data = await call("/plugin-console/disk-audit"); // 只读:无 body ⇒ GET
6445
+ setDiskAudit(data ?? null);
6446
+ if (data !== null && data.ok === true) setDiskNote({ text: fill(t("diskSummary"), [["mb", String(data.totalMb ?? 0)], ["n", String((data.entries ?? []).length)], ["safe", String(data.safeMb ?? 0)], ["reclaim", String(data.reclaimableMb ?? 0)]]) + (data.truncated === true ? "(统计已截断)" : ""), level: null });
6447
+ } catch (error) { setDiskNote({ text: String((error && error.message) || error), level: "error" }); }
6448
+ finally { setDiskBusy(false); }
6449
+ };
6450
+ // confirm=false ⇒ 服务端**只回计划**(一个文件都不动);confirm=true 才会真删。
6451
+ const diskCleanup = async (confirm) => {
6452
+ if (diskBusy) return;
6453
+ setDiskBusy(true); setDiskNote(null);
6454
+ try {
6455
+ const ids = confirm && diskPlan !== null && Array.isArray(diskPlan.ids) && diskPlan.ids.length > 0
6456
+ ? diskPlan.ids
6457
+ : (diskAudit !== null && Array.isArray(diskAudit.entries) ? diskAudit.entries.filter((x) => x.level === "safe").map((x) => x.id) : []);
6458
+ const data = await call("/plugin-console/disk-cleanup", { ids: ids, confirm: confirm === true });
6459
+ if (data !== null && data.needsConfirm === true) {
6460
+ // 第一步的回报:把计划(哪些项 / 能省多少)留下,等用户点「确认清理」
6461
+ const plan = data.plan ?? {};
6462
+ setDiskPlan({ ids: (plan.plan ?? []).map((p) => p.id), freedMb: plan.freedMb ?? 0 });
6463
+ setDiskNote({ text: fill(t("diskPlanReady"), [["n", String((plan.plan ?? []).length)], ["mb", String(plan.freedMb ?? 0)]]) + ((plan.refused ?? []).length === 0 ? "" : " · 已拒绝 " + plan.refused.length + " 项"), level: null });
6464
+ } else if (data !== null) {
6465
+ setDiskPlan(null);
6466
+ setDiskNote({ text: String(data.note ?? t("diskDone")), level: data.ok === true ? null : "error" });
6467
+ await diskCheck(); // 删完重新体检,让面板显示真实的新体积
6468
+ }
6469
+ } catch (error) { setDiskNote({ text: String((error && error.message) || error), level: "error" }); }
6470
+ finally { setDiskBusy(false); }
6471
+ };
6250
6472
  const load = async () => {
6251
6473
  try {
6252
6474
  // link 清单:独立 try(拿不到时这块显示"读取失败",绝不静默)
@@ -6496,6 +6718,28 @@ function healthSignature(health) {
6496
6718
  lockFix !== null && lockFix.ok !== true && Array.isArray(lockFix.problems) && lockFix.problems.length > 0
6497
6719
  ? lockFix.problems.map((p) => p.hint).join("\n")
6498
6720
  : undefined),
6721
+ // ★ 0.5.84 加法(用户第 7 项):`<DSH_HOME>/plugin-console` 的**磁盘体检 + 显式确认后清理**。
6722
+ // 两段式(不让人闭眼点):先「磁盘体检」(GET /disk-audit,纯只读)→ 再「清理安全项」
6723
+ // (POST /disk-cleanup,**不带 confirm** ⇒ 服务端只回计划)→ 最后「确认清理」(confirm:true)。
6724
+ // 哪些能删、哪些绝不能删**全由服务端判**(domain/disk-audit.js):真机实测最大的 2.1 GB
6725
+ // 恰恰是当前回滚点指向的备份,面板不自己按体积猜。
6726
+ row("trash", t("diskTitle"),
6727
+ diskAudit === null ? t("diskDesc")
6728
+ : diskAudit.ok !== true ? t("diskFailed") + ":" + String(diskAudit.error ?? "-")
6729
+ : fill(t("diskSummary"), [["mb", String(diskAudit.totalMb ?? 0)], ["n", String((diskAudit.entries ?? []).length)], ["safe", String(diskAudit.safeMb ?? 0)], ["reclaim", String(diskAudit.reclaimableMb ?? 0)]]),
6730
+ el("div", { style: { display: "flex", gap: "8px", flex: "0 0 auto" } },
6731
+ el("button", { type: "button", className: styles.toggle, disabled: busy || diskBusy, title: t("diskDesc"), onClick: diskCheck }, t("diskCheckBtn")),
6732
+ diskAudit !== null && diskAudit.ok === true && (diskAudit.safeBytes ?? 0) > 0
6733
+ ? el("button", { type: "button", className: styles.toggle, disabled: busy || diskBusy, title: t("diskSafeHint"), onClick: () => diskCleanup(false) }, t("diskCleanupBtn"))
6734
+ : null,
6735
+ diskPlan !== null
6736
+ ? el("button", { type: "button", className: styles.toggle, disabled: busy || diskBusy, title: t("diskConfirmHint"), onClick: () => diskCleanup(true) }, fill(t("diskConfirmBtn"), [["mb", String(diskPlan.freedMb ?? 0)]]))
6737
+ : null),
6738
+ diskAudit !== null && diskAudit.ok === true && Array.isArray(diskAudit.entries)
6739
+ ? diskAudit.entries.filter((x) => x.level !== "protected").slice(0, 6)
6740
+ .map((x) => (x.level === "safe" ? "✓ " : "! ") + x.name + " " + x.mb + " MB").join("\n") || t("diskNoCandidate")
6741
+ : undefined),
6742
+ diskNote === null ? null : el("div", { style: { fontSize: "12px", padding: "0 12px 6px", color: diskNote.level === "error" ? "#d33" : "inherit", wordBreak: "break-word" } }, diskNote.text),
6499
6743
  row("pulse", t("safetyAdaptTitle"), t("safetyAdaptDesc"), el("div", { style: { display: "flex", gap: "8px", flex: "0 0 auto" } },
6500
6744
  el("button", { type: "button", className: styles.toggle, disabled: busy, onClick: () => preflight(false) }, t("fwPreflightBtn")),
6501
6745
  el("button", { type: "button", className: styles.toggle, disabled: busy, onClick: () => preflight(true) }, t("fwPreflightPatch")))),