@noob-stupid/dsh-plugin-console 0.5.54 → 0.5.55

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
@@ -120,6 +120,7 @@ window.__ModuleLoader__.load({
120
120
  skippedBundlesTitle: "被框架跳过的 bundle({n} 个)—— 它们装了但整层没被装载,所以插件列表里看不到;鼠标悬停看每一项的出路",
121
121
  hostedNoChannel: "本实例由桌面端外壳托管:框架升级与回滚请走桌面端(此处不可用)",
122
122
  linksTitle: "开发式链接(link):共 {n} 个,自足 {ready} 个 —— ✓ 自足 · · 缺东西(悬停看原因)",
123
+ healthSummary: "需要处理 {n} 类问题",
123
124
  linksFailed: "开发式链接清单读取失败",
124
125
  actionReconcileLabel: "重建 pnpm-lock.yaml",
125
126
  actionRun: "执行",
@@ -713,6 +714,7 @@ window.__ModuleLoader__.load({
713
714
  skippedBundlesTitle: "{n} bundle(s) skipped by the framework - installed but never loaded, which is why they are absent from the plugin list; hover an item for the way out",
714
715
  hostedNoChannel: "Hosted by the desktop shell: use the desktop app for framework upgrade and rollback (unavailable here)",
715
716
  linksTitle: "Dev links (link:): {n} total, {ready} self-sufficient - checkmark = ready, dot = something missing (hover for why)",
717
+ healthSummary: "{n} issue type(s) need attention",
716
718
  linksFailed: "Failed to read the dev-link inventory",
717
719
  actionReconcileLabel: "Rebuild pnpm-lock.yaml",
718
720
  actionRun: "Run",
@@ -4587,6 +4589,15 @@ window.__ModuleLoader__.load({
4587
4589
  }, t("compatNoticeDismiss")),
4588
4590
  )
4589
4591
  : null,
4592
+ // ★ 0.5.55 加法:**一行健康摘要** —— 面板有 176 个按钮、信息散在六处,
4593
+ // 过去没有任何地方能回答"整体健康吗、该先处理哪件"。这里把服务端已折好的
4594
+ // 分项计数摆在最上面(不新增判据、不新增 IO:全部来自 /state 已有的块)。
4595
+ state.status === "ready" && state.data.health && state.data.health.issues > 0
4596
+ && Array.isArray(state.data.health.items)
4597
+ ? el("p", { className: styles.message, "data-error": "true", style: { marginTop: "8px", marginBottom: "0" } },
4598
+ fill(t("healthSummary"), [["n", String(state.data.health.issues)]]) + ":"
4599
+ + state.data.health.items.map((x) => x.label + " " + String(x.count)).join(" · "))
4600
+ : null,
4590
4601
  // ★ 0.5.53 加法(真机 2026-10-15):**被框架整层跳过的 bundle**。
4591
4602
  // 为什么必须放在这里(不是列表里):被跳过的 bundle **不产生任何 loader 行**,
4592
4603
  // 所以在插件列表里根本没有位置可挂 —— 用户看到的是"我明明有,却不显示"。
@@ -0,0 +1,64 @@
1
+ // L1 · domain —— 安装健康摘要(0.5.55 加法)
2
+ //
3
+ // 为什么需要(审计结论 + 用户 2026-10-15「优化到你能做到的最好」):
4
+ // 面板有 176 个按钮、3 个区块,信息散在「兼容门控 / peer 否决 / 持久化 / 依赖锁 / 框架残留 / 闭包预检」
5
+ // 六处。用户打开面板时**没有一个地方能回答**:"我这套安装现在整体健康吗?有哪几件事需要我处理?"
6
+ // 于是该点的找不到、不该点的全在眼前 —— 这不是功能不足,是**入口缺失**。
7
+ //
8
+ // 本模块刻意**只做折叠、不新增判据**:
9
+ // 输入全部是 /state 里**已经算好**的东西(否决行、被跳过的 bundle、待适配行、补丁自愈结果),
10
+ // 输出一句话摘要 + 分项计数。**不读盘、不联网、不 spawn** —— 因此可以放进 /state 的返回里,
11
+ // 不带来任何额外 IO 成本。
12
+ //
13
+ // 判据口径(与各自真源保持一致,别在这里另立标准):
14
+ // · veto ← entries[].veto(domain/peer-veto.js 的结论)
15
+ // · skippedBundle← skippedBundles(domain/bundle-veto.js 的结论;只在 reliable 时计入)
16
+ // · pendingCompat← compatPending.pending(兼容门控的待适配行)
17
+ // · patchHeal ← patchHeal.uncertain / autoDisabled(补丁自愈的遗留项)
18
+ //
19
+ // 分层:L1 domain —— 纯函数,收已解开的数组/对象(因此**不 import 任何其它模块**)。
20
+
21
+ /** 严重度:blocker 会让人"看不到/用不了",warn 只是"有风险"。 */
22
+ const SEVERITY = { blocker: 'blocker', warn: 'warn' }
23
+
24
+ /**
25
+ * 把已算好的各块折叠成一份健康摘要。
26
+ *
27
+ * @param {object} p
28
+ * @param {Array} [p.entries] /state 的 entries(含 veto)
29
+ * @param {{items?: Array, reliable?: boolean}} [p.skippedBundles] bundle 级否决结果
30
+ * @param {Array} [p.pendingCompat] 待适配行
31
+ * @param {Array} [p.patchUncertain] 补丁自愈里"说不准"的行 id
32
+ * @returns {{ issues: number, items: Array<{kind: string, severity: string, count: number, label: string}> }}
33
+ */
34
+ function buildInstallHealth(input = {}) {
35
+ // 体检模块自己不能崩:null / undefined / 非对象一律当空输入(tests/test-install-health.mjs ⑥ 钉住)
36
+ const src = input !== null && typeof input === 'object' ? input : {}
37
+ const list = Array.isArray(src.entries) ? src.entries : []
38
+ const skippedBundles = src.skippedBundles ?? null
39
+ const pendingCompat = Array.isArray(src.pendingCompat) ? src.pendingCompat : []
40
+ const patchUncertain = Array.isArray(src.patchUncertain) ? src.patchUncertain : []
41
+ const items = []
42
+
43
+ // ① bundle 级:被框架**整层跳过** —— 最严重,因为它在列表里连位置都没有(用户看不到)
44
+ const skipped = skippedBundles !== null && skippedBundles !== undefined && skippedBundles.reliable === true && Array.isArray(skippedBundles.items)
45
+ ? skippedBundles.items.length
46
+ : 0
47
+ if (skipped > 0) items.push({ kind: 'skipped-bundle', severity: SEVERITY.blocker, count: skipped, label: '被框架跳过的 bundle(装了但没装载,列表里看不到)' })
48
+
49
+ // ② 行级:被 peer 否决(可见但未加载)
50
+ const vetoed = list.filter((e) => e !== null && e !== undefined && e.veto !== null && e.veto !== undefined)
51
+ if (vetoed.length > 0) items.push({ kind: 'vetoed', severity: SEVERITY.blocker, count: vetoed.length, label: '被框架否决的插件行(未加载)' })
52
+
53
+ // ③ 待适配(兼容门控挂着的行)
54
+ const pending = Array.isArray(pendingCompat) ? pendingCompat.length : 0
55
+ if (pending > 0) items.push({ kind: 'pending-compat', severity: SEVERITY.warn, count: pending, label: '待适配确认的行' })
56
+
57
+ // ④ 补丁自愈里"说不准"的行(工具改过但无法确认是否它想要的)
58
+ const uncertain = Array.isArray(patchUncertain) ? patchUncertain.length : 0
59
+ if (uncertain > 0) items.push({ kind: 'patch-uncertain', severity: SEVERITY.warn, count: uncertain, label: '补丁自愈无法确认的行' })
60
+
61
+ return { issues: items.length, items }
62
+ }
63
+
64
+ export { SEVERITY, buildInstallHealth }
@@ -10,6 +10,7 @@ import { peerVetoFor } from '../domain/peer-veto.js'
10
10
  import { readExtraBundleRows } from '../domain/install-job.js'
11
11
  import { bundleVetoList } from '../domain/bundle-veto.js'
12
12
  import { buildLinkInventory } from '../domain/link-inventory.js'
13
+ import { buildInstallHealth } from '../domain/install-health.js'
13
14
  import { readBundlesSync } from '../domain/persist.js'
14
15
  import { failedJobEvidence } from '../domain/install-verify.js'
15
16
  import { installJobView, readGithubAuth } from '../domain/install.js'
@@ -157,7 +158,18 @@ async function routeStateGet(req, res, rc) {
157
158
  const shape = detectHostShape()
158
159
  host = { hosted: shape.hosted === true, kind: shape.kind, notice: shape.hosted === true ? shape.notice : null }
159
160
  } catch {}
160
- sendJson(res, 200, { ok: true, entries, skippedBundles, host, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? null } }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
161
+ // ★ 0.5.55 加法:把已经算好的各块**折成一行健康摘要**(不新增 IO、不新增判据)。
162
+ // 面板 176 个按钮、信息散在六处,用户没有一个地方能回答"整体健康吗、我该先处理哪件"。
163
+ let health = { issues: 0, items: [] }
164
+ try {
165
+ health = buildInstallHealth({
166
+ entries,
167
+ skippedBundles,
168
+ pendingCompat: compatPending?.pending ?? [],
169
+ patchUncertain: patchHeal === null ? [] : (patchHeal.uncertain ?? []),
170
+ })
171
+ } catch {}
172
+ sendJson(res, 200, { ok: true, entries, skippedBundles, host, health, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? null } }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
161
173
  return
162
174
  }
163
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.54",
3
+ "version": "0.5.55",
4
4
  "description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
5
5
  "repository": {
6
6
  "type": "git",