@noob-stupid/dsh-plugin-console 0.3.67 → 0.4.1
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 +126 -31
- 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 +434 -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 +160 -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,679 @@
|
|
|
1
|
+
// L2 · routes —— 框架升级(POST /framework-upgrade)
|
|
2
|
+
// 分层 Step 8b:从 lib/index.js 的 handle() 原样搬出(只搬移未改逻辑)。
|
|
3
|
+
// 注:这个 handler 661 行(内置大段 PowerShell 脚本模板),是守卫「单文件 ≤ 600 行」的受限例外(见守卫 SIZE_EXCEPTIONS);
|
|
4
|
+
// 待抽包阶段把脚本模板挪进 domain/framework-script.js 后撤销例外。
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
7
|
+
import { writeFile } from 'node:fs/promises'
|
|
8
|
+
import { execFile } from 'node:child_process'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
12
|
+
import { createRequire } from 'node:module'
|
|
13
|
+
import { FRAMEWORK_BACKUP_ROOT, backupProfileSnapshot, checkpointFrameworkTree, preflightDisableIncompatible, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, resolveFrameworkUpgradePlan } from '../domain/framework.js'
|
|
14
|
+
import { fwIntegrityCheck } from '../infra/fw-integrity-check.js'
|
|
15
|
+
import { CORE_PATCH_ROW_IDS } from '../domain/patch.js'
|
|
16
|
+
import { migrateAgentConfigsForUpgrade } from '../domain/presets.js'
|
|
17
|
+
import { planQuarantine } from '../domain/quarantine.js'
|
|
18
|
+
import { listEntries, webPort } from '../domain/runtime.js'
|
|
19
|
+
import { copyTree } from '../infra/fsx.js'
|
|
20
|
+
import { fetchJsonUrl } from '../infra/http.js'
|
|
21
|
+
import { sendError, sendJson } from '../infra/httpd.js'
|
|
22
|
+
import { dshHome, findPatchPath, pluginRoot } from '../infra/paths.js'
|
|
23
|
+
import { setFwCheckCache } from '../state.js'
|
|
24
|
+
|
|
25
|
+
async function routeFrameworkUpgrade(req, res, rc) {
|
|
26
|
+
const backupProfileSnapshot = rc.deps.backupProfileSnapshot
|
|
27
|
+
const preflightDisableIncompatible = rc.deps.preflightDisableIncompatible
|
|
28
|
+
const webPort = rc.deps.webPort
|
|
29
|
+
const listEntries = rc.deps.listEntries
|
|
30
|
+
const ctx = rc.ctx
|
|
31
|
+
const url = rc.url
|
|
32
|
+
const pathname = rc.pathname
|
|
33
|
+
const method = rc.method
|
|
34
|
+
const body = rc.body
|
|
35
|
+
// 框架升级入口:一键流程 = 备份配置快照 + 备份框架本体(回滚点)+ 自动升级
|
|
36
|
+
// (npx 缓存 dsh 本体 + profile 官方配套包)+ 失败自动回滚 + 重启提示。
|
|
37
|
+
// 升级完成重启后,框架适配逻辑自动:备份新版本快照 + 重打框架补丁 + 版本提示。
|
|
38
|
+
let current = null
|
|
39
|
+
let dshDir = null
|
|
40
|
+
// 面板自报名(自报名一致性校验用):从插件自身 package.json 读,与部署目录比对
|
|
41
|
+
let selfName = null
|
|
42
|
+
try {
|
|
43
|
+
// 面板自报名(自报名一致性校验用):从插件自身 package.json 读,与部署目录比对
|
|
44
|
+
// (Step 1 顺带修正:原来这里写的是 lib/package.json —— 恒抛错被 catch 吞掉,
|
|
45
|
+
// selfName 一直是 null,生成脚本时只能退回 hardcode 兜底;现在走真实包根)
|
|
46
|
+
const selfPkg = JSON.parse(readFileSync(join(pluginRoot(), 'package.json'), 'utf8'))
|
|
47
|
+
selfName = typeof selfPkg.name === 'string' ? selfPkg.name : null
|
|
48
|
+
} catch {}
|
|
49
|
+
try {
|
|
50
|
+
const require = createRequire(ctx.baseUrl ?? 'file:///')
|
|
51
|
+
const dshPkgPath = require.resolve('@deepseek-ai/dsh/package.json')
|
|
52
|
+
const dshPkg = JSON.parse(readFileSync(dshPkgPath, 'utf8'))
|
|
53
|
+
current = dshPkg.version ?? null
|
|
54
|
+
dshDir = dirname(dshPkgPath) // .../node_modules/@deepseek-ai/dsh
|
|
55
|
+
} catch {
|
|
56
|
+
// 兜底:ctx.baseUrl 不可用时从插件自身目录解析(与 resolveDshBin 同源)
|
|
57
|
+
try {
|
|
58
|
+
const requireLocal = createRequire(join(pluginRoot(), 'package.json'))
|
|
59
|
+
const dshPkgPath = requireLocal.resolve('@deepseek-ai/dsh/package.json')
|
|
60
|
+
const dshPkg = JSON.parse(readFileSync(dshPkgPath, 'utf8'))
|
|
61
|
+
current = dshPkg.version ?? null
|
|
62
|
+
dshDir = dirname(dshPkgPath)
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
// 拉元数据 → 候选版本列表 → 本次目标(helper 里同时做「用户自选版本」的白名单校验)
|
|
66
|
+
const plan = await resolveFrameworkUpgradePlan({ current, wanted: body.version, fetchJson: fetchJsonUrl })
|
|
67
|
+
const { latest, next, alpha, versions, tagDefault, registryError } = plan
|
|
68
|
+
if (plan.rejected !== null) {
|
|
69
|
+
sendError(res, 400, `升级目标 ${plan.rejected} 不在可选列表内(只接受比当前 ${current ?? '?'} 更新的已发布版本;降级请用「回滚到上一版」)`)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
const target = plan.target
|
|
73
|
+
const profileDir = dirname(findPatchPath(ctx))
|
|
74
|
+
// 1) 升级前打包备份现有配置(patch / profile package.json / 插件清单)
|
|
75
|
+
let backupDir = null
|
|
76
|
+
try {
|
|
77
|
+
if (current !== null) backupDir = backupProfileSnapshot(profileDir, current, ctx)
|
|
78
|
+
} catch {}
|
|
79
|
+
const hasUpdate = target !== null
|
|
80
|
+
const steps = []
|
|
81
|
+
let upgraded = false
|
|
82
|
+
// 1.5) 预设/config 迁移门禁(写在生成升级脚本之前):框架新版不再接受的旧字段
|
|
83
|
+
//(如 0.1.5 的 persona.text → prefix)会让升级后**预设挂载失败、服务起不来**——
|
|
84
|
+
// 适配门只扫插件包,扫不到预设;这里就地改名并留 .bak,避免整次升级被一个字段搞死。
|
|
85
|
+
let configMigrations = []
|
|
86
|
+
if (hasUpdate) {
|
|
87
|
+
try {
|
|
88
|
+
configMigrations = migrateAgentConfigsForUpgrade(profileDir, target)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
steps.push(`预设配置迁移检查失败:${error instanceof Error ? error.message : String(error)}`)
|
|
91
|
+
}
|
|
92
|
+
for (const m of configMigrations) {
|
|
93
|
+
steps.push(`预设字段迁移:${m.file} 第 ${m.line} 行 ${m.from} → ${m.to}(${m.pkg},已留 .bak 备份)`)
|
|
94
|
+
}
|
|
95
|
+
if (configMigrations.length > 0) {
|
|
96
|
+
steps.push(`自动迁移了 ${configMigrations.length} 处框架新版不再接受的预设字段(不迁移会让升级后预设挂载失败、服务拉不起来)`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 1.6) 不适配行预禁用(用户硬要求:「更新框架后所有不适配的必须先禁用」)。
|
|
100
|
+
// 适配门此前只有「执行侧」(读清单→锁启用→更新后解锁),**检测侧从未实现**:
|
|
101
|
+
// 那份 compat-pending.json 一直是人工/一次性脚本产出的(v0.3.25 起遗留至今)。
|
|
102
|
+
// 这里补上检测侧:升级脚本执行**前**扫描全部可开关行,判定 fail 的就地禁用 + 记入清单,
|
|
103
|
+
// 保证新框架 boot 时不会被某行 import 失败拖垮(loader 单行失败 = 整个服务起不来)。
|
|
104
|
+
let preflight = { disabled: [], skipped: [] }
|
|
105
|
+
if (hasUpdate) {
|
|
106
|
+
try {
|
|
107
|
+
preflight = await preflightDisableIncompatible({ ports: ctx, profileDir, patchPath: findPatchPath(ctx), targetVersion: target })
|
|
108
|
+
} catch (error) {
|
|
109
|
+
steps.push(`不适配行预禁用失败(不阻塞升级,改由启动失败隔离兜底):${error instanceof Error ? error.message : String(error)}`)
|
|
110
|
+
}
|
|
111
|
+
for (const row of preflight.disabled) {
|
|
112
|
+
steps.push(`已预先禁用不适配行:${row.rowId}(${row.moduleName}@${row.version ?? '?'})— ${row.reason ?? '判定不兼容目标框架'}`)
|
|
113
|
+
}
|
|
114
|
+
if (preflight.disabled.length > 0) {
|
|
115
|
+
steps.push(`共预先禁用 ${preflight.disabled.length} 行:新框架启动不会再被它们拖垮;等它们发布适配版本后点「更新并适配」即可解锁`)
|
|
116
|
+
}
|
|
117
|
+
if (preflight.skipped.length > 0) {
|
|
118
|
+
steps.push(`另有 ${preflight.skipped.length} 行无法预判(受保护行/包信息读不到),保持启用,由升级后的启动失败隔离兜底`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// 2) 自动升级框架:运行中的服务锁着 dsh/lib 目录(EBUSY 无法原地替换),
|
|
122
|
+
// 因此生成独立升级脚本(kill 服务 → npm 升级 → 失败回滚 → 拉起服务),后台执行。
|
|
123
|
+
// 与 /restart 同款自守护模式:端口无监听会自动拉起,不会留下"服务起不来"。
|
|
124
|
+
if (hasUpdate && dshDir !== null) {
|
|
125
|
+
const port = webPort(ctx)
|
|
126
|
+
const nodePath = process.execPath
|
|
127
|
+
const binPath = resolveDshBin()
|
|
128
|
+
const npmCli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
|
129
|
+
// 事故教训:npm 的 lib/cli.js 是「函数模块」(module.exports = (process) => validateEngines(...)),
|
|
130
|
+
// 直跑 `node lib/cli.js` 只加载函数定义、不执行命令——立即退出 exit 0(假成功,曾导致
|
|
131
|
+
// "npm 退出码 0 但版本未更新")。唯一正确入口是 bin/npm-cli.js(它调用 cli.js 函数)。
|
|
132
|
+
const corepackJs = join(dirname(process.execPath), 'node_modules', 'corepack', 'dist', 'corepack.js')
|
|
133
|
+
// 预检(事故教训):corepack(pnpm 通道)/ dsh bin.js 缺失时**拒绝生成升级脚本**(不 kill 服务)——
|
|
134
|
+
// 避免"kill 后升级/拉起双双失败,服务永久 down"
|
|
135
|
+
if (!existsSync(corepackJs) || binPath === null || !existsSync(binPath)) {
|
|
136
|
+
steps.push(`升级已取消:无法定位 ${!existsSync(corepackJs) ? 'corepack(pnpm 通道)' : 'dsh bin.js'}(框架安装可能不完整),服务保持运行;请先修复框架再升级`)
|
|
137
|
+
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded: false, backupDir, configMigrations, preflight, steps, hints: steps })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const profileDir2 = profileDir
|
|
141
|
+
// 官方配套包列表(profile package.json 里的 @deepseek-ai/* 依赖)
|
|
142
|
+
let officialList = ''
|
|
143
|
+
try {
|
|
144
|
+
const profilePkg = JSON.parse(readFileSync(join(profileDir2, 'package.json'), 'utf8'))
|
|
145
|
+
const deps = { ...(profilePkg.dependencies ?? {}), ...(profilePkg.devDependencies ?? {}) }
|
|
146
|
+
officialList = Object.keys(deps).filter((d) => d.startsWith('@deepseek-ai/')).join(' ')
|
|
147
|
+
} catch {}
|
|
148
|
+
const rollbackDir = backupDir !== null ? join(backupDir, 'dsh-package-backup') : null
|
|
149
|
+
// 官方配套包参数(Start-Process ArgumentList 需要逐包独立元素;包名无空格,单引号安全)
|
|
150
|
+
const pkgArgs = officialList !== '' ? officialList.split(' ').map((p) => `'${p}'`) : []
|
|
151
|
+
const taskName = `DSH-FW-Upgrade-${process.pid}`
|
|
152
|
+
// 备份 dsh 目录(回滚点)
|
|
153
|
+
try {
|
|
154
|
+
if (rollbackDir !== null) {
|
|
155
|
+
mkdirSync(rollbackDir, { recursive: true })
|
|
156
|
+
copyTree(dshDir, rollbackDir)
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
159
|
+
// 安全护栏(事故教训):回滚点必须真实有效(含 lib/bin.js),否则升级失败时无副本可恢复
|
|
160
|
+
// ——曾因备份缺失导致回滚后框架目录为空、服务无法重启。备份无效直接取消升级。
|
|
161
|
+
if (rollbackDir === null || !existsSync(join(rollbackDir, 'lib', 'bin.js'))) {
|
|
162
|
+
sendError(res, 500, `框架备份失败(回滚点${rollbackDir !== null ? `:${rollbackDir}` : '(无)'}缺失或无效),已取消升级——请检查磁盘空间/权限后重试`)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
// 框架安装根识别(事故教训 2026-09-04):require.resolve 返回 .pnpm 内部 realpath,
|
|
166
|
+
// 旧逻辑 dirname(dirname(dshDir)) 指向 .pnpm/<entry>/node_modules——「重链跳过:.pnpm 目录不存在」、
|
|
167
|
+
// 依赖修复 0 个,pnpm 还在错误 cwd 里把新 CLI 原位覆盖进旧 .pnpm 目录,旧树就此被毁。
|
|
168
|
+
const fwRoot = resolveFrameworkRootNodeModules(dshDir)
|
|
169
|
+
if (fwRoot === null || !existsSync(join(fwRoot, '.pnpm')) || !existsSync(join(fwRoot, '@deepseek-ai', 'dsh', 'lib', 'bin.js'))) {
|
|
170
|
+
sendError(res, 500, `无法定位框架安装根(期望含 .pnpm 与 @deepseek-ai/dsh 的顶层 node_modules,实际:${fwRoot ?? '未找到'})——已取消升级,请修复框架安装后重试`)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
// 框架全树 checkpoint(可靠回滚点):升级前镜像全部 @deepseek-ai 版本包(自包内容)+ 顶层 scope + lock
|
|
174
|
+
let fwCheckpoint = null
|
|
175
|
+
try {
|
|
176
|
+
const cpRoot = backupDir !== null ? backupDir : join(FRAMEWORK_BACKUP_ROOT(), current ?? 'unknown')
|
|
177
|
+
fwCheckpoint = checkpointFrameworkTree(fwRoot, cpRoot)
|
|
178
|
+
steps.push(`框架全树 checkpoint 完成:${fwCheckpoint.dest}(镜像 ${fwCheckpoint.mirrored} 个 @deepseek-ai 版本包;升级失败/拉起失败会自动全树回滚,也可用框架卡片的「回滚到上一版」一键回滚)`)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
steps.push(`框架全树 checkpoint 失败:${error instanceof Error ? error.message : String(error)}(回滚点降级为 CLI 备份)`)
|
|
181
|
+
}
|
|
182
|
+
// 一键回滚记录:框架卡片「回滚到上一版」按钮读取
|
|
183
|
+
try {
|
|
184
|
+
writeFileSync(join(dshHome(), 'plugin-console', 'framework-rollback.json'), JSON.stringify({ from: current, to: target, checkpointDir: fwCheckpoint?.dest ?? null, cliBackupDir: rollbackDir, fwRoot, at: Date.now() }, null, 2), 'utf8')
|
|
185
|
+
} catch {}
|
|
186
|
+
const ps1 = join(tmpdir(), `fw-upgrade-${process.pid}.ps1`)
|
|
187
|
+
const logFile = join(dshHome(), 'plugin-console', 'fw-upgrade.log')
|
|
188
|
+
const stateFile = join(dshHome(), 'plugin-console', 'fw-upgrade-state.txt')
|
|
189
|
+
// 启动失败隔离所需的两个文件(升级脚本在服务起不来时调用):
|
|
190
|
+
// ① 分析器:直接 import 本插件的 planQuarantine,避免把判断逻辑再写一份到 PowerShell;
|
|
191
|
+
// ② 候选行快照:升级发起时的可开关行(决策器据此把日志里的模块名映射回行 id)。
|
|
192
|
+
const qHelperPath = join(dshHome(), 'plugin-console', 'fw-analyze-boot.mjs')
|
|
193
|
+
const qCandidatesPath = join(dshHome(), 'plugin-console', 'fw-quarantine-candidates.json')
|
|
194
|
+
const qRecordPath = join(dshHome(), 'plugin-console', 'fw-quarantine.json')
|
|
195
|
+
let thirdPartyRows = []
|
|
196
|
+
try {
|
|
197
|
+
thirdPartyRows = listEntries(ctx)
|
|
198
|
+
.filter((e) => e.enabled && e.toggleable && e.rowId !== 'plugin-console'
|
|
199
|
+
&& !CORE_PATCH_ROW_IDS.has(e.rowId)
|
|
200
|
+
&& typeof e.moduleName === 'string' && !e.moduleName.startsWith('@deepseek-ai/'))
|
|
201
|
+
.map((e) => e.rowId)
|
|
202
|
+
} catch {}
|
|
203
|
+
try {
|
|
204
|
+
const selfLibUrl = pathToFileURL(join(pluginRoot(), 'lib', 'index.js')).href
|
|
205
|
+
const helperSource = [
|
|
206
|
+
'// 自动生成:启动失败隔离分析器(升级脚本在服务起不来时调用;逻辑在插件内,本文件只做转发)',
|
|
207
|
+
`import { planQuarantine } from ${JSON.stringify(selfLibUrl)}`,
|
|
208
|
+
"import { readFileSync } from 'node:fs'",
|
|
209
|
+
'const [logPath, candPath] = process.argv.slice(2)',
|
|
210
|
+
"let logText = ''",
|
|
211
|
+
"try { logText = readFileSync(logPath, 'utf8').split(/\\r?\\n/u).slice(-400).join('\\n') } catch {}",
|
|
212
|
+
'let candidates = []',
|
|
213
|
+
'try { candidates = JSON.parse(readFileSync(candPath, "utf8")) } catch {}',
|
|
214
|
+
'try { process.stdout.write(JSON.stringify(planQuarantine({ logText, candidates }))) } catch (error) { process.stdout.write(JSON.stringify({ error: String(error && error.message ? error.message : error) })) }',
|
|
215
|
+
'',
|
|
216
|
+
].join('\n')
|
|
217
|
+
writeFileSync(qHelperPath, helperSource, 'utf8')
|
|
218
|
+
const candidateSnapshot = listEntries(ctx)
|
|
219
|
+
.filter((e) => typeof e.rowId === 'string' && e.rowId !== '')
|
|
220
|
+
.map((e) => ({ rowId: e.rowId, moduleName: e.moduleName, toggleable: e.toggleable === true, enabled: e.enabled === true }))
|
|
221
|
+
writeFileSync(qCandidatesPath, JSON.stringify(candidateSnapshot, null, 2), 'utf8')
|
|
222
|
+
} catch {}
|
|
223
|
+
try { mkdirSync(dirname(logFile), { recursive: true }) } catch {}
|
|
224
|
+
// PowerShell 字符串里反斜杠是字面量(无 \\ 转义):JSON.stringify 产生的 \\ 必须还原为 \,
|
|
225
|
+
// 否则所有路径无效(npm 调不起来、回滚无效、拉起失败——曾导致升级脚本空跑)
|
|
226
|
+
// PowerShell 双引号字面量:反斜杠还原 + 空串产出 ''(JSON 的 "" 被单引号包裹会变成字面量 "" ,
|
|
227
|
+
// 曾导致 $cp = '""' → Test-Path 恒假 → 全树回滚被静默跳过)+ $ / 反引号转义(路径含 $ 不被插值)
|
|
228
|
+
const ps = (s) => {
|
|
229
|
+
const j = JSON.stringify(String(s)).replace(/\\\\/gu, '\\')
|
|
230
|
+
if (j === '""') return "''"
|
|
231
|
+
return j.replace(/`/gu, '``').replace(/\$/gu, '`$')
|
|
232
|
+
}
|
|
233
|
+
const patchFilePath = join(profileDir, 'cordis.patch.yml')
|
|
234
|
+
// 拉起服务的 PowerShell 片段(隔离重试用;与升级/回滚路径共用 relaunchPrelude 里的同一份实现)
|
|
235
|
+
const launchSnippet = (tag) => `if (-not $ok -and -not $started) { if (Invoke-DshRelaunch '${tag}') { $started = $true } }; Beat`
|
|
236
|
+
const lines = [
|
|
237
|
+
`$state = ${ps(stateFile)}`,
|
|
238
|
+
`$log = ${ps(logFile)}`,
|
|
239
|
+
"function Log($m) { try { Add-Content -Path $log -Value ((Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' ' + $m) -Encoding UTF8 } catch {} }",
|
|
240
|
+
"function SetState($s, $m) {",
|
|
241
|
+
// v0.3.37:失败时把**崩溃前最后到达的阶段**一起写进状态文件(stage=…)——
|
|
242
|
+
// 否则界面只能看到 failed,把已经成功的步骤全打成 ✕(2026-09-11 用户实测就是这个观感:
|
|
243
|
+
// 框架其实升好了,卡片却整列红叉)。$script:stage 未设置时 [string] 会安全地变成空串。
|
|
244
|
+
" try { if ($s -eq 'failed') { Set-Content -Path $state -Value ($s + '|' + $m + '|stage=' + [string]$script:stage) -Encoding UTF8; return } } catch {}",
|
|
245
|
+
" try { if ($s -ne 'done' -and $s -ne 'idle') { $script:stage = $s }; Set-Content -Path $state -Value ($s + '|' + $m) -Encoding UTF8; Beat } catch {}",
|
|
246
|
+
"}",
|
|
247
|
+
relaunchPrelude({ nodePath, pluginDir: pluginRoot(), fwRoot, target, ps }),
|
|
248
|
+
// 全局异常兜底(事故教训):脚本任何未捕获异常都会无声死亡、状态永远卡住、服务没人拉起。
|
|
249
|
+
// trap 捕获后:写 failed 状态 + 日志 + 尝试拉起服务 + 自删任务 + 退出
|
|
250
|
+
"trap {",
|
|
251
|
+
" try { SetState 'failed' ('升级脚本异常终止:' + $_.Exception.Message) } catch {}",
|
|
252
|
+
// 事故教训(2026-09-10):只记消息无法定位是哪一行崩的 —— 连出错位置一起写日志
|
|
253
|
+
" try { Log ('升级脚本异常终止:' + $_.Exception.Message + ' @ ' + $_.InvocationInfo.PositionMessage) } catch {}",
|
|
254
|
+
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if (-not $c) { [void](Invoke-DshRelaunch '异常兜底') } } catch {}`,
|
|
255
|
+
` schtasks /delete /f /tn ${taskName} 2>$null`,
|
|
256
|
+
" exit 1",
|
|
257
|
+
"}",
|
|
258
|
+
"SetState 'starting' '备份配置…'",
|
|
259
|
+
"Log '框架升级脚本启动'",
|
|
260
|
+
// 流程优化(用户建议):先在线下载安装(服务保持运行、页面不断、进度可见),
|
|
261
|
+
// 成功后最后重启服务生效;在线替换失败(服务占用框架目录 EBUSY)才停止服务重试
|
|
262
|
+
`SetState 'installing' '升级框架 ${current} -> ${target}(npm 下载中,服务保持在线)'`,
|
|
263
|
+
`Log '升级框架本体 ${current} -> ${target}…'`,
|
|
264
|
+
// 关键:npm 的工作目录必须是缓存 node_modules(schtasks 默认 cwd 是 system32/用户目录,
|
|
265
|
+
// 不指定会装错位置——缓存永远不更新的根因)
|
|
266
|
+
`Set-Location -Path ${ps(fwRoot)}`,
|
|
267
|
+
// npm 安装函数:Start-Process 跑 npm-cli.js(唯一正确入口——lib/cli.js 是函数模块,
|
|
268
|
+
// 直跑只加载不执行,假成功 exit 0)+ --force(显式指定版本但已在 package.json 的
|
|
269
|
+
// semver 范围内时 npm 会跳过安装,假成功)+ 15 分钟总时长硬超时。注意:**不能加
|
|
270
|
+
// -RedirectStandardOutput/-RedirectStandardError**——schtasks 任务环境下带重定向的
|
|
271
|
+
// Start-Process 子进程会启动即异常退出(ExitCode 读不到、输出 0 字节,实测);
|
|
272
|
+
// 无重定向时 ExitCode 正常(实测 exit=0)。npm 自身 debug 日志(cache\_logs)留档。
|
|
273
|
+
// 超时策略:总时长 15 分钟硬上限 + npm debug 日志 5 分钟无更新判定卡死(网络黑洞/挂起时
|
|
274
|
+
// 快速失败换 registry,不用干等 15 分钟);等待期间每 15 秒更新进度状态(客户端可见)。
|
|
275
|
+
// 成功后校验 package.json 版本 === target(防 npm 假成功)。
|
|
276
|
+
// 另注意:npm install 在 schtasks 环境启动慢(约 1-2 分钟 0 日志,初始化/编译缓存),
|
|
277
|
+
// 属正常,5 分钟卡死阈值不会误杀。
|
|
278
|
+
`function Install-Framework($reg) {`,
|
|
279
|
+
` $startAt = Get-Date`,
|
|
280
|
+
` $deadline = $startAt.AddMinutes(15)`,
|
|
281
|
+
` $stallDeadline = $startAt.AddMinutes(5)`,
|
|
282
|
+
` $lastLogM = (Get-Date)`,
|
|
283
|
+
` $logSeen = $false`,
|
|
284
|
+
` $cacheDir = ''`,
|
|
285
|
+
` try {`,
|
|
286
|
+
` $npmrcFile = Join-Path $env:USERPROFILE '.npmrc'`,
|
|
287
|
+
` if (Test-Path $npmrcFile) { $m = Select-String -Path $npmrcFile -Pattern '^cache\\s*=\\s*(.+)$' | Select-Object -Last 1; if ($m) { $cacheDir = $m.Matches[0].Groups[1].Value.Trim().Trim('"') } }`,
|
|
288
|
+
` } catch {}`,
|
|
289
|
+
` if ($cacheDir -eq '') { $fb = Join-Path $env:APPDATA 'npm-cache'; if (Test-Path (Join-Path $fb '_logs')) { $cacheDir = $fb } }`,
|
|
290
|
+
` $npmOut = $log + '.npm.out.txt'`,
|
|
291
|
+
` $marker = $log + '.pnpm.marker'`,
|
|
292
|
+
` try { Remove-Item $marker -Force -ErrorAction SilentlyContinue } catch {}`,
|
|
293
|
+
` $proc = $null`,
|
|
294
|
+
` try {`,
|
|
295
|
+
` # pnpm 通道 + 黑框实时进度:start 独立窗口(有真实 stdout,黑框显示 pnpm 进度条,`,
|
|
296
|
+
` # 标题 DSH-Upgrade);pnpm 结束后 cmd 写 marker(含退出码),脚本轮询 marker 判断完成`,
|
|
297
|
+
` $cmdLine = 'start "DSH-Upgrade" cmd /c "title DSH-Upgrade && ${nodePath} ${corepackJs} pnpm install @deepseek-ai/dsh@${target} --force --config.dangerouslyAllowAllBuilds=true --registry ' + $reg + ' & echo DSH-DONE:%ERRORLEVEL% > ' + $marker + '"'`,
|
|
298
|
+
` $proc = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', $cmdLine) -PassThru -WindowStyle Hidden`,
|
|
299
|
+
` } catch {`,
|
|
300
|
+
` Log ('启动 pnpm 失败:' + $_.Exception.Message)`,
|
|
301
|
+
` return 1`,
|
|
302
|
+
` }`,
|
|
303
|
+
` $done = $false`,
|
|
304
|
+
` $code = 1`,
|
|
305
|
+
` while (-not $done -and (Get-Date) -lt $deadline) {`,
|
|
306
|
+
` Start-Sleep -Seconds 5`,
|
|
307
|
+
` if (Test-Path $marker) {`,
|
|
308
|
+
` $mc = Get-Content $marker -Raw -ErrorAction SilentlyContinue`,
|
|
309
|
+
` $mm = [regex]::Match($mc, 'DSH-DONE:(\\d+)')`,
|
|
310
|
+
` if ($mm.Success) { $code = [int]$mm.Groups[1].Value; $done = $true }`,
|
|
311
|
+
` }`,
|
|
312
|
+
` if (-not $done) {`,
|
|
313
|
+
` SetState 'installing' ('升级框架 ${current} -> ${target}(已等待 ' + [int]((Get-Date) - $startAt).TotalSeconds + ' 秒,进度见 DSH-Upgrade 窗口)')`,
|
|
314
|
+
` }`,
|
|
315
|
+
` }`,
|
|
316
|
+
` if (-not $done) {`,
|
|
317
|
+
` Log 'pnpm 超过 15 分钟未完成,判定超时'`,
|
|
318
|
+
` try { taskkill /F /FI "WINDOWTITLE eq DSH-Upgrade" 2>$null | Out-Null } catch {}`,
|
|
319
|
+
` return 1`,
|
|
320
|
+
` }`,
|
|
321
|
+
` if ($code -eq 0) {`,
|
|
322
|
+
` try {`,
|
|
323
|
+
// 事故教训(2026-09-10):版本校验必须看「启动器可见路径」(顶层 @deepseek-ai\dsh),
|
|
324
|
+
// 而不是 require.resolve 得到的 .pnpm 内部路径——旧逻辑查 .pnpm 里的旧副本,
|
|
325
|
+
// 于是 pnpm 明明装好了却报「版本未更新,按失败处理」,白等两轮。
|
|
326
|
+
` $v = (Get-Content ${ps(join(fwRoot, '@deepseek-ai', 'dsh', 'package.json'))} -Raw | ConvertFrom-Json).version`,
|
|
327
|
+
` if ($v -ne '${target}') { Log ('pnpm 退出码 0 但顶层可见版本未更新(顶层 ' + $v + ',目标 ${target}),按失败处理'); $code = 1 }`,
|
|
328
|
+
` } catch { Log 'pnpm 退出码 0 但无法读取顶层版本,按失败处理'; $code = 1 }`,
|
|
329
|
+
` }`,
|
|
330
|
+
fwIntegrityCheck({ target, nodePath, fwRoot }),
|
|
331
|
+
` return $code`,
|
|
332
|
+
`}`,
|
|
333
|
+
// 回滚函数(2026-09-04 事故后升级为「全树优先」):robocopy 支持长路径(Copy-Item 对 >260
|
|
334
|
+
// 字符路径静默失败,曾导致回滚后框架目录缺失、服务无法重启);回滚前确保服务已停
|
|
335
|
+
// (进程 cwd 会锁住框架目录)。checkpoint 存在 → 恢复全部 @deepseek-ai 版本包 + 顶层 scope;
|
|
336
|
+
// checkpoint 缺失 → CLI 兜底。
|
|
337
|
+
`function Invoke-Rollback {`,
|
|
338
|
+
` SetState 'rollback' '升级失败,回滚框架(全树/CLI 备份)…'`,
|
|
339
|
+
` Log '升级失败,回滚框架…'`,
|
|
340
|
+
// 事故教训(2026-09-10):整个回滚体包 try/catch —— 脚本崩在回滚里(曾报
|
|
341
|
+
// 「无法将参数绑定到参数 Path」,回滚没跑完、旧树半新半旧),必须记录出错位置而不是
|
|
342
|
+
// 只留一句 trap 消息;回滚失败也要明确写状态,别让人以为已回滚。
|
|
343
|
+
` try {`,
|
|
344
|
+
` try { $svc = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($svc) { $svc | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }; Start-Sleep -Seconds 3 } } catch {}`,
|
|
345
|
+
` $cp = ${ps(fwCheckpoint?.dest ?? '')}`,
|
|
346
|
+
` if ($cp -and (Test-Path (Join-Path $cp '.pnpm'))) {`,
|
|
347
|
+
` $restored = 0`,
|
|
348
|
+
` $entries = Get-ChildItem -Path (Join-Path $cp '.pnpm') -Directory -ErrorAction SilentlyContinue`,
|
|
349
|
+
` foreach ($e in $entries) {`,
|
|
350
|
+
` if ($e.Name -notlike '*+*') { continue }`,
|
|
351
|
+
` $name = @($e.Name -split '\\+')[1] -replace '@.*$', ''`,
|
|
352
|
+
` if (-not $name) { continue }`,
|
|
353
|
+
` $src = Join-Path $e.FullName ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
354
|
+
` $dst = Join-Path (Join-Path ${ps(fwRoot)} ('.pnpm\\' + $e.Name)) ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
355
|
+
` if (Test-Path (Join-Path $src 'package.json')) { New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null; robocopy $src $dst /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null; $restored++ }`,
|
|
356
|
+
` }`,
|
|
357
|
+
` $topSrc = Join-Path $cp 'top-@deepseek-ai'`,
|
|
358
|
+
` if (Test-Path $topSrc) { Remove-Item ${ps(join(fwRoot, '@deepseek-ai'))} -Recurse -Force -ErrorAction SilentlyContinue; robocopy $topSrc ${ps(join(fwRoot, '@deepseek-ai'))} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null }`,
|
|
359
|
+
` try { Copy-Item (Join-Path $cp 'lock.yaml') ${ps(join(fwRoot, '.pnpm', 'lock.yaml'))} -Force -ErrorAction SilentlyContinue } catch {}`,
|
|
360
|
+
` Log ('全树回滚完成:恢复 ' + $restored + ' 个版本包 + 顶层 scope')`,
|
|
361
|
+
` $script:rolledBack = $true`,
|
|
362
|
+
` return`,
|
|
363
|
+
` }`,
|
|
364
|
+
` # CLI 兜底(checkpoint 缺失或损坏)`,
|
|
365
|
+
` if (-not (Test-Path ${ps(join(rollbackDir, 'lib', 'bin.js'))})) { Log ('回滚失败:CLI 备份也缺失(' + ${ps(rollbackDir)} + ')'); return }`,
|
|
366
|
+
` Remove-Item ${ps(dshDir)} -Recurse -Force -ErrorAction SilentlyContinue`,
|
|
367
|
+
` robocopy ${ps(rollbackDir)} ${ps(dshDir)} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null`,
|
|
368
|
+
` if (Test-Path ${ps(join(dshDir, 'lib', 'bin.js'))}) { Log '回滚完成(CLI 恢复)'; $script:rolledBack = $true } else { Log '回滚失败:复制失败,请手动修复框架安装' }`,
|
|
369
|
+
` } catch {`,
|
|
370
|
+
` Log ('回滚过程异常:' + $_.Exception.Message + ' @ ' + $_.InvocationInfo.PositionMessage)`,
|
|
371
|
+
` SetState 'failed' ('回滚过程异常,框架可能处于混合状态:' + $_.Exception.Message)`,
|
|
372
|
+
` }`,
|
|
373
|
+
`}`,
|
|
374
|
+
`$script:rolledBack = $false`,
|
|
375
|
+
`$code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
376
|
+
`if ($code -ne 0) { Log ('npmmirror 安装失败(exit=' + $code + '),切换 registry.npmjs.org 重试一次'); $code = Install-Framework 'https://registry.npmjs.org' }`,
|
|
377
|
+
`if ($code -ne 0) {`,
|
|
378
|
+
` # 在线安装失败(服务可能占用框架目录导致替换失败):停止服务后重试一次`,
|
|
379
|
+
` SetState 'stopped' '在线安装失败(框架目录可能被服务占用),停止服务后重试…'`,
|
|
380
|
+
` Log '在线安装失败,停止服务后重试'`,
|
|
381
|
+
` try { $svc = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($svc) { $svc | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }; Start-Sleep -Seconds 3 } } catch {}`,
|
|
382
|
+
` $code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
383
|
+
` if ($code -ne 0) { Log ('(停服后)npmmirror 安装失败(exit=' + $code + '),切换 registry.npmjs.org 重试一次'); $code = Install-Framework 'https://registry.npmjs.org' }`,
|
|
384
|
+
`}`,
|
|
385
|
+
`if ($code -ne 0) { Invoke-Rollback } else {`,
|
|
386
|
+
` Log '框架本体升级完成'`,
|
|
387
|
+
// 事故教训(2026-09-10):启动器(桌面端/`npx dsh`)用的是**顶层** @deepseek-ai\dsh;
|
|
388
|
+
// 若它是 npm 时代的真实目录,pnpm 无法把它换成链接 → 顶层永远停在旧版,
|
|
389
|
+
// 桌面端下次自己拉服务就跑旧框架(版本错配 → 拉起失败 → 又提示升级,循环)。
|
|
390
|
+
// 这里校验「启动器可见版本」,是实体目录就改名备份后重装一次,让 pnpm 重建链接。
|
|
391
|
+
` $visible = ${ps(join(fwRoot, '@deepseek-ai', 'dsh'))}`,
|
|
392
|
+
` $vv = ''`,
|
|
393
|
+
` try { $vv = (Get-Content (Join-Path $visible 'package.json') -Raw | ConvertFrom-Json).version } catch {}`,
|
|
394
|
+
` if ($vv -ne '${target}') {`,
|
|
395
|
+
` $item = Get-Item $visible -Force -ErrorAction SilentlyContinue`,
|
|
396
|
+
` if ($item -and -not $item.LinkType) {`,
|
|
397
|
+
` $bakName = 'dsh.npm-backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss')`,
|
|
398
|
+
` try { Rename-Item -Path $visible -NewName $bakName -ErrorAction Stop; Log ('顶层为旧版实体目录(' + $vv + ')→ 已改名备份 ' + $bakName + ',重装以重建链接…') } catch { Log ('顶层实体目录改名失败:' + $_.Exception.Message) }`,
|
|
399
|
+
` $code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
400
|
+
` if ($code -ne 0) { $code = Install-Framework 'https://registry.npmjs.org' }`,
|
|
401
|
+
` $vv = ''`,
|
|
402
|
+
` try { $vv = (Get-Content (Join-Path $visible 'package.json') -Raw | ConvertFrom-Json).version } catch {}`,
|
|
403
|
+
` if ($vv -eq '${target}') { Log '顶层可见版本已修正为 ${target}' } else { Log ('顶层可见版本仍为 ' + $vv + '(拉起不受影响:Node 按链接真实路径解析依赖)') }`,
|
|
404
|
+
` } else { Log ('顶层可见版本异常(' + $vv + ')且非实体目录,跳过改名(LinkType=' + $item.LinkType + ')') }`,
|
|
405
|
+
` } else { Log '顶层可见版本校验通过' }`,
|
|
406
|
+
`}`,
|
|
407
|
+
// 回滚后跳过重链/依赖修复/版本对齐(它们按新树预期运行,会对已恢复的旧树造成二次破坏)
|
|
408
|
+
`if (-not $script:rolledBack) {`,
|
|
409
|
+
// 重新链接框架配套包(事故教训):pnpm 升级到新版本时 .pnpm 目录重建,
|
|
410
|
+
// 但顶层 node_modules/@deepseek-ai/* 不会自动切换,仍指向旧版(如 0.1.0-rc.7)——
|
|
411
|
+
// 导致框架混版本(如 dsh-llm-deepseek 旧版无 vision 模型)、部分功能异常。
|
|
412
|
+
// 升级成功后:遍历顶层 @deepseek-ai 的 dsh-* 包,读 .pnpm 主目录里的最新版本,
|
|
413
|
+
// 若与顶层不一致则重建为 Junction(旧版备份 .bak-rc7)。
|
|
414
|
+
`$relinked = 0`,
|
|
415
|
+
`function Relink-FrameworkPackages($version) {`,
|
|
416
|
+
` # dshDir = .../node_modules/@deepseek-ai/dsh → nodeModules = .../node_modules`,
|
|
417
|
+
` $nodeModules = ${ps(fwRoot)}`,
|
|
418
|
+
` $pnpm = Join-Path $nodeModules '.pnpm'`,
|
|
419
|
+
` if (-not (Test-Path $pnpm)) { Log ('重链跳过:.pnpm 目录不存在'); return }`,
|
|
420
|
+
` $topPkgs = Get-ChildItem -Path (Join-Path $nodeModules '@deepseek-ai') -Directory | Where-Object { $_.Name -like 'dsh-*' }`,
|
|
421
|
+
` # 版本数值比较(修复:字符串比较在版本号位数变化时出错,如 0.1.10 < 0.1.9;PS5.1 兼容,不用三元运算符)`,
|
|
422
|
+
` function Compare-Version($a, $b) {`,
|
|
423
|
+
` if ($a -eq $b) { return 0 }`,
|
|
424
|
+
` $pa = [regex]::Match($a, '^(\\d+)\\.(\\d+)\\.(\\d+)(?:-(?:[a-z]+\\.)?(\\d+))?' )`,
|
|
425
|
+
` $pb = [regex]::Match($b, '^(\\d+)\\.(\\d+)\\.(\\d+)(?:-(?:[a-z]+\\.)?(\\d+))?' )`,
|
|
426
|
+
` if (-not $pa.Success -or -not $pb.Success) { return ($a.CompareTo($b)) }`,
|
|
427
|
+
` for ($i = 1; $i -le 4; $i++) {`,
|
|
428
|
+
` if ($pa.Groups[$i].Success) { $va = [int]$pa.Groups[$i].Value } else { $va = 2147483647 }`,
|
|
429
|
+
` if ($pb.Groups[$i].Success) { $vb = [int]$pb.Groups[$i].Value } else { $vb = 2147483647 }`,
|
|
430
|
+
` if ($va -ne $vb) { if ($va -lt $vb) { return -1 } else { return 1 } }`,
|
|
431
|
+
` }`,
|
|
432
|
+
` return 0`,
|
|
433
|
+
` }`,
|
|
434
|
+
` foreach ($top in $topPkgs) {`,
|
|
435
|
+
` $name = $top.Name`,
|
|
436
|
+
` $topPkgJson = Join-Path $top.FullName 'package.json'`,
|
|
437
|
+
` if (-not (Test-Path $topPkgJson)) { continue }`,
|
|
438
|
+
` try { $topVer = (Get-Content $topPkgJson -Raw | ConvertFrom-Json).version } catch { continue }`,
|
|
439
|
+
` # 找 .pnpm 里该包的所有主目录(@deepseek-ai+dsh-<name>@<ver>_<hash> 或 @deepseek-ai+dsh-<name>@<ver>-rc.x_<hash>)`,
|
|
440
|
+
` $cands = Get-ChildItem -Path $pnpm -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match ('^@deepseek-ai\\\\+dsh-' + [regex]::Escape($name.Substring(4)) + '@') }`,
|
|
441
|
+
` $best = $null; $bestVer = ''`,
|
|
442
|
+
` foreach ($c in $cands) {`,
|
|
443
|
+
` $inner = Join-Path $c.FullName ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
444
|
+
` $pj = Join-Path $inner 'package.json'`,
|
|
445
|
+
` if (-not (Test-Path $pj)) { continue }`,
|
|
446
|
+
` try { $v = (Get-Content $pj -Raw | ConvertFrom-Json).version } catch { continue }`,
|
|
447
|
+
` if ($null -eq $best -or (Compare-Version $v $bestVer) -gt 0) { $bestVer = $v; $best = $inner }`,
|
|
448
|
+
` }`,
|
|
449
|
+
` if ($null -eq $best) { continue }`,
|
|
450
|
+
` if ($bestVer -eq $topVer) { continue }`,
|
|
451
|
+
` try {`,
|
|
452
|
+
` $bak = $top.FullName + '.bak-' + $topVer`,
|
|
453
|
+
` if (Test-Path $bak) { Remove-Item $bak -Recurse -Force -ErrorAction SilentlyContinue }`,
|
|
454
|
+
` Rename-Item -LiteralPath $top.FullName -NewName ($name + '.bak-' + $topVer) -Force`,
|
|
455
|
+
` cmd /c mklink /J "$($top.FullName)" "$($best)" 2>$null | Out-Null`,
|
|
456
|
+
` if (Test-Path (Join-Path $top.FullName 'package.json')) { $script:relinked++; Log ('重链 ' + $name + ': ' + $topVer + ' -> ' + $bestVer) } else { Log ('重链失败:' + $name) }`,
|
|
457
|
+
` } catch { Log ('重链异常:' + $name + ' ' + $_.Exception.Message) }`,
|
|
458
|
+
` }`,
|
|
459
|
+
`}`,
|
|
460
|
+
`Relink-FrameworkPackages '${target}'`,
|
|
461
|
+
`Log ('重链框架配套包完成(' + $script:relinked + ' 个)')`,
|
|
462
|
+
// 依赖树完整性验证(事故教训:升级只装框架主包,pnpm 不重装依赖树——dsh-client-* 等
|
|
463
|
+
// 39 个客户端组件仍停旧版(如 0.1.0-rc.7),新旧混版本导致输入框传图卡死等异常。
|
|
464
|
+
// 修复:扫描 web-app 声明的 @deepseek-ai 依赖,与顶层实际版本比对,
|
|
465
|
+
// 不一致则按声明版本从 npm registry 精确下载 tarball 修复(绕过 pnpm dist-tags 远古版陷阱)。
|
|
466
|
+
`$treeFixed = 0`,
|
|
467
|
+
`function Verify-DependencyTree($refVersion) {`,
|
|
468
|
+
` # dshDir = .../node_modules/@deepseek-ai/dsh → nodeModules = .../node_modules`,
|
|
469
|
+
` $nodeModules = ${ps(fwRoot)}`,
|
|
470
|
+
` $webApp = Join-Path $nodeModules '@deepseek-ai\\dsh-web-app\\package.json'`,
|
|
471
|
+
` if (-not (Test-Path $webApp)) { Log ('依赖树验证跳过:dsh-web-app 未找到'); return }`,
|
|
472
|
+
` try { $deps = (Get-Content $webApp -Raw | ConvertFrom-Json).dependencies } catch { Log ('依赖树验证跳过:读取失败'); return }`,
|
|
473
|
+
` foreach ($dep in $deps.PSObject.Properties) {`,
|
|
474
|
+
` $name = $dep.Name`,
|
|
475
|
+
` if (-not ($name -match '^@deepseek-ai/')) { continue }`,
|
|
476
|
+
` $short = $name.Substring(13)`,
|
|
477
|
+
` $topDir = Join-Path $nodeModules (@('@deepseek-ai', $short) -join '\')`,
|
|
478
|
+
` $topPkgJson = Join-Path $topDir 'package.json'`,
|
|
479
|
+
` if (-not (Test-Path $topPkgJson)) { continue }`,
|
|
480
|
+
` try { $curVer = (Get-Content $topPkgJson -Raw | ConvertFrom-Json).version } catch { continue }`,
|
|
481
|
+
` if ($curVer -eq $refVersion) { continue }`,
|
|
482
|
+
` try {`,
|
|
483
|
+
` # 按声明版本精确下载 npm tarball(registry 的 dist-tags.latest 是远古版不可信,必须显式版本)`,
|
|
484
|
+
` $tmpDir = Join-Path $env:TEMP ('dsh-depfix-' + [guid]::NewGuid().ToString('N'))`,
|
|
485
|
+
` New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null`,
|
|
486
|
+
` $tgz = Join-Path $tmpDir 'pkg.tgz'`,
|
|
487
|
+
` $encName = $name.Replace('/','%2f')`,
|
|
488
|
+
` $url = 'https://registry.npmmirror.com/' + $encName + '/-/' + $short + '-' + $refVersion + '.tgz'`,
|
|
489
|
+
` curl.exe -s -L -m 60 -o $tgz $url`,
|
|
490
|
+
` if (-not (Test-Path $tgz) -or (Get-Item $tgz).Length -lt 1000) {`,
|
|
491
|
+
` $url = 'https://registry.npmjs.org/' + $encName + '/-/' + $short + '-' + $refVersion + '.tgz'`,
|
|
492
|
+
` curl.exe -s -L -m 60 -o $tgz $url`,
|
|
493
|
+
` }`,
|
|
494
|
+
` if (-not (Test-Path $tgz) -or (Get-Item $tgz).Length -lt 1000) { Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue; continue }`,
|
|
495
|
+
` tar -xzf $tgz -C $tmpDir`,
|
|
496
|
+
` $ver = (Get-Content (Join-Path $tmpDir 'package\\package.json') -Raw | ConvertFrom-Json).version`,
|
|
497
|
+
` if ($ver -eq $refVersion) {`,
|
|
498
|
+
` $bak = $topDir + '.bak-' + $curVer`,
|
|
499
|
+
` if (Test-Path $bak) { Remove-Item $bak -Recurse -Force -ErrorAction SilentlyContinue }`,
|
|
500
|
+
` Rename-Item -LiteralPath $topDir -NewName ($short + '.bak-' + $curVer) -Force`,
|
|
501
|
+
` Copy-Item (Join-Path $tmpDir 'package') $topDir -Recurse -Force`,
|
|
502
|
+
` $script:treeFixed++`,
|
|
503
|
+
` Log ('依赖修复 ' + $short + ': ' + $curVer + ' -> ' + $ver)`,
|
|
504
|
+
` }`,
|
|
505
|
+
` Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue`,
|
|
506
|
+
` } catch { Log ('依赖修复异常:' + $short + ' ' + $_.Exception.Message) }`,
|
|
507
|
+
` }`,
|
|
508
|
+
`}`,
|
|
509
|
+
`Verify-DependencyTree '${target}'`,
|
|
510
|
+
`Log ('依赖树完整性修复完成(' + $script:treeFixed + ' 个)')`,
|
|
511
|
+
// 自报名一致性校验(事故教训:面板自身部署名与代码自报名不一致崩溃)——
|
|
512
|
+
// 曾把 @noob-stupid/dsh-plugin-console 的代码装进 @deepseek-ai/dsh-plugin-console 目录,
|
|
513
|
+
// 浏览器端报 loaded without registering "@deepseek-ai/dsh-plugin-console"(加载器期望
|
|
514
|
+
// 目录名 = 包名 = 登记 ID,三者必须一致)。校验面板自身:export const name / client.js
|
|
515
|
+
// 注册 id 必须与部署目录名一致,不一致则日志告警(升级脚本不自动改名——避免破坏运行时)。
|
|
516
|
+
`function Verify-SelfNameConsistency($expectedName) {`,
|
|
517
|
+
` $selfDir = ${ps(pluginRoot())}`,
|
|
518
|
+
` $indexJs = Join-Path $selfDir 'lib\\index.js'`,
|
|
519
|
+
` $clientJs = Join-Path $selfDir 'lib\\client.js'`,
|
|
520
|
+
` if (-not (Test-Path $indexJs)) { Log ('自报名校验跳过:index.js 未找到'); return }`,
|
|
521
|
+
` # 计算部署目录的完整包名(scoped 包为 @scope/name,非 scoped 包为 name)`,
|
|
522
|
+
` $leaf = Split-Path $selfDir -Leaf`,
|
|
523
|
+
` $parentLeaf = Split-Path (Split-Path $selfDir -Parent) -Leaf`,
|
|
524
|
+
` if ($parentLeaf.StartsWith('@')) { $deployName = $parentLeaf + '/' + $leaf } else { $deployName = $leaf }`,
|
|
525
|
+
` $idx = Get-Content $indexJs -Raw`,
|
|
526
|
+
` $nameOk = $idx.Contains("export const name = '" + $expectedName + "'")`,
|
|
527
|
+
` $clientOk = $true`,
|
|
528
|
+
` if (Test-Path $clientJs) {`,
|
|
529
|
+
` $cli = Get-Content $clientJs -Raw`,
|
|
530
|
+
` $clientOk = $cli.Contains('id: "' + $expectedName + '"')`,
|
|
531
|
+
` }`,
|
|
532
|
+
` $dirOk = ($deployName -eq $expectedName)`,
|
|
533
|
+
` if ($nameOk -and $clientOk -and $dirOk) { Log ('自报名一致性 OK:' + $expectedName + ' @ ' + $deployName) } else { Log ('⚠️ 自报名一致性异常:部署目录=' + $deployName + ',期望 ' + $expectedName + '(index=' + $nameOk + ' client=' + $clientOk + ' dir=' + $dirOk + ')——请检查部署目录与包名是否匹配') }`,
|
|
534
|
+
`}`,
|
|
535
|
+
`Verify-SelfNameConsistency '${selfName ?? '@noob-stupid/dsh-plugin-console'}'`,
|
|
536
|
+
pkgArgs.length > 0 ? `SetState 'pkg' '更新官方配套包…'\n Log '更新官方配套包…'\n Set-Location -Path ${ps(profileDir2)}\n if (Test-Path ${ps(corepackJs)}) {\n $pp = Start-Process -FilePath ${ps(nodePath)} -ArgumentList @(${ps(corepackJs)}, 'pnpm', 'update', ${pkgArgs.join(', ')}, '--no-optional', '--registry', 'https://registry.npmmirror.com') -PassThru -WindowStyle Hidden\n if (-not $pp.WaitForExit(300000)) { Stop-Process -Id $pp.Id -Force -ErrorAction SilentlyContinue; Log '官方配套包更新超时(5 分钟),已跳过' } else { try { Log ('官方配套包更新结束(exit=' + $pp.ExitCode + ')') } catch { Log '官方配套包更新结束(退出码读取失败)' } }\n }` : '',
|
|
537
|
+
"}",
|
|
538
|
+
"SetState 'relaunching' '升级完成,重启 DSH 服务生效…'",
|
|
539
|
+
"Log '重启 DSH 服务生效…'",
|
|
540
|
+
// 升级全程服务保持在线(先下载后重启):这里停掉旧服务(内存仍是旧代码),拉起新版本生效
|
|
541
|
+
`try { $svc = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($svc) { $svc | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }; Start-Sleep -Seconds 3; Log '旧服务已停止(重启生效)' } } catch {}`,
|
|
542
|
+
'$ok = $false',
|
|
543
|
+
'$started = $false',
|
|
544
|
+
`for ($i = 0; $i -lt 20; $i++) {`,
|
|
545
|
+
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
546
|
+
` if (-not $ok -and -not $started) { if (Invoke-DshRelaunch '升级后') { $started = $true } }; Beat`,
|
|
547
|
+
' Start-Sleep -Seconds 5',
|
|
548
|
+
'}',
|
|
549
|
+
// ── 启动失败隔离(用户硬要求):新框架起不来时先隔离肇事者再重试,不直接回滚 ──
|
|
550
|
+
// 判定逻辑全在 Node 决策器(fw-analyze-boot.mjs → planQuarantine);这里只执行方案。
|
|
551
|
+
`function Invoke-Quarantine {`,
|
|
552
|
+
` $helper = ${ps(qHelperPath)}`,
|
|
553
|
+
` if (-not (Test-Path $helper)) { Log '隔离分析器缺失,跳过隔离'; return $false }`,
|
|
554
|
+
` $logPath = Join-Path (Split-Path $log) 'fw-relaunch.log'`,
|
|
555
|
+
` if (-not (Test-Path $logPath)) { $logPath = $log }`,
|
|
556
|
+
` $out = ''`,
|
|
557
|
+
` try { $out = (& ${ps(nodePath)} $helper $logPath ${ps(qCandidatesPath)} 2>$null | Select-Object -Last 1) } catch { Log ('隔离分析调用失败:' + $_.Exception.Message); return $false }`,
|
|
558
|
+
` if (-not $out) { Log '隔离分析无输出'; return $false }`,
|
|
559
|
+
` $plan = $null`,
|
|
560
|
+
` try { $plan = $out | ConvertFrom-Json } catch { Log '隔离分析输出无法解析'; return $false }`,
|
|
561
|
+
` $acted = $false; $doneP = @(); $doneR = @(); $mode = 'targeted'`,
|
|
562
|
+
` foreach ($p in @($plan.presets)) {`,
|
|
563
|
+
` if (-not $p.exists) { continue }`,
|
|
564
|
+
` try { Rename-Item -LiteralPath $p.file -NewName ((Split-Path $p.file -Leaf) + '.broken-' + (Get-Date -Format 'yyyyMMdd-HHmmss')) -ErrorAction Stop; $doneP += $p.name; $acted = $true; Log ('已隔离预设:' + $p.name + '(改名 .broken,不再参与挂载)') } catch { Log ('预设隔离失败:' + $p.name + ' ' + $_.Exception.Message) }`,
|
|
565
|
+
` }`,
|
|
566
|
+
` foreach ($r in @($plan.rows)) {`,
|
|
567
|
+
` try {`,
|
|
568
|
+
` $has = Select-String -Path ${ps(patchFilePath)} -Pattern ('^- id: ' + [regex]::Escape($r) + '\\s*$') -Quiet -ErrorAction SilentlyContinue`,
|
|
569
|
+
` if (-not $has) { Add-Content -Path ${ps(patchFilePath)} -Value ('- id: ' + $r) -Encoding UTF8; Add-Content -Path ${ps(patchFilePath)} -Value ' disabled: true' -Encoding UTF8 }`,
|
|
570
|
+
` $doneR += $r; $acted = $true; Log ('已禁用不适配行:' + $r)`,
|
|
571
|
+
` } catch { Log ('禁用行失败:' + $r + ' ' + $_.Exception.Message) }`,
|
|
572
|
+
` }`,
|
|
573
|
+
` if (-not $acted -and $plan.safeMode) {`,
|
|
574
|
+
` $mode = 'safe-mode'`,
|
|
575
|
+
` foreach ($r in @(${thirdPartyRows.length > 0 ? thirdPartyRows.map((r) => ps(r)).join(', ') : "''"})) {`,
|
|
576
|
+
` try { $has = Select-String -Path ${ps(patchFilePath)} -Pattern ('^- id: ' + [regex]::Escape($r) + '\\s*$') -Quiet -ErrorAction SilentlyContinue; if (-not $has) { Add-Content -Path ${ps(patchFilePath)} -Value ('- id: ' + $r) -Encoding UTF8; Add-Content -Path ${ps(patchFilePath)} -Value ' disabled: true' -Encoding UTF8 }; $doneR += $r; $acted = $true } catch {}`,
|
|
577
|
+
` }`,
|
|
578
|
+
` if ($acted) { Log ('安全模式:已禁用 ' + $doneR.Count + ' 个第三方行,先让服务起来(适配后逐个解锁)') }`,
|
|
579
|
+
` }`,
|
|
580
|
+
` if ($acted) {`,
|
|
581
|
+
` try {`,
|
|
582
|
+
` $rec = [pscustomobject]@{ at = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'); mode = $mode; presets = $doneP; rows = $doneR; lines = $plan.lines } | ConvertTo-Json -Depth 4`,
|
|
583
|
+
` Set-Content -Path ${ps(qRecordPath)} -Value $rec -Encoding UTF8`,
|
|
584
|
+
` } catch {}`,
|
|
585
|
+
` } else { Log '隔离分析未定位到可隔离对象' }`,
|
|
586
|
+
` return $acted`,
|
|
587
|
+
`}`,
|
|
588
|
+
`if (-not $ok -and -not $script:rolledBack) {`,
|
|
589
|
+
` Log '服务拉起失败 → 先做启动失败隔离(隔离肇事者后重试,不直接回滚)'`,
|
|
590
|
+
` for ($q = 1; $q -le 3; $q++) {`,
|
|
591
|
+
` if (-not (Invoke-Quarantine)) { Log ('第 ' + $q + ' 轮:未定位到可隔离对象,停止隔离'); break }`,
|
|
592
|
+
` $ok = $false; $started = $false`,
|
|
593
|
+
` for ($i = 0; $i -lt 12; $i++) {`,
|
|
594
|
+
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
595
|
+
` ${launchSnippet('隔离后重试')}`,
|
|
596
|
+
` Start-Sleep -Seconds 5`,
|
|
597
|
+
` }`,
|
|
598
|
+
` if ($ok) { Log ('第 ' + $q + ' 轮隔离后服务已起来(被隔离项可在控制台解锁)'); break }`,
|
|
599
|
+
` Log ('第 ' + $q + ' 轮隔离后仍失败')`,
|
|
600
|
+
` }`,
|
|
601
|
+
`}`,
|
|
602
|
+
// 隔离也救不回才回滚(2026-09-04 教训:自动全树回滚并再拉起一次)
|
|
603
|
+
`if (-not $ok -and -not $script:rolledBack) {`,
|
|
604
|
+
` Log '服务拉起失败,尝试自动回滚到升级前版本…'`,
|
|
605
|
+
` Invoke-Rollback`,
|
|
606
|
+
` $ok = $false`,
|
|
607
|
+
` $started = $false`,
|
|
608
|
+
` for ($i = 0; $i -lt 20; $i++) {`,
|
|
609
|
+
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
610
|
+
` if (-not $ok -and -not $started) { if (Invoke-DshRelaunch '回滚后') { $started = $true } }; Beat`,
|
|
611
|
+
` Start-Sleep -Seconds 5`,
|
|
612
|
+
` }`,
|
|
613
|
+
`}`,
|
|
614
|
+
`if ($ok) { if ($script:rolledBack) { SetState 'failed' '升级失败,已自动回滚到升级前版本(服务已恢复)' ; Log '升级失败已自动回滚,服务已恢复' } else { SetState 'done' '升级完成,服务已监听 ${port}' ; Log '服务已监听 ${port},升级完成' } } else { SetState 'failed' '拉起失败且自动回滚未生效:请手动运行 node ${binPath} web' ; Log '拉起失败且自动回滚未生效:请手动运行 node ${binPath} web(参考本日志排查)' }`,
|
|
615
|
+
`schtasks /delete /f /tn ${taskName} 2>$null`,
|
|
616
|
+
].filter((l) => l !== '').join('\r\n')
|
|
617
|
+
// 初始状态(备份完成、脚本即将执行)
|
|
618
|
+
try { writeFileSync(stateFile, 'starting|备份配置完成,启动升级脚本…', 'utf8') } catch {}
|
|
619
|
+
// 版本检查缓存作废:升级后 [框架] 面板要立刻显示新版本(否则 5 分钟内还显示"可升级到…")
|
|
620
|
+
setFwCheckCache(null)
|
|
621
|
+
// UTF-8 BOM:powershell 5.1 按 ANSI 读无 BOM 文件,中文会乱码——加 BOM 保证解析正确。
|
|
622
|
+
// 执行方式:schtasks 一次性计划任务(Task Scheduler 启动,独立于服务进程树/Job Object)——
|
|
623
|
+
// 服务进程被杀时,其子进程(execFile/detached 的 powershell)会被 Job/进程树连带杀死,
|
|
624
|
+
// 曾多次卡死在「停止服务」之后;计划任务彻底脱离,杀服务绝对影响不到升级脚本。
|
|
625
|
+
writeFile(ps1, `\uFEFF${lines}`, 'utf8').then(
|
|
626
|
+
() => {
|
|
627
|
+
const ps1Posix = ps1.replace(/\\/gu, '/')
|
|
628
|
+
// 无引号 /tr(重要):Task Scheduler 对带引号命令的解析会把 Command 拆坏成
|
|
629
|
+
// `"powershell ... -File \"`(非有效可执行文件)——任务显示 Ready、/run 报 SUCCESS
|
|
630
|
+
// 但永不执行(曾导致升级/重启脚本反复"已启动"却不动作、服务不停止)。实测无引号
|
|
631
|
+
// 格式(exe 与脚本路径均无空格时)任务正常执行、脚本完整跑通。仅当脚本路径含空格
|
|
632
|
+
// 时才退回带引号格式(schtasks 引号解析在服务 execFile 上下文不可靠,此时宁可用它)。
|
|
633
|
+
const tr = / /.test(ps1Posix)
|
|
634
|
+
? `"powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \\"${ps1Posix}\\""`
|
|
635
|
+
: `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ${ps1Posix}`
|
|
636
|
+
execFile('schtasks.exe', ['/create', '/f', '/tn', taskName, '/tr', tr, '/sc', 'once', '/st', '00:00'], { windowsHide: true }, (error) => {
|
|
637
|
+
if (error) {
|
|
638
|
+
// 兜底:schtasks 不可用时退回 detached(可能仍受 Job 影响,但尽力)
|
|
639
|
+
try { writeFileSync(stateFile, 'starting|升级脚本将通过 detached 启动(schtasks 不可用)|detached', 'utf8') } catch {}
|
|
640
|
+
execFile('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', ps1], { windowsHide: true, detached: true, stdio: 'ignore' }, () => {})
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
try { writeFileSync(stateFile, 'starting|升级脚本已通过计划任务启动|schtasks', 'utf8') } catch {}
|
|
644
|
+
// create 回调里立即 /run 会因任务注册未完成而静默失败(曾导致任务从未执行):
|
|
645
|
+
// 延迟 800ms 再 run;run 仍失败则退回 detached 兜底
|
|
646
|
+
setTimeout(() => {
|
|
647
|
+
execFile('schtasks.exe', ['/run', '/tn', taskName], { windowsHide: true }, (runError) => {
|
|
648
|
+
if (runError) {
|
|
649
|
+
try { writeFileSync(stateFile, 'starting|计划任务运行失败,改用 detached 兜底|detached-fallback', 'utf8') } catch {}
|
|
650
|
+
execFile('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', ps1], { windowsHide: true, detached: true, stdio: 'ignore' }, () => {})
|
|
651
|
+
}
|
|
652
|
+
})
|
|
653
|
+
}, 800)
|
|
654
|
+
})
|
|
655
|
+
},
|
|
656
|
+
() => {},
|
|
657
|
+
)
|
|
658
|
+
upgraded = true
|
|
659
|
+
steps.push(`框架升级脚本已启动:${current} → ${target}(在线下载安装,服务保持运行,成功后自动重启生效,约 1-3 分钟)`)
|
|
660
|
+
steps.push(`升级前配置已打包:${backupDir ?? '(跳过)'};框架本体回滚点:${rollbackDir ?? '(跳过)'}`)
|
|
661
|
+
} else if (hasUpdate) {
|
|
662
|
+
steps.push('检测到新版本,但无法定位框架安装目录(@deepseek-ai/dsh 解析失败),已取消升级——请修复框架安装后重试')
|
|
663
|
+
} else if (!hasUpdate) {
|
|
664
|
+
steps.push(registryError !== null ? `版本检测失败(${registryError}),无法确认是否有新版本——请检查网络后重试` : '当前已是最新版本,无需升级')
|
|
665
|
+
}
|
|
666
|
+
// 4) 配置移植说明:cordis.patch.yml / bundles 位于 profile(升级不触碰),天然保留;
|
|
667
|
+
// 重启后框架适配逻辑自动备份新版本快照 + 重打补丁
|
|
668
|
+
const hints = [
|
|
669
|
+
`现有配置已打包备份:${backupDir ?? '(跳过)'}。升级不触碰 profile 配置(cordis.patch.yml / bundles 天然保留)。`,
|
|
670
|
+
'重要:升级全程约 1-3 分钟,服务保持在线(进度实时可见),仅最后重启生效时页面短暂断开——期间请勿手动拉起服务或重启桌面端,否则会中断升级。',
|
|
671
|
+
...steps,
|
|
672
|
+
'升级完成后请重启 DSH 服务生效——重启后 Hub 自动:备份新版本配置快照、重打框架补丁(issue #5 容错)、给出适配提示。',
|
|
673
|
+
'官方配套组件(@deepseek-ai/*)随框架一起升级,请勿在市场单独更新。',
|
|
674
|
+
]
|
|
675
|
+
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded, backupDir, registryError, configMigrations, preflight, steps, hints })
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export { routeFrameworkUpgrade }
|