@noob-stupid/dsh-plugin-console 0.5.67 → 0.5.71
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
|
@@ -1970,6 +1970,11 @@ function healthSignature(health) {
|
|
|
1970
1970
|
const [repoInfo, setRepoInfo] = react.useState(null);
|
|
1971
1971
|
const [subpackages, setSubpackages] = react.useState(null);
|
|
1972
1972
|
const [installing, setInstalling] = react.useState(null);
|
|
1973
|
+
// ★ 0.5.70(用户 2026-10-18):**按目标并行**的在飞集合。installing 只留作展示,
|
|
1974
|
+
// 真正的并发闸门是 inFlight —— 同一目标才拦,不同插件可同时更新。
|
|
1975
|
+
const [inFlight, setInFlight] = react.useState(() => new Set());
|
|
1976
|
+
const markInFlight = (key) => { if (typeof key === "string" && key !== "") setInFlight((prev) => { const n = new Set(prev); n.add(key); return n; }); };
|
|
1977
|
+
const clearInFlight = (key) => { if (typeof key === "string" && key !== "") setInFlight((prev) => { const n = new Set(prev); n.delete(key); return n; }); };
|
|
1973
1978
|
const [details, setDetails] = react.useState(null);
|
|
1974
1979
|
// 已安装插件"检测更新":null=未检测 / {status:'checking'} / {status:'ready', latest, error}
|
|
1975
1980
|
const [updateCheck, setUpdateCheck] = react.useState(null);
|
|
@@ -2928,7 +2933,7 @@ function healthSignature(health) {
|
|
|
2928
2933
|
const pkgName = entry.moduleName;
|
|
2929
2934
|
setUpdateCheck({ status: "checking" });
|
|
2930
2935
|
call("/plugin-console/check-update", { packageName: pkgName }).then(
|
|
2931
|
-
(data) => setUpdateCheck({ status: "ready", latest: data.latest ?? null, source: data.source ?? "npm", error: data.error ?? null }),
|
|
2936
|
+
(data) => setUpdateCheck({ status: "ready", latest: data.latest ?? null, next: data.next ?? null, beta: data.beta ?? null, target: data.target ?? null, source: data.source ?? "npm", error: data.error ?? null }),
|
|
2932
2937
|
(error) => setUpdateCheck({ status: "ready", latest: null, source: "npm", error: friendlyGithubError(error).message }),
|
|
2933
2938
|
);
|
|
2934
2939
|
};
|
|
@@ -3411,6 +3416,8 @@ function healthSignature(health) {
|
|
|
3411
3416
|
aiConsent(jobId, true);
|
|
3412
3417
|
}
|
|
3413
3418
|
if (data.status !== "installing") {
|
|
3419
|
+
// ★ 0.5.70:任务结束 ⇒ 释放该目标的并发锁(其它插件的更新不受影响)
|
|
3420
|
+
clearInFlight(data.packageName);
|
|
3414
3421
|
stopJobPolling(jobId);
|
|
3415
3422
|
if (data.status === "done") {
|
|
3416
3423
|
if (data.kind === "skill") {
|
|
@@ -3792,11 +3799,14 @@ function healthSignature(health) {
|
|
|
3792
3799
|
const startJob = (repo, packageName, source = "github", kind = "plugin", update = false, subdir = null) => {
|
|
3793
3800
|
// 防并发(与 addLocal 一致):已有安装/更新进行中时忽略新点击(更新按钮此前可绕过,
|
|
3794
3801
|
// 反复点击会产生几十个并发任务装同一插件)
|
|
3795
|
-
|
|
3802
|
+
// 只拦**同一个目标**:不同插件可以并行更新(服务端是作业模型,天然支持)
|
|
3803
|
+
const jobKey = repo || packageName;
|
|
3804
|
+
if (inFlight.has(jobKey)) {
|
|
3796
3805
|
setMessage(t("installBusy"));
|
|
3797
3806
|
return;
|
|
3798
3807
|
}
|
|
3799
|
-
|
|
3808
|
+
markInFlight(jobKey);
|
|
3809
|
+
setInstalling(jobKey);
|
|
3800
3810
|
setReloadHint(false);
|
|
3801
3811
|
setRestartHint(false);
|
|
3802
3812
|
setMessage(null);
|
|
@@ -3814,6 +3824,7 @@ function healthSignature(health) {
|
|
|
3814
3824
|
},
|
|
3815
3825
|
(error) => {
|
|
3816
3826
|
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
3827
|
+
clearInFlight(jobKey);
|
|
3817
3828
|
setInstalling(null);
|
|
3818
3829
|
},
|
|
3819
3830
|
);
|
|
@@ -4026,7 +4037,7 @@ function healthSignature(health) {
|
|
|
4026
4037
|
el("button", {
|
|
4027
4038
|
type: "button",
|
|
4028
4039
|
className: styles.toggle,
|
|
4029
|
-
disabled: (updateCheck !== null && updateCheck.status === "checking") ||
|
|
4040
|
+
disabled: (updateCheck !== null && updateCheck.status === "checking") || inFlight.has(entry.moduleName),
|
|
4030
4041
|
onClick: () => checkUpdate(entry),
|
|
4031
4042
|
}, updateCheck !== null && updateCheck.status === "checking" ? t("checkingUpdate") : t("checkUpdate")),
|
|
4032
4043
|
el("button", {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// 改用 `link:<DSH_HOME>/plugin-src/<包名>`:pnpm 的 link 协议只建符号链接,不经 registry 解析、
|
|
20
20
|
// 不经 tarball 完整性校验,lock 删掉重建、node_modules 清空重装都稳定通过。
|
|
21
21
|
|
|
22
|
-
import { existsSync, rmSync, mkdirSync, realpathSync } from 'node:fs'
|
|
22
|
+
import { existsSync, readFileSync, rmSync, mkdirSync, realpathSync } from 'node:fs'
|
|
23
23
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
24
24
|
import { copyTree } from '../infra/fsx.js'
|
|
25
25
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
@@ -213,7 +213,50 @@ const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u
|
|
|
213
213
|
* · `command` **只供展示/复制**,服务端执行时**只认 payload.action 这个白名单类型**,argv 自己拼;
|
|
214
214
|
* · payload 里只有动作类型 + 包名 + 版本 + profile 名(回显用),**没有任何命令字符串**位置。
|
|
215
215
|
*/
|
|
216
|
+
/**
|
|
217
|
+
* 读 profile 清单里某个包的**当前来源形态**(只读;读不到返回 null)。
|
|
218
|
+
* 用途:决定"缺口该怎么补" —— 路径型来源才该按 link: 钉,registry 来源该对齐 lock。
|
|
219
|
+
*/
|
|
220
|
+
function profileDepSpec(profileDir, packageName) {
|
|
221
|
+
if (typeof profileDir !== 'string' || profileDir === '') return null
|
|
222
|
+
if (typeof packageName !== 'string' || packageName === '') return null
|
|
223
|
+
try {
|
|
224
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'))
|
|
225
|
+
const spec = manifest?.dependencies?.[packageName]
|
|
226
|
+
return typeof spec === 'string' && spec !== '' ? spec : null
|
|
227
|
+
} catch { return null }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 路径型来源(link:/file:)—— 只有这一族"按 link: 钉住"才说得通。 */
|
|
231
|
+
function isPathSourceSpec(spec) {
|
|
232
|
+
return typeof spec === 'string' && /^(?:link:|file:)/u.test(spec)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* **纯 registry 形态**的版本/范围(只有这一族"对齐 lock"才是正解)。
|
|
237
|
+
* 刻意收窄:git+https / https tarball / link: / file: / workspace: / portal: 一律**不算** ——
|
|
238
|
+
* 对它们推进 reconcile-lock 是错的(lock 对账解决不了来源问题),实测被 route-inventory 缺陷②抓到。
|
|
239
|
+
*/
|
|
240
|
+
function isRegistryRangeSpec(spec) {
|
|
241
|
+
if (typeof spec !== 'string') return false
|
|
242
|
+
const s = spec.trim()
|
|
243
|
+
if (s === '' || s === '*' || s === 'x') return true
|
|
244
|
+
if (/^(?:\^|~|>=|<=|>|<|=)\s*v?\d/u.test(s)) return true
|
|
245
|
+
if (/^v?\d+(?:\.\d+)?\.x$/u.test(s)) return true
|
|
246
|
+
return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(s)
|
|
247
|
+
}
|
|
248
|
+
|
|
216
249
|
function suggestedPinAction({ packageName, version = null, profileDir = null, profile = null, home = undefined, reason = null } = {}) {
|
|
250
|
+
// ★ 0.5.69(用户 2026-10-18):**registry 来源的包不该被劝成 link:**
|
|
251
|
+
// 它的问题是 lock 与清单不一致,正确出路是「重建 lock」;把 registry 包改成 link: 会绕过
|
|
252
|
+
// registry 解析与 tarball 完整性校验,还会让依赖(如 zod)被当成插件钉进 plugin-src。
|
|
253
|
+
const currentSpec = profileDepSpec(profileDir, packageName)
|
|
254
|
+
if (currentSpec !== null && isRegistryRangeSpec(currentSpec)) {
|
|
255
|
+
const why = reason !== null && reason !== undefined && reason !== ''
|
|
256
|
+
? `${reason};该包是 registry 来源(${currentSpec}),应对齐 lock 而不是改成 link:`
|
|
257
|
+
: `${packageName} 是 registry 来源(${currentSpec}),应对齐 lock 而不是改成 link:`
|
|
258
|
+
return suggestedReconcileAction({ profileDir, profile, reason: why })
|
|
259
|
+
}
|
|
217
260
|
const dir = join(home ?? dshHome(), PLUGIN_SRC_DIR, ...String(packageName).split('/'))
|
|
218
261
|
const spec = linkSpecFor(dir)
|
|
219
262
|
const name = profile ?? profileNameOf(profileDir)
|
|
@@ -144,7 +144,7 @@ async function planDependencySpec({ profileDir, packageName, version, registries
|
|
|
144
144
|
return {
|
|
145
145
|
form: 'link',
|
|
146
146
|
spec: link,
|
|
147
|
-
note: `${packageName}:${why}
|
|
147
|
+
note: `${packageName}:${why};已按**源码安装(本地物化)**记录(${link})—— 不经 npm registry 解析、不经 tarball 完整性校验,pnpm 重建 lock 也能装上`,
|
|
148
148
|
suggestion: null,
|
|
149
149
|
}
|
|
150
150
|
}
|
|
@@ -214,7 +214,7 @@ async function declareProfileDependency(profileDir, packageName, version = null,
|
|
|
214
214
|
? {
|
|
215
215
|
form: 'link',
|
|
216
216
|
spec: canonicalSourceSpec(realSpec),
|
|
217
|
-
note: `${packageName}
|
|
217
|
+
note: `${packageName}:**源码安装(本地物化)** —— 该包在 npm registry 上找不到,只能拿到源码;已按路径型来源记录(${realSpec})。不经 registry 解析、不经 tarball 完整性校验,缺失依赖与框架 peer 由 link 自足性补齐`,
|
|
218
218
|
suggestion: null,
|
|
219
219
|
}
|
|
220
220
|
: await planDependencySpec({ profileDir, packageName, version: resolved, registries, probe, home, profile, sourceSpec: installSpec })
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// L1 · domain —— 「已安装·待重启」的**持久化**(0.5.70 加法)
|
|
2
|
+
//
|
|
3
|
+
// 用户 2026-10-18:「那个重启后生效徽章你不是有吗…memory 插件不就是这样的吗之前」
|
|
4
|
+
// —— 徽章确实早就有(revoke.js#pendingRestartJobs),但它的数据源是**内存里的 installJobs**,
|
|
5
|
+
// 宿主一重启就清空 ⇒ 更新完插件、重启一次之后,徽章连同那条记录一起消失,插件看起来"没了"。
|
|
6
|
+
// 实测:installJobs=0 / pendingRestart=0 / recentFailures=0,而包其实已经更新成功(0.7.2 → 0.7.8)。
|
|
7
|
+
//
|
|
8
|
+
// 本模块把"待重启"落成一份**小文件**(按包名),于是它能活过重启:
|
|
9
|
+
// · 写入:/state 计算时,若某条 done 作业的包**不在运行中的 entries**,就记下来(仅在集合变化时写盘);
|
|
10
|
+
// · 读取:重启后从文件里读回来,仍然显示徽章;
|
|
11
|
+
// · 清除:一旦该包出现在 entries 里(说明已经加载生效),自动从文件里删掉 —— 不留陈旧记录。
|
|
12
|
+
//
|
|
13
|
+
// 严格只读原则的例外说明:这里确实写盘,但写的是**控制台自己的状态文件**(不是 profile、不是 lock),
|
|
14
|
+
// 且只在集合变化时写一次;任何异常都不抛出,降级为"没有持久记录"(徽章退化到旧行为,不比现在更差)。
|
|
15
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
16
|
+
import { dirname, join } from 'node:path'
|
|
17
|
+
import { dshHome } from '../infra/paths.js'
|
|
18
|
+
|
|
19
|
+
const PENDING_RESTART_FILE = 'pending-restart.json'
|
|
20
|
+
|
|
21
|
+
function pendingRestartPath(home = undefined) {
|
|
22
|
+
return join(home ?? dshHome(), 'plugin-console', PENDING_RESTART_FILE)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const readJsonSafe = (file) => {
|
|
26
|
+
try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return null }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 读持久记录 → [{ packageName, version, at }](读不到/坏文件 = 空数组)。 */
|
|
30
|
+
function readPendingRestart({ home = undefined } = {}) {
|
|
31
|
+
const value = readJsonSafe(pendingRestartPath(home))
|
|
32
|
+
const list = value !== null && typeof value === 'object' && Array.isArray(value.items) ? value.items : []
|
|
33
|
+
return list
|
|
34
|
+
.filter((x) => x !== null && x !== undefined && typeof x.packageName === 'string' && x.packageName !== '')
|
|
35
|
+
.map((x) => ({ packageName: x.packageName, version: typeof x.version === 'string' ? x.version : null, at: Number.isFinite(Number(x.at)) ? Number(x.at) : null }))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 把当前"待重启"集合写穿到文件(**集合不变就不写盘**)。
|
|
40
|
+
* @returns {{ changed: boolean, items: Array }}
|
|
41
|
+
*/
|
|
42
|
+
function syncPendingRestart(items, { home = undefined } = {}) {
|
|
43
|
+
const next = (Array.isArray(items) ? items : [])
|
|
44
|
+
.filter((x) => x !== null && x !== undefined && typeof x.packageName === 'string' && x.packageName !== '')
|
|
45
|
+
.map((x) => ({ packageName: x.packageName, version: typeof x.version === 'string' ? x.version : null, at: Number.isFinite(Number(x.at)) ? Number(x.at) : Date.now() }))
|
|
46
|
+
const prev = readPendingRestart({ home })
|
|
47
|
+
const key = (list) => list.map((x) => `${x.packageName}@${x.version ?? '?'}`).sort().join('|')
|
|
48
|
+
if (key(prev) === key(next)) return { changed: false, items: prev }
|
|
49
|
+
try {
|
|
50
|
+
const file = pendingRestartPath(home)
|
|
51
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
52
|
+
writeFileSync(file, `${JSON.stringify({ items: next }, null, 2)}\n`, 'utf8')
|
|
53
|
+
return { changed: true, items: next }
|
|
54
|
+
} catch {
|
|
55
|
+
// 写不进去就如实退化:本次仍返回新集合(界面照常显示),只是活不过重启
|
|
56
|
+
return { changed: false, items: next, writeError: true }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 合并:内存里刚判定的待重启 + 文件里历史遗留的(**已经在 entries 里出现过的一律丢弃**)。
|
|
62
|
+
* @param {Array} fresh 内存判定结果(pendingRestartJobs 的输出)
|
|
63
|
+
* @param {Set<string>} liveNames 当前运行中的包名集合
|
|
64
|
+
*/
|
|
65
|
+
function mergePendingRestart(fresh, liveNames, { home = undefined } = {}) {
|
|
66
|
+
const live = liveNames instanceof Set ? liveNames : new Set()
|
|
67
|
+
const persisted = readPendingRestart({ home }).filter((x) => !live.has(x.packageName))
|
|
68
|
+
const merged = new Map()
|
|
69
|
+
for (const x of persisted) merged.set(x.packageName, { packageName: x.packageName, version: x.version ?? null, at: x.at ?? null, persisted: true })
|
|
70
|
+
for (const x of Array.isArray(fresh) ? fresh : []) {
|
|
71
|
+
if (live.has(x.packageName)) continue
|
|
72
|
+
merged.set(x.packageName, { ...x, at: Date.now(), persisted: false })
|
|
73
|
+
}
|
|
74
|
+
const items = [...merged.values()]
|
|
75
|
+
// 写穿:把"合并后的待重启集合"作为新基线(集合不变则不写盘)
|
|
76
|
+
syncPendingRestart(items.map((x) => ({ packageName: x.packageName, version: x.version ?? null, at: x.at ?? null })), { home })
|
|
77
|
+
return items
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { PENDING_RESTART_FILE, pendingRestartPath, readPendingRestart, syncPendingRestart, mergePendingRestart }
|
|
@@ -386,7 +386,7 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
386
386
|
|
|
387
387
|
const depNotes = []
|
|
388
388
|
// 物化 + link: 计划(registry 查无此包时唯一安全的写回形式)。
|
|
389
|
-
// ★ 2026-09-27 改错:这里**只产出"打算写什么"和"为什么"**,不再抢先断言"
|
|
389
|
+
// ★ 2026-09-27 改错:这里**只产出"打算写什么"和"为什么"**,不再抢先断言"已按源码安装(本地物化)记录依赖"——
|
|
390
390
|
// 旧版在 pnpm add 之前就把这句写进 depNote,于是"写回失败/被后一步覆盖"时面板仍然报"已钉住",
|
|
391
391
|
// 而磁盘上留着的是裸版本号(真机 desktop profile 的实际状态)。最终文案一律在写回之后**读回真实值**再生成。
|
|
392
392
|
const linkPlan = (row, why) => {
|
|
@@ -496,7 +496,7 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
496
496
|
const wrote = writeOf.get(item.name) ?? null
|
|
497
497
|
if (wrote !== null && (wrote.changed === true || wrote.unchanged === true)) {
|
|
498
498
|
if (item.reason !== null) {
|
|
499
|
-
depNotes.push(`${item.name}:${item.reason}
|
|
499
|
+
depNotes.push(`${item.name}:${item.reason},已按源码安装(本地物化)记录依赖(${actual})—— lock 的 importer 条目已**定点写入**(${wrote.version ?? ''}),不经 npm registry 解析、不经 tarball 完整性校验`)
|
|
500
500
|
}
|
|
501
501
|
continue
|
|
502
502
|
}
|
|
@@ -513,7 +513,7 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
513
513
|
}
|
|
514
514
|
if (actual === item.spec) {
|
|
515
515
|
if (item.spec.startsWith('link:') && item.reason !== null) {
|
|
516
|
-
depNotes.push(`${item.name}:${item.reason}
|
|
516
|
+
depNotes.push(`${item.name}:${item.reason},已按源码安装(本地物化)记录依赖(${actual})—— 不经 npm registry 解析、不经 tarball 完整性校验,pnpm 重建 lock 也能装上`)
|
|
517
517
|
}
|
|
518
518
|
continue
|
|
519
519
|
}
|
|
@@ -22,6 +22,7 @@ import { installJobView, readGithubAuth } from '../domain/install.js'
|
|
|
22
22
|
import { readPluginDetails } from '../domain/market.js'
|
|
23
23
|
import { healPatchSafety, readPatchState } from '../domain/patch.js'
|
|
24
24
|
import { pendingRestartJobs } from '../domain/revoke.js'
|
|
25
|
+
import { mergePendingRestart } from '../domain/pending-restart.js'
|
|
25
26
|
import { listEntries } from '../domain/runtime.js'
|
|
26
27
|
import { sendError, sendJson } from '../infra/httpd.js'
|
|
27
28
|
import { dshHome, entryPkgMeta, findPatchPath, pluginRoot, profileDirOf, rowIdOf } from '../infra/paths.js'
|
|
@@ -122,7 +123,23 @@ async function routeStateGet(req, res, rc) {
|
|
|
122
123
|
// 已安装但尚未生效的任务(2026-09-20 真装真卸演练实测):bundle 型插件要重启才被加载,
|
|
123
124
|
// 装完 /install 返回 entryId: null、/state 里新增 loader 条目 = 0 —— 面板过去完全看不出
|
|
124
125
|
// 「装了但还没生效」,也无法撤销。前端据此渲染「已安装·重启后生效」徽标 + 按 jobId 删除。
|
|
125
|
-
|
|
126
|
+
// ★ 0.5.70(用户 2026-10-18):徽章要能活过重启 —— 内存判定的结果与**持久记录**合并;
|
|
127
|
+
// 已经在 entries 里出现过的包会自动从持久记录里清掉(不留陈旧项)。
|
|
128
|
+
const livePackageNames = (() => {
|
|
129
|
+
const set = new Set()
|
|
130
|
+
try {
|
|
131
|
+
for (const e of listEntries(ctx)) {
|
|
132
|
+
const mn = typeof e?.moduleName === 'string' ? e.moduleName : ''
|
|
133
|
+
if (mn === '' || mn.startsWith('cordis:')) continue
|
|
134
|
+
set.add(mn)
|
|
135
|
+
const m = /^(@[^/]+\/[^/]+)(?:\/.*)?$/u.exec(mn)
|
|
136
|
+
if (m) set.add(m[1])
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
139
|
+
return set
|
|
140
|
+
})()
|
|
141
|
+
let pendingRestart = []
|
|
142
|
+
try { pendingRestart = mergePendingRestart(pendingRestartJobs([...installJobs.values()], listEntries(ctx)), livePackageNames) } catch { pendingRestart = pendingRestartJobs([...installJobs.values()], listEntries(ctx)) }
|
|
126
143
|
// 框架升级检测与适配(备份快照 + 重打框架补丁),try 包裹不阻塞 state 返回
|
|
127
144
|
let framework = null
|
|
128
145
|
try { framework = detectFrameworkUpgrade(ctx) } catch {}
|
|
@@ -205,12 +222,15 @@ async function routeStateGet(req, res, rc) {
|
|
|
205
222
|
|
|
206
223
|
// ★ 0.5.55 加法:把已经算好的各块**折成一行健康摘要**(不新增 IO、不新增判据)。
|
|
207
224
|
// 面板 176 个按钮、信息散在六处,用户没有一个地方能回答"整体健康吗、我该先处理哪件"。
|
|
225
|
+
const liveRowIds = new Set(entries.map((e) => e.rowId))
|
|
208
226
|
let health = { issues: 0, items: [] }
|
|
209
227
|
try {
|
|
210
228
|
health = buildInstallHealth({
|
|
211
229
|
entries,
|
|
212
230
|
skippedBundles,
|
|
213
|
-
|
|
231
|
+
// ★ 0.5.68:与界面同口径 —— 只统计「仍待适配」且**目标行仍存在**的记录。
|
|
232
|
+
// 否则会把已采纳的历史记录、以及指向已删除行的陈旧记录都算成"待处理"(实测 24 vs 界面 0)。
|
|
233
|
+
pendingCompat: (compatPending?.pending ?? []).filter((p) => (p?.status ?? 'pending') === 'pending' && liveRowIds.has(p?.rowId)),
|
|
214
234
|
patchUncertain: patchHeal === null ? [] : (patchHeal.uncertain ?? []),
|
|
215
235
|
})
|
|
216
236
|
} catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.71",
|
|
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",
|