@noob-stupid/dsh-plugin-console 0.5.33 → 0.5.35
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 +516 -220
- package/lib/server/domain/compat-verdict.js +241 -0
- package/lib/server/domain/compat.js +84 -16
- package/lib/server/domain/framework-cleanup.js +225 -24
- package/lib/server/domain/framework-residuals.js +52 -3
- package/lib/server/domain/market-search.js +242 -0
- package/lib/server/domain/settings-api-scan.js +16 -1
- package/lib/server/routes/market.js +108 -35
- package/lib/server/routes/plugins.js +24 -13
- package/lib/server/routes/state.js +14 -3
- package/package.json +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// L1 · domain —— compat-verdict.js(适配门结论的**判据版本戳** + 过期结论的读取代发重算;0.5.34 加法)
|
|
2
|
+
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
|
+
//
|
|
4
|
+
// ── 真机背景(2026-10-01,用户实测)────────────────────────────────────────────────
|
|
5
|
+
// `~/.dsh/plugin-console/compat-pending.json` 里 `dshmarket` 的记录仍是
|
|
6
|
+
// `check:"fail"` + `checkNote:"源码仍引用 0.1.2 起已删除的 dsh-settings API(…)——实际不兼容…"`
|
|
7
|
+
// —— 那是 **0.5.33 收紧判据之前**的误报结论(真机复核:现在 0 命中)。面板照旧把它当事实展示
|
|
8
|
+
// (「实际不兼容,启用会让整个服务启动崩溃」+「待适配」永久钉住),**持续误导用户**。
|
|
9
|
+
//
|
|
10
|
+
// ── 本模块只做两件事(**不放宽任何判据**)──────────────────────────────────────────
|
|
11
|
+
// ① 给结论打**判据版本戳**:记录里的 `scanVerdict.version` 标明"这条结论是哪一版判据算的"
|
|
12
|
+
// (戳来自 settings-api-scan.js 的 `SCAN_CRITERIA_VERSION`,判据改一次就要改它)。
|
|
13
|
+
// ② **读取代发**:读清单时若记录的戳落后于当前判据 → 用**当前判据只读重算**
|
|
14
|
+
// · 结论变了 → 回写(`check` / `checkNote` / `scanVerdict`,含 previous + rescinded + 来源证据)
|
|
15
|
+
// · 结论没变 → **零写盘**(幂等;同一条记录不会每次读都写盘)
|
|
16
|
+
// · 包解析不到 → 不猜:如实标注"无法复核"(保留旧结论,绝不假装成当前判据的结论)
|
|
17
|
+
// `fail` 永远不会因为"戳旧"就被改成 `pass` —— 只有**真的用当前判据重算**出非 fail 才算撤回;
|
|
18
|
+
// 真不兼容的(重算仍 fail)一个字都不动。
|
|
19
|
+
//
|
|
20
|
+
// 行数预算:独立模块,留在 600 行架构硬顶内。
|
|
21
|
+
|
|
22
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { dirname, join } from 'node:path'
|
|
24
|
+
import { dshHome } from '../infra/paths.js'
|
|
25
|
+
import { SCAN_CRITERIA_VERSION } from './settings-api-scan.js'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 进程内重算备忘(纯性能,不影响判据):"结论未变 ⇒ 不写盘"意味着记录上的判据戳**一直是旧的**,
|
|
29
|
+
* 于是每次 `GET /state`(面板每隔几秒轮询一次)都会对同一批包重跑源码扫描(单包上限 120 文件 / 400KB)。
|
|
30
|
+
* 键含包路径 + 版本 + package.json 的 mtime:包一变(升级/改源码)就重算,包没变就复用。
|
|
31
|
+
*/
|
|
32
|
+
const VERDICT_MEMO = new Map()
|
|
33
|
+
const MEMO_LIMIT = 200
|
|
34
|
+
|
|
35
|
+
/** 判据版本戳(当前判据的版本;判据变了就跟着 settings-api-scan.js 一起变)。 */
|
|
36
|
+
const SCANNER_VERSION = SCAN_CRITERIA_VERSION
|
|
37
|
+
|
|
38
|
+
/** 适配门清单文件(原在 compat.js;0.5.34 搬到本模块,供"读取代发"一处收口)。 */
|
|
39
|
+
const compatPendingFile = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 「这条禁用记录是我们自己写进去的」判据(纯函数)。
|
|
43
|
+
* 只有这类记录才允许在**误报被撤回**时给用户一键解锁入口 —— 用户手写的禁用块不在此列。
|
|
44
|
+
*/
|
|
45
|
+
const SELF_INFLICTED_SOURCES = new Set([
|
|
46
|
+
'preflight-disabled-before-upgrade', // 框架升级前的预扫(dshmarket 就是这条)
|
|
47
|
+
'boot-quarantine', // 启动失败隔离
|
|
48
|
+
'auto-preflight', // 指纹变化自动预检
|
|
49
|
+
])
|
|
50
|
+
function isSelfInflictedSource(source) {
|
|
51
|
+
return typeof source === 'string' && SELF_INFLICTED_SOURCES.has(source)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 记录上的判据戳(缺失 = 旧时代结论:必须重算)。 */
|
|
55
|
+
function stampOf(record) {
|
|
56
|
+
const s = record?.scanVerdict
|
|
57
|
+
if (s === null || typeof s !== 'object') return { version: null, decision: null, reason: null, at: null, rescinded: false, evidence: null }
|
|
58
|
+
return {
|
|
59
|
+
version: typeof s.version === 'string' ? s.version : null,
|
|
60
|
+
decision: typeof s.decision === 'string' ? s.decision : null,
|
|
61
|
+
reason: typeof s.reason === 'string' ? s.reason : null,
|
|
62
|
+
at: Number.isFinite(s.at) ? s.at : null,
|
|
63
|
+
rescinded: s.rescinded === true,
|
|
64
|
+
evidence: typeof s.evidence === 'string' ? s.evidence : null,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 这条记录的结论是不是"旧判据算的"(纯函数)。 */
|
|
69
|
+
function isStaleVerdict(record, version = SCANNER_VERSION) {
|
|
70
|
+
return stampOf(record).version !== version
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 纯判据:给一组记录算出「哪些结论过期、用当前判据重算后是什么」(IO 全由 `recompute` 注入)。
|
|
75
|
+
* `recompute(record)` → `{ decision, reason }`;解析不到包时返回 `{ unresolved: '原因' }`。
|
|
76
|
+
* 返回每条的 `{ rowId, status, decision, reason, storedDecision, storedNote, rescinded, evidence }`,
|
|
77
|
+
* status ∈ fresh(戳已是最新,无需重算)/ updated(结论变了 → 调用方回写)/ unchanged(结论没变 → **零写盘**)
|
|
78
|
+
* / unresolved(复核不了 → 如实标注,保留旧结论)。
|
|
79
|
+
*/
|
|
80
|
+
function planVerdictRefresh(records, { version = SCANNER_VERSION, recompute } = {}) {
|
|
81
|
+
const out = []
|
|
82
|
+
for (const rec of (Array.isArray(records) ? records : [])) {
|
|
83
|
+
if (rec === null || typeof rec !== 'object' || typeof rec.rowId !== 'string' || rec.rowId === '') continue
|
|
84
|
+
const storedDecision = typeof rec.check === 'string' && rec.check !== '' ? rec.check : null
|
|
85
|
+
const storedNote = typeof rec.checkNote === 'string' ? rec.checkNote : null
|
|
86
|
+
const stamp = stampOf(rec)
|
|
87
|
+
if (stamp.version === version) {
|
|
88
|
+
out.push({ rowId: rec.rowId, status: 'fresh', decision: storedDecision, reason: stamp.reason ?? storedNote, storedDecision, storedNote, rescinded: stamp.rescinded, evidence: stamp.evidence, version })
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
// 没给重算器(或明确要求只读)→ 不猜:标成"过期、未复核"
|
|
92
|
+
if (typeof recompute !== 'function') {
|
|
93
|
+
out.push({ rowId: rec.rowId, status: 'stale', decision: storedDecision, reason: storedNote, storedDecision, storedNote, rescinded: false, evidence: null, version: null })
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
let next = null
|
|
97
|
+
try {
|
|
98
|
+
next = recompute(rec)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
next = { unresolved: String(error?.message ?? error) }
|
|
101
|
+
}
|
|
102
|
+
if (next === null || next === undefined) next = { unresolved: '重算没有返回结论' }
|
|
103
|
+
if (typeof next.unresolved === 'string' && next.unresolved !== '') {
|
|
104
|
+
out.push({ rowId: rec.rowId, status: 'unresolved', decision: storedDecision, reason: storedNote, storedDecision, storedNote, rescinded: false, evidence: null, version: null, unresolved: next.unresolved })
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
const decision = typeof next.decision === 'string' && next.decision !== '' ? next.decision : 'unknown'
|
|
108
|
+
const reason = typeof next.reason === 'string' ? next.reason : null
|
|
109
|
+
out.push({
|
|
110
|
+
rowId: rec.rowId,
|
|
111
|
+
status: decision === storedDecision ? 'unchanged' : 'updated',
|
|
112
|
+
decision,
|
|
113
|
+
reason,
|
|
114
|
+
storedDecision,
|
|
115
|
+
storedNote,
|
|
116
|
+
// 「旧判据判 fail,当前判据不再 fail」= 误报被撤回(这是唯一允许解除"待适配"的情形)
|
|
117
|
+
rescinded: storedDecision === 'fail' && decision !== 'fail',
|
|
118
|
+
evidence: isSelfInflictedSource(rec.source) ? String(rec.source) : null,
|
|
119
|
+
version,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
return out
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 读取代发(IO 版):把 `planVerdictRefresh` 的结论落到记录上,**结论变了才写盘**(写后读回核实)。
|
|
127
|
+
* `deps`:`{ resolvePkg, readPkg, check, writeText, readText, now }` —— 全部可注入(离线测试)。
|
|
128
|
+
* 返回 `{ version, entries, updated, unchanged, unresolved, fresh, wrote, writeError, file }`。
|
|
129
|
+
*/
|
|
130
|
+
function refreshCompatPendingVerdicts({ pending, baseDir, file = null, deps = {} } = {}) {
|
|
131
|
+
const {
|
|
132
|
+
resolvePkg, readPkg, check, now = () => Date.now(), stat = statSync,
|
|
133
|
+
readText = (p) => readFileSync(p, 'utf8'), writeText = (p, data) => writeFileSync(p, data, 'utf8'),
|
|
134
|
+
memo = VERDICT_MEMO,
|
|
135
|
+
} = deps
|
|
136
|
+
const target = file ?? compatPendingFile()
|
|
137
|
+
const records = Array.isArray(pending?.pending) ? pending.pending : []
|
|
138
|
+
const fwVer = typeof pending?.frameworkVersion === 'string' ? pending.frameworkVersion : null
|
|
139
|
+
const recompute = (rec) => {
|
|
140
|
+
if (typeof rec.moduleName !== 'string' || rec.moduleName === '') return { unresolved: '记录里没有 moduleName(无法定位已装包)' }
|
|
141
|
+
if (typeof resolvePkg !== 'function' || typeof readPkg !== 'function' || typeof check !== 'function') return { unresolved: '读取代发缺少解析器(内部接线问题)' }
|
|
142
|
+
let pkgPath = null
|
|
143
|
+
try { pkgPath = resolvePkg(rec.moduleName, baseDir) } catch (error) { return { unresolved: '解析包失败:' + String(error?.message ?? error) } }
|
|
144
|
+
if (pkgPath === null || pkgPath === undefined) return { unresolved: `本机解析不到 ${rec.moduleName}` }
|
|
145
|
+
let pkg = null
|
|
146
|
+
try { pkg = readPkg(pkgPath) } catch (error) { return { unresolved: '读取包信息失败:' + String(error?.message ?? error) } }
|
|
147
|
+
if (pkg === null || pkg === undefined) return { unresolved: '读取包信息失败' }
|
|
148
|
+
const mtimeMs = (() => { try { return stat(pkgPath).mtimeMs } catch { return 0 } })()
|
|
149
|
+
const key = `${rec.rowId}|${pkgPath}|${pkg.version ?? ''}|${mtimeMs}|${SCANNER_VERSION}`
|
|
150
|
+
if (memo instanceof Map && memo.has(key)) return memo.get(key)
|
|
151
|
+
const verdict = check(pkg, fwVer ?? '?', dirname(pkgPath))
|
|
152
|
+
const out = { decision: verdict?.decision ?? 'unknown', reason: verdict?.reason ?? null }
|
|
153
|
+
if (memo instanceof Map) {
|
|
154
|
+
if (memo.size >= MEMO_LIMIT) memo.clear()
|
|
155
|
+
memo.set(key, out)
|
|
156
|
+
}
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
159
|
+
const entries = planVerdictRefresh(records, { recompute })
|
|
160
|
+
const byRow = new Map(entries.map((e) => [e.rowId, e]))
|
|
161
|
+
const updated = []
|
|
162
|
+
const at = now()
|
|
163
|
+
for (const rec of records) {
|
|
164
|
+
const e = byRow.get(rec?.rowId)
|
|
165
|
+
if (e === undefined || e.status !== 'updated') continue
|
|
166
|
+
const previous = { decision: e.storedDecision, note: e.storedNote }
|
|
167
|
+
rec.check = e.decision
|
|
168
|
+
rec.checkNote = e.reason
|
|
169
|
+
rec.scanVerdict = { version: SCANNER_VERSION, decision: e.decision, reason: e.reason, at, previous, rescinded: e.rescinded, evidence: e.evidence }
|
|
170
|
+
rec.verdictUpdatedAt = at
|
|
171
|
+
updated.push({ rowId: e.rowId, from: e.storedDecision, to: e.decision, rescinded: e.rescinded, evidence: e.evidence })
|
|
172
|
+
}
|
|
173
|
+
const result = {
|
|
174
|
+
version: SCANNER_VERSION,
|
|
175
|
+
file: target,
|
|
176
|
+
entries,
|
|
177
|
+
updated,
|
|
178
|
+
unchanged: entries.filter((e) => e.status === 'unchanged').map((e) => e.rowId),
|
|
179
|
+
unresolved: entries.filter((e) => e.status === 'unresolved').map((e) => ({ rowId: e.rowId, reason: e.unresolved })),
|
|
180
|
+
fresh: entries.filter((e) => e.status === 'fresh').map((e) => e.rowId),
|
|
181
|
+
wrote: false,
|
|
182
|
+
writeError: null,
|
|
183
|
+
}
|
|
184
|
+
if (updated.length === 0) return result // ② 结论未变(或无可重算)→ **零写盘**
|
|
185
|
+
const payload = { ...pending, pending: records, updatedAt: new Date(at).toISOString() }
|
|
186
|
+
try {
|
|
187
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
188
|
+
writeText(target, JSON.stringify(payload, null, 2), 'utf8')
|
|
189
|
+
} catch (error) {
|
|
190
|
+
result.writeError = '回写适配门清单失败:' + String(error?.message ?? error)
|
|
191
|
+
return result
|
|
192
|
+
}
|
|
193
|
+
// 读回核实:只有真的落盘且内容一致才算写成功(绝不谎报"已更正")
|
|
194
|
+
try {
|
|
195
|
+
const back = JSON.parse(readText(target))
|
|
196
|
+
const rec = (back?.pending ?? []).find((p) => p.rowId === updated[0].rowId)
|
|
197
|
+
const ok = rec !== undefined && rec.check === updated[0].to && stampOf(rec).version === SCANNER_VERSION
|
|
198
|
+
if (!ok) result.writeError = '回写后读回内容与期望不一致(结论没真正落盘)'
|
|
199
|
+
} catch (error) {
|
|
200
|
+
result.writeError = '回写后读回失败:' + String(error?.message ?? error)
|
|
201
|
+
}
|
|
202
|
+
result.wrote = result.writeError === null
|
|
203
|
+
return result
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 一条记录对外的"结论视图"(面板/门控摘要用):当前判据下的结论 + 是否过期 + 是否已撤回。
|
|
208
|
+
* `entry` = `refreshCompatPendingVerdicts()` 的 `entries` 里对应那条(可省 → 只看记录上的判据戳)。
|
|
209
|
+
*/
|
|
210
|
+
function verdictView(record, entry = null) {
|
|
211
|
+
const stamp = stampOf(record)
|
|
212
|
+
const status = entry?.status ?? (stamp.version === SCANNER_VERSION ? 'fresh' : 'stale')
|
|
213
|
+
const evidence = entry?.evidence ?? stamp.evidence ?? (isSelfInflictedSource(record?.source) ? String(record.source) : null)
|
|
214
|
+
if (status === 'updated') {
|
|
215
|
+
return { decision: entry.decision, reason: entry.reason, version: entry.version, stale: false, rescinded: entry.rescinded === true, evidence, unresolved: null, status, previous: { decision: entry.storedDecision, note: entry.storedNote } }
|
|
216
|
+
}
|
|
217
|
+
if (status === 'fresh') {
|
|
218
|
+
return { decision: stamp.decision ?? (typeof record?.check === 'string' ? record.check : null), reason: stamp.reason ?? (typeof record?.checkNote === 'string' ? record.checkNote : null), version: stamp.version, stale: false, rescinded: stamp.rescinded, evidence, unresolved: null, status }
|
|
219
|
+
}
|
|
220
|
+
if (status === 'unchanged') {
|
|
221
|
+
// 结论与旧判据一致(已用当前判据复核过,但**不写盘**):结论可信,判据戳仍是旧的
|
|
222
|
+
return { decision: entry.decision, reason: entry.reason, version: entry.version, stale: true, rescinded: false, evidence, unresolved: null, status }
|
|
223
|
+
}
|
|
224
|
+
if (status === 'unresolved') {
|
|
225
|
+
return { decision: entry.decision, reason: entry.reason, version: null, stale: true, rescinded: false, evidence: null, unresolved: entry.unresolved, status }
|
|
226
|
+
}
|
|
227
|
+
// 'stale':本轮没有重算(没给基准/只读)→ 如实标"旧判据、未复核"
|
|
228
|
+
return { decision: stamp.decision ?? (typeof record?.check === 'string' ? record.check : null), reason: stamp.reason ?? (typeof record?.checkNote === 'string' ? record.checkNote : null), version: stamp.version, stale: true, rescinded: stamp.rescinded, evidence, unresolved: null, status: 'stale' }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
SCANNER_VERSION,
|
|
233
|
+
SELF_INFLICTED_SOURCES,
|
|
234
|
+
compatPendingFile,
|
|
235
|
+
isSelfInflictedSource,
|
|
236
|
+
isStaleVerdict,
|
|
237
|
+
planVerdictRefresh,
|
|
238
|
+
refreshCompatPendingVerdicts,
|
|
239
|
+
stampOf,
|
|
240
|
+
verdictView,
|
|
241
|
+
}
|
|
@@ -13,6 +13,8 @@ import { listEntries } from './runtime.js'
|
|
|
13
13
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
14
14
|
import { dshHome, findPatchPath, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
|
|
15
15
|
import { semverRangeMatch, semverRangeMatchLoose } from '../infra/semver.js'
|
|
16
|
+
// 0.5.34 加法:适配门结论的**判据版本戳** + 过期结论的读取代发重算(见该模块头注释)。
|
|
17
|
+
import { SCANNER_VERSION, compatPendingFile, isSelfInflictedSource, planVerdictRefresh, refreshCompatPendingVerdicts, verdictView } from './compat-verdict.js'
|
|
16
18
|
// 0.5.33 改错:已删除 API 的源码扫描搬进独立 domain 模块(判据收紧为"真的从 dsh-settings 绑定进来")。
|
|
17
19
|
// 这三个名字继续由本模块导出,对外契约不变。
|
|
18
20
|
import { REMOVED_SETTINGS_SYMBOLS, referencesRemovedSymbol, scanSettingsApiUsage } from './settings-api-scan.js'
|
|
@@ -82,8 +84,6 @@ function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
|
|
|
82
84
|
// 支持 0.1.x 系列(0.1.0 / 0.1.1 等);0.2 / 1.0 等破坏性大版本才标记不支持
|
|
83
85
|
const SUPPORTED_WEB_APP_PATTERN = /^0\.1\.\d+/u
|
|
84
86
|
|
|
85
|
-
const COMPAT_PENDING_FILE = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
|
|
86
|
-
|
|
87
87
|
/**
|
|
88
88
|
* 启用前冒烟检查(服务永不崩机制):在独立子进程中动态 import 插件主模块,
|
|
89
89
|
* 捕获 loader 会遇到的 import/resolution 错误(SyntaxError、缺失导出、模块缺失)。
|
|
@@ -126,7 +126,49 @@ function probePluginImport(moduleName, profileDir) {
|
|
|
126
126
|
})
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
129
|
+
/** 适配门清单文件(0.5.34 起由 compat-verdict.js 定义,供"读取代发"一处收口)。 */
|
|
130
|
+
const COMPAT_PENDING_FILE = compatPendingFile
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 读取框架升级适配门清单(compat-pending.json);损坏/缺失返回 `{ pending: null, ... }`。
|
|
134
|
+
*
|
|
135
|
+
* 0.5.34 加法(**读取代发**):给 `baseDir`(当前 profile 目录)时,对**判据戳落后**的记录
|
|
136
|
+
* 用当前判据只读重算:结论变了才回写(幂等:结论不变**零写盘**),解析不到包就如实标注"无法复核"。
|
|
137
|
+
* 真不兼容的(重算仍 fail)一个字都不动 —— 这条只解决"过期结论仍当事实展示",不放宽任何判据。
|
|
138
|
+
* 返回 `{ pending, refresh, verdicts }`:`verdicts` = rowId → 结论视图(面板/门控摘要用)。
|
|
139
|
+
*/
|
|
140
|
+
function readCompatPendingWithVerdicts({ baseDir = null, deps = {} } = {}) {
|
|
141
|
+
let raw = null
|
|
142
|
+
try { raw = JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return { pending: null, refresh: null, verdicts: new Map() } }
|
|
143
|
+
if (raw === null || typeof raw !== 'object') return { pending: null, refresh: null, verdicts: new Map() }
|
|
144
|
+
let refresh = null
|
|
145
|
+
if (baseDir !== null && baseDir !== undefined && Array.isArray(raw.pending) && raw.pending.length > 0) {
|
|
146
|
+
try {
|
|
147
|
+
refresh = refreshCompatPendingVerdicts({
|
|
148
|
+
pending: raw,
|
|
149
|
+
baseDir,
|
|
150
|
+
deps: {
|
|
151
|
+
resolvePkg: (name, base) => resolvePackageJson(name, base),
|
|
152
|
+
readPkg: (p) => JSON.parse(readFileSync(p, 'utf8')),
|
|
153
|
+
check: checkPluginFrameworkCompat,
|
|
154
|
+
...deps,
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// 复核本身出问题也必须如实回报(不静默):结论按记录原样展示,附上失败原因
|
|
159
|
+
refresh = { version: SCANNER_VERSION, entries: [], updated: [], unchanged: [], unresolved: [], fresh: [], wrote: false, writeError: '读取代发重算失败:' + String(error?.message ?? error), file: COMPAT_PENDING_FILE() }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const entries = Array.isArray(refresh?.entries) ? refresh.entries : planVerdictRefresh(raw.pending)
|
|
163
|
+
const verdicts = new Map()
|
|
164
|
+
for (const rec of raw.pending) {
|
|
165
|
+
if (rec === null || typeof rec !== 'object' || typeof rec.rowId !== 'string') continue
|
|
166
|
+
verdicts.set(rec.rowId, verdictView(rec, entries.find((e) => e.rowId === rec.rowId) ?? null))
|
|
167
|
+
}
|
|
168
|
+
return { pending: raw, refresh, verdicts }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 读取清单(原契约不变:只读、不做复核)。需要"读取代发"的路径用 readCompatPendingWithVerdicts。 */
|
|
130
172
|
function readCompatPending() {
|
|
131
173
|
try { return JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return null }
|
|
132
174
|
}
|
|
@@ -283,15 +325,18 @@ async function detectCompat(baseUrl) {
|
|
|
283
325
|
}
|
|
284
326
|
|
|
285
327
|
/** 待适配行的「现在是否已适配」检测(只提示,不自动解锁——用户定案:我点才开)。
|
|
286
|
-
* 返回 Map<rowId, {version, check, note}
|
|
328
|
+
* 返回 Map<rowId, {version, check, note, basis}>:可解锁有两种**互不放松**的来路 ——
|
|
329
|
+
* ① `version-changed`:插件真的更新过(版本 ≠ 记录版本)且源码扫描不再 fail(原判据,不变);
|
|
330
|
+
* ② `verdict-rescinded`(0.5.34 加法):记录里的 `fail` 是**旧判据的误报**,当前判据重算后不再 fail。
|
|
331
|
+
* 真不兼容(重算仍 fail)**一个都不放行**;②也不需要"版本变化"——否则用户会被永久钉在「待适配」。 */
|
|
287
332
|
function detectAdoptablePending(ports) {
|
|
288
333
|
const out = new Map()
|
|
289
|
-
|
|
334
|
+
let profileDir = null
|
|
335
|
+
try { profileDir = dirname(findPatchPath(ports)) } catch { return out }
|
|
336
|
+
const { pending, verdicts } = readCompatPendingWithVerdicts({ baseDir: profileDir })
|
|
290
337
|
if (pending === null) return out
|
|
291
338
|
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : null
|
|
292
339
|
if (fwVer === null) return out
|
|
293
|
-
let profileDir = null
|
|
294
|
-
try { profileDir = dirname(findPatchPath(ports)) } catch { return out }
|
|
295
340
|
for (const p of (pending.pending ?? []).filter((x) => (x.status ?? 'pending') === 'pending')) {
|
|
296
341
|
try {
|
|
297
342
|
const pkgPath = resolvePackageJson(p.moduleName, profileDir)
|
|
@@ -300,9 +345,18 @@ function detectAdoptablePending(ports) {
|
|
|
300
345
|
const version = typeof pkg.version === 'string' ? pkg.version : null
|
|
301
346
|
const changed = version !== null && version !== p.version
|
|
302
347
|
const check = checkPluginFrameworkCompat(pkg, fwVer, dirname(pkgPath))
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
348
|
+
if (check.decision === 'fail') continue
|
|
349
|
+
const view = verdicts.get(p.rowId) ?? null
|
|
350
|
+
const rescinded = view?.rescinded === true
|
|
351
|
+
if (!changed && !rescinded) continue
|
|
352
|
+
out.set(p.rowId, {
|
|
353
|
+
version,
|
|
354
|
+
check: check.decision,
|
|
355
|
+
note: check.reason ?? null,
|
|
356
|
+
basis: changed ? 'version-changed' : 'verdict-rescinded',
|
|
357
|
+
previousVerdict: view?.storedDecision ?? null,
|
|
358
|
+
evidence: view?.evidence ?? null,
|
|
359
|
+
})
|
|
306
360
|
} catch {}
|
|
307
361
|
}
|
|
308
362
|
return out
|
|
@@ -317,8 +371,11 @@ function detectAdoptablePending(ports) {
|
|
|
317
371
|
*
|
|
318
372
|
* pending —— 当前被拦下/待解锁的行(含原因、来源、是否已通过源码扫描可解锁)
|
|
319
373
|
* adopted —— 曾经被拦、现已适配解锁的行(让用户能核对"到底动过哪些行")
|
|
374
|
+
*
|
|
375
|
+
* 0.5.34 加法:每行多带 `verdict`(当前判据下的结论视图:`stale` / `rescinded` / `unresolved`)——
|
|
376
|
+
* 旧判据的结论不许再被当成事实展示;**复核不了**(包解析不到)的行在 `note` 上如实标注。
|
|
320
377
|
*/
|
|
321
|
-
function gatingSummary(ports, compatPending, ctxEntries = null) {
|
|
378
|
+
function gatingSummary(ports, compatPending, ctxEntries = null, verdicts = null) {
|
|
322
379
|
const rec = compatPending ?? { frameworkVersion: null, upgradeFrom: null, pending: [] }
|
|
323
380
|
const all = Array.isArray(rec.pending) ? rec.pending : []
|
|
324
381
|
let info = new Map()
|
|
@@ -329,11 +386,14 @@ function gatingSummary(ports, compatPending, ctxEntries = null) {
|
|
|
329
386
|
for (const e of Array.isArray(ctxEntries) ? ctxEntries : []) {
|
|
330
387
|
if (typeof e?.rowId === 'string' && e.rowId !== '') byRow.set(e.rowId, e)
|
|
331
388
|
}
|
|
389
|
+
const byVerdict = verdicts instanceof Map ? verdicts : new Map()
|
|
332
390
|
const row = (p) => {
|
|
333
391
|
const meta = info.get(p.rowId) ?? null
|
|
334
392
|
const entry = byRow.get(p.rowId) ?? null
|
|
335
393
|
const moduleName = p.moduleName ?? meta?.moduleName ?? entry?.moduleName ?? null
|
|
336
394
|
const name = moduleName ?? p.rowId ?? '(未知行)'
|
|
395
|
+
const verdict = byVerdict.get(p.rowId) ?? verdictView(p)
|
|
396
|
+
const note = p.checkNote ?? null
|
|
337
397
|
return {
|
|
338
398
|
rowId: p.rowId ?? null,
|
|
339
399
|
name,
|
|
@@ -342,12 +402,16 @@ function gatingSummary(ports, compatPending, ctxEntries = null) {
|
|
|
342
402
|
version: p.version ?? meta?.version ?? entry?.version ?? null,
|
|
343
403
|
status: p.status ?? 'pending',
|
|
344
404
|
check: p.check ?? null,
|
|
345
|
-
|
|
405
|
+
// 复核不了的行:**如实标注**"旧判据结论、未复核"(不许继续当事实;也不猜新结论)
|
|
406
|
+
note: verdict.unresolved === null || verdict.unresolved === undefined
|
|
407
|
+
? note
|
|
408
|
+
: `${note ?? ''}(旧判据结论,未能复核:${verdict.unresolved})`,
|
|
346
409
|
source: p.source ?? null,
|
|
347
410
|
adoptedAt: p.adoptedAt ?? null,
|
|
348
411
|
forcedAt: p.forcedAt ?? null,
|
|
349
412
|
adoptedBy: p.adoptedBy ?? null,
|
|
350
413
|
enabled: entry === null ? null : entry.enabled === true,
|
|
414
|
+
verdict,
|
|
351
415
|
}
|
|
352
416
|
}
|
|
353
417
|
const isPending = (p) => (p.status ?? 'pending') === 'pending'
|
|
@@ -391,9 +455,10 @@ function rowIdModuleMap(ports) {
|
|
|
391
455
|
/**
|
|
392
456
|
* 框架升级适配门:安装/更新完成后自动校验兼容性,
|
|
393
457
|
* 通过(版本已变化 + 未在声明/依赖层面明确不兼容)则移除补丁禁用块并解锁启用。
|
|
458
|
+
* 0.5.34:走**读取代发**(`profileDir` 已知)—— 顺手把过期判据的结论复核/回写,避免旧误报继续挂在这里。
|
|
394
459
|
*/
|
|
395
460
|
async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patchPath, ports }) {
|
|
396
|
-
const pending =
|
|
461
|
+
const { pending } = readCompatPendingWithVerdicts({ baseDir: profileDir })
|
|
397
462
|
if (!pending || !Array.isArray(pending.pending)) return { ran: false }
|
|
398
463
|
const interested = new Set([packageName, ...(syncedNames ?? [])].filter((n) => n !== null))
|
|
399
464
|
const targets = pending.pending.filter((p) => (p.status ?? 'pending') === 'pending' && interested.has(p.moduleName))
|
|
@@ -438,7 +503,7 @@ async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patc
|
|
|
438
503
|
/** AI 赋能框架适配预检:查 registry 最新版声明 + 兼容门清单,给出权威说明。 */
|
|
439
504
|
async function frameworkCompatReportFor(source, ports, profileDir) {
|
|
440
505
|
const fwVer = currentFrameworkVersion(ports)
|
|
441
|
-
const pending =
|
|
506
|
+
const { pending, verdicts } = readCompatPendingWithVerdicts({ baseDir: profileDir })
|
|
442
507
|
const norm = String(source ?? '').trim()
|
|
443
508
|
const isNpm = norm.startsWith('@') || !norm.includes('/')
|
|
444
509
|
const installed = (() => {
|
|
@@ -448,7 +513,10 @@ async function frameworkCompatReportFor(source, ports, profileDir) {
|
|
|
448
513
|
return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
|
|
449
514
|
} catch { return null }
|
|
450
515
|
})()
|
|
451
|
-
|
|
516
|
+
// 命中行带上当前判据的结论视图(`fail` 仍然是 fail;过期/复核不了会如实标注)
|
|
517
|
+
const pendHits = (pending?.pending ?? [])
|
|
518
|
+
.filter((p) => (p.status ?? 'pending') === 'pending' && p.moduleName === norm)
|
|
519
|
+
.map((p) => ({ ...p, verdict: verdicts.get(p.rowId) ?? verdictView(p) }))
|
|
452
520
|
if (!isNpm) {
|
|
453
521
|
return {
|
|
454
522
|
kind: 'repo', frameworkVersion: fwVer, packageName: null, latest: null, check: null,
|
|
@@ -487,4 +555,4 @@ async function frameworkCompatReportFor(source, ports, profileDir) {
|
|
|
487
555
|
summary: parts.length > 0 ? parts.join(';') : `框架 ${fwVer},无待适配记录`,
|
|
488
556
|
}
|
|
489
557
|
}
|
|
490
|
-
export { gatingSummary, COMPAT_PENDING_FILE, COMPAT_GATE_FILE, COMPAT_ACK_FILE, COMPAT_GATE_DEFAULTS, NOTICE_MODES, DEFAULT_NOTICE_MODE, REMOVED_SETTINGS_SYMBOLS, FIBER_STATE, FIBER_PHASE, SUPPORTED_WEB_APP_PATTERN, referencesRemovedSymbol, scanSettingsApiUsage, isFrameworkOwnedPackage, checkPluginFrameworkCompat, probePluginImport, readCompatPending, writeCompatPending, readCompatGate, writeCompatGate, readCompatAck, writeCompatAck, resolveNoticeState, frameworkCheckPromptText, CONSOLE_VERSION, detectCompat, detectAdoptablePending, rowIdModuleMap, frameworkCompatReportFor, maybeAutoAdaptCompat }
|
|
558
|
+
export { gatingSummary, COMPAT_PENDING_FILE, COMPAT_GATE_FILE, COMPAT_ACK_FILE, COMPAT_GATE_DEFAULTS, NOTICE_MODES, DEFAULT_NOTICE_MODE, REMOVED_SETTINGS_SYMBOLS, FIBER_STATE, FIBER_PHASE, SUPPORTED_WEB_APP_PATTERN, referencesRemovedSymbol, scanSettingsApiUsage, isFrameworkOwnedPackage, checkPluginFrameworkCompat, probePluginImport, readCompatPending, readCompatPendingWithVerdicts, writeCompatPending, verdictView, SCANNER_VERSION, readCompatGate, writeCompatGate, readCompatAck, writeCompatAck, resolveNoticeState, frameworkCheckPromptText, CONSOLE_VERSION, detectCompat, detectAdoptablePending, rowIdModuleMap, frameworkCompatReportFor, maybeAutoAdaptCompat }
|