@noob-stupid/dsh-plugin-console 0.3.24 → 0.3.26
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 +117 -7
- package/README.zh.md +105 -7
- package/lib/client.js +474 -68
- package/lib/index.js +899 -69
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -200,6 +200,171 @@ function entryPkgMeta(moduleName, baseUrl) {
|
|
|
200
200
|
return meta
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/** 最小 semver:解析(含 prerelease/build)。 */
|
|
204
|
+
function parseSemverText(v) {
|
|
205
|
+
const m = String(v ?? '').trim().replace(/^v/u, '').match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u)
|
|
206
|
+
if (!m) return null
|
|
207
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: m[4] ?? null }
|
|
208
|
+
}
|
|
209
|
+
function compareSemverText(a, b) {
|
|
210
|
+
if (a.major !== b.major) return a.major - b.major
|
|
211
|
+
if (a.minor !== b.minor) return a.minor - b.minor
|
|
212
|
+
if (a.patch !== b.patch) return a.patch - b.patch
|
|
213
|
+
if (a.pre === null && b.pre === null) return 0
|
|
214
|
+
if (a.pre === null) return 1
|
|
215
|
+
if (b.pre === null) return -1
|
|
216
|
+
const pa = a.pre.split('.')
|
|
217
|
+
const pb = b.pre.split('.')
|
|
218
|
+
const len = Math.max(pa.length, pb.length)
|
|
219
|
+
for (let i = 0; i < len; i += 1) {
|
|
220
|
+
const xa = pa[i]
|
|
221
|
+
const xb = pb[i]
|
|
222
|
+
if (xa === undefined) return -1
|
|
223
|
+
if (xb === undefined) return 1
|
|
224
|
+
const na = /^\d+$/u.test(xa)
|
|
225
|
+
const nb = /^\d+$/u.test(xb)
|
|
226
|
+
if (na && nb) { const d = Number(xa) - Number(xb); if (d !== 0) return d; continue }
|
|
227
|
+
if (na) return -1
|
|
228
|
+
if (nb) return 1
|
|
229
|
+
const d = xa < xb ? -1 : xa > xb ? 1 : 0
|
|
230
|
+
if (d !== 0) return d
|
|
231
|
+
}
|
|
232
|
+
return 0
|
|
233
|
+
}
|
|
234
|
+
/** 单段范围匹配(^ ~ >= <= > < = 精确;返回 true = 满足)。 */
|
|
235
|
+
function semverCompareOne(v, raw) {
|
|
236
|
+
const rText = String(raw).trim()
|
|
237
|
+
const m = rText.match(/^(\^|~|>=|<=|>|<|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/u)
|
|
238
|
+
if (!m) return true
|
|
239
|
+
const op = m[1] ?? ''
|
|
240
|
+
const base = { major: Number(m[2]), minor: m[3] === undefined ? 0 : Number(m[3]), patch: m[4] === undefined ? 0 : Number(m[4]), pre: m[5] ?? null }
|
|
241
|
+
const hasMinor = m[3] !== undefined
|
|
242
|
+
const hasPatch = m[4] !== undefined
|
|
243
|
+
// npm 语义:prerelease 版本只与同 [major,minor,patch] 且范围带 prerelease 的声明匹配
|
|
244
|
+
const preAllowed = v.pre === null || (v.major === base.major && v.minor === base.minor && v.patch === base.patch && base.pre !== null)
|
|
245
|
+
switch (op) {
|
|
246
|
+
case '':
|
|
247
|
+
case '=':
|
|
248
|
+
return preAllowed && compareSemverText(v, base) === 0
|
|
249
|
+
case '<':
|
|
250
|
+
return preAllowed && compareSemverText(v, base) < 0
|
|
251
|
+
case '<=':
|
|
252
|
+
return preAllowed && compareSemverText(v, base) <= 0
|
|
253
|
+
case '>':
|
|
254
|
+
return preAllowed && compareSemverText(v, base) > 0
|
|
255
|
+
case '>=':
|
|
256
|
+
return preAllowed && compareSemverText(v, base) >= 0
|
|
257
|
+
case '~': {
|
|
258
|
+
if (!preAllowed) return false
|
|
259
|
+
if (!hasMinor) return v.major === base.major
|
|
260
|
+
if (!hasPatch) return v.major === base.major && v.minor === base.minor
|
|
261
|
+
return compareSemverText(v, base) >= 0 && !(v.major === base.major && v.minor > base.minor) && v.major === base.major
|
|
262
|
+
}
|
|
263
|
+
case '^': {
|
|
264
|
+
if (!preAllowed) return false
|
|
265
|
+
const upper = base.major === 0
|
|
266
|
+
? (base.minor === 0 ? { major: 0, minor: 0, patch: base.patch + 1, pre: null } : { major: 0, minor: base.minor + 1, patch: 0, pre: null })
|
|
267
|
+
: { major: base.major + 1, minor: 0, patch: 0, pre: null }
|
|
268
|
+
return compareSemverText(v, base) >= 0 && compareSemverText(v, upper) < 0
|
|
269
|
+
}
|
|
270
|
+
default:
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** 范围匹配:支持多个段以逗号/空白分隔(AND)与 `||`(OR)。 */
|
|
275
|
+
function semverRangeMatch(versionText, rangeText) {
|
|
276
|
+
const v = parseSemverText(versionText)
|
|
277
|
+
if (!v) return false
|
|
278
|
+
const alternatives = String(rangeText ?? '').split(/\s*\|\|\s*/u).filter(Boolean)
|
|
279
|
+
if (alternatives.length === 0) return false
|
|
280
|
+
return alternatives.some((alt) => {
|
|
281
|
+
const parts = alt.split(/\s*[,\s]\s*/u).filter(Boolean)
|
|
282
|
+
return parts.length > 0 && parts.every((part) => semverCompareOne(v, part))
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 宽松声明匹配(仅用于插件「显式声明兼容范围」):prerelease 版本按同线发布版判定——
|
|
287
|
+
* 作者声明 `>=0.1.2` 即代表支持 0.1.2 线,框架运行在 0.1.2-rc.1 应判定兼容。
|
|
288
|
+
*/
|
|
289
|
+
function semverRangeMatchLoose(versionText, rangeText) {
|
|
290
|
+
if (semverRangeMatch(versionText, rangeText)) return true
|
|
291
|
+
const v = parseSemverText(versionText)
|
|
292
|
+
if (v === null || v.pre === null) return false
|
|
293
|
+
return semverRangeMatch(`${v.major}.${v.minor}.${v.patch}`, rangeText)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 扫描已安装包源码,检测对已删除的 dsh-settings API 的引用
|
|
298
|
+
* (2026-09-04 教训:0.1.2-rc.1 起 settingsNamespace / installSettingsSection 已删除,
|
|
299
|
+
* 静态声明检查判 pass 是假通过;真实兼容性只有模块 import 时见分晓,而 loader 单行失败
|
|
300
|
+
* = 整个服务启动崩溃)。返回命中的符号列表,空数组 = 干净。
|
|
301
|
+
*/
|
|
302
|
+
function scanSettingsApiUsage(pkgDir) {
|
|
303
|
+
const found = []
|
|
304
|
+
const seen = new Set()
|
|
305
|
+
const walk = (dir, depth) => {
|
|
306
|
+
if (depth > 3) return
|
|
307
|
+
let entries = []
|
|
308
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
const full = join(dir, entry.name)
|
|
311
|
+
if (entry.isDirectory()) {
|
|
312
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
313
|
+
walk(full, depth + 1)
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
if (!entry.isFile() || !/\.(?:js|mjs|cjs)$/u.test(entry.name)) continue
|
|
317
|
+
if (seen.has(full)) continue
|
|
318
|
+
seen.add(full)
|
|
319
|
+
if (seen.size > 120) return
|
|
320
|
+
try {
|
|
321
|
+
const text = readFileSync(full, 'utf8').slice(0, 400000)
|
|
322
|
+
for (const sym of ['settingsNamespace', 'installSettingsSection']) {
|
|
323
|
+
if (text.includes(sym) && !found.includes(sym)) found.push(sym)
|
|
324
|
+
}
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
walk(pkgDir, 0)
|
|
329
|
+
return found
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 插件-框架兼容校验(框架升级适配门):
|
|
334
|
+
* 1) 显式兼容声明(dsh.engines.framework / dsh.compat.framework / engines.dsh):以声明为准;
|
|
335
|
+
* 2) 扫描依赖/peerDeps 的 @deepseek-ai/*:任一范围明确不含框架版本 → fail;
|
|
336
|
+
* 3) 【硬判据】提供 pkgDir 时扫描包源码:命中已删除的 dsh-settings API
|
|
337
|
+
* (settingsNamespace / installSettingsSection)→ 直接 fail(2026-09-04 教训:
|
|
338
|
+
* 0.3.6 全家仍引用旧 API,静态声明/依赖检查判 pass 是假通过,loader 单行失败 = 服务崩溃);
|
|
339
|
+
* 4) 无声明且依赖全部满足 → unknown(新版变动不大;由调用方在「版本已变化」前提下放行)。
|
|
340
|
+
* 返回 { decision: 'pass'|'fail'|'unknown', reason }。
|
|
341
|
+
*/
|
|
342
|
+
function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
|
|
343
|
+
const declared = pkg?.dsh?.engines?.framework ?? pkg?.dsh?.compat?.framework ?? pkg?.engines?.dsh ?? null
|
|
344
|
+
if (typeof declared === 'string' && declared !== '') {
|
|
345
|
+
const ok = semverRangeMatchLoose(frameworkVersion, declared)
|
|
346
|
+
if (!ok) return { decision: 'fail', reason: `声明兼容范围 ${declared} 不满足当前框架 ${frameworkVersion}` }
|
|
347
|
+
}
|
|
348
|
+
// 硬判据:实际源码扫描(比声明/依赖更接近真实兼容性)
|
|
349
|
+
if (pkgDir !== null && typeof pkgDir === 'string' && semverRangeMatchLoose(frameworkVersion, '>=0.1.2')) {
|
|
350
|
+
const broken = scanSettingsApiUsage(pkgDir)
|
|
351
|
+
if (broken.length > 0) {
|
|
352
|
+
return { decision: 'fail', reason: `源码仍引用 0.1.2 起已删除的 dsh-settings API(${broken.join('、')})——实际不兼容,启用会让整个服务启动崩溃` }
|
|
353
|
+
}
|
|
354
|
+
if (typeof declared === 'string' && declared !== '') return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion},且源码无已删除 API 引用` }
|
|
355
|
+
} else if (typeof declared === 'string' && declared !== '') {
|
|
356
|
+
return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion}` }
|
|
357
|
+
}
|
|
358
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.peerDependencies ?? {}), ...(pkg?.optionalDependencies ?? {}) }
|
|
359
|
+
const hits = Object.entries(deps).filter(([name]) => /^@deepseek-ai\//u.test(name))
|
|
360
|
+
const failing = hits.filter(([, range]) => typeof range === 'string' && range !== '' && !semverRangeMatch(frameworkVersion, range))
|
|
361
|
+
if (failing.length > 0) {
|
|
362
|
+
return { decision: 'fail', reason: `依赖声明不满足:${failing.map(([n, r]) => `${n}@${r}`).join('、')}(当前框架 ${frameworkVersion})` }
|
|
363
|
+
}
|
|
364
|
+
if (hits.length === 0) return { decision: 'unknown', reason: '未声明对 @deepseek-ai/* 的依赖,无法从声明判定(版本已更新时放行)' }
|
|
365
|
+
return { decision: 'unknown', reason: '依赖范围包含当前框架版本但未显式声明兼容(版本已更新时放行)' }
|
|
366
|
+
}
|
|
367
|
+
|
|
203
368
|
/** 读取用户额外 bundle(非官方模板)的补丁插入行 id 与包名,用于"额外插件"判定。 */
|
|
204
369
|
async function readExtraBundleRows(profileDir) {
|
|
205
370
|
const rows = new Set()
|
|
@@ -683,6 +848,18 @@ async function detectCompat(baseUrl) {
|
|
|
683
848
|
|
|
684
849
|
const FRAMEWORK_STATE_FILE = () => join(dshHome(), 'plugin-console', 'framework-state.json')
|
|
685
850
|
const FRAMEWORK_BACKUP_ROOT = () => join(dshHome(), 'plugin-console', 'framework-backups')
|
|
851
|
+
const COMPAT_PENDING_FILE = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
|
|
852
|
+
|
|
853
|
+
/** 读取框架升级适配门清单(compat-pending.json);损坏/缺失返回 null。 */
|
|
854
|
+
function readCompatPending() {
|
|
855
|
+
try { return JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return null }
|
|
856
|
+
}
|
|
857
|
+
function writeCompatPending(payload) {
|
|
858
|
+
try {
|
|
859
|
+
mkdirSync(dirname(COMPAT_PENDING_FILE()), { recursive: true })
|
|
860
|
+
writeFileSync(COMPAT_PENDING_FILE(), JSON.stringify(payload, null, 2), 'utf8')
|
|
861
|
+
} catch {}
|
|
862
|
+
}
|
|
686
863
|
|
|
687
864
|
/** 定位 @deepseek-ai/dsh-app-boot(与 @deepseek-ai/dsh 同级)。 */
|
|
688
865
|
function locateAppBootFile(baseUrl) {
|
|
@@ -1356,56 +1533,114 @@ async function fetchSkillMeta(repo, branch, skillDir) {
|
|
|
1356
1533
|
* 全部条目并发 + raw 主站与镜像双通道竞速,单条 4 秒封顶。
|
|
1357
1534
|
* 聚合仓库(根包 private+workspaces)额外检查子包是否有 dsh.bundle 清单:
|
|
1358
1535
|
* 任一子包可 `dsh plugin add` 直装 → aggregateInstallable = true(★ 筛选会包含它)。 */
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1536
|
+
const ENRICH_CACHE_FILE = () => join(dshHome(), 'plugin-console', 'enrich-cache.json')
|
|
1537
|
+
const ENRICH_CACHE_TTL = 24 * 60 * 60 * 1000
|
|
1538
|
+
|
|
1539
|
+
function readEnrichCache() {
|
|
1540
|
+
try {
|
|
1541
|
+
const j = JSON.parse(readFileSync(ENRICH_CACHE_FILE(), 'utf8'))
|
|
1542
|
+
return j !== null && typeof j === 'object' ? j : {}
|
|
1543
|
+
} catch { return {} }
|
|
1544
|
+
}
|
|
1545
|
+
function writeEnrichCache(cache) {
|
|
1546
|
+
try {
|
|
1547
|
+
mkdirSync(dirname(ENRICH_CACHE_FILE()), { recursive: true })
|
|
1548
|
+
const entries = Object.entries(cache).sort((a, b) => (b[1]?.at ?? 0) - (a[1]?.at ?? 0)).slice(0, 2000)
|
|
1549
|
+
writeFileSync(ENRICH_CACHE_FILE(), JSON.stringify(Object.fromEntries(entries), null, 2), 'utf8')
|
|
1550
|
+
} catch {}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/** 单项识别:官方通道 / 聚合仓库 / 技能 / 套装(失败返回 official=null)。 */
|
|
1554
|
+
async function enrichItemOne(item) {
|
|
1555
|
+
let official = /^deepseek-ai\//u.test(item.fullName ?? '') ? true : null
|
|
1556
|
+
let aggregate = false
|
|
1557
|
+
let aggregateInstallable = false
|
|
1558
|
+
let hasSkill = false
|
|
1559
|
+
let hasSuite = false
|
|
1560
|
+
try {
|
|
1561
|
+
const branch = item.defaultBranch ?? 'main'
|
|
1562
|
+
const base = `https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/package.json`
|
|
1563
|
+
const [pkgResult, skillResult, suiteResult] = await Promise.allSettled([
|
|
1564
|
+
Promise.any([
|
|
1565
|
+
curlText(base, 4000),
|
|
1566
|
+
curlText(`https://ghproxy.net/${base}`, 4000),
|
|
1567
|
+
]),
|
|
1568
|
+
detectSkillRepo(item.fullName, branch),
|
|
1569
|
+
rawTextWithFallback(item.fullName, branch, '.gitmodules'),
|
|
1570
|
+
])
|
|
1571
|
+
if (suiteResult.status === 'fulfilled' && suiteResult.value !== null) {
|
|
1572
|
+
hasSuite = true
|
|
1573
|
+
}
|
|
1574
|
+
if (pkgResult.status === 'fulfilled') {
|
|
1575
|
+
const pkg = JSON.parse(pkgResult.value)
|
|
1576
|
+
if (typeof pkg.dsh?.bundle?.patch === 'string') {
|
|
1577
|
+
official = true
|
|
1578
|
+
} else if (pkg.private === true && (Array.isArray(pkg.workspaces) || /(^|-)dsh[-/]/u.test(String(pkg.name ?? '')))) {
|
|
1579
|
+
aggregate = true
|
|
1580
|
+
try {
|
|
1581
|
+
const tree = await curlJson(`https://api.github.com/repos/${item.fullName}/git/trees/${encodeURIComponent(branch)}?recursive=1`, 6000)
|
|
1582
|
+
const pkgPaths = (tree.tree ?? [])
|
|
1583
|
+
.filter((n) => n.type === 'blob' && /^packages\/[^/]+\/package\.json$/u.test(n.path))
|
|
1584
|
+
.map((n) => n.path)
|
|
1585
|
+
.slice(0, 12)
|
|
1586
|
+
if (pkgPaths.length > 0) {
|
|
1587
|
+
const subs = await Promise.all(pkgPaths.map((p) => curlText(`https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/${p}`, 4000)
|
|
1588
|
+
.then((t) => { try { return JSON.parse(t) } catch { return null } })
|
|
1589
|
+
.catch(() => null)))
|
|
1590
|
+
if (subs.some((sp) => sp && typeof sp.dsh?.bundle?.patch === 'string')) {
|
|
1591
|
+
aggregateInstallable = true
|
|
1399
1592
|
}
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1593
|
+
}
|
|
1594
|
+
} catch {}
|
|
1402
1595
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1596
|
+
}
|
|
1597
|
+
if (skillResult.status === 'fulfilled' && skillResult.value?.hasSkill === true) {
|
|
1598
|
+
hasSkill = true
|
|
1599
|
+
}
|
|
1600
|
+
} catch {}
|
|
1601
|
+
return { ...item, official, aggregate, aggregateInstallable, hasSkill, hasSuite }
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* 批量识别(★ 筛选数据源):并发限流 + 24h 结果缓存。
|
|
1606
|
+
* 网络黑洞期(raw.githubusercontent 大部分拉取失败)自动回退缓存中的上次判定,
|
|
1607
|
+
* 保证「只看官方」不因瞬时网络而坍缩成 0/1 条。
|
|
1608
|
+
*/
|
|
1609
|
+
async function enrichItems(items) {
|
|
1610
|
+
const cache = readEnrichCache()
|
|
1611
|
+
const out = new Array(items.length)
|
|
1612
|
+
let next = 0
|
|
1613
|
+
let cacheDirty = false
|
|
1614
|
+
async function worker() {
|
|
1615
|
+
while (true) {
|
|
1616
|
+
const i = next
|
|
1617
|
+
next += 1
|
|
1618
|
+
if (i >= items.length) return
|
|
1619
|
+
const item = items[i]
|
|
1620
|
+
const key = `${item.fullName}@${item.defaultBranch ?? 'main'}`
|
|
1621
|
+
const cached = cache[key]
|
|
1622
|
+
try {
|
|
1623
|
+
if (cached !== undefined && typeof cached?.at === 'number' && Date.now() - cached.at < ENRICH_CACHE_TTL && cached.data && (cached.data.official !== null || item.fullName.startsWith('deepseek-ai/'))) {
|
|
1624
|
+
out[i] = { ...item, ...cached.data }
|
|
1625
|
+
continue
|
|
1626
|
+
}
|
|
1627
|
+
let result = await enrichItemOne(item)
|
|
1628
|
+
// 本次失败 → 回退缓存(哪怕已过期),避免「看天吃饭」
|
|
1629
|
+
if ((result.official === null && !/^deepseek-ai\//u.test(item.fullName ?? '')) && cached?.data) {
|
|
1630
|
+
result = { ...item, ...cached.data }
|
|
1631
|
+
} else {
|
|
1632
|
+
cache[key] = { at: Date.now(), data: { official: result.official, aggregate: result.aggregate, aggregateInstallable: result.aggregateInstallable, hasSkill: result.hasSkill, hasSuite: result.hasSuite } }
|
|
1633
|
+
cacheDirty = true
|
|
1634
|
+
}
|
|
1635
|
+
out[i] = result
|
|
1636
|
+
} catch {
|
|
1637
|
+
out[i] = cached?.data ? { ...item, ...cached.data } : { ...item, official: null, aggregate: false, aggregateInstallable: false, hasSkill: false, hasSuite: false }
|
|
1405
1638
|
}
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
}))
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
await Promise.all(Array.from({ length: Math.min(12, items.length) }, worker))
|
|
1642
|
+
if (cacheDirty) writeEnrichCache(cache)
|
|
1643
|
+
return out
|
|
1409
1644
|
}
|
|
1410
1645
|
|
|
1411
1646
|
function githubRepoInfo(repo) {
|
|
@@ -1838,6 +2073,72 @@ const aiJobs = new Map()
|
|
|
1838
2073
|
let aiJobSeq = 0
|
|
1839
2074
|
/** 组件注册表(服务型组件:启动/停止/状态控制按钮的数据源)。 */
|
|
1840
2075
|
const COMPONENTS_FILE = join(homedir(), '.dsh', 'plugin-console', 'components.json')
|
|
2076
|
+
/** AI 赋能任务持久化文件(DSH 重启不丢计划/结果;running 任务重启后标记中断)。 */
|
|
2077
|
+
const AI_JOBS_FILE = join(homedir(), '.dsh', 'plugin-console', 'ai-jobs.json')
|
|
2078
|
+
/** 仓库落地根目录(可配置,默认 ~/.dsh/repos)。 */
|
|
2079
|
+
let reposDirCache = null
|
|
2080
|
+
const REPO_LAND_CONF = join(homedir(), '.dsh', 'plugin-console', 'repo-land.json')
|
|
2081
|
+
function getReposDir() {
|
|
2082
|
+
if (reposDirCache !== null) return reposDirCache
|
|
2083
|
+
try {
|
|
2084
|
+
if (existsSync(REPO_LAND_CONF)) {
|
|
2085
|
+
const conf = JSON.parse(readFileSync(REPO_LAND_CONF, 'utf8'))
|
|
2086
|
+
if (typeof conf.dir === 'string' && conf.dir.trim() !== '') {
|
|
2087
|
+
reposDirCache = conf.dir.trim()
|
|
2088
|
+
return reposDirCache
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
} catch {}
|
|
2092
|
+
reposDirCache = join(homedir(), '.dsh', 'repos')
|
|
2093
|
+
return reposDirCache
|
|
2094
|
+
}
|
|
2095
|
+
function setReposDir(dir) {
|
|
2096
|
+
reposDirCache = String(dir ?? '').trim()
|
|
2097
|
+
mkdirSync(dirname(REPO_LAND_CONF), { recursive: true })
|
|
2098
|
+
writeFileSync(REPO_LAND_CONF, JSON.stringify({ dir: reposDirCache }, null, 2), 'utf8')
|
|
2099
|
+
return reposDirCache
|
|
2100
|
+
}
|
|
2101
|
+
/** 已落地仓库列表:扫描 dir 下两级目录(owner/name 下含 .git)。 */
|
|
2102
|
+
function listLandedRepos() {
|
|
2103
|
+
const dir = getReposDir()
|
|
2104
|
+
const out = []
|
|
2105
|
+
try {
|
|
2106
|
+
if (!existsSync(dir)) return out
|
|
2107
|
+
for (const owner of readdirSync(dir, { withFileTypes: true })) {
|
|
2108
|
+
if (!owner.isDirectory() || owner.name.startsWith('.')) continue
|
|
2109
|
+
const ownerDir = join(dir, owner.name)
|
|
2110
|
+
for (const entry of readdirSync(ownerDir, { withFileTypes: true })) {
|
|
2111
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
2112
|
+
const repoDir = join(ownerDir, entry.name)
|
|
2113
|
+
if (existsSync(join(repoDir, '.git'))) {
|
|
2114
|
+
out.push({ owner: owner.name, name: entry.name, repo: `${owner.name}/${entry.name}`, path: repoDir })
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
} catch {}
|
|
2119
|
+
return out
|
|
2120
|
+
}
|
|
2121
|
+
function saveAiJobs() {
|
|
2122
|
+
try {
|
|
2123
|
+
mkdirSync(dirname(AI_JOBS_FILE), { recursive: true })
|
|
2124
|
+
const arr = [...aiJobs.values()].map((j) => ({ id: j.id, source: j.source, status: j.status, stage: j.stage, plan: j.plan ?? null, logText: j.logText ?? '', stepStates: j.stepStates ?? [], progress: j.progress ?? null, error: j.error ?? null, createdAt: j.createdAt, finishedAt: j.finishedAt ?? null }))
|
|
2125
|
+
writeFileSync(AI_JOBS_FILE, JSON.stringify(arr, null, 2), 'utf8')
|
|
2126
|
+
} catch {}
|
|
2127
|
+
}
|
|
2128
|
+
function loadAiJobs() {
|
|
2129
|
+
try {
|
|
2130
|
+
if (!existsSync(AI_JOBS_FILE)) return
|
|
2131
|
+
const arr = JSON.parse(readFileSync(AI_JOBS_FILE, 'utf8'))
|
|
2132
|
+
for (const j of arr) {
|
|
2133
|
+
if (!j || typeof j.id !== 'string') continue
|
|
2134
|
+
let job = j
|
|
2135
|
+
if (job.status === 'running') {
|
|
2136
|
+
job = { ...job, status: 'failed', error: 'DSH 重启,任务中断(可重新发起)', finishedAt: Date.now() }
|
|
2137
|
+
}
|
|
2138
|
+
aiJobs.set(job.id, job)
|
|
2139
|
+
}
|
|
2140
|
+
} catch {}
|
|
2141
|
+
}
|
|
1841
2142
|
/** 静态插件索引内存缓存(/market-index 用)。 */
|
|
1842
2143
|
let marketIndexCache = null
|
|
1843
2144
|
|
|
@@ -1859,6 +2160,7 @@ function installJobView(job) {
|
|
|
1859
2160
|
source: job.source ?? 'github',
|
|
1860
2161
|
curlNote: job.curlNote ?? null,
|
|
1861
2162
|
bundleNote: job.bundleNote ?? null,
|
|
2163
|
+
compatNote: job.compatNote ?? null,
|
|
1862
2164
|
kind: job.kind ?? 'plugin',
|
|
1863
2165
|
skillName: job.skillName ?? null,
|
|
1864
2166
|
skillDir: job.skillDir ?? null,
|
|
@@ -2453,6 +2755,8 @@ function aiJobView(job) {
|
|
|
2453
2755
|
logText: (job.logText ?? '').slice(-6000),
|
|
2454
2756
|
createdAt: job.createdAt,
|
|
2455
2757
|
finishedAt: job.finishedAt ?? null,
|
|
2758
|
+
workspace: job.parentCwd ?? null,
|
|
2759
|
+
frameworkCheck: job.frameworkCheck ?? null,
|
|
2456
2760
|
}
|
|
2457
2761
|
}
|
|
2458
2762
|
|
|
@@ -2588,6 +2892,7 @@ function isAllowedWritePath(p, profileDir) {
|
|
|
2588
2892
|
norm(homedir()) + '/.openviking',
|
|
2589
2893
|
norm(homedir()) + '/.cache/openviking',
|
|
2590
2894
|
norm(homedir()) + '/.local/share/openviking',
|
|
2895
|
+
norm(homedir()) + '/.dsh/repos',
|
|
2591
2896
|
norm(profileDir),
|
|
2592
2897
|
'D:/OpenVikingData',
|
|
2593
2898
|
]
|
|
@@ -2611,7 +2916,16 @@ function jobLog(job, line) {
|
|
|
2611
2916
|
|
|
2612
2917
|
/** 已知组件内置模板:跳过 AI 调研,直接产出经过验证的部署计划(当前仅 OpenViking)。 */
|
|
2613
2918
|
function builtinPlanFor(source) {
|
|
2614
|
-
|
|
2919
|
+
// 精确匹配 OpenViking 服务器本体(npm/pip 包名 openviking 或 volcengine/OpenViking 仓库),
|
|
2920
|
+
// 防止宽泛子串匹配误命中 openclaw_openviking_skill 之类的第三方包(曾实际发生误判)。
|
|
2921
|
+
const norm = String(source).trim().toLowerCase()
|
|
2922
|
+
.replace(/^https?:\/\//u, '')
|
|
2923
|
+
.replace(/^www\./u, '')
|
|
2924
|
+
.replace(/^github\.com\//u, '')
|
|
2925
|
+
.replace(/^raw\.githubusercontent\.com\//u, '')
|
|
2926
|
+
.replace(/\.git$/u, '')
|
|
2927
|
+
.replace(/\/$/u, '')
|
|
2928
|
+
if (norm !== 'openviking' && norm !== 'volcengine/openviking') return null
|
|
2615
2929
|
const home = homedir()
|
|
2616
2930
|
const storage = /^[\x00-\x7F]+$/u.test(home) ? `${home}/.openviking/data` : 'D:/OpenVikingData'
|
|
2617
2931
|
const vlm = resolveVlmForOpenViking()
|
|
@@ -2691,6 +3005,9 @@ async function aiEmpowerPlan(job, ctx, profileDir) {
|
|
|
2691
3005
|
'你是 DSH 插件控制台挂载的「AI 赋能」部署规划器。用户想把下面的组件完整部署到本机。',
|
|
2692
3006
|
`目标来源:${job.source}`,
|
|
2693
3007
|
'',
|
|
3008
|
+
'## 框架适配检测(控制台权威结果,规划时必须引用并向用户解释)',
|
|
3009
|
+
frameworkCheckPromptText(job.frameworkCheck),
|
|
3010
|
+
'',
|
|
2694
3011
|
'【任务】只做只读调研与规划,禁止执行任何安装/写文件/启动服务/杀进程命令(curl 的 GET 抓取、gh api 查询、读取文件是允许的)。',
|
|
2695
3012
|
'调研材料:npm README / GitHub README / docs/ 目录 / package.json / install 说明。',
|
|
2696
3013
|
'全部调研完成后,输出且只输出一个 ```json ... ``` 代码块,内容为部署计划;代码块之外不要输出任何文字。',
|
|
@@ -2732,7 +3049,14 @@ async function aiEmpowerPlan(job, ctx, profileDir) {
|
|
|
2732
3049
|
try {
|
|
2733
3050
|
const agents = ctx.get('agents')
|
|
2734
3051
|
const list = typeof agents?.roots === 'function' ? agents.roots() : (typeof agents?.list === 'function' ? agents.list() : [])
|
|
2735
|
-
|
|
3052
|
+
// 父代理选择:优先 cwd 与 DSH 服务工作区一致的会话(子代理 cwd 继承父会话),
|
|
3053
|
+
// 避免多工作区/多会话时借到别的 workspace 的会话导致调研错位
|
|
3054
|
+
const svcCwd = String(process.cwd() ?? '').replace(/[\\/]+$/u, '')
|
|
3055
|
+
const cwdOf = (a) => {
|
|
3056
|
+
try { return String(a?.session?.header?.cwd ?? a?.cwd ?? '').replace(/[\\/]+$/u, '') } catch { return '' }
|
|
3057
|
+
}
|
|
3058
|
+
parent = list.find((a) => cwdOf(a) !== '' && cwdOf(a) === svcCwd) ?? list[0] ?? null
|
|
3059
|
+
job.parentCwd = parent ? cwdOf(parent) : null
|
|
2736
3060
|
} catch {}
|
|
2737
3061
|
if (parent === null) {
|
|
2738
3062
|
job.status = 'failed'
|
|
@@ -2787,6 +3111,7 @@ async function aiEmpowerPlan(job, ctx, profileDir) {
|
|
|
2787
3111
|
job.error = `AI 赋能规划异常:${error instanceof Error ? error.message : String(error)}`
|
|
2788
3112
|
}
|
|
2789
3113
|
job.finishedAt = Date.now()
|
|
3114
|
+
saveAiJobs()
|
|
2790
3115
|
}
|
|
2791
3116
|
|
|
2792
3117
|
/** 从子代理输出中提取 ```json ... ``` 计划。 */
|
|
@@ -2846,6 +3171,7 @@ async function aiEmpowerExecute(job, ctx, profileDir, selected) {
|
|
|
2846
3171
|
job.progress = { done, total: chosenList.length }
|
|
2847
3172
|
job.finishedAt = Date.now()
|
|
2848
3173
|
jobLog(job, job.status === 'done' ? '🎉 AI 赋能部署完成' : '⛔ AI 赋能部署中止')
|
|
3174
|
+
saveAiJobs()
|
|
2849
3175
|
}
|
|
2850
3176
|
|
|
2851
3177
|
/** 单步执行器:动作白名单 + 路径白名单 + 破坏性命令拦截。 */
|
|
@@ -3032,7 +3358,23 @@ async function compStatus(id) {
|
|
|
3032
3358
|
}
|
|
3033
3359
|
}
|
|
3034
3360
|
const running = Boolean(record.pid && processAlive(record.pid)) || healthy === true
|
|
3035
|
-
return { id, name: record.name, running, healthy, pid: record.pid ?? null, port: record.port ?? null }
|
|
3361
|
+
return { id, name: record.name, running, healthy, pid: record.pid ?? null, port: record.port ?? null, autoStart: record.autoStart === true }
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
/** DSH 启动时自动拉起标记 autoStart 的组件(幂等:已健康/已运行则跳过)。 */
|
|
3365
|
+
async function autostartComponents() {
|
|
3366
|
+
for (const c of findComponents()) {
|
|
3367
|
+
if (c.autoStart !== true) continue
|
|
3368
|
+
if (c.healthUrl) {
|
|
3369
|
+
try {
|
|
3370
|
+
const res = await fetch(c.healthUrl, { signal: AbortSignal.timeout(2500) })
|
|
3371
|
+
if (res.ok) continue
|
|
3372
|
+
} catch {}
|
|
3373
|
+
} else if (c.pid && processAlive(c.pid)) {
|
|
3374
|
+
continue
|
|
3375
|
+
}
|
|
3376
|
+
try { await compStart(c.id) } catch {}
|
|
3377
|
+
}
|
|
3036
3378
|
}
|
|
3037
3379
|
|
|
3038
3380
|
function processAlive(pid) {
|
|
@@ -3072,6 +3414,190 @@ async function syncAggregateSubpackageVersions(profileDir, packageName, registri
|
|
|
3072
3414
|
return updated
|
|
3073
3415
|
}
|
|
3074
3416
|
|
|
3417
|
+
/** 当前运行框架版本(@deepseek-ai/dsh package.json)。 */
|
|
3418
|
+
function currentFrameworkVersion(ctx) {
|
|
3419
|
+
try {
|
|
3420
|
+
const require = createRequire(ctx?.baseUrl ?? 'file:///')
|
|
3421
|
+
const pkgPath = require.resolve('@deepseek-ai/dsh/package.json')
|
|
3422
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
3423
|
+
return typeof pkg.version === 'string' ? pkg.version : null
|
|
3424
|
+
} catch {
|
|
3425
|
+
return null
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
/** 定位框架安装根(顶层 node_modules):优先运行进程入口,其次从包目录上溯找含 .pnpm 的 node_modules。 */
|
|
3430
|
+
function resolveFrameworkRootNodeModules(fromDir) {
|
|
3431
|
+
try {
|
|
3432
|
+
const entry = process.argv[1]
|
|
3433
|
+
if (typeof entry === 'string' && /bin\.js$/u.test(entry)) {
|
|
3434
|
+
const nm = dirname(dirname(entry))
|
|
3435
|
+
if (existsSync(join(nm, '.pnpm')) && existsSync(join(nm, '@deepseek-ai'))) return nm
|
|
3436
|
+
}
|
|
3437
|
+
} catch {}
|
|
3438
|
+
let dir = typeof fromDir === 'string' && fromDir !== '' ? fromDir : null
|
|
3439
|
+
for (let i = 0; i < 24 && dir !== null; i += 1) {
|
|
3440
|
+
if (basename(dir) === 'node_modules' && existsSync(join(dir, '.pnpm'))) return dir
|
|
3441
|
+
const parent = dirname(dir)
|
|
3442
|
+
if (parent === dir) break
|
|
3443
|
+
dir = parent
|
|
3444
|
+
}
|
|
3445
|
+
return null
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
/**
|
|
3449
|
+
* 框架全树 checkpoint(可靠回滚点):镜像 .pnpm 中所有 @deepseek-ai 条目「自包」内容 +
|
|
3450
|
+
* 顶层 @deepseek-ai scope + lock.yaml。只镜像自包、不跟随依赖 junction,避免重复拷贝;
|
|
3451
|
+
* 恢复时按同路径写回 .pnpm 条目即可让整个依赖世界回到升级前。
|
|
3452
|
+
*/
|
|
3453
|
+
function checkpointFrameworkTree(fwRoot, destRoot) {
|
|
3454
|
+
const pnpmRoot = join(fwRoot, '.pnpm')
|
|
3455
|
+
const dest = join(destRoot, 'fw-tree', String(Date.now()))
|
|
3456
|
+
mkdirSync(dest, { recursive: true })
|
|
3457
|
+
let mirrored = 0
|
|
3458
|
+
const selfNameOf = (entryName) => entryName.slice('@deepseek-ai+'.length).split('@')[0]
|
|
3459
|
+
for (const entry of readdirSync(pnpmRoot, { withFileTypes: true })) {
|
|
3460
|
+
if (!entry.isDirectory() || !entry.name.startsWith('@deepseek-ai+')) continue
|
|
3461
|
+
const name = selfNameOf(entry.name)
|
|
3462
|
+
const selfDir = join(pnpmRoot, entry.name, 'node_modules', '@deepseek-ai', name)
|
|
3463
|
+
if (!existsSync(join(selfDir, 'package.json'))) continue
|
|
3464
|
+
copyTree(selfDir, join(dest, '.pnpm', entry.name, 'node_modules', '@deepseek-ai', name))
|
|
3465
|
+
mirrored += 1
|
|
3466
|
+
}
|
|
3467
|
+
const topScope = join(fwRoot, '@deepseek-ai')
|
|
3468
|
+
if (existsSync(topScope)) copyTree(topScope, join(dest, 'top-@deepseek-ai'))
|
|
3469
|
+
try { copyFileSync(join(pnpmRoot, 'lock.yaml'), join(dest, 'lock.yaml')) } catch {}
|
|
3470
|
+
try { copyFileSync(join(fwRoot, 'package.json'), join(dest, 'fw-package.json')) } catch {}
|
|
3471
|
+
return { dest, mirrored }
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
/** 移除补丁中的单行 disabled/forced 覆盖块(兼容门解锁用)。 */
|
|
3475
|
+
function removeDisableBlock(patchPath, rowId) {
|
|
3476
|
+
return queuedWrite(async () => {
|
|
3477
|
+
const { text } = await readPatchState(patchPath)
|
|
3478
|
+
const overrideRe = new RegExp(`^- id: ${escapeRegExp(rowId)}\\s*\\r?\\n {2}disabled: (true|false)\\s*\\r?\\n`, 'mu')
|
|
3479
|
+
const next = text.replace(overrideRe, '')
|
|
3480
|
+
if (next !== text) await writeFile(patchPath, sanitizePatchText(next), 'utf8')
|
|
3481
|
+
})
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
/** 从「子包版本对齐」记录解析纯包名(如 '@linxin666/dsh-pet@0.2.1(新装)')。 */
|
|
3485
|
+
function syncNameFromNote(note) {
|
|
3486
|
+
const m = String(note ?? '').match(/^((?:@[^/]+\/)?[^@]+)/u)
|
|
3487
|
+
return m ? m[1] : null
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
/**
|
|
3491
|
+
* 框架升级适配门:安装/更新完成后自动校验兼容性,
|
|
3492
|
+
* 通过(版本已变化 + 未在声明/依赖层面明确不兼容)则移除补丁禁用块并解锁启用。
|
|
3493
|
+
*/
|
|
3494
|
+
async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patchPath, ctx }) {
|
|
3495
|
+
const pending = readCompatPending()
|
|
3496
|
+
if (!pending || !Array.isArray(pending.pending)) return { ran: false }
|
|
3497
|
+
const interested = new Set([packageName, ...(syncedNames ?? [])].filter((n) => n !== null))
|
|
3498
|
+
const targets = pending.pending.filter((p) => (p.status ?? 'pending') === 'pending' && interested.has(p.moduleName))
|
|
3499
|
+
if (targets.length === 0) return { ran: false }
|
|
3500
|
+
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : '?'
|
|
3501
|
+
const adopted = []
|
|
3502
|
+
const kept = []
|
|
3503
|
+
for (const p of targets) {
|
|
3504
|
+
let pkg = null
|
|
3505
|
+
let pkgDir = null
|
|
3506
|
+
try {
|
|
3507
|
+
const require = createRequire(join(profileDir, 'package.json'))
|
|
3508
|
+
const pkgPath = require.resolve(`${p.moduleName}/package.json`)
|
|
3509
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
3510
|
+
pkgDir = dirname(pkgPath)
|
|
3511
|
+
} catch {}
|
|
3512
|
+
const version = typeof pkg?.version === 'string' ? pkg.version : null
|
|
3513
|
+
const changed = version !== null && version !== p.version
|
|
3514
|
+
const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'unknown', reason: '无法读取包信息' }
|
|
3515
|
+
if (changed && check.decision !== 'fail') {
|
|
3516
|
+
try { await removeDisableBlock(patchPath, p.rowId) } catch {}
|
|
3517
|
+
p.status = 'adopted'
|
|
3518
|
+
p.adoptedAt = Date.now()
|
|
3519
|
+
p.adoptedVersion = version
|
|
3520
|
+
p.adoptedFramework = fwVer
|
|
3521
|
+
p.check = check.decision
|
|
3522
|
+
p.checkNote = check.reason ?? null
|
|
3523
|
+
adopted.push({ rowId: p.rowId, moduleName: p.moduleName, version, check: check.decision })
|
|
3524
|
+
} else {
|
|
3525
|
+
kept.push({ rowId: p.rowId, moduleName: p.moduleName, reason: !changed ? '版本未变化(无更新可适配)' : (check.reason ?? '兼容校验未通过') })
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
if (adopted.length > 0 || kept.length > 0) writeCompatPending(pending)
|
|
3529
|
+
const note = adopted.length > 0
|
|
3530
|
+
? `兼容适配:已解锁 ${adopted.length} 行(${adopted.map((a) => a.rowId).join('、')})`
|
|
3531
|
+
+ (kept.length > 0 ? `;仍待适配 ${kept.length} 行(${kept.map((k) => `${k.rowId} ${k.reason}`).join('、')})` : '')
|
|
3532
|
+
: `兼容适配:暂未解锁(${kept.map((k) => `${k.rowId} ${k.reason}`).join('、')})`
|
|
3533
|
+
return { ran: true, adopted, kept, note }
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
/** AI 赋能框架适配预检:查 registry 最新版声明 + 兼容门清单,给出权威说明。 */
|
|
3537
|
+
async function frameworkCompatReportFor(source, ctx, profileDir) {
|
|
3538
|
+
const fwVer = currentFrameworkVersion(ctx)
|
|
3539
|
+
const pending = readCompatPending()
|
|
3540
|
+
const norm = String(source ?? '').trim()
|
|
3541
|
+
const isNpm = norm.startsWith('@') || !norm.includes('/')
|
|
3542
|
+
const installed = (() => {
|
|
3543
|
+
try {
|
|
3544
|
+
const require = createRequire(join(profileDir, 'package.json'))
|
|
3545
|
+
const pkgPath = require.resolve(`${norm}/package.json`)
|
|
3546
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
|
|
3547
|
+
} catch { return null }
|
|
3548
|
+
})()
|
|
3549
|
+
const pendHits = (pending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending' && p.moduleName === norm)
|
|
3550
|
+
if (!isNpm) {
|
|
3551
|
+
return {
|
|
3552
|
+
kind: 'repo', frameworkVersion: fwVer, packageName: null, latest: null, check: null,
|
|
3553
|
+
pending: pendHits, installedVersion: null,
|
|
3554
|
+
summary: pendHits.length > 0
|
|
3555
|
+
? `该来源命中兼容门清单(${pending.frameworkVersion}),GitHub 仓库无法在线预检——更新/部署完成后会由控制台自动校验解锁`
|
|
3556
|
+
: `GitHub 仓库来源无法预检 npm 声明,部署后以实际运行为准(框架当前 ${fwVer ?? '未知'})`,
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
const name = norm.startsWith('@')
|
|
3560
|
+
? '@' + norm.slice(1).split('@')[0]
|
|
3561
|
+
: norm.split('@')[0]
|
|
3562
|
+
let latest = null
|
|
3563
|
+
let versionMeta = null
|
|
3564
|
+
try {
|
|
3565
|
+
const data = await fetchJsonUrl(`https://registry.npmmirror.com/${name.replace('/', '%2f')}`)
|
|
3566
|
+
latest = data?.['dist-tags']?.latest ?? data?.['dist-tags']?.next ?? null
|
|
3567
|
+
if (latest !== null) versionMeta = data?.versions?.[latest] ?? null
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
return {
|
|
3570
|
+
kind: 'npm', frameworkVersion: fwVer, packageName: name, latest: null, check: null,
|
|
3571
|
+
pending: pendHits, installedVersion: installed,
|
|
3572
|
+
summary: `registry 预检失败(${error instanceof Error ? error.message : String(error)});本机已装 ${installed ?? '?'}${pendHits.length > 0 ? ',处于兼容门禁用中' : ''}`,
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
const check = versionMeta !== null ? checkPluginFrameworkCompat(versionMeta, fwVer) : null
|
|
3576
|
+
const parts = []
|
|
3577
|
+
if (pendHits.length > 0) parts.push(`已在兼容门清单(${pending.upgradeFrom ?? '?'} → ${pending.frameworkVersion})中强制禁用,当前已装 ${installed ?? pendHits[0]?.version ?? '?'}`)
|
|
3578
|
+
if (latest !== null) {
|
|
3579
|
+
if (check?.decision === 'fail') parts.push(`最新版 ${latest} 不满足框架 ${fwVer}(${check.reason})——请勿启用,等待适配版本`)
|
|
3580
|
+
else if (check?.decision === 'pass') parts.push(`最新版 ${latest} 声明兼容框架 ${fwVer},可放心启用`)
|
|
3581
|
+
else parts.push(`最新版 ${latest} 未声明框架兼容(${check?.reason ?? '无声明'}),建议更新后启用并观察`)
|
|
3582
|
+
}
|
|
3583
|
+
return {
|
|
3584
|
+
kind: 'npm', frameworkVersion: fwVer, packageName: name, latest, check, pending: pendHits, installedVersion: installed,
|
|
3585
|
+
summary: parts.length > 0 ? parts.join(';') : `框架 ${fwVer},无待适配记录`,
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
/** 把框架适配检测结果格式化为给子代理的提示段。 */
|
|
3590
|
+
function frameworkCheckPromptText(fc) {
|
|
3591
|
+
if (fc === null || fc === undefined) return '(不可用)'
|
|
3592
|
+
return [
|
|
3593
|
+
`- 当前框架版本:${fc.frameworkVersion ?? '未知'}`,
|
|
3594
|
+
`- ${fc.kind === 'npm' ? `npm 包 ${fc.packageName ?? '?'}` : '来源类型'}:registry 最新 ${fc.latest ?? '未知'}${fc.installedVersion ? `;本机已装 ${fc.installedVersion}` : ''}`,
|
|
3595
|
+
fc.pending && fc.pending.length > 0 ? `- ⚠ 已命中兼容门:${fc.pending.map((p) => p.rowId).join('、')} 处于强制禁用(版本 ${fc.pending[0]?.version ?? '?'}),升级后仅更新并通过校验才解锁` : null,
|
|
3596
|
+
fc.check ? `- 声明/依赖校验:${fc.check.decision === 'fail' ? '不兼容' : fc.check.decision === 'pass' ? '兼容' : '未知'}(${fc.check.reason})` : null,
|
|
3597
|
+
'- 计划总结必须向用户说明上述适配结论;若校验为 fail,不要建议启用,应建议「更新到最新版后再启用」。',
|
|
3598
|
+
].filter((s) => s !== null).join('\n')
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3075
3601
|
async function runInstallJob(job, ctx) {
|
|
3076
3602
|
try {
|
|
3077
3603
|
job.stage = 'preparing'
|
|
@@ -3302,9 +3828,11 @@ async function runInstallJob(job, ctx) {
|
|
|
3302
3828
|
}
|
|
3303
3829
|
job.packageName = installedName
|
|
3304
3830
|
// 聚合包子包版本对齐(确保更新后所有子包也到新版声明版本)
|
|
3831
|
+
let syncedNames = []
|
|
3305
3832
|
try {
|
|
3306
3833
|
const synced = await syncAggregateSubpackageVersions(profileDir, installedName, registries)
|
|
3307
3834
|
if (synced.length > 0) {
|
|
3835
|
+
syncedNames = synced.map(syncNameFromNote).filter((n) => n !== null)
|
|
3308
3836
|
job.bundleNote = (job.bundleNote ? job.bundleNote + ';' : '') + `子包版本对齐:更新 ${synced.length} 个(${synced.join('、')})`
|
|
3309
3837
|
}
|
|
3310
3838
|
} catch {}
|
|
@@ -3318,6 +3846,11 @@ async function runInstallJob(job, ctx) {
|
|
|
3318
3846
|
+ (integrity.missing.length > 0 ? `,仍缺失 ${integrity.missing.join('、')}(网络恢复后建议重新更新)` : '')
|
|
3319
3847
|
}
|
|
3320
3848
|
} catch {}
|
|
3849
|
+
// 框架升级适配门:更新完成后自动校验兼容性,通过则移除禁用块解锁启用
|
|
3850
|
+
try {
|
|
3851
|
+
const adapt = await maybeAutoAdaptCompat({ profileDir, packageName: installedName, syncedNames, patchPath, ctx })
|
|
3852
|
+
if (adapt !== null && adapt.ran === true) job.compatNote = adapt.note
|
|
3853
|
+
} catch {}
|
|
3321
3854
|
job.stage = 'configuring'
|
|
3322
3855
|
if (await detectBundleOnly(profileDir, installedName)) {
|
|
3323
3856
|
// 官方 dsh plugin add 行为:声明 dsh.bundle 的包追加为 profile bundle 层,
|
|
@@ -3364,6 +3897,10 @@ function listEntries(ctx) { const entries = []
|
|
|
3364
3897
|
|
|
3365
3898
|
/** 应用插件:注册 /plugin-console 路由。 */
|
|
3366
3899
|
export function apply(ctx) {
|
|
3900
|
+
// 恢复 AI 赋能任务(restart 后 running 置失败,plan-ready/结果保留)
|
|
3901
|
+
loadAiJobs()
|
|
3902
|
+
// DSH 启动时自动拉起标记了「自启动」的服务器组件(幂等,不阻塞启动)
|
|
3903
|
+
autostartComponents().catch(() => {})
|
|
3367
3904
|
ctx.effect(() => {
|
|
3368
3905
|
const route = {
|
|
3369
3906
|
kind: 'prefix',
|
|
@@ -3405,6 +3942,15 @@ async function handle(ctx, req, res) {
|
|
|
3405
3942
|
const patchPath = findPatchPath(ctx)
|
|
3406
3943
|
const patch = await readPatchState(patchPath)
|
|
3407
3944
|
const extraRows = await readExtraBundleRows(dirname(patchPath))
|
|
3945
|
+
const compatPending = readCompatPending()
|
|
3946
|
+
const compatPendingRows = new Set((compatPending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => p.rowId))
|
|
3947
|
+
let rollbackRec = null
|
|
3948
|
+
try {
|
|
3949
|
+
const rr = JSON.parse(readFileSync(join(dshHome(), 'plugin-console', 'framework-rollback.json'), 'utf8'))
|
|
3950
|
+
if (rr !== null && typeof rr.checkpointDir === 'string' && existsSync(join(rr.checkpointDir, '.pnpm'))) {
|
|
3951
|
+
rollbackRec = { from: rr.from ?? null, to: rr.to ?? null, at: rr.at ?? null }
|
|
3952
|
+
}
|
|
3953
|
+
} catch {}
|
|
3408
3954
|
const entries = listEntries(ctx).map((entry) => {
|
|
3409
3955
|
const meta = entryPkgMeta(entry.moduleName, ctx.baseUrl ?? 'file:///')
|
|
3410
3956
|
return {
|
|
@@ -3415,6 +3961,7 @@ async function handle(ctx, req, res) {
|
|
|
3415
3961
|
installDate: meta?.installDate ?? null,
|
|
3416
3962
|
version: meta?.version ?? null,
|
|
3417
3963
|
repository: meta?.repository ?? null,
|
|
3964
|
+
pendingCompat: compatPendingRows.has(entry.rowId),
|
|
3418
3965
|
}
|
|
3419
3966
|
})
|
|
3420
3967
|
const compat = await detectCompat(ctx.baseUrl ?? 'file:///')
|
|
@@ -3429,7 +3976,7 @@ async function handle(ctx, req, res) {
|
|
|
3429
3976
|
const selfPkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
|
|
3430
3977
|
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
3431
3978
|
} catch {}
|
|
3432
|
-
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c) })) })
|
|
3979
|
+
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null })) }, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
3433
3980
|
return
|
|
3434
3981
|
}
|
|
3435
3982
|
|
|
@@ -3626,6 +4173,15 @@ async function handle(ctx, req, res) {
|
|
|
3626
4173
|
sendError(res, 400, '不能停用插件控制台自身')
|
|
3627
4174
|
return
|
|
3628
4175
|
}
|
|
4176
|
+
if (enabled) {
|
|
4177
|
+
// 框架升级适配门(事故教训):兼容门清单内的行保持强制禁用,直到更新并通过校验
|
|
4178
|
+
const compatPending = readCompatPending()
|
|
4179
|
+
const pend = (compatPending?.pending ?? []).find((p) => p.rowId === rowId && (p.status ?? 'pending') === 'pending')
|
|
4180
|
+
if (pend) {
|
|
4181
|
+
sendError(res, 409, `「${rowId}」仍在框架升级适配门中(${compatPending.frameworkVersion ?? '?'})被强制禁用:请先点击「更新并适配」,更新完成并通过兼容校验后系统自动解锁`)
|
|
4182
|
+
return
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
3629
4185
|
const patchPath = findPatchPath(ctx)
|
|
3630
4186
|
const result = enabled
|
|
3631
4187
|
? await enableEntry(patchPath, rowId)
|
|
@@ -3692,6 +4248,7 @@ async function handle(ctx, req, res) {
|
|
|
3692
4248
|
const query = raw === '' ? DEFAULT_SEARCH : raw
|
|
3693
4249
|
const page = Math.max(Math.min(Number.parseInt(String(body.page), 10) || 1, 5), 1)
|
|
3694
4250
|
const source = typeof body.source === 'string' && body.source !== '' ? body.source : 'github'
|
|
4251
|
+
const all = body.all === true
|
|
3695
4252
|
const auth = readGithubAuth()
|
|
3696
4253
|
let items = []
|
|
3697
4254
|
if (body.multi === true) {
|
|
@@ -3702,7 +4259,7 @@ async function handle(ctx, req, res) {
|
|
|
3702
4259
|
(async () => {
|
|
3703
4260
|
try {
|
|
3704
4261
|
const data = await githubJson(
|
|
3705
|
-
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
4262
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(all ? query : `${query} topic:dsh-plugin`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
3706
4263
|
req.signal,
|
|
3707
4264
|
auth.token,
|
|
3708
4265
|
)
|
|
@@ -3801,7 +4358,7 @@ async function handle(ctx, req, res) {
|
|
|
3801
4358
|
items = normalizePlatformItems(data, 'main').map((item) => ({ ...item, source, sourceName: custom.name }))
|
|
3802
4359
|
} else {
|
|
3803
4360
|
const data = await githubJson(
|
|
3804
|
-
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
4361
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(all ? query : `${query} topic:dsh-plugin`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
3805
4362
|
req.signal,
|
|
3806
4363
|
auth.token,
|
|
3807
4364
|
)
|
|
@@ -4308,6 +4865,27 @@ async function handle(ctx, req, res) {
|
|
|
4308
4865
|
sendError(res, 500, `框架备份失败(回滚点${rollbackDir !== null ? `:${rollbackDir}` : '(无)'}缺失或无效),已取消升级——请检查磁盘空间/权限后重试`)
|
|
4309
4866
|
return
|
|
4310
4867
|
}
|
|
4868
|
+
// 框架安装根识别(事故教训 2026-09-04):require.resolve 返回 .pnpm 内部 realpath,
|
|
4869
|
+
// 旧逻辑 dirname(dirname(dshDir)) 指向 .pnpm/<entry>/node_modules——「重链跳过:.pnpm 目录不存在」、
|
|
4870
|
+
// 依赖修复 0 个,pnpm 还在错误 cwd 里把新 CLI 原位覆盖进旧 .pnpm 目录,旧树就此被毁。
|
|
4871
|
+
const fwRoot = resolveFrameworkRootNodeModules(dshDir)
|
|
4872
|
+
if (fwRoot === null || !existsSync(join(fwRoot, '.pnpm')) || !existsSync(join(fwRoot, '@deepseek-ai', 'dsh', 'lib', 'bin.js'))) {
|
|
4873
|
+
sendError(res, 500, `无法定位框架安装根(期望含 .pnpm 与 @deepseek-ai/dsh 的顶层 node_modules,实际:${fwRoot ?? '未找到'})——已取消升级,请修复框架安装后重试`)
|
|
4874
|
+
return
|
|
4875
|
+
}
|
|
4876
|
+
// 框架全树 checkpoint(可靠回滚点):升级前镜像全部 @deepseek-ai 版本包(自包内容)+ 顶层 scope + lock
|
|
4877
|
+
let fwCheckpoint = null
|
|
4878
|
+
try {
|
|
4879
|
+
const cpRoot = backupDir !== null ? backupDir : join(FRAMEWORK_BACKUP_ROOT(), current ?? 'unknown')
|
|
4880
|
+
fwCheckpoint = checkpointFrameworkTree(fwRoot, cpRoot)
|
|
4881
|
+
steps.push(`框架全树 checkpoint 完成:${fwCheckpoint.dest}(镜像 ${fwCheckpoint.mirrored} 个 @deepseek-ai 版本包;升级失败/拉起失败会自动全树回滚,也可用框架卡片的「回滚到上一版」一键回滚)`)
|
|
4882
|
+
} catch (error) {
|
|
4883
|
+
steps.push(`框架全树 checkpoint 失败:${error instanceof Error ? error.message : String(error)}(回滚点降级为 CLI 备份)`)
|
|
4884
|
+
}
|
|
4885
|
+
// 一键回滚记录:框架卡片「回滚到上一版」按钮读取
|
|
4886
|
+
try {
|
|
4887
|
+
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')
|
|
4888
|
+
} catch {}
|
|
4311
4889
|
const ps1 = join(tmpdir(), `fw-upgrade-${process.pid}.ps1`)
|
|
4312
4890
|
const logFile = join(dshHome(), 'plugin-console', 'fw-upgrade.log')
|
|
4313
4891
|
const stateFile = join(dshHome(), 'plugin-console', 'fw-upgrade-state.txt')
|
|
@@ -4337,7 +4915,7 @@ async function handle(ctx, req, res) {
|
|
|
4337
4915
|
`Log '升级框架本体 ${current} -> ${target}…'`,
|
|
4338
4916
|
// 关键:npm 的工作目录必须是缓存 node_modules(schtasks 默认 cwd 是 system32/用户目录,
|
|
4339
4917
|
// 不指定会装错位置——缓存永远不更新的根因)
|
|
4340
|
-
`Set-Location -Path ${ps(
|
|
4918
|
+
`Set-Location -Path ${ps(fwRoot)}`,
|
|
4341
4919
|
// npm 安装函数:Start-Process 跑 npm-cli.js(唯一正确入口——lib/cli.js 是函数模块,
|
|
4342
4920
|
// 直跑只加载不执行,假成功 exit 0)+ --force(显式指定版本但已在 package.json 的
|
|
4343
4921
|
// semver 范围内时 npm 会跳过安装,假成功)+ 15 分钟总时长硬超时。注意:**不能加
|
|
@@ -4400,17 +4978,36 @@ async function handle(ctx, req, res) {
|
|
|
4400
4978
|
` }`,
|
|
4401
4979
|
` return $code`,
|
|
4402
4980
|
`}`,
|
|
4403
|
-
//
|
|
4404
|
-
//
|
|
4981
|
+
// 回滚函数(2026-09-04 事故后升级为「全树优先」):robocopy 支持长路径(Copy-Item 对 >260
|
|
4982
|
+
// 字符路径静默失败,曾导致回滚后框架目录缺失、服务无法重启);回滚前确保服务已停
|
|
4983
|
+
// (进程 cwd 会锁住框架目录)。checkpoint 存在 → 恢复全部 @deepseek-ai 版本包 + 顶层 scope;
|
|
4984
|
+
// checkpoint 缺失 → CLI 兜底。
|
|
4405
4985
|
`function Invoke-Rollback {`,
|
|
4406
|
-
` SetState 'rollback' '
|
|
4407
|
-
` Log '
|
|
4408
|
-
`
|
|
4409
|
-
`
|
|
4410
|
-
`
|
|
4986
|
+
` SetState 'rollback' '升级失败,回滚框架(全树/CLI 备份)…'`,
|
|
4987
|
+
` Log '升级失败,回滚框架…'`,
|
|
4988
|
+
` 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 {}`,
|
|
4989
|
+
` $cp = '${ps(fwCheckpoint?.dest ?? '')}'`,
|
|
4990
|
+
` if ($cp -ne '' -and (Test-Path (Join-Path $cp '.pnpm'))) {`,
|
|
4991
|
+
` $restored = 0`,
|
|
4992
|
+
` $entries = Get-ChildItem -Path (Join-Path $cp '.pnpm') -Directory -ErrorAction SilentlyContinue`,
|
|
4993
|
+
` foreach ($e in $entries) {`,
|
|
4994
|
+
` $name = @($e.Name -split '\\+')[1].Split('@')[0]`,
|
|
4995
|
+
` $src = Join-Path $e.FullName ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
4996
|
+
` $dst = Join-Path (Join-Path '${ps(fwRoot)}' ('.pnpm\\' + $e.Name)) ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
4997
|
+
` 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++ }`,
|
|
4998
|
+
` }`,
|
|
4999
|
+
` $topSrc = Join-Path $cp 'top-@deepseek-ai'`,
|
|
5000
|
+
` 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 }`,
|
|
5001
|
+
` try { Copy-Item (Join-Path $cp 'lock.yaml') '${ps(join(fwRoot, '.pnpm', 'lock.yaml'))}' -Force -ErrorAction SilentlyContinue } catch {}`,
|
|
5002
|
+
` Log ('全树回滚完成:恢复 ' + $restored + ' 个版本包 + 顶层 scope')`,
|
|
5003
|
+
` $script:rolledBack = $true`,
|
|
5004
|
+
` return`,
|
|
5005
|
+
` }`,
|
|
5006
|
+
` # CLI 兜底(checkpoint 缺失或损坏)`,
|
|
5007
|
+
` if (-not (Test-Path ${ps(join(rollbackDir, 'lib', 'bin.js'))})) { Log ('回滚失败:CLI 备份也缺失(' + ${ps(rollbackDir)} + ')'); return }`,
|
|
4411
5008
|
` Remove-Item ${ps(dshDir)} -Recurse -Force -ErrorAction SilentlyContinue`,
|
|
4412
5009
|
` robocopy ${ps(rollbackDir)} ${ps(dshDir)} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null`,
|
|
4413
|
-
` if (Test-Path ${ps(join(dshDir, 'lib', 'bin.js'))}) { Log '
|
|
5010
|
+
` if (Test-Path ${ps(join(dshDir, 'lib', 'bin.js'))}) { Log '回滚完成(CLI 恢复)'; $script:rolledBack = $true } else { Log '回滚失败:复制失败,请手动修复框架安装' }`,
|
|
4414
5011
|
`}`,
|
|
4415
5012
|
`$script:rolledBack = $false`,
|
|
4416
5013
|
`$code = Install-Framework 'https://registry.npmmirror.com'`,
|
|
@@ -4424,6 +5021,8 @@ async function handle(ctx, req, res) {
|
|
|
4424
5021
|
` if ($code -ne 0) { Log ('(停服后)npmmirror 安装失败(exit=' + $code + '),切换 registry.npmjs.org 重试一次'); $code = Install-Framework 'https://registry.npmjs.org' }`,
|
|
4425
5022
|
`}`,
|
|
4426
5023
|
`if ($code -ne 0) { Invoke-Rollback } else { Log '框架本体升级完成' }`,
|
|
5024
|
+
// 回滚后跳过重链/依赖修复/版本对齐(它们按新树预期运行,会对已恢复的旧树造成二次破坏)
|
|
5025
|
+
`if (-not $script:rolledBack) {`,
|
|
4427
5026
|
// 重新链接框架配套包(事故教训):pnpm 升级到新版本时 .pnpm 目录重建,
|
|
4428
5027
|
// 但顶层 node_modules/@deepseek-ai/* 不会自动切换,仍指向旧版(如 0.1.0-rc.7)——
|
|
4429
5028
|
// 导致框架混版本(如 dsh-llm-deepseek 旧版无 vision 模型)、部分功能异常。
|
|
@@ -4432,7 +5031,7 @@ async function handle(ctx, req, res) {
|
|
|
4432
5031
|
`$relinked = 0`,
|
|
4433
5032
|
`function Relink-FrameworkPackages($version) {`,
|
|
4434
5033
|
` # dshDir = .../node_modules/@deepseek-ai/dsh → nodeModules = .../node_modules`,
|
|
4435
|
-
` $nodeModules = ${ps(
|
|
5034
|
+
` $nodeModules = ${ps(fwRoot)}`,
|
|
4436
5035
|
` $pnpm = Join-Path $nodeModules '.pnpm'`,
|
|
4437
5036
|
` if (-not (Test-Path $pnpm)) { Log ('重链跳过:.pnpm 目录不存在'); return }`,
|
|
4438
5037
|
` $topPkgs = Get-ChildItem -Path (Join-Path $nodeModules '@deepseek-ai') -Directory | Where-Object { $_.Name -like 'dsh-*' }`,
|
|
@@ -4470,7 +5069,7 @@ async function handle(ctx, req, res) {
|
|
|
4470
5069
|
` $bak = $top.FullName + '.bak-' + $topVer`,
|
|
4471
5070
|
` if (Test-Path $bak) { Remove-Item $bak -Recurse -Force -ErrorAction SilentlyContinue }`,
|
|
4472
5071
|
` Rename-Item -LiteralPath $top.FullName -NewName ($name + '.bak-' + $topVer) -Force`,
|
|
4473
|
-
` cmd /c mklink /J "$
|
|
5072
|
+
` cmd /c mklink /J "$($top.FullName)" "$($best)" 2>$null | Out-Null`,
|
|
4474
5073
|
` if (Test-Path (Join-Path $top.FullName 'package.json')) { $script:relinked++; Log ('重链 ' + $name + ': ' + $topVer + ' -> ' + $bestVer) } else { Log ('重链失败:' + $name) }`,
|
|
4475
5074
|
` } catch { Log ('重链异常:' + $name + ' ' + $_.Exception.Message) }`,
|
|
4476
5075
|
` }`,
|
|
@@ -4484,7 +5083,7 @@ async function handle(ctx, req, res) {
|
|
|
4484
5083
|
`$treeFixed = 0`,
|
|
4485
5084
|
`function Verify-DependencyTree($refVersion) {`,
|
|
4486
5085
|
` # dshDir = .../node_modules/@deepseek-ai/dsh → nodeModules = .../node_modules`,
|
|
4487
|
-
` $nodeModules = ${ps(
|
|
5086
|
+
` $nodeModules = ${ps(fwRoot)}`,
|
|
4488
5087
|
` $webApp = Join-Path $nodeModules '@deepseek-ai\\dsh-web-app\\package.json'`,
|
|
4489
5088
|
` if (-not (Test-Path $webApp)) { Log ('依赖树验证跳过:dsh-web-app 未找到'); return }`,
|
|
4490
5089
|
` try { $deps = (Get-Content $webApp -Raw | ConvertFrom-Json).dependencies } catch { Log ('依赖树验证跳过:读取失败'); return }`,
|
|
@@ -4552,6 +5151,7 @@ async function handle(ctx, req, res) {
|
|
|
4552
5151
|
`}`,
|
|
4553
5152
|
`Verify-SelfNameConsistency '${selfName ?? '@noob-stupid/dsh-plugin-console'}'`,
|
|
4554
5153
|
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 }` : '',
|
|
5154
|
+
"}",
|
|
4555
5155
|
"SetState 'relaunching' '升级完成,重启 DSH 服务生效…'",
|
|
4556
5156
|
"Log '重启 DSH 服务生效…'",
|
|
4557
5157
|
// 升级全程服务保持在线(先下载后重启):这里停掉旧服务(内存仍是旧代码),拉起新版本生效
|
|
@@ -4563,7 +5163,19 @@ async function handle(ctx, req, res) {
|
|
|
4563
5163
|
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "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'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { Start-Process -FilePath ${ps(nodePath)} -ArgumentList $binNow,'web' -WindowStyle Hidden; $started = $true; Log '已发起拉起服务' } else { Log 'bin.js 缺失,跳过拉起' } }`,
|
|
4564
5164
|
' Start-Sleep -Seconds 5',
|
|
4565
5165
|
'}',
|
|
4566
|
-
|
|
5166
|
+
// 拉起失败不再只留提示(2026-09-04 教训):自动全树回滚并再拉起一次
|
|
5167
|
+
`if (-not $ok -and -not $script:rolledBack) {`,
|
|
5168
|
+
` Log '服务拉起失败,尝试自动回滚到升级前版本…'`,
|
|
5169
|
+
` Invoke-Rollback`,
|
|
5170
|
+
` $ok = $false`,
|
|
5171
|
+
` $started = $false`,
|
|
5172
|
+
` for ($i = 0; $i -lt 20; $i++) {`,
|
|
5173
|
+
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
5174
|
+
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "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'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { Start-Process -FilePath ${ps(nodePath)} -ArgumentList $binNow,'web' -WindowStyle Hidden; $started = $true; Log '已发起拉起服务(回滚后)' } else { Log 'bin.js 缺失,跳过拉起' } }`,
|
|
5175
|
+
` Start-Sleep -Seconds 5`,
|
|
5176
|
+
` }`,
|
|
5177
|
+
`}`,
|
|
5178
|
+
`if ($ok) { if ($script:rolledBack) { SetState 'failed' '升级失败,已自动回滚到升级前版本(服务已恢复)' ; Log '升级失败已自动回滚,服务已恢复' } else { SetState 'done' '升级完成,服务已监听 ${port}' ; Log '服务已监听 ${port},升级完成' } } else { SetState 'failed' '拉起失败且自动回滚未生效:请手动运行 node ${binPath} web' ; Log '拉起失败且自动回滚未生效:请手动运行 node ${binPath} web(参考本日志排查)' }`,
|
|
4567
5179
|
`schtasks /delete /f /tn ${taskName} 2>$null`,
|
|
4568
5180
|
].filter((l) => l !== '').join('\r\n')
|
|
4569
5181
|
// 初始状态(备份完成、脚本即将执行)
|
|
@@ -4626,6 +5238,87 @@ async function handle(ctx, req, res) {
|
|
|
4626
5238
|
return
|
|
4627
5239
|
}
|
|
4628
5240
|
|
|
5241
|
+
if (pathname === `${ROUTE_PREFIX}/framework-rollback`) {
|
|
5242
|
+
// 一键回滚(2026-09-04 事故后的新能力):读 framework-rollback.json(升级时写入),
|
|
5243
|
+
// 生成分离脚本:停服 → 全树恢复(.pnpm 自包镜像 + 顶层 scope + lock)→ 拉起 → 状态。
|
|
5244
|
+
let rec = null
|
|
5245
|
+
try { rec = JSON.parse(readFileSync(join(dshHome(), 'plugin-console', 'framework-rollback.json'), 'utf8')) } catch {}
|
|
5246
|
+
if (rec === null || typeof rec.checkpointDir !== 'string' || typeof rec.fwRoot !== 'string'
|
|
5247
|
+
|| !existsSync(join(rec.checkpointDir, '.pnpm')) || !existsSync(join(rec.fwRoot, '.pnpm'))) {
|
|
5248
|
+
sendError(res, 409, '没有可用的框架全树回滚点(framework-rollback.json 缺失或 checkpoint 已清理)')
|
|
5249
|
+
return
|
|
5250
|
+
}
|
|
5251
|
+
const port = webPort(ctx)
|
|
5252
|
+
const nodePath = process.execPath
|
|
5253
|
+
const taskName = `DSH-FW-Rollback-${process.pid}`
|
|
5254
|
+
const ps1 = join(tmpdir(), `fw-rollback-${process.pid}.ps1`)
|
|
5255
|
+
const logFile = join(dshHome(), 'plugin-console', 'fw-upgrade.log')
|
|
5256
|
+
const stateFile = join(dshHome(), 'plugin-console', 'fw-upgrade-state.txt')
|
|
5257
|
+
const ps = (s) => JSON.stringify(s).replace(/\\\\/gu, '\\')
|
|
5258
|
+
const lines = [
|
|
5259
|
+
`$state = ${ps(stateFile)}`,
|
|
5260
|
+
`$log = ${ps(logFile)}`,
|
|
5261
|
+
"function Log($m) { try { Add-Content -Path $log -Value ((Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' ' + $m) -Encoding UTF8 } catch {} }",
|
|
5262
|
+
"function SetState($s, $m) { try { Set-Content -Path $state -Value ($s + '|' + $m) -Encoding UTF8 } catch {} }",
|
|
5263
|
+
"trap {",
|
|
5264
|
+
" try { SetState 'failed' ('回滚脚本异常终止:' + $_.Exception.Message) } catch {}",
|
|
5265
|
+
` schtasks /delete /f /tn ${taskName} 2>$null`,
|
|
5266
|
+
" exit 1",
|
|
5267
|
+
"}",
|
|
5268
|
+
"SetState 'rollback' '回滚到升级前版本…'",
|
|
5269
|
+
"Log '一键回滚脚本启动'",
|
|
5270
|
+
`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 {}`,
|
|
5271
|
+
"Log '服务已停止(回滚生效)'",
|
|
5272
|
+
`$cp = ${ps(rec.checkpointDir)}`,
|
|
5273
|
+
`$restored = 0`,
|
|
5274
|
+
`$entries = Get-ChildItem -Path (Join-Path $cp '.pnpm') -Directory -ErrorAction SilentlyContinue`,
|
|
5275
|
+
`foreach ($e in $entries) {`,
|
|
5276
|
+
` $name = @($e.Name -split '\\+')[1].Split('@')[0]`,
|
|
5277
|
+
` $src = Join-Path $e.FullName ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
5278
|
+
` $dst = Join-Path (Join-Path ${ps(rec.fwRoot)} ('.pnpm\\' + $e.Name)) ('node_modules\\@deepseek-ai\\' + $name)`,
|
|
5279
|
+
` 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++ }`,
|
|
5280
|
+
`}`,
|
|
5281
|
+
`$topSrc = Join-Path $cp 'top-@deepseek-ai'`,
|
|
5282
|
+
`if (Test-Path $topSrc) { Remove-Item ${ps(join(rec.fwRoot, '@deepseek-ai'))} -Recurse -Force -ErrorAction SilentlyContinue; robocopy $topSrc ${ps(join(rec.fwRoot, '@deepseek-ai'))} /E /NFL /NDL /NJH /NJS /R:1 /W:1 | Out-Null }`,
|
|
5283
|
+
`try { Copy-Item (Join-Path $cp 'lock.yaml') ${ps(join(rec.fwRoot, '.pnpm', 'lock.yaml'))} -Force -ErrorAction SilentlyContinue } catch {}`,
|
|
5284
|
+
`Log ('全树回滚完成:恢复 ' + $restored + ' 个版本包 + 顶层 scope,拉起验证中…')`,
|
|
5285
|
+
`$ok = $false`,
|
|
5286
|
+
`$started = $false`,
|
|
5287
|
+
`for ($i = 0; $i -lt 20; $i++) {`,
|
|
5288
|
+
` try { $c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c.Count -gt 0) { $ok = $true; break } } catch {}`,
|
|
5289
|
+
` if (-not $ok -and -not $started) { $binNow = ''; try { $env:DSH_RESOLVE_ROOT = '${join(dirname(fileURLToPath(import.meta.url)), '..')}'; $binNow = (& ${ps(nodePath)} -e "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'))" 2>$null | Select-Object -Last 1) } catch {}; if ($binNow -ne '' -and (Test-Path $binNow)) { Start-Process -FilePath ${ps(nodePath)} -ArgumentList $binNow,'web' -WindowStyle Hidden; $started = $true; Log '已发起拉起服务(回滚后)' } else { Log 'bin.js 缺失,跳过拉起' } }`,
|
|
5290
|
+
` Start-Sleep -Seconds 5`,
|
|
5291
|
+
`}`,
|
|
5292
|
+
`if ($ok) { SetState 'done' ('已回滚到升级前版本 ${rec.from ?? '?'},服务正常') ; Log '回滚完成,服务已恢复' } else { SetState 'failed' '回滚后服务拉起失败:请手动运行 node ${resolveDshBin() ?? '<bin>'} web' ; Log '回滚后拉起失败' }`,
|
|
5293
|
+
`schtasks /delete /f /tn ${taskName} 2>$null`,
|
|
5294
|
+
].filter((l) => l !== '').join('\r\n')
|
|
5295
|
+
try { writeFileSync(stateFile, 'rollback|回滚脚本已启动…', 'utf8') } catch {}
|
|
5296
|
+
writeFile(ps1, `\uFEFF${lines}`, 'utf8').then(
|
|
5297
|
+
() => {
|
|
5298
|
+
const ps1Posix = ps1.replace(/\\/gu, '/')
|
|
5299
|
+
const tr = / /.test(ps1Posix)
|
|
5300
|
+
? `"powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \\"${ps1Posix}\\""`
|
|
5301
|
+
: `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ${ps1Posix}`
|
|
5302
|
+
execFile('schtasks.exe', ['/create', '/f', '/tn', taskName, '/tr', tr, '/sc', 'once', '/st', '00:00'], { windowsHide: true }, (error) => {
|
|
5303
|
+
if (error) {
|
|
5304
|
+
execFile('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', ps1], { windowsHide: true, detached: true, stdio: 'ignore' }, () => {})
|
|
5305
|
+
return
|
|
5306
|
+
}
|
|
5307
|
+
setTimeout(() => {
|
|
5308
|
+
execFile('schtasks.exe', ['/run', '/tn', taskName], { windowsHide: true }, (runError) => {
|
|
5309
|
+
if (runError) {
|
|
5310
|
+
execFile('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', ps1], { windowsHide: true, detached: true, stdio: 'ignore' }, () => {})
|
|
5311
|
+
}
|
|
5312
|
+
})
|
|
5313
|
+
}, 800)
|
|
5314
|
+
})
|
|
5315
|
+
},
|
|
5316
|
+
() => {},
|
|
5317
|
+
)
|
|
5318
|
+
sendJson(res, 200, { ok: true, from: rec.from ?? null, checkpointDir: rec.checkpointDir })
|
|
5319
|
+
return
|
|
5320
|
+
}
|
|
5321
|
+
|
|
4629
5322
|
if (pathname === `${ROUTE_PREFIX}/clean-residuals`) {
|
|
4630
5323
|
// 清理残余备份/旧子包:删除 .old-* 备份目录,以及聚合包未声明的 @linxin666 旧子包
|
|
4631
5324
|
const profileDir = dirname(findPatchPath(ctx))
|
|
@@ -4858,6 +5551,8 @@ async function handle(ctx, req, res) {
|
|
|
4858
5551
|
const profileDir = dirname(patchPath)
|
|
4859
5552
|
const jobId = `ai-${Date.now()}-${++aiJobSeq}`
|
|
4860
5553
|
const job = { id: jobId, source, status: 'running', stage: 'planning', createdAt: Date.now(), logText: '', stepStates: [] }
|
|
5554
|
+
// 框架适配预检(兼容门 + registry 声明):规划期即给出权威说明,供子代理引用与用户查看
|
|
5555
|
+
try { job.frameworkCheck = await frameworkCompatReportFor(source, ctx, profileDir) } catch { job.frameworkCheck = null }
|
|
4861
5556
|
aiJobs.set(jobId, job)
|
|
4862
5557
|
const builtin = builtinPlanFor(source)
|
|
4863
5558
|
if (builtin !== null) {
|
|
@@ -4865,7 +5560,8 @@ async function handle(ctx, req, res) {
|
|
|
4865
5560
|
job.status = 'plan-ready'
|
|
4866
5561
|
job.stage = 'builtin'
|
|
4867
5562
|
job.finishedAt = Date.now()
|
|
4868
|
-
|
|
5563
|
+
saveAiJobs()
|
|
5564
|
+
sendJson(res, 200, { ok: true, jobId, frameworkCheck: job.frameworkCheck })
|
|
4869
5565
|
return
|
|
4870
5566
|
}
|
|
4871
5567
|
aiEmpowerPlan(job, ctx, profileDir).catch((error) => {
|
|
@@ -4873,7 +5569,7 @@ async function handle(ctx, req, res) {
|
|
|
4873
5569
|
job.error = `规划任务异常:${error instanceof Error ? error.message : String(error)}`
|
|
4874
5570
|
job.finishedAt = Date.now()
|
|
4875
5571
|
})
|
|
4876
|
-
sendJson(res, 200, { ok: true, jobId })
|
|
5572
|
+
sendJson(res, 200, { ok: true, jobId, frameworkCheck: job.frameworkCheck })
|
|
4877
5573
|
return
|
|
4878
5574
|
}
|
|
4879
5575
|
|
|
@@ -4887,9 +5583,30 @@ async function handle(ctx, req, res) {
|
|
|
4887
5583
|
sendJson(res, 200, { ok: true, ...aiJobView(job) })
|
|
4888
5584
|
return
|
|
4889
5585
|
}
|
|
5586
|
+
if (pathname === `${ROUTE_PREFIX}/ai-empower/list`) {
|
|
5587
|
+
// 并发任务列表(轻量视图,不含日志全文):面板展示/切换多个并发 AI 赋能任务
|
|
5588
|
+
const list = [...aiJobs.values()].slice(-10).reverse().map((job) => ({
|
|
5589
|
+
jobId: job.id,
|
|
5590
|
+
source: job.source,
|
|
5591
|
+
status: job.status,
|
|
5592
|
+
stage: job.stage,
|
|
5593
|
+
type: job.plan?.type ?? null,
|
|
5594
|
+
displayName: job.plan?.displayName ?? null,
|
|
5595
|
+
progress: job.progress ?? { done: 0, total: (job.plan?.steps ?? []).length },
|
|
5596
|
+
error: job.error ?? null,
|
|
5597
|
+
createdAt: job.createdAt,
|
|
5598
|
+
}))
|
|
5599
|
+
sendJson(res, 200, { ok: true, tasks: list })
|
|
5600
|
+
return
|
|
5601
|
+
}
|
|
4890
5602
|
if (pathname === `${ROUTE_PREFIX}/ai-empower/run`) {
|
|
4891
5603
|
const jobId = typeof body.jobId === 'string' ? body.jobId : ''
|
|
4892
5604
|
const selected = Array.isArray(body.steps) ? body.steps : null
|
|
5605
|
+
// 纵深防御:必须显式确认(前端「同意并部署」按钮携带 confirmed:true),防止任何绕过同意直接执行
|
|
5606
|
+
if (body.confirmed !== true) {
|
|
5607
|
+
sendError(res, 400, '未确认部署:请先在面板勾选步骤并点击「同意并部署」')
|
|
5608
|
+
return
|
|
5609
|
+
}
|
|
4893
5610
|
const job = aiJobs.get(jobId)
|
|
4894
5611
|
if (!job) {
|
|
4895
5612
|
sendError(res, 404, '没有这个 AI 赋能任务')
|
|
@@ -4905,6 +5622,7 @@ async function handle(ctx, req, res) {
|
|
|
4905
5622
|
job.status = 'failed'
|
|
4906
5623
|
job.error = `执行任务异常:${error instanceof Error ? error.message : String(error)}`
|
|
4907
5624
|
job.finishedAt = Date.now()
|
|
5625
|
+
saveAiJobs()
|
|
4908
5626
|
})
|
|
4909
5627
|
sendJson(res, 200, { ok: true, jobId })
|
|
4910
5628
|
return
|
|
@@ -4922,6 +5640,7 @@ async function handle(ctx, req, res) {
|
|
|
4922
5640
|
job.status = 'failed'
|
|
4923
5641
|
job.error = '已取消执行(用户中断)'
|
|
4924
5642
|
job.finishedAt = Date.now()
|
|
5643
|
+
saveAiJobs()
|
|
4925
5644
|
}
|
|
4926
5645
|
sendJson(res, 200, { ok: true, jobId })
|
|
4927
5646
|
return
|
|
@@ -4934,13 +5653,124 @@ async function handle(ctx, req, res) {
|
|
|
4934
5653
|
try {
|
|
4935
5654
|
enriched.push(await compStatus(c.id))
|
|
4936
5655
|
} catch {
|
|
4937
|
-
enriched.push({ id: c.id, name: c.name, running: false, healthy: null, pid: null, port: c.port ?? null })
|
|
5656
|
+
enriched.push({ id: c.id, name: c.name, running: false, healthy: null, pid: null, port: c.port ?? null, autoStart: c.autoStart === true })
|
|
4938
5657
|
}
|
|
4939
5658
|
}
|
|
4940
5659
|
sendJson(res, 200, { ok: true, components: enriched.map((x) => ({ ...x, uiUrl: compUiUrl(compFind(x.id) ?? {}) })) })
|
|
4941
5660
|
return
|
|
4942
5661
|
}
|
|
4943
5662
|
|
|
5663
|
+
if (pathname === `${ROUTE_PREFIX}/repo-clone`) {
|
|
5664
|
+
// 仓库落地:从 GitHub 克隆任意项目到 <配置路径>/<owner>/<name>(不限于 DSH 插件)
|
|
5665
|
+
const raw = typeof body.repo === 'string' ? body.repo.trim() : ''
|
|
5666
|
+
let repo = raw
|
|
5667
|
+
.replace(/^https?:\/\/github\.com\//u, '')
|
|
5668
|
+
.replace(/^git@github\.com:/u, '')
|
|
5669
|
+
.replace(/\.git$/u, '')
|
|
5670
|
+
.replace(/\/+$/u, '')
|
|
5671
|
+
const m = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/u.exec(repo)
|
|
5672
|
+
if (!m) {
|
|
5673
|
+
sendError(res, 400, '仓库格式不正确:请提供 owner/repo 或 GitHub 仓库链接')
|
|
5674
|
+
return
|
|
5675
|
+
}
|
|
5676
|
+
const [, owner, name] = m
|
|
5677
|
+
const root = getReposDir()
|
|
5678
|
+
const target = join(root, owner, name)
|
|
5679
|
+
if (existsSync(target)) {
|
|
5680
|
+
sendError(res, 400, `已存在:${target}(如需更新请先手动处理该目录)`)
|
|
5681
|
+
return
|
|
5682
|
+
}
|
|
5683
|
+
try {
|
|
5684
|
+
mkdirSync(root, { recursive: true })
|
|
5685
|
+
await execFileAsync('git.exe', ['clone', '--depth', '1', `https://github.com/${owner}/${name}.git`, target], { cwd: root, timeout: 600000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 })
|
|
5686
|
+
sendJson(res, 200, { ok: true, owner, name, path: target })
|
|
5687
|
+
} catch (error) {
|
|
5688
|
+
sendError(res, 500, `克隆失败:${error instanceof Error ? error.message : String(error)}`)
|
|
5689
|
+
}
|
|
5690
|
+
return
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
if (pathname === `${ROUTE_PREFIX}/repo-list`) {
|
|
5694
|
+
try {
|
|
5695
|
+
sendJson(res, 200, { ok: true, dir: getReposDir(), repos: listLandedRepos() })
|
|
5696
|
+
} catch (error) {
|
|
5697
|
+
sendError(res, 500, error instanceof Error ? error.message : String(error))
|
|
5698
|
+
}
|
|
5699
|
+
return
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
if (pathname === `${ROUTE_PREFIX}/repo-land-config`) {
|
|
5703
|
+
const dir = typeof body.dir === 'string' ? body.dir.trim() : ''
|
|
5704
|
+
if (dir === '' || !/^[A-Za-z]:[\\/]/.test(dir) && !/^\\\\/.test(dir)) {
|
|
5705
|
+
sendError(res, 400, '保存路径不合法:请提供绝对路径(如 D:\\dsh\\repos)')
|
|
5706
|
+
return
|
|
5707
|
+
}
|
|
5708
|
+
try {
|
|
5709
|
+
setReposDir(dir)
|
|
5710
|
+
sendJson(res, 200, { ok: true, dir: getReposDir() })
|
|
5711
|
+
} catch (error) {
|
|
5712
|
+
sendError(res, 500, error instanceof Error ? error.message : String(error))
|
|
5713
|
+
}
|
|
5714
|
+
return
|
|
5715
|
+
}
|
|
5716
|
+
|
|
5717
|
+
if (pathname === `${ROUTE_PREFIX}/repo-remove`) {
|
|
5718
|
+
const target = typeof body.path === 'string' ? body.path : ''
|
|
5719
|
+
const rootNorm = getReposDir().replace(/[\\/]+$/u, '')
|
|
5720
|
+
const tNorm = target.replace(/[\\/]+$/u, '')
|
|
5721
|
+
if (!tNorm.startsWith(rootNorm) || tNorm === rootNorm || !existsSync(target)) {
|
|
5722
|
+
sendError(res, 400, '路径不在仓库落地目录内或不存在')
|
|
5723
|
+
return
|
|
5724
|
+
}
|
|
5725
|
+
try {
|
|
5726
|
+
rmSync(target, { recursive: true, force: true })
|
|
5727
|
+
sendJson(res, 200, { ok: true })
|
|
5728
|
+
} catch (error) {
|
|
5729
|
+
sendError(res, 500, `删除失败:${error instanceof Error ? error.message : String(error)}`)
|
|
5730
|
+
}
|
|
5731
|
+
return
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
if (pathname === `${ROUTE_PREFIX}/repo-open`) {
|
|
5735
|
+
const target = typeof body.path === 'string' ? body.path : ''
|
|
5736
|
+
const rootNorm = getReposDir().replace(/[\\/]+$/u, '')
|
|
5737
|
+
const tNorm = target.replace(/[\\/]+$/u, '')
|
|
5738
|
+
if (!tNorm.startsWith(rootNorm) || tNorm === rootNorm) {
|
|
5739
|
+
sendError(res, 400, '路径不在仓库落地目录内')
|
|
5740
|
+
return
|
|
5741
|
+
}
|
|
5742
|
+
if (!existsSync(target)) {
|
|
5743
|
+
sendError(res, 404, `目录不存在:${target}`)
|
|
5744
|
+
return
|
|
5745
|
+
}
|
|
5746
|
+
try {
|
|
5747
|
+
execFile('explorer.exe', [target], { windowsHide: true, detached: true }).unref()
|
|
5748
|
+
sendJson(res, 200, { ok: true })
|
|
5749
|
+
} catch (error) {
|
|
5750
|
+
sendError(res, 500, `打开失败:${error instanceof Error ? error.message : String(error)}`)
|
|
5751
|
+
}
|
|
5752
|
+
return
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5755
|
+
if (pathname === `${ROUTE_PREFIX}/component/autostart`) {
|
|
5756
|
+
const id = typeof body.id === 'string' ? body.id : ''
|
|
5757
|
+
const enabled = body.enabled === true
|
|
5758
|
+
if (id === '') {
|
|
5759
|
+
sendError(res, 400, '缺少组件 id')
|
|
5760
|
+
return
|
|
5761
|
+
}
|
|
5762
|
+
if (!compFind(id)) {
|
|
5763
|
+
sendError(res, 404, '组件不存在')
|
|
5764
|
+
return
|
|
5765
|
+
}
|
|
5766
|
+
compUpsert({ id, autoStart: enabled })
|
|
5767
|
+
if (enabled) {
|
|
5768
|
+
compStart(id).catch(() => {})
|
|
5769
|
+
}
|
|
5770
|
+
sendJson(res, 200, { ok: true, id, autoStart: enabled })
|
|
5771
|
+
return
|
|
5772
|
+
}
|
|
5773
|
+
|
|
4944
5774
|
if (pathname === `${ROUTE_PREFIX}/component/start`) {
|
|
4945
5775
|
const id = typeof body.id === 'string' ? body.id : ''
|
|
4946
5776
|
if (id === '') {
|