@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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// L1 · domain —— framework-stale-restore.js(「改名残留」能不能恢复的**判据**;0.5.99 改错)
|
|
2
|
+
//
|
|
3
|
+
// ── 真机现场(2026-11-02,用户实测)────────────────────────────────────────────────
|
|
4
|
+
// 框架包里出现大量「**真名目录存在,但是空壳**(没有 package.json),真正内容被改名成了
|
|
5
|
+
// `<name>.stale-<ver>-<ts>`」:
|
|
6
|
+
// 真名 cordis\ → 空(无 package.json)
|
|
7
|
+
// cordis.stale-4.0.4-1793185607671\ → 有内容(package.json + lib)
|
|
8
|
+
// 该目录下总计 564 项,其中 276 个是 .stale-*。后果:`@deepseek-ai/cordis` 解析失败
|
|
9
|
+
// (`ERR_MODULE_NOT_FOUND`),插件的 `import` 直接起不来;把 178 个空壳从 .stale-* 恢复后成功。
|
|
10
|
+
//
|
|
11
|
+
// ── 旧判据的缺陷(本次修的正是它)──────────────────────────────────────────────────
|
|
12
|
+
// 旧实现是「真名**缺失**(exists=false)才恢复;真名已存在就跳过」。真机现场恰恰是
|
|
13
|
+
// 「真名存在但是空壳」⇒ 被跳过 ⇒ 坏状态一直留着,框架一直起不来。
|
|
14
|
+
//
|
|
15
|
+
// ── 新判据(三条,全部可离线断言)──────────────────────────────────────────────────
|
|
16
|
+
// ① **可用**的定义是「这个目录里有 package.json」;目录存在但没有 package.json = **空壳 = 不可用**,
|
|
17
|
+
// 在判据里与"缺失"同等对待(都算"真名此刻不可用")。
|
|
18
|
+
// ② 恢复前必须校验候选的 `package.json.name` 与真名一致(`<scope>/<real>`)——
|
|
19
|
+
// 不一致一律不动,记 `failed`(防张冠李戴:把别的包改成这个名字)。
|
|
20
|
+
// ③ 三类结果分开报告:`restore`(可恢复)/ `noCandidate`(真缺内容且没有可用的 .stale-*)/
|
|
21
|
+
// `failed`(候选名字不符、候选本身不可用等)—— 调用方要能原样写进升级日志。
|
|
22
|
+
//
|
|
23
|
+
// 本模块只做**判据与计划**(读目录 + 读 package.json,纯读、零写盘);改名/读回核实/回滚在
|
|
24
|
+
// domain/framework-cleanup.js#restoreStaleBackups(动作层),两处调用方共用同一条路。
|
|
25
|
+
import { readdirSync, readFileSync, lstatSync } from 'node:fs'
|
|
26
|
+
import { basename, join } from 'node:path'
|
|
27
|
+
|
|
28
|
+
/** 空壳挪开后用的后缀(**挪开不删**,保留可回溯)。 */
|
|
29
|
+
const EMPTY_SUFFIX = '.empty-'
|
|
30
|
+
/** 改名残留的后缀(`<real>.stale-<ver>-<ts>`)。 */
|
|
31
|
+
const STALE_MARK = '.stale-'
|
|
32
|
+
|
|
33
|
+
function errText(error) {
|
|
34
|
+
return String(error?.message ?? error)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 从目录名拆出真名(`cordis.stale-4.0.4-123` → `cordis`);不是残留就返回 null。 */
|
|
38
|
+
function staleRealNameOf(name) {
|
|
39
|
+
if (typeof name !== 'string') return null
|
|
40
|
+
const at = name.indexOf(STALE_MARK)
|
|
41
|
+
if (at <= 0) return null
|
|
42
|
+
return name.slice(0, at)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 真名对应的**包名**(scope 目录下是 `@scope/real`;不是 scope 目录就用真名本身)。 */
|
|
46
|
+
function expectedPackageName(scopeDir, real) {
|
|
47
|
+
const scope = basename(String(scopeDir ?? ''))
|
|
48
|
+
return scope.startsWith('@') ? `${scope}/${real}` : real
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 空壳挪开后的名字:`<real>.empty-<stamp>`。 */
|
|
52
|
+
function emptyShellName(real, stamp) {
|
|
53
|
+
return `${real}${EMPTY_SUFFIX}${stamp}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 读 `<dir>/package.json`:`{ ok, name, version, error }`(读不到/解析不了都如实带原因)。 */
|
|
57
|
+
function packageJsonOf(dir, { readText = (p) => readFileSync(p, 'utf8') } = {}) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = JSON.parse(readText(join(dir, 'package.json')))
|
|
60
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, name: null, version: null, error: 'package.json 不是对象' }
|
|
61
|
+
return { ok: true, name: typeof raw.name === 'string' ? raw.name : null, version: typeof raw.version === 'string' ? raw.version : null, error: null }
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { ok: false, name: null, version: null, error: errText(error) }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 名字里那个"被占用"的东西(空壳 / 链接 / 文件 / 真实目录):lstat 能立住就算被占着。 */
|
|
68
|
+
function isOccupied(dir, { lstat = lstatSync } = {}) {
|
|
69
|
+
try { lstat(dir); return true } catch { return false }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* **可用**的判据(用户 2026-11-02 定案):目录里有**能读出来的 package.json**。
|
|
74
|
+
* 注意它不看包名 —— 「是不是这个包」由 `packageJsonOf().name` 与真名比对,两件事分开判。
|
|
75
|
+
*/
|
|
76
|
+
function isUsablePackageDir(dir, deps = {}) {
|
|
77
|
+
const lstat = deps.lstat ?? lstatSync
|
|
78
|
+
let stat = null
|
|
79
|
+
try { stat = lstat(dir) } catch { return false }
|
|
80
|
+
try { if (stat?.isDirectory?.() === false && stat?.isSymbolicLink?.() !== true) return false } catch {}
|
|
81
|
+
return packageJsonOf(dir, deps).ok === true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 候选排序用的时间戳(`<real>.stale-<ver>-<ts>` 里尾巴那串数字;没有就是 0)。 */
|
|
85
|
+
function candidateStamp(name) {
|
|
86
|
+
const m = String(name ?? '').match(/-(\d{6,})$/u)
|
|
87
|
+
return m === null ? 0 : Number(m[1])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 挑要恢复的那份候选:包名必须与真名一致(防张冠李戴),多份时取**时间戳最新**的那份。
|
|
92
|
+
* 返回 `{ picked, mismatch, unusable }` —— `mismatch` 是"有 package.json 但名字不对"的候选,
|
|
93
|
+
* 它们不是可以恢复的对象,但**必须**在报告里点名(否则用户不知道为什么没恢复)。
|
|
94
|
+
*/
|
|
95
|
+
function pickRestoreCandidate({ scopeDir, real, candidates, deps = {} } = {}) {
|
|
96
|
+
const expected = expectedPackageName(scopeDir, real)
|
|
97
|
+
const usable = []
|
|
98
|
+
const mismatch = []
|
|
99
|
+
const unusable = []
|
|
100
|
+
for (const name of Array.isArray(candidates) ? candidates : []) {
|
|
101
|
+
const info = packageJsonOf(join(scopeDir, name), deps)
|
|
102
|
+
if (info.ok !== true) { unusable.push({ name, error: info.error }); continue }
|
|
103
|
+
if (info.name !== expected) { mismatch.push({ name, nameInPkg: info.name, expected }); continue }
|
|
104
|
+
usable.push({ name, version: info.version, stamp: candidateStamp(name) })
|
|
105
|
+
}
|
|
106
|
+
usable.sort((a, b) => (b.stamp - a.stamp) || (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))
|
|
107
|
+
return { picked: usable.length > 0 ? usable[0] : null, usableCount: usable.length, mismatch, unusable }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 计划一次「改名残留修复」(**纯读,零写盘**)。返回每个真名一项:
|
|
112
|
+
* `{ real, action: 'restore'|'skip'|'noCandidate'|'failed', staleName, reason, detail,
|
|
113
|
+
* realOccupied, realUsable, candidates, mismatch, unusable }`
|
|
114
|
+
*
|
|
115
|
+
* 分类规则(顺序即优先级):
|
|
116
|
+
* ① 真名**可用** ⇒ `skip`(reason `real-name-exists`)—— 健康树上一个字节都不动(幂等);
|
|
117
|
+
* ② 真名不可用(缺失 **或空壳**)+ 有可用候选 ⇒ `restore`;
|
|
118
|
+
* ③ 真名不可用 + 候选里**有名字不符的** ⇒ `failed`(reason `name-mismatch`,一个都不动);
|
|
119
|
+
* ④ 真名不可用 + 候选都不可用(没 package.json)或压根没有候选 ⇒ `noCandidate`。
|
|
120
|
+
*/
|
|
121
|
+
function planStaleRestore({ scopeDir, deps = {} } = {}) {
|
|
122
|
+
const empty = { ok: false, items: [], problem: null }
|
|
123
|
+
if (typeof scopeDir !== 'string' || scopeDir === '') return { ...empty, problem: '没有给 scopeDir' }
|
|
124
|
+
let entries = []
|
|
125
|
+
try { entries = deps.readdir ? deps.readdir(scopeDir) : readdirSync(scopeDir, { withFileTypes: true }) } catch (error) {
|
|
126
|
+
const missing = error?.code === 'ENOENT'
|
|
127
|
+
return { ...empty, ok: missing, problem: missing ? null : '列不出目录:' + errText(error) }
|
|
128
|
+
}
|
|
129
|
+
const byReal = new Map()
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const name = typeof entry === 'string' ? entry : entry?.name
|
|
132
|
+
const isDir = typeof entry === 'string' ? true : entry?.isDirectory?.() === true
|
|
133
|
+
if (typeof name !== 'string' || !isDir) continue
|
|
134
|
+
const real = staleRealNameOf(name)
|
|
135
|
+
if (real === null || real === '') continue
|
|
136
|
+
if (!byReal.has(real)) byReal.set(real, [])
|
|
137
|
+
byReal.get(real).push(name)
|
|
138
|
+
}
|
|
139
|
+
// 真名一侧也要扫:**空壳本身没有 .stale-* 兄弟**时(真缺内容)同样要报 `noCandidate`,
|
|
140
|
+
// 否则升级日志里那批"真名在、内容没了"的行一个字都不会出现(真机就是这样被漏掉的)。
|
|
141
|
+
const realNames = new Set()
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const name = typeof entry === 'string' ? entry : entry?.name
|
|
144
|
+
const isDir = typeof entry === 'string' ? true : entry?.isDirectory?.() === true
|
|
145
|
+
if (typeof name !== 'string' || !isDir) continue
|
|
146
|
+
if (name.includes(STALE_MARK) || name.includes(EMPTY_SUFFIX)) continue // 残留/挪开的空壳不是真名
|
|
147
|
+
realNames.add(name)
|
|
148
|
+
}
|
|
149
|
+
for (const real of realNames) if (!byReal.has(real)) byReal.set(real, [])
|
|
150
|
+
const items = []
|
|
151
|
+
for (const real of [...byReal.keys()].sort()) {
|
|
152
|
+
const candidates = byReal.get(real).slice().sort()
|
|
153
|
+
const target = join(scopeDir, real)
|
|
154
|
+
const realOccupied = isOccupied(target, deps)
|
|
155
|
+
const realUsable = realOccupied && isUsablePackageDir(target, deps)
|
|
156
|
+
const base = { real, staleName: null, realOccupied, realUsable, candidates }
|
|
157
|
+
if (realUsable) {
|
|
158
|
+
// 健康行:有候选也只是"旁边留着一份旧残留",不动(幂等:健康树上零动作、零报告项)
|
|
159
|
+
if (candidates.length === 0) continue
|
|
160
|
+
items.push({ ...base, action: 'skip', reason: 'real-name-exists', detail: `真名目录里已有 package.json —— 不动旁边那份 ${candidates[0]}` })
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
const pick = pickRestoreCandidate({ scopeDir, real, candidates, deps })
|
|
164
|
+
if (pick.picked !== null) {
|
|
165
|
+
items.push({
|
|
166
|
+
...base,
|
|
167
|
+
action: 'restore',
|
|
168
|
+
staleName: pick.picked.name,
|
|
169
|
+
reason: realOccupied ? 'real-name-unusable' : 'real-name-missing',
|
|
170
|
+
detail: realOccupied
|
|
171
|
+
? `真名目录存在但没有可读的 package.json(空壳)—— 用 ${pick.picked.name} 恢复,空壳挪成 ${EMPTY_SUFFIX}<ts>`
|
|
172
|
+
: `真名缺失 —— 用 ${pick.picked.name} 恢复`,
|
|
173
|
+
mismatch: pick.mismatch,
|
|
174
|
+
unusable: pick.unusable,
|
|
175
|
+
})
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
if (pick.mismatch.length > 0) {
|
|
179
|
+
items.push({
|
|
180
|
+
...base,
|
|
181
|
+
action: 'failed',
|
|
182
|
+
reason: 'name-mismatch',
|
|
183
|
+
detail: `候选的 package.json.name 与真名不符(应为 ${expectedPackageName(scopeDir, real)}):${pick.mismatch.map((m) => `${m.name}=${String(m.nameInPkg)}`).join('、')} —— 一个都不动`,
|
|
184
|
+
mismatch: pick.mismatch,
|
|
185
|
+
unusable: pick.unusable,
|
|
186
|
+
})
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
items.push({
|
|
190
|
+
...base,
|
|
191
|
+
action: 'noCandidate',
|
|
192
|
+
reason: 'no-usable-stale',
|
|
193
|
+
detail: pick.unusable.length > 0
|
|
194
|
+
? `真名不可用,且 ${pick.unusable.length} 份 .stale-* 都没有可读的 package.json(不可作为候选)`
|
|
195
|
+
: '真名不可用,且没有任何 .stale-* 候选(框架真缺内容,需要重装)',
|
|
196
|
+
mismatch: pick.mismatch,
|
|
197
|
+
unusable: pick.unusable,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, items, problem: null }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
EMPTY_SUFFIX,
|
|
205
|
+
STALE_MARK,
|
|
206
|
+
candidateStamp,
|
|
207
|
+
emptyShellName,
|
|
208
|
+
expectedPackageName,
|
|
209
|
+
isOccupied,
|
|
210
|
+
isUsablePackageDir,
|
|
211
|
+
packageJsonOf,
|
|
212
|
+
pickRestoreCandidate,
|
|
213
|
+
planStaleRestore,
|
|
214
|
+
staleRealNameOf,
|
|
215
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// L0 · infra(paths.js)—— 分层 Step 从 lib/index.js 搬出,只搬移未改逻辑
|
|
2
2
|
// 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
3
|
|
|
4
|
-
import { readFileSync, existsSync, statSync } from 'node:fs'
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
6
6
|
import { homedir } from 'node:os'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
@@ -12,10 +12,39 @@ function dshHome() {
|
|
|
12
12
|
return process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* 默认 profile 用户补丁层路径(无 include 条目可推导时的兜底)。
|
|
17
|
+
*
|
|
18
|
+
* 硬编码 `profiles/web` 是真实事故(issue #27):桌面端机器上 `cordis.yml` 里没有
|
|
19
|
+
* `cordis:include` 条目时兜底必然生效,于是所有补丁写入都落到早已废弃的 `web` profile,
|
|
20
|
+
* 正在用的 `desktop` 静默收不到——开关插件、改禁用都没反应,而且不报错。
|
|
21
|
+
*
|
|
22
|
+
* 现在按可信度排候选,绝不写死 profile 名:
|
|
23
|
+
* ① DSH_PROFILE_DIR(桌面端/CLI 会显式设置,最可信)
|
|
24
|
+
* ② DSH_PROFILE(只给名字时拼路径)
|
|
25
|
+
* ③ `profiles/` 下真实存在补丁文件的目录(desktop 优先,其余按名字排序保证确定性)
|
|
26
|
+
*/
|
|
16
27
|
function defaultPatchPath() {
|
|
17
28
|
const home = dshHome()
|
|
18
|
-
|
|
29
|
+
const explicitDir = process.env.DSH_PROFILE_DIR?.trim()
|
|
30
|
+
if (explicitDir !== undefined && explicitDir !== '') return join(explicitDir, 'cordis.patch.yml')
|
|
31
|
+
const candidates = []
|
|
32
|
+
const profileName = process.env.DSH_PROFILE?.trim()
|
|
33
|
+
if (profileName !== undefined && profileName !== '') candidates.push(join(home, 'profiles', profileName, 'cordis.patch.yml'))
|
|
34
|
+
candidates.push(join(home, 'profiles', 'desktop', 'cordis.patch.yml'))
|
|
35
|
+
try {
|
|
36
|
+
const base = join(home, 'profiles')
|
|
37
|
+
for (const name of readdirSync(base).sort()) {
|
|
38
|
+
if (name === 'node_modules' || name === 'desktop') continue
|
|
39
|
+
candidates.push(join(base, name, 'cordis.patch.yml'))
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// profiles 读不到就只用已知候选
|
|
43
|
+
}
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
if (existsSync(candidate)) return candidate
|
|
46
|
+
}
|
|
47
|
+
return candidates[0]
|
|
19
48
|
}
|
|
20
49
|
|
|
21
50
|
/** 从 loader 树推导 profile 的 cordis.patch.yml 绝对路径。 */
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// L2 · routes —— 门控的「按需检查」(0.5.100 加法):`POST /plugin-console/compat-gate {action:'scan-current'}`
|
|
2
|
+
//
|
|
3
|
+
// 用户 2026-11-02 原话:「我们一般情况下都不预检吧」——compat-pending 只在升级前预扫与启动失败
|
|
4
|
+
// 隔离时写入,平时门控一条记录都没有,而"0 条"会被误读成"全部兼容"。这个动作让用户随时查一次:
|
|
5
|
+
// 对当前补丁里的行逐行跑适配判定,把**判为不适配**的结论记进 compat-pending(`source: 'manual-scan'`),
|
|
6
|
+
// 并回一份统计(检查了几行 / 通过 / 不适配 / 无法判定)。
|
|
7
|
+
//
|
|
8
|
+
// 三条硬边界(判据在 domain/compat-scan.js,写盘纪律在 domain/compat-pending-records.js):
|
|
9
|
+
// · 判据复用 `checkPluginFrameworkCompat`(与框架升级预扫同一条链),本文件不新写任何判据;
|
|
10
|
+
// · **绝不禁用任何行、绝不碰补丁文件** —— 禁用是用户的动作(/toggle);
|
|
11
|
+
// · 写盘:备份 → 原子写 → 读回核实 → 失败回滚;坏文件 ⇒ 409 拒绝覆盖(保住用户数据)。
|
|
12
|
+
import { dirname } from 'node:path'
|
|
13
|
+
import { planCurrentScan, recordsFromScan } from '../domain/compat-scan.js'
|
|
14
|
+
import { applyPendingWrite, countPendingRecords, liveRowIdsFromPatch, planScanMerge, readPendingRecords } from '../domain/compat-pending-records.js'
|
|
15
|
+
import { currentFrameworkVersion } from '../domain/framework.js'
|
|
16
|
+
import { readPatchState } from '../domain/patch.js'
|
|
17
|
+
import { listEntries } from '../domain/runtime.js'
|
|
18
|
+
import { sendError, sendJson } from '../infra/httpd.js'
|
|
19
|
+
import { findPatchPath } from '../infra/paths.js'
|
|
20
|
+
|
|
21
|
+
/** 读回核实的判据:本次写进去的每条 fail 记录都必须真的在盘上,且 check === 'fail'。 */
|
|
22
|
+
function verifyScanWrite(expected) {
|
|
23
|
+
return (back) => {
|
|
24
|
+
const rows = (Array.isArray(back?.pending) ? back.pending : []).filter((r) => r !== null && typeof r === 'object')
|
|
25
|
+
const missing = expected.filter((id) => !rows.some((r) => r.rowId === id && r.check === 'fail'))
|
|
26
|
+
if (missing.length > 0) return `这些记录没真正落盘:${missing.slice(0, 5).join('、')}`
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function routeCompatScanCurrent(req, res, rc) {
|
|
32
|
+
const at = Date.now()
|
|
33
|
+
let profileDir = null
|
|
34
|
+
let patchRowIds = new Set()
|
|
35
|
+
try {
|
|
36
|
+
const patchPath = findPatchPath(rc.ctx)
|
|
37
|
+
profileDir = dirname(patchPath)
|
|
38
|
+
patchRowIds = liveRowIdsFromPatch(await readPatchState(patchPath))
|
|
39
|
+
} catch {
|
|
40
|
+
sendError(res, 503, '读不到 profile 目录/补丁文件,无法确定要检查的行;本次扫描没有写任何东西', { ok: false, code: 'patch-unreadable' })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
let entries = []
|
|
44
|
+
try { entries = listEntries(rc.ctx) } catch { entries = [] }
|
|
45
|
+
let frameworkVersion = null
|
|
46
|
+
try { frameworkVersion = currentFrameworkVersion(rc.ctx) } catch { frameworkVersion = null }
|
|
47
|
+
const scan = planCurrentScan({ entries, profileDir, frameworkVersion })
|
|
48
|
+
const records = recordsFromScan(scan, { at })
|
|
49
|
+
const read = readPendingRecords()
|
|
50
|
+
const merge = planScanMerge({
|
|
51
|
+
read,
|
|
52
|
+
records,
|
|
53
|
+
frameworkVersion,
|
|
54
|
+
lastScan: {
|
|
55
|
+
at,
|
|
56
|
+
frameworkVersion: scan.frameworkVersion,
|
|
57
|
+
checked: scan.checked,
|
|
58
|
+
pass: scan.pass,
|
|
59
|
+
fail: scan.fail,
|
|
60
|
+
unknown: scan.unknown,
|
|
61
|
+
skipped: scan.skipped.length,
|
|
62
|
+
source: 'manual-scan',
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
const counts = (() => {
|
|
66
|
+
try { return countPendingRecords(read.records, patchRowIds) } catch { return null }
|
|
67
|
+
})()
|
|
68
|
+
const base = {
|
|
69
|
+
ok: true,
|
|
70
|
+
action: 'scan-current',
|
|
71
|
+
frameworkVersion: scan.frameworkVersion,
|
|
72
|
+
// 扫描统计(面板只放一句短句;明细在 skipped/results 里)
|
|
73
|
+
checked: scan.checked,
|
|
74
|
+
pass: scan.pass,
|
|
75
|
+
fail: scan.fail,
|
|
76
|
+
unknown: scan.unknown,
|
|
77
|
+
skipped: scan.skipped,
|
|
78
|
+
results: scan.results,
|
|
79
|
+
// 落盘情况
|
|
80
|
+
recorded: records.length,
|
|
81
|
+
added: merge.added ?? [],
|
|
82
|
+
updated: merge.updated ?? [],
|
|
83
|
+
changed: false,
|
|
84
|
+
file: read.file,
|
|
85
|
+
backup: null,
|
|
86
|
+
verified: false,
|
|
87
|
+
counts,
|
|
88
|
+
error: scan.error,
|
|
89
|
+
note: null,
|
|
90
|
+
}
|
|
91
|
+
if (merge.ok !== true) {
|
|
92
|
+
// 坏文件:结论不写盘,但扫描统计照样如实回(用户至少知道"现在查出来是什么")
|
|
93
|
+
sendError(res, merge.status ?? 500, merge.error ?? '扫描结果写入失败', { ...base, error: merge.error ?? scan.error })
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (merge.changed !== true) {
|
|
97
|
+
sendJson(res, 200, {
|
|
98
|
+
...base,
|
|
99
|
+
changed: false,
|
|
100
|
+
note: scan.checked === 0
|
|
101
|
+
? '没有可检查的行(补丁里没有可判定的插件行),未写任何记录'
|
|
102
|
+
: `检查 ${scan.checked} 行:通过 ${scan.pass} / 不适配 ${scan.fail} / 无法判定 ${scan.unknown};记录没有变化(无需写盘)`,
|
|
103
|
+
})
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const write = applyPendingWrite({ file: read.file, next: merge.next, verify: verifyScanWrite(records.map((r) => r.rowId)), tag: 'scan' })
|
|
107
|
+
const payload = {
|
|
108
|
+
...base,
|
|
109
|
+
ok: write.ok === true,
|
|
110
|
+
changed: write.ok === true,
|
|
111
|
+
backup: write.backup ?? null,
|
|
112
|
+
verified: write.verified === true,
|
|
113
|
+
error: write.error ?? scan.error,
|
|
114
|
+
note: write.ok === true
|
|
115
|
+
? `检查 ${scan.checked} 行:通过 ${scan.pass} / 不适配 ${scan.fail} / 无法判定 ${scan.unknown}`
|
|
116
|
+
+ (records.length > 0 ? `;已记下 ${records.length} 条待适配结论(只记录,未禁用任何行)` : ';没有不适配的行,未新增记录')
|
|
117
|
+
: null,
|
|
118
|
+
}
|
|
119
|
+
if (write.ok === true) sendJson(res, 200, payload)
|
|
120
|
+
else sendError(res, write.status ?? 500, write.error ?? '扫描结果写入失败', payload)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { routeCompatScanCurrent }
|
|
@@ -26,6 +26,11 @@ import { DEFAULT_TIMEOUT_MS, autoPreflightView, runAutoPreflight } from '../doma
|
|
|
26
26
|
import { runAutoPreflightScan } from '../domain/auto-preflight-run.js'
|
|
27
27
|
import { NOTICE_MODES, detectCompat, readCompatAck, readCompatGate, readCompatPending, rowIdModuleMap, writeCompatAck, writeCompatGate, writeCompatPending } from '../domain/compat.js'
|
|
28
28
|
import { compareFingerprint, computeFingerprint, planDesktopHostSwitch, readCompatMode, readFingerprint, readRollbackRecord, stampFingerprint, writeCompatMode } from '../domain/compat-state.js'
|
|
29
|
+
// 0.5.98 加法:适配门**记录**的删除(真待适配判据 + 写盘纪律都在该模块;本路由只映射状态码)
|
|
30
|
+
import { applyPendingForget, countPendingRecords, liveRowIdsFromPatch, planForgetRecord, planForgetStale, readPendingRecords } from '../domain/compat-pending-records.js'
|
|
31
|
+
import { readPatchState } from '../domain/patch.js'
|
|
32
|
+
// 0.5.100 加法:门控的「按需检查」(只记录结论,绝不禁用任何行)
|
|
33
|
+
import { routeCompatScanCurrent } from './compat-scan.js'
|
|
29
34
|
import { currentFrameworkVersion, detectHostShape } from '../domain/framework.js'
|
|
30
35
|
import { applyDependencyBackfill, planDependencyBackfill } from '../domain/manifest.js'
|
|
31
36
|
import { isProtectedModule } from '../domain/runtime.js'
|
|
@@ -455,4 +460,88 @@ async function routeCompatAck(req, res, rc) {
|
|
|
455
460
|
const ack = writeCompatAck(version)
|
|
456
461
|
sendJson(res, 200, { ok: true, ack, noticeMode: readCompatGate().noticeMode })
|
|
457
462
|
}
|
|
458
|
-
|
|
463
|
+
/**
|
|
464
|
+
* POST /plugin-console/compat-gate 的**动作分派**(0.5.98 起;0.5.100 多一个 `scan-current`)。
|
|
465
|
+
*
|
|
466
|
+
* `{ action: 'forget', rowId }` 删一条记录 → 见下(routeCompatPendingForget 的本体)
|
|
467
|
+
* `{ action: 'forget-stale' }` 清掉失效记录 → 同上
|
|
468
|
+
* `{ action: 'scan-current' }` **按需检查**(0.5.100 加法,用户「平时不预检」的痛点)
|
|
469
|
+
* → routes/compat-scan.js(只记录结论,绝不禁用任何行)
|
|
470
|
+
* 没有 `action`(或不是已知动作)⇒ **逐字**走原来的总开关逻辑(既有契约零变化)
|
|
471
|
+
*
|
|
472
|
+
* 为什么都挤在**既有的 /compat-gate** 上而不新开路由:门控面板的所有写入口必须同源
|
|
473
|
+
* (否则又要登记一份路由清单,两处判据迟早分叉)。装配层的分流见 routes/index.js#routeCompatGateAny。
|
|
474
|
+
*/
|
|
475
|
+
async function routeCompatGateAction(req, res, rc) {
|
|
476
|
+
const action = typeof rc.body?.action === 'string' ? rc.body.action.trim() : ''
|
|
477
|
+
if (action === 'scan-current') return routeCompatScanCurrent(req, res, rc)
|
|
478
|
+
return routeCompatPendingForget(req, res, rc)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* POST /plugin-console/compat-gate `{ action: 'forget' | 'forget-stale' }`
|
|
483
|
+
* —— 适配门**记录**的删除(0.5.98 加法,用户 2026-10-28 明确要求)。
|
|
484
|
+
*
|
|
485
|
+
* 语义(三条硬边界,用户定案):
|
|
486
|
+
* · 只删 **记录**:不动插件、不动补丁文件、不动 bundles 清单;
|
|
487
|
+
* · `forget` 删指定 rowId;`forget-stale` 只清「status=pending 且该行已不在补丁里」的失效记录,
|
|
488
|
+
* 真 pending 与 adopted 一条都不动;没有可清的 ⇒ `changed:false` 且**零写盘**;
|
|
489
|
+
* · 删不存在的 rowId ⇒ 404 + 明确原因(绝不静默成功);坏文件 ⇒ 409 拒绝覆盖(保住用户数据)。
|
|
490
|
+
*
|
|
491
|
+
* 写盘纪律全在 domain/compat-pending-records.js:备份 → 原子写(临时文件 + rename)→ 读回核实 → 失败回滚。
|
|
492
|
+
* 「行是否存在」只取补丁解析的既有产物(domain/patch.js#readPatchState),不另写一套正则。
|
|
493
|
+
*/
|
|
494
|
+
async function routeCompatPendingForget(req, res, rc) {
|
|
495
|
+
const body = rc.body ?? {}
|
|
496
|
+
const action = typeof body.action === 'string' ? body.action.trim() : ''
|
|
497
|
+
if (action !== 'forget' && action !== 'forget-stale') {
|
|
498
|
+
sendError(res, 400, `未知动作 ${JSON.stringify(body.action)}(可选 forget / forget-stale)`, { ok: false, error: '未知动作', code: 'unknown-action' })
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
const read = readPendingRecords()
|
|
502
|
+
// 判据唯一真源:补丁此刻真实存在的行(与 /state 里 patch.disables/forced/inserts 同源)
|
|
503
|
+
let liveRowIds = new Set()
|
|
504
|
+
try {
|
|
505
|
+
const patchPath = findPatchPath(rc.ctx)
|
|
506
|
+
liveRowIds = liveRowIdsFromPatch(await readPatchState(patchPath))
|
|
507
|
+
} catch {
|
|
508
|
+
// 读不到补丁 ⇒ 只能认定"没有任何行存在";此时 forget-stale 会把 pending 全判成 stale,
|
|
509
|
+
// 那等于凭一个读失败删用户记录 —— 所以这条通道**必须停下**(宁可红,也不报假的绿)。
|
|
510
|
+
sendError(res, 503, '读不到 profile 补丁文件,无法判断哪些记录还指向真实的行;本次删除已停止(文件未改动)', { ok: false, code: 'patch-unreadable' })
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
const plan = action === 'forget'
|
|
514
|
+
? planForgetRecord({ read, rowId: body.rowId })
|
|
515
|
+
: planForgetStale({ read, liveRowIds })
|
|
516
|
+
const result = plan.ok === true && plan.changed === true ? applyPendingForget(plan) : plan
|
|
517
|
+
const removed = Array.isArray(result.removedRowIds) ? result.removedRowIds : []
|
|
518
|
+
const removedCount = removed.length
|
|
519
|
+
const note = result.ok !== true
|
|
520
|
+
? null
|
|
521
|
+
: (action === 'forget'
|
|
522
|
+
? `已删除记录 ${String(body.rowId)}(其余记录原样保留;插件与补丁一个字都没动)`
|
|
523
|
+
: (removedCount > 0
|
|
524
|
+
? `已清理 ${removedCount} 条失效记录(真实待适配与历史记录一条没动;插件与补丁一个字都没动)`
|
|
525
|
+
: '没有可清理的失效记录(文件未改动)'))
|
|
526
|
+
const payload = {
|
|
527
|
+
ok: result.ok === true,
|
|
528
|
+
action,
|
|
529
|
+
changed: result.changed === true,
|
|
530
|
+
removed,
|
|
531
|
+
removedCount,
|
|
532
|
+
keptCount: Array.isArray(result.keptRowIds) ? result.keptRowIds.length : 0,
|
|
533
|
+
// 删完之后按**同一判据**重算一遍(面板据此重绘;不复用旧计数,避免"删了但数字没动")
|
|
534
|
+
counts: (() => {
|
|
535
|
+
try { return countPendingRecords(readPendingRecords().records, liveRowIds) } catch { return null }
|
|
536
|
+
})(),
|
|
537
|
+
file: read.file,
|
|
538
|
+
backup: result.backup ?? null,
|
|
539
|
+
verified: result.verified === true,
|
|
540
|
+
error: result.error ?? null,
|
|
541
|
+
note,
|
|
542
|
+
}
|
|
543
|
+
if (result.ok === true) sendJson(res, 200, payload)
|
|
544
|
+
else sendError(res, result.status ?? 400, result.error ?? '删除记录失败', payload)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export { hostShapeOf, maybeSwitchToObserver, maybeWriteSnapshot, preflightTimeoutMs, resetHostSwitchChecked, routeCompatAck, routeCompatGateAction, routeCompatMode, routeCompatPendingForget, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled, scheduleAutoPreflight }
|
|
@@ -16,7 +16,7 @@ import { readBody, sendError } from '../infra/httpd.js'
|
|
|
16
16
|
import { routeAiConsent, routeAiEmpowerCancel, routeAiEmpowerList, routeAiEmpowerPlan, routeAiEmpowerRun, routeAiEmpowerStatus } from './ai.js'
|
|
17
17
|
import { routeRunSuggested } from './actions.js'
|
|
18
18
|
import { routeComponentAutostart, routeComponentStart, routeComponentStatus, routeComponentStop, routeComponents, routeRepoClone, routeRepoLandConfig, routeRepoList, routeRepoOpen, routeRepoRemove } from './components.js'
|
|
19
|
-
import { routeCompatAck, routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled } from './compat.js'
|
|
19
|
+
import { routeCompatAck, routeCompatGateAction, routeCompatMode, routeCompatPendingForget, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled } from './compat.js'
|
|
20
20
|
import { routeSafeBoot } from './safe-boot.js'
|
|
21
21
|
// issue #22 ②③ 加法:用户备注 / 用户徽章 / 用户自建分组(独立文件,与插件列表完全解耦)
|
|
22
22
|
import { routeAnnotationsGet, routeAnnotationsPost } from './annotations.js'
|
|
@@ -81,6 +81,23 @@ function routeDeps() {
|
|
|
81
81
|
return { runInstallJob, runSuiteInstallJob, webPort, readExtraBundleRows, detectAdoptablePending, listEntries, detectCompat, detectFrameworkUpgrade, DEFAULT_SEARCH, aiEmpowerExecute, aiEmpowerPlan, backupProfileSnapshot, currentFrameworkVersion, frameworkCompatReportFor, isProtectedModule, pnpmRemove, preflightDisableIncompatible, rowIdModuleMap, fetchJson: fetchJsonUrl }
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* `/compat-gate` 的**只增不改**分流(0.5.98 加法)。
|
|
86
|
+
*
|
|
87
|
+
* 为什么分流装在这一层:门控面板的写入口必须挂在**既有的 /compat-gate** 上(不新开路由,
|
|
88
|
+
* 否则又要登记一份清单);而那条路由的 handler(routes/framework.js#routeCompatGate)所在文件
|
|
89
|
+
* 已顶格 600/600 行(tests/test-architecture-guard.mjs 的硬门槛),**一个字都不能加**。
|
|
90
|
+
* 于是"分流"留在装配层,"动作实现"落在 routes/compat.js(适配门记录那一族)。
|
|
91
|
+
*
|
|
92
|
+
* 契约:body 里没有 `action`(或不是非空字符串)⇒ **逐字**走原来的总开关逻辑,
|
|
93
|
+
* 既有请求/响应一字不变;带 `action` 才进动作分派那一族(0.5.98 删记录 / 0.5.100 按需检查)。
|
|
94
|
+
*/
|
|
95
|
+
async function routeCompatGateAny(req, res, rc) {
|
|
96
|
+
const action = typeof rc.body?.action === 'string' ? rc.body.action : ''
|
|
97
|
+
if (action !== '') return routeCompatGateAction(req, res, rc)
|
|
98
|
+
return routeCompatGate(req, res, rc)
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
async function handle(ctx, req, res) {
|
|
85
102
|
const url = new URL(req.url ?? '/', 'http://x')
|
|
86
103
|
const pathname = url.pathname
|
|
@@ -126,7 +143,7 @@ const ROUTES = [
|
|
|
126
143
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/skill-toggle`, handler: routeSkillToggle },
|
|
127
144
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/install`, handler: routeInstall },
|
|
128
145
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/install-status`, handler: routeInstallStatus }, { methods: ['POST'], path: `${ROUTE_PREFIX}/framework-check`, handler: routeFrameworkCheck },
|
|
129
|
-
{ methods: ['POST'], path: `${ROUTE_PREFIX}/compat-gate`, handler:
|
|
146
|
+
{ methods: ['POST'], path: `${ROUTE_PREFIX}/compat-gate`, handler: routeCompatGateAny },
|
|
130
147
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/check-update`, handler: routeCheckUpdate },
|
|
131
148
|
// issue #22 ① 加法:批量检测(已安装列表的「一键检测全部更新」)。
|
|
132
149
|
// ★ 与 /check-update 共用同一个探针(probePackageUpdate),判据不分叉;并发上限在 domain/update-batch.js。
|
|
@@ -12,6 +12,8 @@ import { bundleVetoList } from '../domain/bundle-veto.js'
|
|
|
12
12
|
import { buildLinkInventory } from '../domain/link-inventory.js'
|
|
13
13
|
import { buildInstallHealth } from '../domain/install-health.js'
|
|
14
14
|
import { applyExemption } from '../domain/compat-exempt.js'
|
|
15
|
+
// 0.5.98 加法:适配门记录的「真待适配」计数(唯一真源,见 domain/compat-pending-records.js)
|
|
16
|
+
import { annotateGateRows, countPendingRecords, liveRowIdsFromPatch } from '../domain/compat-pending-records.js'
|
|
15
17
|
import { addDevRoot, readDevRoots, removeDevRoot } from '../domain/dev-roots.js'
|
|
16
18
|
import { applyPinDevDir, planPinDevDir } from '../domain/pin-dev-dir.js'
|
|
17
19
|
import { reconcileLockfile } from '../domain/selfupdate.js'
|
|
@@ -68,6 +70,13 @@ async function routeStateGet(req, res, rc) {
|
|
|
68
70
|
const compatPending = pendingState.pending
|
|
69
71
|
const compatVerdicts = pendingState.verdicts
|
|
70
72
|
const compatPendingRows = new Set((compatPending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => p.rowId))
|
|
73
|
+
// ★ 0.5.98 加法(用户 2026-10-28 实测):**「真待适配」的唯一口径**。
|
|
74
|
+
// 现场:compat-pending.json 里 20 条 adopted + 3 条 pending 且 moduleName 为 null(补丁行早已不存在),
|
|
75
|
+
// 面板把 `pending` 数组长度当"待适配",用户看到"有 N 行待适配",其实一行都没有。
|
|
76
|
+
// 判据只认 domain/compat-pending-records.js#isActionablePending(status=pending 且该行仍在补丁里);
|
|
77
|
+
// 「行是否存在」取的是**补丁解析的既有产物**(上面 patch 的 disables/forced/inserts),不另写正则。
|
|
78
|
+
const patchRowIds = liveRowIdsFromPatch(patch)
|
|
79
|
+
const pendingCounts = countPendingRecords(compatPending?.pending ?? [], patchRowIds)
|
|
71
80
|
// 兼容门总开关 + 自动检测(只提示不自动开)
|
|
72
81
|
const compatGate = readCompatGate()
|
|
73
82
|
const adoptable = compatGate.autoDetect ? detectAdoptablePending(ctx) : new Map()
|
|
@@ -264,7 +273,28 @@ async function routeStateGet(req, res, rc) {
|
|
|
264
273
|
} catch (error) {
|
|
265
274
|
lockAutofix = { error: error instanceof Error ? error.message : String(error), scheduled: false }
|
|
266
275
|
}
|
|
267
|
-
|
|
276
|
+
// ★ 0.5.98 加法:门控明细行补两个**只增不改**的字段 —— `rowMissing`(这一行的 rowId 已不在
|
|
277
|
+
// 当前补丁里)与 `actionable`(与计数同一个判据)。面板据此如实标注「该行已不存在」并给删除入口;
|
|
278
|
+
// 既有字段(pendingCount / rows 的全部旧字段)一个不少、语义不变。
|
|
279
|
+
// ★ 0.5.101 加法(用户 2026-11-02):**运行期加载失败**的行(`fiberPhase === 'failed'`)。
|
|
280
|
+
// 真机现场:`experimental-inspector` 因 `@deepseek-ai/cordis` 解析失败而挂载失败,而门控**完全
|
|
281
|
+
// 看不到它** —— 因为这条失败不经过预扫/隔离链。这里只补一个**视图**字段:
|
|
282
|
+
// **绝不写 compat-pending.json**(那是适配裁决记录,不能被运行期故障污染)、不改任何既有字段。
|
|
283
|
+
const loadFailed = entries
|
|
284
|
+
.filter((e) => e.fiberPhase === 'failed' && typeof e.rowId === 'string' && e.rowId !== '')
|
|
285
|
+
.map((e) => ({ rowId: e.rowId, moduleName: e.moduleName ?? null, version: e.version ?? null, fiberPhase: 'failed', enabled: e.enabled === true }))
|
|
286
|
+
const gatingOut = (() => {
|
|
287
|
+
const g = gatingSummary(ctx, compatPending, entries, compatVerdicts)
|
|
288
|
+
return {
|
|
289
|
+
...g,
|
|
290
|
+
pending: annotateGateRows(g.pending, patchRowIds),
|
|
291
|
+
adopted: annotateGateRows(g.adopted, patchRowIds),
|
|
292
|
+
// 只增字段:加载失败的行 + 计数(面板据此决定分区显不显示)
|
|
293
|
+
loadFailed,
|
|
294
|
+
loadFailedCount: loadFailed.length,
|
|
295
|
+
}
|
|
296
|
+
})()
|
|
297
|
+
sendJson(res, 200, { ok: true, entries: allEntries, 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, lockAutofix, 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: gatingOut, 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 }, actionableCount: pendingCounts.actionableCount, staleCount: pendingCounts.staleCount, lastScan: compatPending.lastScan ?? 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 })) })
|
|
268
298
|
return
|
|
269
299
|
}
|
|
270
300
|
|
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.99",
|
|
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",
|