@noob-stupid/dsh-plugin-console 0.3.67 → 0.4.0

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.
Files changed (48) hide show
  1. package/lib/client.js +126 -31
  2. package/lib/index.js +60 -9687
  3. package/lib/server/domain/ai-run.js +479 -0
  4. package/lib/server/domain/ai.js +246 -0
  5. package/lib/server/domain/compat.js +474 -0
  6. package/lib/server/domain/components.js +108 -0
  7. package/lib/server/domain/dep-source.js +122 -0
  8. package/lib/server/domain/format-contract.js +265 -0
  9. package/lib/server/domain/format-scan.js +431 -0
  10. package/lib/server/domain/framework.js +393 -0
  11. package/lib/server/domain/install-job.js +561 -0
  12. package/lib/server/domain/install.js +599 -0
  13. package/lib/server/domain/jobs.js +28 -0
  14. package/lib/server/domain/market.js +409 -0
  15. package/lib/server/domain/patch.js +203 -0
  16. package/lib/server/domain/presets.js +93 -0
  17. package/lib/server/domain/quarantine.js +224 -0
  18. package/lib/server/domain/release-source.js +504 -0
  19. package/lib/server/domain/repoland.js +119 -0
  20. package/lib/server/domain/revoke.js +184 -0
  21. package/lib/server/domain/runtime.js +118 -0
  22. package/lib/server/domain/selfupdate.js +319 -0
  23. package/lib/server/domain/skills.js +234 -0
  24. package/lib/server/domain/sources.js +297 -0
  25. package/lib/server/domain/suite.js +220 -0
  26. package/lib/server/infra/exec.js +98 -0
  27. package/lib/server/infra/fsx.js +163 -0
  28. package/lib/server/infra/fw-integrity-check.js +37 -0
  29. package/lib/server/infra/http.js +373 -0
  30. package/lib/server/infra/httpd.js +51 -0
  31. package/lib/server/infra/mask.js +19 -0
  32. package/lib/server/infra/paths.js +177 -0
  33. package/lib/server/infra/semver.js +168 -0
  34. package/lib/server/routes/ai.js +172 -0
  35. package/lib/server/routes/components.js +254 -0
  36. package/lib/server/routes/framework-preflight.js +154 -0
  37. package/lib/server/routes/framework-upgrade.js +679 -0
  38. package/lib/server/routes/framework.js +544 -0
  39. package/lib/server/routes/github-login.js +198 -0
  40. package/lib/server/routes/index.js +128 -0
  41. package/lib/server/routes/install.js +116 -0
  42. package/lib/server/routes/market.js +415 -0
  43. package/lib/server/routes/plugins.js +562 -0
  44. package/lib/server/routes/skills.js +107 -0
  45. package/lib/server/routes/sources.js +437 -0
  46. package/lib/server/routes/state.js +125 -0
  47. package/lib/server/state.js +22 -0
  48. package/package.json +1 -1
@@ -0,0 +1,474 @@
1
+ // L1 · domain —— compat.js(框架兼容判定与适配门:设置 API 扫描 / 兼容门读写 / 待适配记录 / 隔离记录合并的前置部分 / 启动失败分析;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑。注:detectCompat 等吃运行上下文的留到 Step 8)
2
+ // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
3
+
4
+ import { readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync } from 'node:fs'
5
+ import { readFile } from 'node:fs/promises'
6
+ import { spawn } from 'node:child_process'
7
+ import { dirname, join, sep, resolve } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { createRequire } from 'node:module'
10
+ import { currentFrameworkVersion } from './framework.js'
11
+ import { removeDisableBlock } from './patch.js'
12
+ import { listEntries } from './runtime.js'
13
+ import { fetchJsonUrl } from '../infra/http.js'
14
+ import { dshHome, findPatchPath, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
15
+ import { semverRangeMatch, semverRangeMatchLoose } from '../infra/semver.js'
16
+
17
+ /** Cordis Fiber 状态映射(与 dsh-host-plugin-inventory 一致)。 */
18
+ const FIBER_STATE = { PENDING: 0, LOADING: 1, ACTIVE: 2, FAILED: 3, DISPOSED: 4, UNLOADING: 5 }
19
+
20
+ const FIBER_PHASE = {
21
+ [FIBER_STATE.PENDING]: 'pending',
22
+ [FIBER_STATE.LOADING]: 'loading',
23
+ [FIBER_STATE.ACTIVE]: 'active',
24
+ [FIBER_STATE.FAILED]: 'failed',
25
+ [FIBER_STATE.DISPOSED]: null,
26
+ [FIBER_STATE.UNLOADING]: 'unloading',
27
+ }
28
+
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
+ /** 包是否由框架自带(解析到 profile 目录之外,如 npx/pnpm 缓存里的 @deepseek-ai/*)。
87
+ * v0.3.35:这类行随框架一起发布,禁用**不是**正确处置(正确处置是回滚框架),
88
+ * 而且一旦判定有误就会把框架功能砍掉——所以升级预扫永不自动禁用它们。 */
89
+ function isFrameworkOwnedPackage(pkgDir, profileDir) {
90
+ if (typeof pkgDir !== 'string' || pkgDir === '' || typeof profileDir !== 'string' || profileDir === '') return false
91
+ const real = (p) => { try { return realpathSync(p) } catch { return p } }
92
+ const dir = real(pkgDir).toLowerCase()
93
+ let base = real(profileDir).toLowerCase()
94
+ if (!base.endsWith(sep)) base += sep
95
+ return !dir.startsWith(base)
96
+ }
97
+
98
+ /**
99
+ * 插件-框架兼容校验(框架升级适配门):
100
+ * 1) 显式兼容声明(dsh.engines.framework / dsh.compat.framework / engines.dsh):以声明为准;
101
+ * 2) 扫描依赖/peerDeps 的 @deepseek-ai/*:任一范围明确不含框架版本 → fail;
102
+ * 3) 【硬判据】提供 pkgDir 时扫描包源码:命中已删除的 dsh-settings API
103
+ * (settingsNamespace / installSettingsSection)→ 直接 fail(2026-09-04 教训:
104
+ * 0.3.6 全家仍引用旧 API,静态声明/依赖检查判 pass 是假通过,loader 单行失败 = 服务崩溃);
105
+ * 4) 无声明且依赖全部满足 → unknown(新版变动不大;由调用方在「版本已变化」前提下放行)。
106
+ * 返回 { decision: 'pass'|'fail'|'unknown', reason }。
107
+ */
108
+ function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
109
+ const declared = pkg?.dsh?.engines?.framework ?? pkg?.dsh?.compat?.framework ?? pkg?.engines?.dsh ?? null
110
+ if (typeof declared === 'string' && declared !== '') {
111
+ const ok = semverRangeMatchLoose(frameworkVersion, declared)
112
+ if (!ok) return { decision: 'fail', reason: `声明兼容范围 ${declared} 不满足当前框架 ${frameworkVersion}` }
113
+ }
114
+ // 硬判据:实际源码扫描(比声明/依赖更接近真实兼容性)
115
+ if (pkgDir !== null && typeof pkgDir === 'string' && semverRangeMatchLoose(frameworkVersion, '>=0.1.2')) {
116
+ const broken = scanSettingsApiUsage(pkgDir)
117
+ if (broken.length > 0) {
118
+ return { decision: 'fail', reason: `源码仍引用 0.1.2 起已删除的 dsh-settings API(${broken.join('、')})——实际不兼容,启用会让整个服务启动崩溃` }
119
+ }
120
+ if (typeof declared === 'string' && declared !== '') return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion},且源码无已删除 API 引用` }
121
+ // 源码扫描干净 = 权威判据:依赖范围可能滞后旧版("假拒绝"),不作为 fail。
122
+ return { decision: 'unknown', reason: '源码扫描无已删除 API 引用(声明/依赖范围为参考,不作为不兼容判据)' }
123
+ } else if (typeof declared === 'string' && declared !== '') {
124
+ return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion}` }
125
+ }
126
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.peerDependencies ?? {}), ...(pkg?.optionalDependencies ?? {}) }
127
+ const hits = Object.entries(deps).filter(([name]) => /^@deepseek-ai\//u.test(name))
128
+ const failing = hits.filter(([, range]) => typeof range === 'string' && range !== '' && !semverRangeMatch(frameworkVersion, range))
129
+ if (failing.length > 0) {
130
+ return { decision: 'fail', reason: `依赖声明不满足:${failing.map(([n, r]) => `${n}@${r}`).join('、')}(当前框架 ${frameworkVersion})` }
131
+ }
132
+ if (hits.length === 0) return { decision: 'unknown', reason: '未声明对 @deepseek-ai/* 的依赖,无法从声明判定(版本已更新时放行)' }
133
+ return { decision: 'unknown', reason: '依赖范围包含当前框架版本但未显式声明兼容(版本已更新时放行)' }
134
+ }
135
+
136
+ // 支持 0.1.x 系列(0.1.0 / 0.1.1 等);0.2 / 1.0 等破坏性大版本才标记不支持
137
+ const SUPPORTED_WEB_APP_PATTERN = /^0\.1\.\d+/u
138
+
139
+ const COMPAT_PENDING_FILE = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
140
+
141
+ /**
142
+ * 启用前冒烟检查(服务永不崩机制):在独立子进程中动态 import 插件主模块,
143
+ * 捕获 loader 会遇到的 import/resolution 错误(SyntaxError、缺失导出、模块缺失)。
144
+ * 子进程失败不影响控制台;探测失败/超时按"不通过"处理(宁可拒绝,不冒险拖崩服务)。
145
+ */
146
+ function probePluginImport(moduleName, profileDir) {
147
+ return new Promise((resolve) => {
148
+ const script = [
149
+ 'import { createRequire } from "node:module";',
150
+ 'import path from "node:path";',
151
+ 'try {',
152
+ ' const req = createRequire(path.join(process.argv[1], "package.json"));',
153
+ ' const mainPath = req.resolve(process.argv[2]);',
154
+ ' const { pathToFileURL } = await import("node:url");',
155
+ ' await import(pathToFileURL(mainPath).href);',
156
+ ' console.log("PROBE_OK");',
157
+ '} catch (e) { console.log("PROBE_ERR:" + String(e && e.message ? e.message : e).slice(0, 400)); process.exitCode = 1; }',
158
+ ].join('\n')
159
+ let child = null
160
+ try {
161
+ child = spawn(process.execPath, ['--input-type=module', '-e', script, profileDir, moduleName], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
162
+ } catch {
163
+ resolve({ ok: true, detail: 'probe 无法启动(放行)' })
164
+ return
165
+ }
166
+ let out = ''
167
+ let timer = null
168
+ const finish = (ok, detail) => {
169
+ if (timer !== null) clearTimeout(timer)
170
+ resolve({ ok, detail })
171
+ }
172
+ timer = setTimeout(() => { try { child.kill() } catch {}; finish(false, '探测超时') }, 8000)
173
+ child.stdout.on('data', (c) => { out += c })
174
+ child.stderr.on('data', (c) => { out += c })
175
+ child.on('error', () => finish(true, 'probe 进程错误(放行)'))
176
+ child.on('close', (code) => {
177
+ if (out.includes('PROBE_OK')) finish(true, 'import OK')
178
+ else finish(false, (out.match(/PROBE_ERR:([^\n]*)/u)?.[1] ?? out.slice(0, 300)).trim())
179
+ })
180
+ })
181
+ }
182
+
183
+ /** 读取框架升级适配门清单(compat-pending.json);损坏/缺失返回 null。 */
184
+ function readCompatPending() {
185
+ try { return JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return null }
186
+ }
187
+
188
+ function writeCompatPending(payload) {
189
+ try {
190
+ mkdirSync(dirname(COMPAT_PENDING_FILE()), { recursive: true })
191
+ writeFileSync(COMPAT_PENDING_FILE(), JSON.stringify(payload, null, 2), 'utf8')
192
+ } catch {}
193
+ }
194
+
195
+ /** 兼容门总开关(用户定案 2026-09-11):用户可关掉自动行为,回到纯手动。
196
+ * autoDisable —— 升级前是否自动禁用判定不适配的行(关掉 = 只提示不动开关)
197
+ * autoDetect —— 打开控制台时是否自动检测「已适配」(关掉 = 不显示可解锁提示) */
198
+ const COMPAT_GATE_FILE = () => join(dshHome(), 'plugin-console', 'compat-gate.json')
199
+
200
+ const COMPAT_GATE_DEFAULTS = { autoDisable: true, autoDetect: true }
201
+
202
+ function readCompatGate() {
203
+ try {
204
+ const raw = JSON.parse(readFileSync(COMPAT_GATE_FILE(), 'utf8'))
205
+ return {
206
+ autoDisable: raw?.autoDisable !== false,
207
+ autoDetect: raw?.autoDetect !== false,
208
+ }
209
+ } catch { return { ...COMPAT_GATE_DEFAULTS } }
210
+ }
211
+
212
+ function writeCompatGate(patch) {
213
+ const next = { ...readCompatGate(), ...patch }
214
+ try {
215
+ mkdirSync(dirname(COMPAT_GATE_FILE()), { recursive: true })
216
+ writeFileSync(COMPAT_GATE_FILE(), JSON.stringify(next, null, 2), 'utf8')
217
+ } catch {}
218
+ return next
219
+ }
220
+
221
+ /** 把框架适配检测结果格式化为给子代理的提示段。 */
222
+ function frameworkCheckPromptText(fc) {
223
+ if (fc === null || fc === undefined) return '(不可用)'
224
+ return [
225
+ `- 当前框架版本:${fc.frameworkVersion ?? '未知'}`,
226
+ `- ${fc.kind === 'npm' ? `npm 包 ${fc.packageName ?? '?'}` : '来源类型'}:registry 最新 ${fc.latest ?? '未知'}${fc.installedVersion ? `;本机已装 ${fc.installedVersion}` : ''}`,
227
+ fc.pending && fc.pending.length > 0 ? `- ⚠ 已命中兼容门:${fc.pending.map((p) => p.rowId).join('、')} 处于强制禁用(版本 ${fc.pending[0]?.version ?? '?'}),升级后仅更新并通过校验才解锁` : null,
228
+ fc.check ? `- 声明/依赖校验:${fc.check.decision === 'fail' ? '不兼容' : fc.check.decision === 'pass' ? '兼容' : '未知'}(${fc.check.reason})` : null,
229
+ '- 计划总结必须向用户说明上述适配结论;若校验为 fail,不要建议启用,应建议「更新到最新版后再启用」。',
230
+ ].filter((s) => s !== null).join('\n')
231
+ }
232
+
233
+ /**
234
+ * 兼容性探测:读取 profile 中 web-app / cli 的版本。
235
+ * 官方破坏性升级(0.2、1.0 等)可能改动本插件依赖的补丁/加载器/插槽接口,
236
+ * 因此面板披露版本并给出提示,而不是默默失效。
237
+ */
238
+ const CONSOLE_VERSION = '0.1.0'
239
+
240
+ async function detectCompat(baseUrl) {
241
+ const result = { consoleVersion: CONSOLE_VERSION, webAppVersion: null, dshVersion: null, supported: true, notice: null }
242
+ try {
243
+ const require = createRequire(baseUrl)
244
+ try {
245
+ const webAppPkg = JSON.parse(await readFile(require.resolve('@deepseek-ai/dsh-web-app/package.json'), 'utf8'))
246
+ result.webAppVersion = webAppPkg.version ?? null
247
+ } catch {}
248
+ try {
249
+ const dshPkg = JSON.parse(await readFile(require.resolve('@deepseek-ai/dsh/package.json'), 'utf8'))
250
+ result.dshVersion = dshPkg.version ?? null
251
+ } catch {}
252
+ } catch {}
253
+ // 兜底:ports.baseUrl 不可用导致 require.resolve 失败时,从插件自身目录解析——
254
+ // 曾导致 dshVersion 为空 → 客户端 current="" → 误判「升级到 latest(rc.7)」(方向相反)
255
+ if (result.dshVersion === null) {
256
+ try {
257
+ const requireLocal = createRequire(join(pluginRoot(), 'package.json'))
258
+ const dshPkg = JSON.parse(readFileSync(requireLocal.resolve('@deepseek-ai/dsh/package.json'), 'utf8'))
259
+ result.dshVersion = dshPkg.version ?? null
260
+ } catch {}
261
+ }
262
+ if (result.webAppVersion !== null && !SUPPORTED_WEB_APP_PATTERN.test(result.webAppVersion)) {
263
+ result.supported = false
264
+ result.notice = `当前 DSH web 包版本 ${result.webAppVersion} 不在受支持的 0.1.x 系列内,插件控制台的部分功能可能因官方破坏性更新而失效;请到 https://github.com/Noob-stupid/dsh-plugin-gating-hub 获取匹配的更新`
265
+ }
266
+ return result
267
+ }
268
+
269
+ /** 待适配行的「现在是否已适配」检测(只提示,不自动解锁——用户定案:我点才开)。
270
+ * 返回 Map<rowId, {version, check, note}>:仅当版本已变化且源码扫描不再 fail 时才算可解锁。 */
271
+ function detectAdoptablePending(ports) {
272
+ const out = new Map()
273
+ const pending = readCompatPending()
274
+ if (pending === null) return out
275
+ const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : null
276
+ if (fwVer === null) return out
277
+ let profileDir = null
278
+ try { profileDir = dirname(findPatchPath(ports)) } catch { return out }
279
+ for (const p of (pending.pending ?? []).filter((x) => (x.status ?? 'pending') === 'pending')) {
280
+ try {
281
+ const pkgPath = resolvePackageJson(p.moduleName, profileDir)
282
+ if (pkgPath === null) continue
283
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
284
+ const version = typeof pkg.version === 'string' ? pkg.version : null
285
+ const changed = version !== null && version !== p.version
286
+ const check = checkPluginFrameworkCompat(pkg, fwVer, dirname(pkgPath))
287
+ if (changed && check.decision !== 'fail') {
288
+ out.set(p.rowId, { version, check: check.decision, note: check.reason ?? null })
289
+ }
290
+ } catch {}
291
+ }
292
+ return out
293
+ }
294
+
295
+ /**
296
+ * 门控面板要的「明细清单」(2026-09-24 用户要求:门控面板应该能显示是哪些插件,有列表数据)。
297
+ *
298
+ * 此前面板只有两个总开关 + 一句「当前待适配:N 行」——看得到数量,看不到**是谁**:
299
+ * compat-pending.json 里有 rowId / moduleName / version / checkNote / 来源 / 时间,
300
+ * 但只下发了 pending 那半边,也没有把 rowId 补成可读模块名。
301
+ *
302
+ * pending —— 当前被拦下/待解锁的行(含原因、来源、是否已通过源码扫描可解锁)
303
+ * adopted —— 曾经被拦、现已适配解锁的行(让用户能核对"到底动过哪些行")
304
+ */
305
+ function gatingSummary(ports, compatPending, ctxEntries = null) {
306
+ const rec = compatPending ?? { frameworkVersion: null, upgradeFrom: null, pending: [] }
307
+ const all = Array.isArray(rec.pending) ? rec.pending : []
308
+ let info = new Map()
309
+ try {
310
+ info = rowIdModuleMap(ports)
311
+ } catch {}
312
+ const byRow = new Map()
313
+ for (const e of Array.isArray(ctxEntries) ? ctxEntries : []) {
314
+ if (typeof e?.rowId === 'string' && e.rowId !== '') byRow.set(e.rowId, e)
315
+ }
316
+ const row = (p) => {
317
+ const meta = info.get(p.rowId) ?? null
318
+ const entry = byRow.get(p.rowId) ?? null
319
+ const moduleName = p.moduleName ?? meta?.moduleName ?? entry?.moduleName ?? null
320
+ const name = moduleName ?? p.rowId ?? '(未知行)'
321
+ return {
322
+ rowId: p.rowId ?? null,
323
+ name,
324
+ shortName: typeof name === 'string' && name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name,
325
+ moduleName,
326
+ version: p.version ?? meta?.version ?? entry?.version ?? null,
327
+ status: p.status ?? 'pending',
328
+ check: p.check ?? null,
329
+ note: p.checkNote ?? null,
330
+ source: p.source ?? null,
331
+ adoptedAt: p.adoptedAt ?? null,
332
+ forcedAt: p.forcedAt ?? null,
333
+ adoptedBy: p.adoptedBy ?? null,
334
+ enabled: entry === null ? null : entry.enabled === true,
335
+ }
336
+ }
337
+ const isPending = (p) => (p.status ?? 'pending') === 'pending'
338
+ const pendingRows = all.filter(isPending).map(row)
339
+ const adoptedRows = all.filter((p) => !isPending(p)).map(row)
340
+ let adoptable = new Map()
341
+ try {
342
+ adoptable = detectAdoptablePending(ports)
343
+ } catch {}
344
+ for (const r of pendingRows) r.adoptable = adoptable.get(r.rowId) ?? null
345
+ return {
346
+ version: rec.frameworkVersion ?? null,
347
+ upgradeFrom: rec.upgradeFrom ?? null,
348
+ quarantineAt: rec.quarantineAt ?? null,
349
+ quarantineLines: Array.isArray(rec.quarantineLines) ? rec.quarantineLines.slice(0, 5) : [],
350
+ pendingCount: pendingRows.length,
351
+ adoptedCount: adoptedRows.length,
352
+ pending: pendingRows,
353
+ adopted: adoptedRows,
354
+ }
355
+ }
356
+
357
+ /** 当前 loader 树里的 rowId → { moduleName, version, enabled }(补 moduleName、对账用)。 */
358
+ function rowIdModuleMap(ports) {
359
+ const out = new Map()
360
+ let entries = []
361
+ try { entries = listEntries(ports) } catch { return out }
362
+ const profileDir = profileDirOf(ports)
363
+ for (const entry of entries) {
364
+ if (typeof entry.rowId !== 'string' || entry.rowId === '') continue
365
+ let version = null
366
+ try {
367
+ const pkgPath = profileDir === null ? null : resolvePackageJson(entry.moduleName, profileDir)
368
+ if (pkgPath !== null) version = JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
369
+ } catch {}
370
+ out.set(entry.rowId, { moduleName: entry.moduleName ?? null, version, enabled: entry.enabled === true })
371
+ }
372
+ return out
373
+ }
374
+
375
+ /**
376
+ * 框架升级适配门:安装/更新完成后自动校验兼容性,
377
+ * 通过(版本已变化 + 未在声明/依赖层面明确不兼容)则移除补丁禁用块并解锁启用。
378
+ */
379
+ async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patchPath, ports }) {
380
+ const pending = readCompatPending()
381
+ if (!pending || !Array.isArray(pending.pending)) return { ran: false }
382
+ const interested = new Set([packageName, ...(syncedNames ?? [])].filter((n) => n !== null))
383
+ const targets = pending.pending.filter((p) => (p.status ?? 'pending') === 'pending' && interested.has(p.moduleName))
384
+ if (targets.length === 0) return { ran: false }
385
+ const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : '?'
386
+ const adopted = []
387
+ const kept = []
388
+ for (const p of targets) {
389
+ let pkg = null
390
+ let pkgDir = null
391
+ try {
392
+ const pkgPath = resolvePackageJson(p.moduleName, profileDir)
393
+ if (pkgPath !== null) {
394
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
395
+ pkgDir = dirname(pkgPath)
396
+ }
397
+ } catch {}
398
+ const version = typeof pkg?.version === 'string' ? pkg.version : null
399
+ const changed = version !== null && version !== p.version
400
+ const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'unknown', reason: '无法读取包信息' }
401
+ if (changed && check.decision !== 'fail') {
402
+ try { await removeDisableBlock(patchPath, p.rowId) } catch {}
403
+ p.status = 'adopted'
404
+ p.adoptedAt = Date.now()
405
+ p.adoptedVersion = version
406
+ p.adoptedFramework = fwVer
407
+ p.check = check.decision
408
+ p.checkNote = check.reason ?? null
409
+ adopted.push({ rowId: p.rowId, moduleName: p.moduleName, version, check: check.decision })
410
+ } else {
411
+ kept.push({ rowId: p.rowId, moduleName: p.moduleName, reason: !changed ? '版本未变化(无更新可适配)' : (check.reason ?? '兼容校验未通过') })
412
+ }
413
+ }
414
+ if (adopted.length > 0 || kept.length > 0) writeCompatPending(pending)
415
+ const note = adopted.length > 0
416
+ ? `兼容适配:已解锁 ${adopted.length} 行(${adopted.map((a) => a.rowId).join('、')})`
417
+ + (kept.length > 0 ? `;仍待适配 ${kept.length} 行(${kept.map((k) => `${k.rowId} ${k.reason}`).join('、')})` : '')
418
+ : `兼容适配:暂未解锁(${kept.map((k) => `${k.rowId} ${k.reason}`).join('、')})`
419
+ return { ran: true, adopted, kept, note }
420
+ }
421
+
422
+ /** AI 赋能框架适配预检:查 registry 最新版声明 + 兼容门清单,给出权威说明。 */
423
+ async function frameworkCompatReportFor(source, ports, profileDir) {
424
+ const fwVer = currentFrameworkVersion(ports)
425
+ const pending = readCompatPending()
426
+ const norm = String(source ?? '').trim()
427
+ const isNpm = norm.startsWith('@') || !norm.includes('/')
428
+ const installed = (() => {
429
+ try {
430
+ const pkgPath = resolvePackageJson(norm, profileDir)
431
+ if (pkgPath === null) return null
432
+ return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
433
+ } catch { return null }
434
+ })()
435
+ const pendHits = (pending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending' && p.moduleName === norm)
436
+ if (!isNpm) {
437
+ return {
438
+ kind: 'repo', frameworkVersion: fwVer, packageName: null, latest: null, check: null,
439
+ pending: pendHits, installedVersion: null,
440
+ summary: pendHits.length > 0
441
+ ? `该来源命中兼容门清单(${pending.frameworkVersion}),GitHub 仓库无法在线预检——更新/部署完成后会由控制台自动校验解锁`
442
+ : `GitHub 仓库来源无法预检 npm 声明,部署后以实际运行为准(框架当前 ${fwVer ?? '未知'})`,
443
+ }
444
+ }
445
+ const name = norm.startsWith('@')
446
+ ? '@' + norm.slice(1).split('@')[0]
447
+ : norm.split('@')[0]
448
+ let latest = null
449
+ let versionMeta = null
450
+ try {
451
+ const data = await fetchJsonUrl(`https://registry.npmmirror.com/${name.replace('/', '%2f')}`)
452
+ latest = data?.['dist-tags']?.latest ?? data?.['dist-tags']?.next ?? null
453
+ if (latest !== null) versionMeta = data?.versions?.[latest] ?? null
454
+ } catch (error) {
455
+ return {
456
+ kind: 'npm', frameworkVersion: fwVer, packageName: name, latest: null, check: null,
457
+ pending: pendHits, installedVersion: installed,
458
+ summary: `registry 预检失败(${error instanceof Error ? error.message : String(error)});本机已装 ${installed ?? '?'}${pendHits.length > 0 ? ',处于兼容门禁用中' : ''}`,
459
+ }
460
+ }
461
+ const check = versionMeta !== null ? checkPluginFrameworkCompat(versionMeta, fwVer) : null
462
+ const parts = []
463
+ if (pendHits.length > 0) parts.push(`已在兼容门清单(${pending.upgradeFrom ?? '?'} → ${pending.frameworkVersion})中强制禁用,当前已装 ${installed ?? pendHits[0]?.version ?? '?'}`)
464
+ if (latest !== null) {
465
+ if (check?.decision === 'fail') parts.push(`最新版 ${latest} 不满足框架 ${fwVer}(${check.reason})——请勿启用,等待适配版本`)
466
+ else if (check?.decision === 'pass') parts.push(`最新版 ${latest} 声明兼容框架 ${fwVer},可放心启用`)
467
+ else parts.push(`最新版 ${latest} 未声明框架兼容(${check?.reason ?? '无声明'}),建议更新后启用并观察`)
468
+ }
469
+ return {
470
+ kind: 'npm', frameworkVersion: fwVer, packageName: name, latest, check, pending: pendHits, installedVersion: installed,
471
+ summary: parts.length > 0 ? parts.join(';') : `框架 ${fwVer},无待适配记录`,
472
+ }
473
+ }
474
+ export { gatingSummary, COMPAT_PENDING_FILE, COMPAT_GATE_FILE, COMPAT_GATE_DEFAULTS, REMOVED_SETTINGS_SYMBOLS, FIBER_STATE, FIBER_PHASE, SUPPORTED_WEB_APP_PATTERN, referencesRemovedSymbol, scanSettingsApiUsage, isFrameworkOwnedPackage, checkPluginFrameworkCompat, probePluginImport, readCompatPending, writeCompatPending, readCompatGate, writeCompatGate, frameworkCheckPromptText, CONSOLE_VERSION, detectCompat, detectAdoptablePending, rowIdModuleMap, frameworkCompatReportFor, maybeAutoAdaptCompat }
@@ -0,0 +1,108 @@
1
+ // L1 · domain —— components.js(服务器组件:注册表 / 增删改 / 进程启停 / 状态 / 自启动;分层 Step 4 从 lib/index.js 搬出,只搬移未改逻辑)
2
+ // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
3
+
4
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
5
+ import { spawn } from 'node:child_process'
6
+ import { dirname } from 'node:path'
7
+ import { homedir } from 'node:os'
8
+ import { execFileAsync, processAlive } from '../infra/exec.js'
9
+ import { componentsFile } from '../infra/paths.js'
10
+
11
+ function findComponents() {
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(componentsFile(), 'utf8'))
14
+ return Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.components) ? parsed.components : [])
15
+ } catch {
16
+ return []
17
+ }
18
+ }
19
+
20
+ function saveComponents(list) {
21
+ mkdirSync(dirname(componentsFile()), { recursive: true })
22
+ writeFileSync(componentsFile(), JSON.stringify(list, null, 2), 'utf8')
23
+ }
24
+
25
+ function compFind(id) {
26
+ return findComponents().find((c) => c.id === id) ?? null
27
+ }
28
+
29
+ /** 组件 UI 地址:显式 uiUrl 优先,否则从 healthUrl 去 /health 或按端口推断。 */
30
+ function compUiUrl(c) {
31
+ if (typeof c?.uiUrl === 'string' && c.uiUrl !== '') return c.uiUrl
32
+ if (typeof c?.healthUrl === 'string' && c.healthUrl !== '') return String(c.healthUrl).replace(/\/health$/u, '')
33
+ if (c?.port) return `http://127.0.0.1:${c.port}/`
34
+ return null
35
+ }
36
+
37
+ function compUpsert(record) {
38
+ const list = findComponents()
39
+ const idx = list.findIndex((c) => c.id === record.id)
40
+ if (idx >= 0) list[idx] = { ...list[idx], ...record }
41
+ else list.push(record)
42
+ saveComponents(list)
43
+ return record.id
44
+ }
45
+
46
+ function compRemove(id) {
47
+ saveComponents(findComponents().filter((c) => c.id !== id))
48
+ }
49
+
50
+ /** 组件控制:启动。 */
51
+ async function compStart(id) {
52
+ const record = compFind(id)
53
+ if (!record) throw new Error('组件不存在')
54
+ if (record.pid && processAlive(record.pid)) return { started: false, note: '已在运行' }
55
+ if (!record.file || !Array.isArray(record.args)) throw new Error('该组件没有记录启动命令')
56
+ const child = spawn(record.file, record.args, { detached: true, stdio: 'ignore', windowsHide: true, cwd: record.cwd ?? homedir(), shell: false })
57
+ child.unref()
58
+ compUpsert({ id, pid: child.pid })
59
+ return { started: true, pid: child.pid }
60
+ }
61
+
62
+ /** 组件控制:停止。 */
63
+ async function compStop(id) {
64
+ const record = compFind(id)
65
+ if (!record) throw new Error('组件不存在')
66
+ if (record.pid) {
67
+ try {
68
+ await execFileAsync('taskkill.exe', ['/PID', String(record.pid), '/F'], { timeout: 30000, windowsHide: true })
69
+ } catch {}
70
+ }
71
+ compUpsert({ id, pid: null })
72
+ return { stopped: true }
73
+ }
74
+
75
+ /** 组件控制:状态(进程存活 + 健康探测;无 pid 但健康通过按运行中处理)。 */
76
+ async function compStatus(id) {
77
+ const record = compFind(id)
78
+ if (!record) throw new Error('组件不存在')
79
+ let healthy = null
80
+ if (record.healthUrl) {
81
+ try {
82
+ const res = await fetch(record.healthUrl, { signal: AbortSignal.timeout(2500) })
83
+ healthy = res.ok
84
+ } catch {
85
+ healthy = false
86
+ }
87
+ }
88
+ const running = Boolean(record.pid && processAlive(record.pid)) || healthy === true
89
+ return { id, name: record.name, running, healthy, pid: record.pid ?? null, port: record.port ?? null, autoStart: record.autoStart === true }
90
+ }
91
+
92
+ /** DSH 启动时自动拉起标记 autoStart 的组件(幂等:已健康/已运行则跳过)。 */
93
+ async function autostartComponents() {
94
+ for (const c of findComponents()) {
95
+ if (c.autoStart !== true) continue
96
+ if (c.healthUrl) {
97
+ try {
98
+ const res = await fetch(c.healthUrl, { signal: AbortSignal.timeout(2500) })
99
+ if (res.ok) continue
100
+ } catch {}
101
+ } else if (c.pid && processAlive(c.pid)) {
102
+ continue
103
+ }
104
+ try { await compStart(c.id) } catch {}
105
+ }
106
+ }
107
+
108
+ export { findComponents, saveComponents, compFind, compUiUrl, compUpsert, compRemove, compStart, compStop, compStatus, autostartComponents }