@noob-stupid/dsh-plugin-console 0.5.32 → 0.5.34
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/README.md +40 -10
- package/README.zh.md +31 -5
- package/lib/client.js +87 -55
- package/lib/server/domain/compat-verdict.js +241 -0
- package/lib/server/domain/compat.js +88 -74
- package/lib/server/domain/framework-cleanup.js +225 -24
- package/lib/server/domain/framework-residuals.js +52 -3
- package/lib/server/domain/peer-veto.js +103 -0
- package/lib/server/domain/settings-api-scan.js +266 -0
- package/lib/server/routes/plugins.js +25 -12
- package/lib/server/routes/state.js +31 -6
- 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
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// L1 · domain —— compat.js(框架兼容判定与适配门:设置 API 扫描 / 兼容门读写 / 待适配记录 / 隔离记录合并的前置部分 / 启动失败分析;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑。注:detectCompat 等吃运行上下文的留到 Step 8)
|
|
2
2
|
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
3
|
|
|
4
|
-
import { readFileSync, writeFileSync,
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs'
|
|
5
5
|
import { readFile } from 'node:fs/promises'
|
|
6
6
|
import { spawn } from 'node:child_process'
|
|
7
7
|
import { dirname, join, sep, resolve } from 'node:path'
|
|
@@ -13,6 +13,11 @@ 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'
|
|
18
|
+
// 0.5.33 改错:已删除 API 的源码扫描搬进独立 domain 模块(判据收紧为"真的从 dsh-settings 绑定进来")。
|
|
19
|
+
// 这三个名字继续由本模块导出,对外契约不变。
|
|
20
|
+
import { REMOVED_SETTINGS_SYMBOLS, referencesRemovedSymbol, scanSettingsApiUsage } from './settings-api-scan.js'
|
|
16
21
|
|
|
17
22
|
/** Cordis Fiber 状态映射(与 dsh-host-plugin-inventory 一致)。 */
|
|
18
23
|
const FIBER_STATE = { PENDING: 0, LOADING: 1, ACTIVE: 2, FAILED: 3, DISPOSED: 4, UNLOADING: 5 }
|
|
@@ -26,63 +31,6 @@ const FIBER_PHASE = {
|
|
|
26
31
|
[FIBER_STATE.UNLOADING]: 'unloading',
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/**
|
|
30
|
-
* 扫描已安装包源码,检测对已删除的 dsh-settings API 的引用
|
|
31
|
-
* (2026-09-04 教训:0.1.2-rc.1 起 settingsNamespace / installSettingsSection 已删除,
|
|
32
|
-
* 静态声明检查判 pass 是假通过;真实兼容性只有模块 import 时见分晓,而 loader 单行失败
|
|
33
|
-
* = 整个服务启动崩溃)。返回命中的符号列表,空数组 = 干净。
|
|
34
|
-
*/
|
|
35
|
-
const REMOVED_SETTINGS_SYMBOLS = ['settingsNamespace', 'installSettingsSection']
|
|
36
|
-
|
|
37
|
-
/** 源码是否**引用**了已删除符号——而不是「恰好包含同名前缀的其它标识符」或「自己定义的同名局部函数」。
|
|
38
|
-
* v0.3.35 修复(真机演练抓到的误伤):原先用 `text.includes('settingsNamespace')` 子串匹配,
|
|
39
|
-
* 而框架自带的 @deepseek-ai/dsh-api-settings-controller 里有个 `settingsNamespaceRequestSchema`,
|
|
40
|
-
* 于是框架自己的 settings 控制器被判「引用已删除 API」→ 升级预扫会把它自动禁用(砍掉框架功能)。
|
|
41
|
-
* 现在要求标识符边界,且排除「本地 const/let/var/function/class 定义且同行没有 dsh-settings 引用」。 */
|
|
42
|
-
function referencesRemovedSymbol(text, sym) {
|
|
43
|
-
const re = new RegExp(`(?<![A-Za-z0-9_$])${sym}(?![A-Za-z0-9_$])`, 'u')
|
|
44
|
-
for (const line of text.split(/\r?\n/u)) {
|
|
45
|
-
const m = re.exec(line)
|
|
46
|
-
if (m === null) continue
|
|
47
|
-
const before = line.slice(0, m.index)
|
|
48
|
-
const localDefinition = /(?:^|[^\w$])(?:const|let|var|function|class|async\s+function)\s*$/u.test(before)
|
|
49
|
-
if (localDefinition && !/dsh-settings/u.test(line)) continue
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function scanSettingsApiUsage(pkgDir) {
|
|
56
|
-
const found = []
|
|
57
|
-
const seen = new Set()
|
|
58
|
-
const walk = (dir, depth) => {
|
|
59
|
-
if (depth > 3) return
|
|
60
|
-
let entries = []
|
|
61
|
-
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
const full = join(dir, entry.name)
|
|
64
|
-
if (entry.isDirectory()) {
|
|
65
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
66
|
-
walk(full, depth + 1)
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
if (!entry.isFile() || !/\.(?:js|mjs|cjs)$/u.test(entry.name)) continue
|
|
70
|
-
if (seen.has(full)) continue
|
|
71
|
-
seen.add(full)
|
|
72
|
-
if (seen.size > 120) return
|
|
73
|
-
try {
|
|
74
|
-
const text = readFileSync(full, 'utf8').slice(0, 400000)
|
|
75
|
-
for (const sym of REMOVED_SETTINGS_SYMBOLS) {
|
|
76
|
-
if (found.includes(sym)) continue
|
|
77
|
-
if (referencesRemovedSymbol(text, sym)) found.push(sym)
|
|
78
|
-
}
|
|
79
|
-
} catch {}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
walk(pkgDir, 0)
|
|
83
|
-
return found
|
|
84
|
-
}
|
|
85
|
-
|
|
86
34
|
/** 包是否由框架自带(解析到 profile 目录之外,如 npx/pnpm 缓存里的 @deepseek-ai/*)。
|
|
87
35
|
* v0.3.35:这类行随框架一起发布,禁用**不是**正确处置(正确处置是回滚框架),
|
|
88
36
|
* 而且一旦判定有误就会把框架功能砍掉——所以升级预扫永不自动禁用它们。 */
|
|
@@ -136,8 +84,6 @@ function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
|
|
|
136
84
|
// 支持 0.1.x 系列(0.1.0 / 0.1.1 等);0.2 / 1.0 等破坏性大版本才标记不支持
|
|
137
85
|
const SUPPORTED_WEB_APP_PATTERN = /^0\.1\.\d+/u
|
|
138
86
|
|
|
139
|
-
const COMPAT_PENDING_FILE = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
|
|
140
|
-
|
|
141
87
|
/**
|
|
142
88
|
* 启用前冒烟检查(服务永不崩机制):在独立子进程中动态 import 插件主模块,
|
|
143
89
|
* 捕获 loader 会遇到的 import/resolution 错误(SyntaxError、缺失导出、模块缺失)。
|
|
@@ -180,7 +126,49 @@ function probePluginImport(moduleName, profileDir) {
|
|
|
180
126
|
})
|
|
181
127
|
}
|
|
182
128
|
|
|
183
|
-
/**
|
|
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。 */
|
|
184
172
|
function readCompatPending() {
|
|
185
173
|
try { return JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return null }
|
|
186
174
|
}
|
|
@@ -337,15 +325,18 @@ async function detectCompat(baseUrl) {
|
|
|
337
325
|
}
|
|
338
326
|
|
|
339
327
|
/** 待适配行的「现在是否已适配」检测(只提示,不自动解锁——用户定案:我点才开)。
|
|
340
|
-
* 返回 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)**一个都不放行**;②也不需要"版本变化"——否则用户会被永久钉在「待适配」。 */
|
|
341
332
|
function detectAdoptablePending(ports) {
|
|
342
333
|
const out = new Map()
|
|
343
|
-
|
|
334
|
+
let profileDir = null
|
|
335
|
+
try { profileDir = dirname(findPatchPath(ports)) } catch { return out }
|
|
336
|
+
const { pending, verdicts } = readCompatPendingWithVerdicts({ baseDir: profileDir })
|
|
344
337
|
if (pending === null) return out
|
|
345
338
|
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : null
|
|
346
339
|
if (fwVer === null) return out
|
|
347
|
-
let profileDir = null
|
|
348
|
-
try { profileDir = dirname(findPatchPath(ports)) } catch { return out }
|
|
349
340
|
for (const p of (pending.pending ?? []).filter((x) => (x.status ?? 'pending') === 'pending')) {
|
|
350
341
|
try {
|
|
351
342
|
const pkgPath = resolvePackageJson(p.moduleName, profileDir)
|
|
@@ -354,9 +345,18 @@ function detectAdoptablePending(ports) {
|
|
|
354
345
|
const version = typeof pkg.version === 'string' ? pkg.version : null
|
|
355
346
|
const changed = version !== null && version !== p.version
|
|
356
347
|
const check = checkPluginFrameworkCompat(pkg, fwVer, dirname(pkgPath))
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
})
|
|
360
360
|
} catch {}
|
|
361
361
|
}
|
|
362
362
|
return out
|
|
@@ -371,8 +371,11 @@ function detectAdoptablePending(ports) {
|
|
|
371
371
|
*
|
|
372
372
|
* pending —— 当前被拦下/待解锁的行(含原因、来源、是否已通过源码扫描可解锁)
|
|
373
373
|
* adopted —— 曾经被拦、现已适配解锁的行(让用户能核对"到底动过哪些行")
|
|
374
|
+
*
|
|
375
|
+
* 0.5.34 加法:每行多带 `verdict`(当前判据下的结论视图:`stale` / `rescinded` / `unresolved`)——
|
|
376
|
+
* 旧判据的结论不许再被当成事实展示;**复核不了**(包解析不到)的行在 `note` 上如实标注。
|
|
374
377
|
*/
|
|
375
|
-
function gatingSummary(ports, compatPending, ctxEntries = null) {
|
|
378
|
+
function gatingSummary(ports, compatPending, ctxEntries = null, verdicts = null) {
|
|
376
379
|
const rec = compatPending ?? { frameworkVersion: null, upgradeFrom: null, pending: [] }
|
|
377
380
|
const all = Array.isArray(rec.pending) ? rec.pending : []
|
|
378
381
|
let info = new Map()
|
|
@@ -383,11 +386,14 @@ function gatingSummary(ports, compatPending, ctxEntries = null) {
|
|
|
383
386
|
for (const e of Array.isArray(ctxEntries) ? ctxEntries : []) {
|
|
384
387
|
if (typeof e?.rowId === 'string' && e.rowId !== '') byRow.set(e.rowId, e)
|
|
385
388
|
}
|
|
389
|
+
const byVerdict = verdicts instanceof Map ? verdicts : new Map()
|
|
386
390
|
const row = (p) => {
|
|
387
391
|
const meta = info.get(p.rowId) ?? null
|
|
388
392
|
const entry = byRow.get(p.rowId) ?? null
|
|
389
393
|
const moduleName = p.moduleName ?? meta?.moduleName ?? entry?.moduleName ?? null
|
|
390
394
|
const name = moduleName ?? p.rowId ?? '(未知行)'
|
|
395
|
+
const verdict = byVerdict.get(p.rowId) ?? verdictView(p)
|
|
396
|
+
const note = p.checkNote ?? null
|
|
391
397
|
return {
|
|
392
398
|
rowId: p.rowId ?? null,
|
|
393
399
|
name,
|
|
@@ -396,12 +402,16 @@ function gatingSummary(ports, compatPending, ctxEntries = null) {
|
|
|
396
402
|
version: p.version ?? meta?.version ?? entry?.version ?? null,
|
|
397
403
|
status: p.status ?? 'pending',
|
|
398
404
|
check: p.check ?? null,
|
|
399
|
-
|
|
405
|
+
// 复核不了的行:**如实标注**"旧判据结论、未复核"(不许继续当事实;也不猜新结论)
|
|
406
|
+
note: verdict.unresolved === null || verdict.unresolved === undefined
|
|
407
|
+
? note
|
|
408
|
+
: `${note ?? ''}(旧判据结论,未能复核:${verdict.unresolved})`,
|
|
400
409
|
source: p.source ?? null,
|
|
401
410
|
adoptedAt: p.adoptedAt ?? null,
|
|
402
411
|
forcedAt: p.forcedAt ?? null,
|
|
403
412
|
adoptedBy: p.adoptedBy ?? null,
|
|
404
413
|
enabled: entry === null ? null : entry.enabled === true,
|
|
414
|
+
verdict,
|
|
405
415
|
}
|
|
406
416
|
}
|
|
407
417
|
const isPending = (p) => (p.status ?? 'pending') === 'pending'
|
|
@@ -445,9 +455,10 @@ function rowIdModuleMap(ports) {
|
|
|
445
455
|
/**
|
|
446
456
|
* 框架升级适配门:安装/更新完成后自动校验兼容性,
|
|
447
457
|
* 通过(版本已变化 + 未在声明/依赖层面明确不兼容)则移除补丁禁用块并解锁启用。
|
|
458
|
+
* 0.5.34:走**读取代发**(`profileDir` 已知)—— 顺手把过期判据的结论复核/回写,避免旧误报继续挂在这里。
|
|
448
459
|
*/
|
|
449
460
|
async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patchPath, ports }) {
|
|
450
|
-
const pending =
|
|
461
|
+
const { pending } = readCompatPendingWithVerdicts({ baseDir: profileDir })
|
|
451
462
|
if (!pending || !Array.isArray(pending.pending)) return { ran: false }
|
|
452
463
|
const interested = new Set([packageName, ...(syncedNames ?? [])].filter((n) => n !== null))
|
|
453
464
|
const targets = pending.pending.filter((p) => (p.status ?? 'pending') === 'pending' && interested.has(p.moduleName))
|
|
@@ -492,7 +503,7 @@ async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patc
|
|
|
492
503
|
/** AI 赋能框架适配预检:查 registry 最新版声明 + 兼容门清单,给出权威说明。 */
|
|
493
504
|
async function frameworkCompatReportFor(source, ports, profileDir) {
|
|
494
505
|
const fwVer = currentFrameworkVersion(ports)
|
|
495
|
-
const pending =
|
|
506
|
+
const { pending, verdicts } = readCompatPendingWithVerdicts({ baseDir: profileDir })
|
|
496
507
|
const norm = String(source ?? '').trim()
|
|
497
508
|
const isNpm = norm.startsWith('@') || !norm.includes('/')
|
|
498
509
|
const installed = (() => {
|
|
@@ -502,7 +513,10 @@ async function frameworkCompatReportFor(source, ports, profileDir) {
|
|
|
502
513
|
return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
|
|
503
514
|
} catch { return null }
|
|
504
515
|
})()
|
|
505
|
-
|
|
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) }))
|
|
506
520
|
if (!isNpm) {
|
|
507
521
|
return {
|
|
508
522
|
kind: 'repo', frameworkVersion: fwVer, packageName: null, latest: null, check: null,
|
|
@@ -541,4 +555,4 @@ async function frameworkCompatReportFor(source, ports, profileDir) {
|
|
|
541
555
|
summary: parts.length > 0 ? parts.join(';') : `框架 ${fwVer},无待适配记录`,
|
|
542
556
|
}
|
|
543
557
|
}
|
|
544
|
-
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 }
|