@noob-stupid/dsh-plugin-console 0.3.66 → 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.
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +127 -32
- package/lib/index.js +60 -9687
- package/lib/server/domain/ai-run.js +479 -0
- package/lib/server/domain/ai.js +246 -0
- package/lib/server/domain/compat.js +474 -0
- package/lib/server/domain/components.js +108 -0
- package/lib/server/domain/dep-source.js +122 -0
- package/lib/server/domain/format-contract.js +265 -0
- package/lib/server/domain/format-scan.js +431 -0
- package/lib/server/domain/framework.js +393 -0
- package/lib/server/domain/install-job.js +561 -0
- package/lib/server/domain/install.js +599 -0
- package/lib/server/domain/jobs.js +28 -0
- package/lib/server/domain/market.js +409 -0
- package/lib/server/domain/patch.js +203 -0
- package/lib/server/domain/presets.js +93 -0
- package/lib/server/domain/quarantine.js +224 -0
- package/lib/server/domain/release-source.js +504 -0
- package/lib/server/domain/repoland.js +119 -0
- package/lib/server/domain/revoke.js +184 -0
- package/lib/server/domain/runtime.js +118 -0
- package/lib/server/domain/selfupdate.js +319 -0
- package/lib/server/domain/skills.js +234 -0
- package/lib/server/domain/sources.js +297 -0
- package/lib/server/domain/suite.js +220 -0
- package/lib/server/infra/exec.js +98 -0
- package/lib/server/infra/fsx.js +163 -0
- package/lib/server/infra/fw-integrity-check.js +37 -0
- package/lib/server/infra/http.js +373 -0
- package/lib/server/infra/httpd.js +51 -0
- package/lib/server/infra/mask.js +19 -0
- package/lib/server/infra/paths.js +177 -0
- package/lib/server/infra/semver.js +168 -0
- package/lib/server/routes/ai.js +172 -0
- package/lib/server/routes/components.js +254 -0
- package/lib/server/routes/framework-preflight.js +154 -0
- package/lib/server/routes/framework-upgrade.js +679 -0
- package/lib/server/routes/framework.js +544 -0
- package/lib/server/routes/github-login.js +198 -0
- package/lib/server/routes/index.js +128 -0
- package/lib/server/routes/install.js +116 -0
- package/lib/server/routes/market.js +415 -0
- package/lib/server/routes/plugins.js +562 -0
- package/lib/server/routes/skills.js +107 -0
- package/lib/server/routes/sources.js +437 -0
- package/lib/server/routes/state.js +125 -0
- package/lib/server/state.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// L1 · domain —— framework.js(升级三件套的可搬部分:状态/备份路径、dsh 可执行文件定位、app boot 定位、容忍补丁、版本比较、重启前奏;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑。注:detectFrameworkUpgrade / backupProfileSnapshot / currentFrameworkVersion 吃运行上下文,留到 Step 8)
|
|
2
|
+
// 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, copyFileSync } from 'node:fs'
|
|
5
|
+
import { execFile } from 'node:child_process'
|
|
6
|
+
import { dirname, join, basename, resolve } from 'node:path'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { createRequire } from 'node:module'
|
|
9
|
+
import { checkPluginFrameworkCompat, isFrameworkOwnedPackage, readCompatGate, readCompatPending, writeCompatPending } from './compat.js'
|
|
10
|
+
import { CORE_PATCH_ROW_IDS, disableEntry } from './patch.js'
|
|
11
|
+
import { listEntries } from './runtime.js'
|
|
12
|
+
import { copyTree } from '../infra/fsx.js'
|
|
13
|
+
import { dshHome, entryPkgMeta, findPatchPath, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
|
|
14
|
+
import { frameworkUpgradeCandidates, isFrameworkVersionNewer, parseFrameworkVersion } from '../infra/semver.js'
|
|
15
|
+
|
|
16
|
+
/** 解析 dsh CLI 的 bin.js 绝对路径(守护拉起用)。 */
|
|
17
|
+
function resolveDshBin() {
|
|
18
|
+
try {
|
|
19
|
+
const requireLocal = createRequire(join(pluginRoot(), 'package.json'))
|
|
20
|
+
return join(dirname(requireLocal.resolve('@deepseek-ai/dsh/package.json')), 'lib', 'bin.js')
|
|
21
|
+
} catch {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const FRAMEWORK_STATE_FILE = () => join(dshHome(), 'plugin-console', 'framework-state.json')
|
|
27
|
+
|
|
28
|
+
const FRAMEWORK_BACKUP_ROOT = () => join(dshHome(), 'plugin-console', 'framework-backups')
|
|
29
|
+
|
|
30
|
+
/** 定位 @deepseek-ai/dsh-app-boot(与 @deepseek-ai/dsh 同级)。 */
|
|
31
|
+
function locateAppBootFile(baseUrl) {
|
|
32
|
+
try {
|
|
33
|
+
const require = createRequire(baseUrl)
|
|
34
|
+
const dshPkg = require.resolve('@deepseek-ai/dsh/package.json')
|
|
35
|
+
const candidate = join(dirname(dshPkg), 'dsh-app-boot', 'lib', 'index.js')
|
|
36
|
+
if (existsSync(candidate)) return candidate
|
|
37
|
+
} catch {}
|
|
38
|
+
const cacheRoots = [
|
|
39
|
+
process.env.NODE_CACHE || '',
|
|
40
|
+
'D:\\node_cache\\_npx',
|
|
41
|
+
join(homedir(), '.npm', '_npx'),
|
|
42
|
+
join(process.env.LOCALAPPDATA || '', 'node_cache', '_npx'),
|
|
43
|
+
].filter(Boolean)
|
|
44
|
+
for (const root of cacheRoots) {
|
|
45
|
+
if (!existsSync(root)) continue
|
|
46
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
47
|
+
if (!entry.isDirectory()) continue
|
|
48
|
+
const candidate = join(root, entry.name, 'node_modules', '@deepseek-ai', 'dsh-app-boot', 'lib', 'index.js')
|
|
49
|
+
if (existsSync(candidate)) return candidate
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 内联框架补丁(dsh-app-boot parsePatchList 容错,issue #5)——DSH 升级后框架文件被覆盖,需重打。 */
|
|
56
|
+
function applyFrameworkTolerancePatchOnce(baseUrl) {
|
|
57
|
+
const target = locateAppBootFile(baseUrl)
|
|
58
|
+
if (!target) return { applied: false, reason: 'dsh-app-boot 未找到' }
|
|
59
|
+
let source = ''
|
|
60
|
+
try { source = readFileSync(target, 'utf8') } catch (error) { return { applied: false, reason: `读取失败:${error.message}` } }
|
|
61
|
+
if (source.includes('tryDropEmptyArrayPlaceholder')) return { applied: false, reason: '已打过补丁' }
|
|
62
|
+
if (!source.includes('function parsePatchList')) return { applied: false, reason: 'parsePatchList 不存在(框架版本可能已变更)' }
|
|
63
|
+
const OLD = 'function parsePatchList(binName, file, content, label) {\n\tlet parsed;\n\ttry {\n\t\tparsed = yaml.load(content, { schema: userPatchesSchema });\n\t} catch (error) {\n\t\tthrow new Error(`${binName}: failed to parse ${label} ${file}: ${String(error)}`);\n\t}'
|
|
64
|
+
const NEW = 'function parsePatchList(binName, file, content, label) {\n\tlet parsed;\n\ttry {\n\t\tparsed = yaml.load(content, { schema: userPatchesSchema });\n\t} catch (error) {\n\t\tconst retried = tryDropEmptyArrayPlaceholder(content);\n\t\tif (retried !== null) {\n\t\t\ttry {\n\t\t\t\tparsed = yaml.load(retried, { schema: userPatchesSchema });\n\t\t\t} catch {\n\t\t\t\tthrow new Error(`${binName}: failed to parse ${label} ${file}: ${String(error)}`);\n\t\t\t}\n\t\t} else {\n\t\t\tthrow new Error(`${binName}: failed to parse ${label} ${file}: ${String(error)}`);\n\t\t}\n\t}'
|
|
65
|
+
const HELPER = '\n/**\n * 容错辅助(issue #5):若文件含顶格空数组占位行(`[]` / `[ ]`,可带行尾注释),视为 no-op 移除。\n */\nfunction tryDropEmptyArrayPlaceholder(content) {\n\tconst lines = String(content).split("\\n");\n\tconst kept = [];\n\tlet dropped = false;\n\tfor (const line of lines) {\n\t\tif (/^\\[\\s*\\]\\s*(?:#.*)?$/u.test(line)) {\n\t\t\tdropped = true;\n\t\t\tcontinue;\n\t\t}\n\t\tkept.push(line);\n\t}\n\tif (!dropped) return null;\n\treturn kept.join("\\n");\n}\n'
|
|
66
|
+
try {
|
|
67
|
+
copyFileSync(target, `${target}.bak-issue5`)
|
|
68
|
+
const next = source.replace(OLD, NEW) + HELPER
|
|
69
|
+
writeFileSync(target, next, 'utf8')
|
|
70
|
+
return { applied: true, target }
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return { applied: false, reason: `应用失败:${error.message}` }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 清理残留的框架升级/回滚/重启计划任务(脚本被强杀时它来不及自删)。
|
|
77
|
+
* v0.3.43:把「重启」与「重启守护」任务也纳入——2026-09-11 现场残留了 5 个 Ready 僵尸任务
|
|
78
|
+
* (DSH-Restart-13804 / -31688 / -3744 / RestartV2 / RestartV3),正是"重启后没人拉起"的证据。
|
|
79
|
+
* 只在服务已经起来了的时候清理是安全的:服务在跑 ⇒ 守护任务无事可做(它自己也会立刻收工)。 */
|
|
80
|
+
function cleanupStaleFwTasks() {
|
|
81
|
+
try {
|
|
82
|
+
execFile('schtasks', ['/query', '/fo', 'CSV', '/nh'], { windowsHide: true, timeout: 20000 }, (error, stdout) => {
|
|
83
|
+
if (error) return
|
|
84
|
+
const names = String(stdout).split(/\r?\n/u)
|
|
85
|
+
.map((line) => (line.match(/^"([^"]*)"/u)?.[1] ?? '').trim())
|
|
86
|
+
.filter((name) => /^\\?DSH-(?:FW-(?:Upgrade|Rollback)|Restart(?:V\d+)?|RestartGuard)-\d+$/u.test(name))
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
execFile('schtasks', ['/delete', '/f', '/tn', name], { windowsHide: true, timeout: 20000 }, () => {})
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 定位框架安装根(顶层 node_modules):优先运行进程入口,其次从包目录上溯找含 .pnpm 的 node_modules。 */
|
|
95
|
+
function resolveFrameworkRootNodeModules(fromDir) {
|
|
96
|
+
try {
|
|
97
|
+
const entry = process.argv[1]
|
|
98
|
+
if (typeof entry === 'string' && /bin\.js$/u.test(entry)) {
|
|
99
|
+
const nm = dirname(dirname(entry))
|
|
100
|
+
if (existsSync(join(nm, '.pnpm')) && existsSync(join(nm, '@deepseek-ai'))) return nm
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
let dir = typeof fromDir === 'string' && fromDir !== '' ? fromDir : null
|
|
104
|
+
for (let i = 0; i < 24 && dir !== null; i += 1) {
|
|
105
|
+
if (basename(dir) === 'node_modules' && existsSync(join(dir, '.pnpm'))) return dir
|
|
106
|
+
const parent = dirname(dir)
|
|
107
|
+
if (parent === dir) break
|
|
108
|
+
dir = parent
|
|
109
|
+
}
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 框架全树 checkpoint(可靠回滚点):镜像 .pnpm 中所有 @deepseek-ai 条目「自包」内容 +
|
|
115
|
+
* 顶层 @deepseek-ai scope + lock.yaml。只镜像自包、不跟随依赖 junction,避免重复拷贝;
|
|
116
|
+
* 恢复时按同路径写回 .pnpm 条目即可让整个依赖世界回到升级前。
|
|
117
|
+
*/
|
|
118
|
+
function checkpointFrameworkTree(fwRoot, destRoot) {
|
|
119
|
+
const pnpmRoot = join(fwRoot, '.pnpm')
|
|
120
|
+
const dest = join(destRoot, 'fw-tree', String(Date.now()))
|
|
121
|
+
mkdirSync(dest, { recursive: true })
|
|
122
|
+
let mirrored = 0
|
|
123
|
+
const selfNameOf = (entryName) => entryName.slice('@deepseek-ai+'.length).split('@')[0]
|
|
124
|
+
for (const entry of readdirSync(pnpmRoot, { withFileTypes: true })) {
|
|
125
|
+
if (!entry.isDirectory() || !entry.name.startsWith('@deepseek-ai+')) continue
|
|
126
|
+
const name = selfNameOf(entry.name)
|
|
127
|
+
const selfDir = join(pnpmRoot, entry.name, 'node_modules', '@deepseek-ai', name)
|
|
128
|
+
if (!existsSync(join(selfDir, 'package.json'))) continue
|
|
129
|
+
copyTree(selfDir, join(dest, '.pnpm', entry.name, 'node_modules', '@deepseek-ai', name))
|
|
130
|
+
mirrored += 1
|
|
131
|
+
}
|
|
132
|
+
const topScope = join(fwRoot, '@deepseek-ai')
|
|
133
|
+
if (existsSync(topScope)) copyTree(topScope, join(dest, 'top-@deepseek-ai'))
|
|
134
|
+
try { copyFileSync(join(pnpmRoot, 'lock.yaml'), join(dest, 'lock.yaml')) } catch {}
|
|
135
|
+
try { copyFileSync(join(fwRoot, 'package.json'), join(dest, 'fw-package.json')) } catch {}
|
|
136
|
+
return { dest, mirrored }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 生成「拉起 DSH 服务」的 PowerShell 前导块(升级脚本 / 回滚脚本共用,v0.3.37 事故修复)。
|
|
141
|
+
*
|
|
142
|
+
* 2026-09-11 事故:脚本在「重启服务」这一步崩溃,服务没人拉起(框架其实已经升级成功,
|
|
143
|
+
* 界面却显示全红)。根因是一条**静默的 null**:
|
|
144
|
+
* $binNow = ''; try { $binNow = (& node -e "…require.resolve…" | Select-Object -Last 1) } catch {}
|
|
145
|
+
* if ($binNow -ne '' -and (Test-Path $binNow)) { … }
|
|
146
|
+
* 当 node 解析那一瞬间失败(新版链接尚未就绪等)时输出为空 → `Select-Object -Last 1` 让
|
|
147
|
+
* `$binNow` 变成 **$null**,而 PowerShell 里 `$null -ne ''` 是 **true**(守卫失效)→
|
|
148
|
+
* `Test-Path $null` 抛「无法将参数绑定到参数"Path",因为该参数是空值」→ 脚本当场终止。
|
|
149
|
+
* 同一段代码原先被复制了 5 份,所以这个坑反复出现。
|
|
150
|
+
*
|
|
151
|
+
* 现在:只保留这一份实现,并且
|
|
152
|
+
* 1) 返回值**永不为 $null**(非字符串一律归一成空串,再做 IsNullOrWhiteSpace 判断);
|
|
153
|
+
* 2) 解析走**多级回退**(不再假设某一处路径一定可用):node resolve → 目标版本的 .pnpm 实体
|
|
154
|
+
* 目录 → 顶层可见链接 → .pnpm 里最新的一个;
|
|
155
|
+
* 3) 全路径参数一律 `Test-Path -LiteralPath`,失败只记录、不抛错。
|
|
156
|
+
*/
|
|
157
|
+
function relaunchPrelude({ nodePath, pluginDir, fwRoot, target, ps }) {
|
|
158
|
+
const probe = "const path=require('path');const p=require.resolve('@deepseek-ai/dsh/package.json',{paths:[process.env.DSH_RESOLVE_ROOT]});console.log(path.join(path.dirname(p),'lib','bin.js'))"
|
|
159
|
+
const pnpmBin = (dirExpr) => `Join-Path ${dirExpr} 'node_modules\\@deepseek-ai\\dsh\\lib\\bin.js'`
|
|
160
|
+
return [
|
|
161
|
+
`$launchLog = ''`,
|
|
162
|
+
// 心跳(v0.3.39):脚本每推进一小步就更新一次心跳文件的时间戳。服务端据此区分
|
|
163
|
+
// 「脚本还在干活」与「脚本进程被系统/启动器杀掉」。2026-09-11 真机事故:回滚脚本
|
|
164
|
+
// 干完活之后被 Ctrl+C 类事件结束(计划任务 Last Result = 0xC000013A),终态没写成,
|
|
165
|
+
// 界面就永远卡在「回滚中…」——有心跳就能判定「脚本已死 + 现实是什么」。
|
|
166
|
+
`$hb = $state + '.hb'`,
|
|
167
|
+
`function Beat { try { Set-Content -Path $hb -Value ([string](Get-Date).Ticks) -Encoding UTF8 } catch {} }`,
|
|
168
|
+
`try { Beat } catch {}`,
|
|
169
|
+
`try { if ($log) { $launchLog = Join-Path (Split-Path -LiteralPath $log) 'fw-relaunch.log' } } catch {}`,
|
|
170
|
+
`if ([string]::IsNullOrWhiteSpace($launchLog)) { $launchLog = Join-Path $env:TEMP 'fw-relaunch.log' }`,
|
|
171
|
+
`function Resolve-DshBin {`,
|
|
172
|
+
` $cand = ''`,
|
|
173
|
+
` try { $env:DSH_RESOLVE_ROOT = ${ps(pluginDir)}; $cand = (& ${ps(nodePath)} -e "${probe}" 2>$null | Select-Object -Last 1) } catch { $cand = '' }`,
|
|
174
|
+
` if ($cand -isnot [string]) { $cand = '' }`,
|
|
175
|
+
` $cand = ([string]$cand).Trim()`,
|
|
176
|
+
` if ($cand -ne '' -and (Test-Path -LiteralPath $cand)) { return $cand }`,
|
|
177
|
+
` try { foreach ($d in @(Get-ChildItem -Path (Join-Path ${ps(fwRoot)} '.pnpm') -Directory -Filter '@deepseek-ai+dsh@${target}*' -ErrorAction SilentlyContinue)) { $c = ${pnpmBin('$d.FullName')}; if (Test-Path -LiteralPath $c) { return $c } } } catch {}`,
|
|
178
|
+
` try { $c = ${ps(join(fwRoot, '@deepseek-ai', 'dsh', 'lib', 'bin.js'))}; if (Test-Path -LiteralPath $c) { return $c } } catch {}`,
|
|
179
|
+
` try { foreach ($d in @(Get-ChildItem -Path (Join-Path ${ps(fwRoot)} '.pnpm') -Directory -Filter '@deepseek-ai+dsh@*' -ErrorAction SilentlyContinue | Sort-Object Name -Descending)) { $c = ${pnpmBin('$d.FullName')}; if (Test-Path -LiteralPath $c) { return $c } } } catch {}`,
|
|
180
|
+
` return ''`,
|
|
181
|
+
`}`,
|
|
182
|
+
`function Invoke-DshRelaunch($tag) {`,
|
|
183
|
+
` $bin = Resolve-DshBin`,
|
|
184
|
+
` if ([string]::IsNullOrWhiteSpace($bin)) { Log ('拉起失败(' + $tag + '):node resolve / .pnpm / 顶层链接 三种方式都找不到 dsh 的 bin.js,请手动启动 DSH'); return $false }`,
|
|
185
|
+
` try { Add-Content -Path $launchLog -Value ((Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' 拉起(' + $tag + '): ' + $bin) -Encoding UTF8 } catch {}`,
|
|
186
|
+
` try { Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', ('"' + ${ps(nodePath)} + '" "' + $bin + '" web >> "' + $launchLog + '" 2>&1')) -WindowStyle Hidden } catch { Log ('拉起进程启动失败(' + $tag + '):' + $_.Exception.Message); return $false }`,
|
|
187
|
+
` Log ('已发起拉起服务(' + $tag + ',输出见 fw-relaunch.log)')`,
|
|
188
|
+
` return $true`,
|
|
189
|
+
`}`,
|
|
190
|
+
].join('\r\n')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 拉 registry 元数据 → 解出「可选升级版本列表 + 本次目标」(2026-09-24 从 framework-upgrade 路由收进来:
|
|
195
|
+
* 那段逻辑 13 行,而路由文件贴着架构守卫上限;收进 domain 后路由只剩一行调用,也更方便被单测覆盖)。
|
|
196
|
+
*
|
|
197
|
+
* @param {object} p
|
|
198
|
+
* @param {string|null} p.current 当前框架版本
|
|
199
|
+
* @param {string} p.wanted 用户显式选择的目标版本(空串 = 没选)
|
|
200
|
+
* @param {(url: string) => Promise<any>} p.fetchJson 取 JSON 的实现(由路由传入,domain 不直接碰网络层)
|
|
201
|
+
* @returns {Promise<{ latest, next, alpha, versions, tagDefault, target, rejected, registryError }>}
|
|
202
|
+
*/
|
|
203
|
+
async function resolveFrameworkUpgradePlan({ current, wanted = '', fetchJson }) {
|
|
204
|
+
let latest = null
|
|
205
|
+
let next = null
|
|
206
|
+
let alpha = null
|
|
207
|
+
let versions = []
|
|
208
|
+
let tagDefault = null
|
|
209
|
+
let registryError = null
|
|
210
|
+
try {
|
|
211
|
+
const data = await fetchJson('https://registry.npmmirror.com/@deepseek-ai%2fdsh')
|
|
212
|
+
latest = data?.['dist-tags']?.latest ?? null
|
|
213
|
+
next = data?.['dist-tags']?.next ?? null
|
|
214
|
+
alpha = data?.['dist-tags']?.alpha ?? null
|
|
215
|
+
const cand = frameworkUpgradeCandidates(data, current)
|
|
216
|
+
versions = cand.versions
|
|
217
|
+
tagDefault = cand.tagDefault
|
|
218
|
+
} catch (error) {
|
|
219
|
+
// 网络黑洞/超时:区分「检测失败」与「无更新」,避免误导用户以为已是最新
|
|
220
|
+
registryError = error instanceof Error ? error.message : String(error)
|
|
221
|
+
}
|
|
222
|
+
const picked = pickFrameworkTarget({ current, latest, next, wanted, candidates: versions })
|
|
223
|
+
return { latest, next, alpha, versions, tagDefault, target: picked.target, rejected: picked.rejected, registryError }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 解析"这次要升到哪个版本"(2026-09-23 抽出:原先 framework-check 与 framework-upgrade 各写了一份,
|
|
228
|
+
* 加了「用户自选版本」后两份逻辑必须完全一致,索性收成一处)。
|
|
229
|
+
*
|
|
230
|
+
* @param {object} p
|
|
231
|
+
* @param {string|null} p.current 当前框架版本
|
|
232
|
+
* @param {string|null} p.latest dist-tags.latest
|
|
233
|
+
* @param {string|null} p.next dist-tags.next
|
|
234
|
+
* @param {string} [p.wanted] 用户在面板里显式选的版本(空串 = 没选)
|
|
235
|
+
* @param {Array<{version: string}>} [p.candidates] 可选候选列表(有 wanted 时必须提供,用作白名单校验)
|
|
236
|
+
* @returns {{ target: string|null, rejected: string|null }} rejected 非空 = 用户选的版本非法
|
|
237
|
+
*/
|
|
238
|
+
function pickFrameworkTarget({ current, latest, next, wanted = '', candidates = [] }) {
|
|
239
|
+
const want = typeof wanted === 'string' ? wanted.trim() : ''
|
|
240
|
+
if (want !== '') {
|
|
241
|
+
const hit = candidates.find((v) => v.version === want)
|
|
242
|
+
return hit === undefined ? { target: null, rejected: want } : { target: hit.version, rejected: null }
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
// 稳定版 latest 优先;latest 不高于当前而 next(预发布渠道)确实更新时,目标取 next。
|
|
246
|
+
// 必须用版本号比较而不是字符串不等(避免 current=0.1.1 稳定版时被 next=0.1.1-rc.3 反向降级)。
|
|
247
|
+
target: latest !== null && current !== null && isFrameworkVersionNewer(latest, current)
|
|
248
|
+
? latest
|
|
249
|
+
: (next !== null && current !== null && isFrameworkVersionNewer(next, current) ? next : null),
|
|
250
|
+
rejected: null,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 目标版本是否 ≥ since(只比 major.minor.patch,忽略 rc 段——0.1.5-rc.1 也算已达 0.1.5 变更)。 */
|
|
255
|
+
function isVersionAtLeast(target, since) {
|
|
256
|
+
const t = parseFrameworkVersion(target)
|
|
257
|
+
const s = parseFrameworkVersion(since)
|
|
258
|
+
if (t === -1 || s === -1) return false
|
|
259
|
+
if (t.maj !== s.maj) return t.maj > s.maj
|
|
260
|
+
if (t.min !== s.min) return t.min > s.min
|
|
261
|
+
return t.pat >= s.pat
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 备份当前 profile 配置快照(按版本目录;框架升级后旧版本目录即升级前配置)。 */
|
|
265
|
+
function backupProfileSnapshot(profileDir, version, ports) {
|
|
266
|
+
const dir = join(FRAMEWORK_BACKUP_ROOT(), version)
|
|
267
|
+
mkdirSync(dir, { recursive: true })
|
|
268
|
+
try { copyFileSync(join(profileDir, 'cordis.patch.yml'), join(dir, 'cordis.patch.yml')) } catch {}
|
|
269
|
+
try { copyFileSync(join(profileDir, 'package.json'), join(dir, 'profile-package.json')) } catch {}
|
|
270
|
+
try {
|
|
271
|
+
const plugins = listEntries(ports).map((e) => {
|
|
272
|
+
const meta = entryPkgMeta(e.moduleName, ports.baseUrl ?? 'file:///', profileDirOf(ports))
|
|
273
|
+
return { rowId: e.rowId, moduleName: e.moduleName, enabled: e.enabled, version: meta?.version ?? null, installDate: meta?.installDate ?? null }
|
|
274
|
+
})
|
|
275
|
+
writeFileSync(join(dir, 'plugins.json'), JSON.stringify(plugins, null, 2), 'utf8')
|
|
276
|
+
} catch {}
|
|
277
|
+
return dir
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 框架升级检测与适配:记录版本 → 每次启动备份配置快照 → 升级/首次时重打框架补丁。 */
|
|
281
|
+
function detectFrameworkUpgrade(ports) {
|
|
282
|
+
let current = null
|
|
283
|
+
try {
|
|
284
|
+
const require = createRequire(ports.baseUrl ?? 'file:///')
|
|
285
|
+
const dshPkg = JSON.parse(readFileSync(require.resolve('@deepseek-ai/dsh/package.json'), 'utf8'))
|
|
286
|
+
current = dshPkg.version ?? null
|
|
287
|
+
} catch {}
|
|
288
|
+
const statePath = FRAMEWORK_STATE_FILE()
|
|
289
|
+
let prev = null
|
|
290
|
+
try { prev = JSON.parse(readFileSync(statePath, 'utf8')) } catch {}
|
|
291
|
+
const upgraded = prev !== null && prev.lastVersion !== null && current !== null && prev.lastVersion !== current
|
|
292
|
+
const result = { version: current, upgraded, from: prev?.lastVersion ?? null, backupDir: null, patchApplied: false, patchNote: null }
|
|
293
|
+
try {
|
|
294
|
+
mkdirSync(dirname(statePath), { recursive: true })
|
|
295
|
+
const profileDir = dirname(findPatchPath(ports))
|
|
296
|
+
if (current !== null) result.backupDir = backupProfileSnapshot(profileDir, current, ports)
|
|
297
|
+
const patch = applyFrameworkTolerancePatchOnce(ports.baseUrl ?? 'file:///')
|
|
298
|
+
result.patchApplied = patch.applied
|
|
299
|
+
result.patchNote = patch.reason ?? null
|
|
300
|
+
writeFileSync(statePath, JSON.stringify({ lastVersion: current, backupAt: Date.now() }), 'utf8')
|
|
301
|
+
} catch {}
|
|
302
|
+
return result
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 当前运行框架版本(@deepseek-ai/dsh package.json)。 */
|
|
306
|
+
function currentFrameworkVersion(ports) {
|
|
307
|
+
try {
|
|
308
|
+
const require = createRequire(ports?.baseUrl ?? 'file:///')
|
|
309
|
+
const pkgPath = require.resolve('@deepseek-ai/dsh/package.json')
|
|
310
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
311
|
+
return typeof pkg.version === 'string' ? pkg.version : null
|
|
312
|
+
} catch {
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 框架升级前置门禁(用户硬要求:「升级后所有不适配的必须先禁用」)。
|
|
318
|
+
* 在升级脚本执行**之前**扫描全部可开关行,对目标框架版本判定为 fail 的行就地写 disabled:true,
|
|
319
|
+
* 并记入 compat-pending(UI 显示「待适配」,更新后一键解锁)。这样新框架 boot 时不会因为
|
|
320
|
+
* 某行 import 失败而整树崩溃(loader 单行失败 = 服务起不来)。
|
|
321
|
+
* 受保护行/核心行/自身永不禁用——禁它们本身就会让服务起不来。
|
|
322
|
+
* 返回 { disabled:[{rowId,moduleName,version,reason}], skipped:[{rowId,reason}] } */
|
|
323
|
+
async function preflightDisableIncompatible({ ports, profileDir, patchPath, targetVersion }) {
|
|
324
|
+
const disabled = []
|
|
325
|
+
const skipped = []
|
|
326
|
+
const gate = readCompatGate()
|
|
327
|
+
if (typeof targetVersion !== 'string' || targetVersion === '') return { disabled, skipped }
|
|
328
|
+
let entries = []
|
|
329
|
+
try { entries = listEntries(ports) } catch { return { disabled, skipped } }
|
|
330
|
+
let pending = readCompatPending()
|
|
331
|
+
if (pending === null || !Array.isArray(pending.pending)) pending = { frameworkVersion: targetVersion, upgradeFrom: null, pending: [] }
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
if (typeof entry.rowId !== 'string' || entry.rowId === '') continue
|
|
334
|
+
if (!entry.enabled) continue
|
|
335
|
+
if (entry.rowId === 'plugin-console') continue // 控制台自己永不禁用
|
|
336
|
+
if (!entry.toggleable || CORE_PATCH_ROW_IDS.has(entry.rowId)) {
|
|
337
|
+
skipped.push({ rowId: entry.rowId, reason: '受保护/核心行(禁用会让服务起不来,改由启动失败隔离兜底)' })
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
let pkg = null
|
|
341
|
+
let pkgDir = null
|
|
342
|
+
try {
|
|
343
|
+
const pkgPath = resolvePackageJson(entry.moduleName, profileDir)
|
|
344
|
+
if (pkgPath !== null) { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); pkgDir = dirname(pkgPath) }
|
|
345
|
+
} catch {}
|
|
346
|
+
if (pkg === null) {
|
|
347
|
+
skipped.push({ rowId: entry.rowId, reason: '无法读取包信息(保持启用,改由启动失败隔离兜底)' })
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
// v0.3.35:框架自带包永不自动禁用(真机演练抓到:框架自己的 settings 控制器被误判成不适配)。
|
|
351
|
+
// 它们与框架同源安装,禁用不是正确处置——正确处置是回滚;误判则直接砍掉框架功能。
|
|
352
|
+
if (isFrameworkOwnedPackage(pkgDir, profileDir)) {
|
|
353
|
+
skipped.push({ rowId: entry.rowId, reason: '框架自带包(与框架同源安装,禁用不是正确处置,改由回滚兜底)' })
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
let check = { decision: 'unknown', reason: '' }
|
|
357
|
+
try { check = checkPluginFrameworkCompat(pkg, targetVersion, pkgDir) } catch (error) { check = { decision: 'unknown', reason: error instanceof Error ? error.message : String(error) } }
|
|
358
|
+
if (check.decision !== 'fail') continue // pass / unknown 一律不禁用(避免过度禁用把功能砍掉)
|
|
359
|
+
if (gate.autoDisable !== true) {
|
|
360
|
+
// 总开关关闭:只报告不动开关(用户定案:自动行为必须可关)
|
|
361
|
+
skipped.push({ rowId: entry.rowId, reason: `判定不适配(${check.reason ?? '不兼容'}),但「升级时自动禁用」已关闭——保持启用,请手动处理` })
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await disableEntry(patchPath, entry.rowId)
|
|
366
|
+
} catch (error) {
|
|
367
|
+
skipped.push({ rowId: entry.rowId, reason: `禁用写入失败:${error instanceof Error ? error.message : String(error)}` })
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
const version = typeof pkg.version === 'string' ? pkg.version : null
|
|
371
|
+
disabled.push({ rowId: entry.rowId, moduleName: entry.moduleName, version, reason: check.reason ?? null })
|
|
372
|
+
const record = {
|
|
373
|
+
rowId: entry.rowId,
|
|
374
|
+
moduleName: entry.moduleName,
|
|
375
|
+
version,
|
|
376
|
+
status: 'pending',
|
|
377
|
+
check: 'fail',
|
|
378
|
+
checkNote: check.reason ?? null,
|
|
379
|
+
forcedAt: Date.now(),
|
|
380
|
+
source: 'preflight-disabled-before-upgrade',
|
|
381
|
+
}
|
|
382
|
+
const at = pending.pending.findIndex((p) => p.rowId === entry.rowId)
|
|
383
|
+
if (at >= 0) pending.pending[at] = { ...pending.pending[at], ...record }
|
|
384
|
+
else pending.pending.push(record)
|
|
385
|
+
}
|
|
386
|
+
if (disabled.length > 0) {
|
|
387
|
+
pending.frameworkVersion = targetVersion
|
|
388
|
+
pending.updatedAt = new Date().toISOString()
|
|
389
|
+
writeCompatPending(pending)
|
|
390
|
+
}
|
|
391
|
+
return { disabled, skipped }
|
|
392
|
+
}
|
|
393
|
+
export { FRAMEWORK_STATE_FILE, FRAMEWORK_BACKUP_ROOT, cleanupStaleFwTasks, resolveDshBin, locateAppBootFile, applyFrameworkTolerancePatchOnce, resolveFrameworkRootNodeModules, checkpointFrameworkTree, relaunchPrelude, isVersionAtLeast, pickFrameworkTarget, resolveFrameworkUpgradePlan, currentFrameworkVersion, backupProfileSnapshot, detectFrameworkUpgrade, preflightDisableIncompatible }
|