@noob-stupid/dsh-plugin-console 0.5.23 → 0.5.25
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 +19 -0
- package/README.zh.md +18 -0
- package/lib/client.js +94 -9
- package/lib/server/domain/allow-builds.js +330 -0
- package/lib/server/domain/dep-source.js +19 -1
- package/lib/server/domain/git-channel.js +33 -2
- package/lib/server/domain/install-diagnose.js +47 -2
- package/lib/server/domain/install-job.js +31 -33
- package/lib/server/domain/install-verify.js +18 -1
- package/lib/server/domain/install.js +3 -3
- package/lib/server/domain/market.js +18 -1
- package/lib/server/domain/plugin-actions.js +95 -9
- package/lib/server/domain/preset-source.js +339 -0
- package/lib/server/domain/repo-size.js +136 -0
- package/lib/server/domain/repoland.js +86 -22
- package/lib/server/domain/suite.js +14 -28
- package/package.json +1 -1
|
@@ -127,6 +127,8 @@ function summarizeCloneErrors(errors) {
|
|
|
127
127
|
if (e.deferred === true && e.timedOut !== true) return `${e.url}(探活失败的源,已在本轮末尾重试)`
|
|
128
128
|
if (e.skipped === true) return `${e.url}(探活失败,已降级到本轮末尾重试)`
|
|
129
129
|
if (e.retrying === true) return `${e.url}(超时,已改用更长超时重试)`
|
|
130
|
+
// 批次 D-②(2026-09-27):稀疏不被支持 → 已降级普通 clone,原因要留在清单里(不再只报"失败")
|
|
131
|
+
if (typeof e.sparseUnsupported === 'string') return `${e.url}(${e.message})`
|
|
130
132
|
if (e.timedOut === true) {
|
|
131
133
|
// 批次 B-⑧(2026-09-27):超时到底"收到多少字节"必须写出来 —— 0 B 就是镜像只连不传(
|
|
132
134
|
// 换源即可),有字节则是真在传、只是慢(值得再等)。旧文案只有"超时"两个字,用户无法判断。
|
|
@@ -232,18 +234,32 @@ function measureProgressBytes(part, deps = {}) {
|
|
|
232
234
|
return bytes > 0 ? bytes : measure(part)
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
/**
|
|
236
|
-
|
|
237
|
+
/** git 稀疏/定向取源码的参数(2026-09-27 加法,批次 D-②):`--filter=blob:none` 只取提交/树对象
|
|
238
|
+
* (不取文件内容),`--sparse` 让工作区只落地根目录文件;需要哪个目录再用
|
|
239
|
+
* `git sparse-checkout set <dir>` 单独拉下来。真机实测(yjh051108/dsh-routing-suite,1334 KB):
|
|
240
|
+
* 骨架 92 KB / 3.9 秒 → `sparse-checkout set preset` 后总共 1.0 MB;而整仓 clone 是 2.34 MB / 17.3 秒。
|
|
241
|
+
* 意义不在这个仓库省了多少,而在**巨仓里的小目录**:整仓 clone 会白拉几百 MB,稀疏只拉那一个目录。 */
|
|
242
|
+
const GIT_SPARSE_FLAGS = ['--filter=blob:none', '--sparse']
|
|
243
|
+
|
|
244
|
+
/** 跑一条 git 命令:支持超时,且**超时即杀掉整棵树并等它真的退出**。
|
|
245
|
+
* 返回 { code, stdout, stderr, timedOut, pid, exited }(stdout 供 ls-tree/show 这类只读命令用)。
|
|
246
|
+
* 2026-09-27 从 runGitClone **原样抽出**(只把 argv 变成形参、cwd 变成选项):稀疏取源码的
|
|
247
|
+
* `sparse-checkout set` / `ls-tree` / `show` 与 clone 共用同一套进程管理(停滞判据 + kill-tree +
|
|
248
|
+
* wait-for-exit),**不新造第二套**。 */
|
|
249
|
+
function runGitArgs(argv, { timeout = GIT_CLONE_TIMEOUT_MS, cwd = null, spawnFn = spawn, killTree = killProcessTree, exitWaitMs = 800, exitWaitMs2 = 300, stallGuard = true } = {}) {
|
|
237
250
|
return new Promise((resolve) => {
|
|
238
251
|
let settled = false
|
|
239
252
|
let timedOut = false
|
|
240
|
-
const finish = (payload) => { if (!settled) { settled = true; resolve(payload) } }
|
|
253
|
+
const finish = (payload) => { if (!settled) { settled = true; resolve({ ...payload, stdout: stdout.trim() }) } }
|
|
241
254
|
let child = null
|
|
242
255
|
let stderr = ''
|
|
256
|
+
let stdout = ''
|
|
257
|
+
const args = [...(stallGuard ? GIT_STALL_ARGS : []), ...argv]
|
|
243
258
|
try {
|
|
244
|
-
child = spawnFn('git',
|
|
259
|
+
child = spawnFn('git', args, {
|
|
245
260
|
windowsHide: true,
|
|
246
261
|
env: gitEnv(),
|
|
262
|
+
...(cwd === null ? {} : { cwd }),
|
|
247
263
|
detached: process.platform !== 'win32', // POSIX:自成进程组,便于 -pid 整体杀
|
|
248
264
|
})
|
|
249
265
|
} catch (error) {
|
|
@@ -262,12 +278,49 @@ function runGitClone(url, dest, timeout, { spawnFn = spawn, killTree = killProce
|
|
|
262
278
|
finish({ code: -1, stderr: stderr.trim(), timedOut: true, pid: child.pid, exited })
|
|
263
279
|
}, timeout)
|
|
264
280
|
child.stderr?.on?.('data', (chunk) => { stderr += String(chunk) })
|
|
281
|
+
child.stdout?.on?.('data', (chunk) => { stdout += String(chunk) })
|
|
265
282
|
child.on('error', (error) => { clearTimeout(timer); if (timedOut) return; finish({ code: -1, stderr: String(error?.message ?? error), timedOut: false, pid: child.pid, exited: true }) })
|
|
266
283
|
// 超时分支自己收尾(要先把退出等完),这里的 close 不能再抢答
|
|
267
284
|
child.on('close', (code) => { clearTimeout(timer); if (timedOut) return; finish({ code: typeof code === 'number' ? code : -1, stderr: stderr.trim(), timedOut: false, pid: child.pid, exited: true }) })
|
|
268
285
|
})
|
|
269
286
|
}
|
|
270
287
|
|
|
288
|
+
/** git 报"我不认识这个选项/子命令"的判据(纯函数,单测覆盖):稀疏取源码不被支持时要能识别,
|
|
289
|
+
* 才能**降级到普通 clone**(而不是把整个候选判死)。真机可能的三类:
|
|
290
|
+
* · 旧 git 不认 `--filter` / `--sparse`(`unknown option` / `error: unknown option`)
|
|
291
|
+
* · 服务端不支持 partial clone(git 只警告,不算失败 → 这里不误判)
|
|
292
|
+
* · `sparse-checkout` 子命令不存在(`git: 'sparse-checkout' is not a git command`) */
|
|
293
|
+
function sparseUnsupportedReason(text) {
|
|
294
|
+
const s = String(text ?? '')
|
|
295
|
+
if (/is not a git command|unknown subcommand|no such command/iu.test(s)) return 'git 不认识 sparse-checkout 子命令(版本过旧)'
|
|
296
|
+
if (/unknown option|unrecognized option|invalid option|usage: git clone/iu.test(s)) return 'git 不认识 --filter/--sparse 选项(版本过旧)'
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 跑一次 git clone:`sparse` 传数组则走稀疏取源码(`--filter=blob:none --sparse`;非空时再
|
|
301
|
+
* `sparse-checkout set` 把那几个目录拉下来,"不支持"由 sparseUnsupportedReason 识别后交给上层降级)。
|
|
302
|
+
* 返回 { code, stderr, timedOut, pid, exited, sparse, sparseUnsupported }。 */
|
|
303
|
+
async function runGitClone(url, dest, timeout, { sparse = null, spawnFn = spawn, killTree = killProcessTree, exitWaitMs = 800, exitWaitMs2 = 300 } = {}) {
|
|
304
|
+
const useSparse = Array.isArray(sparse)
|
|
305
|
+
const args = ['clone', '--depth', '1', ...(useSparse ? GIT_SPARSE_FLAGS : []), '--quiet', url, dest]
|
|
306
|
+
const res = await runGitArgs(args, { timeout, spawnFn, killTree, exitWaitMs, exitWaitMs2 })
|
|
307
|
+
if (res.code !== 0 || !useSparse || sparse.length === 0) {
|
|
308
|
+
return { ...res, sparse: useSparse, sparseUnsupported: sparseUnsupportedReason(res.stderr) }
|
|
309
|
+
}
|
|
310
|
+
// 稀疏克隆成功 → 再把这几个目录单独拉下来(同一个 runGitArgs:超时照样杀树 + 等退出)
|
|
311
|
+
const set = await runGitArgs(['sparse-checkout', 'set', ...sparse.map((d) => String(d))], { timeout, cwd: dest, spawnFn, killTree, exitWaitMs, exitWaitMs2 })
|
|
312
|
+
if (set.code === 0) return { ...res, sparse: true, sparseUnsupported: null }
|
|
313
|
+
return {
|
|
314
|
+
code: -1,
|
|
315
|
+
stderr: `${set.stderr || `git sparse-checkout set 退出码 ${set.code}`}`,
|
|
316
|
+
timedOut: set.timedOut === true,
|
|
317
|
+
pid: set.pid ?? null,
|
|
318
|
+
exited: set.exited !== false,
|
|
319
|
+
sparse: true,
|
|
320
|
+
sparseUnsupported: sparseUnsupportedReason(set.stderr),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
271
324
|
/** git 智能 HTTP 的探活地址(批次 B-⑦②,2026-09-27 改错):从 `HEAD /` 改成
|
|
272
325
|
* `GET <url>/info/refs?service=git-upload-pack` —— 后者是 git 协议的**真实入口**,
|
|
273
326
|
* 响应首行必须是以 4 位十六进制长度开头的 pkt-line(如 `001e# service=git-upload-pack`),
|
|
@@ -332,22 +385,17 @@ async function probeSourceAlive(url, timeoutMs = 4000, deps = {}) {
|
|
|
332
385
|
* 每次尝试都用**全新唯一目录** `.tryN`,成功后才 rename 到 dest —— 这样即使上一轮残留目录被占用,
|
|
333
386
|
* 也不会再出现"一个源失败 → 后面所有源都因目录非空而无效"的连锁失效。
|
|
334
387
|
* 2026-09-26 真机加法:① 超时杀树后**等进程真的退出**(runGitClone 内两轮 kill+wait);
|
|
335
|
-
* ② 失败后的清理走
|
|
336
|
-
*
|
|
337
|
-
* ③ 源策略:**同一个源超时后用更长超时(默认 1.75
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* 手动删除,也不再因为"清不掉"而放弃后面本来可用的源。`deps.removeDir` 这个注入口名字没变。
|
|
343
|
-
* 2026-09-27(本次改错):① 首轮超时 180 秒 → 60 秒(停滞判据已把"只连不传"提前到 ≈20 秒判死);
|
|
344
|
-
* ② "同源 1.75 倍长超时重试"从**无条件**改成**只对"有进度"的源**——
|
|
345
|
-
* 每个失败记录都带上 `bytesReceived`(.git/objects 落盘字节数),0 B 就说明
|
|
346
|
-
* 这个源一点都没传,再用更长超时重试只是把白等拉长(真机 ghproxy 实测 0 B/s)。
|
|
388
|
+
* ② 失败后的清理走 disposeDir(删不掉就改名降级成 `.trash-<ts>-<rand>`,见 infra/fsx.js)
|
|
389
|
+
* —— 残留目录不再需要用户手动删除,也不因"清不掉"放弃后面本来可用的源;
|
|
390
|
+
* ③ 源策略:**同一个源超时后用更长超时(默认 1.75 倍)重试一次**,并把**上次
|
|
391
|
+
* 成功过的源**提到最前来试(本机直连 github 不通、唯一可用源就是 ghproxy)。
|
|
392
|
+
* 2026-09-27(改错):① 首轮超时 180 秒 → 60 秒(停滞判据已把"只连不传"提前到 ≈20 秒判死);
|
|
393
|
+
* ② 长超时重试从**无条件**改成**只对"有进度"的源**(失败记录带 `bytesReceived`,
|
|
394
|
+
* 0 B 就是一点没传,再等只是把白等拉长 —— 真机 ghproxy 实测 0 B/s);
|
|
347
395
|
* 2026-09-27(批次 B-⑦):③ 探活判据换成 `GET <url>/info/refs?service=git-upload-pack` + pkt-line 校验,
|
|
348
|
-
*
|
|
349
|
-
* ④ **探活失败的源不再"一次定生死"
|
|
350
|
-
*
|
|
396
|
+
* 失败归因(网络不可达 / 本地代理·证书拦截)写进错误清单;
|
|
397
|
+
* ④ **探活失败的源不再"一次定生死"**:降级到**最后一轮**再试一次。
|
|
398
|
+
* 2026-09-27(批次 D-②):⑤ `deps.sparse` 走稀疏取源码;不被支持时**降级到普通 clone**(下一轮)。 */
|
|
351
399
|
async function gitCloneRepo(repo, dest, source = 'github', timeout = GIT_CLONE_TIMEOUT_MS, deps = {}) {
|
|
352
400
|
const {
|
|
353
401
|
spawnFn = spawn,
|
|
@@ -366,7 +414,13 @@ async function gitCloneRepo(repo, dest, source = 'github', timeout = GIT_CLONE_T
|
|
|
366
414
|
reuseLanded = true,
|
|
367
415
|
findLanded = findLandedRepo,
|
|
368
416
|
copyLanded = copyTree,
|
|
417
|
+
sparse = null,
|
|
369
418
|
} = deps
|
|
419
|
+
// 批次 D-②(2026-09-27 加法):`sparse` 传数组即走稀疏取源码(`--filter=blob:none --sparse`,
|
|
420
|
+
// 非空时再 `sparse-checkout set` 拉那几个目录)。**稀疏不被支持时降级到普通 clone**:
|
|
421
|
+
// 同一个源的第二轮(更长超时)改用普通 clone,并把原因如实写进错误清单。
|
|
422
|
+
const wantSparse = Array.isArray(sparse)
|
|
423
|
+
let sparseDowngraded = false
|
|
370
424
|
// ★ 已落地仓库优先复用(批次 C-⑩):命中就直接把本地那份搬到 dest,一个网络请求都不发。
|
|
371
425
|
// 跳过 .git 是有意的(copyTree 的既有语义):上层要的是仓库**内容**(package.json/.gitmodules),
|
|
372
426
|
// 本机已有完整仓库,没必要把几百 MB 的 .git 再复制一份。
|
|
@@ -405,15 +459,17 @@ async function gitCloneRepo(repo, dest, source = 'github', timeout = GIT_CLONE_T
|
|
|
405
459
|
partSeq += 1
|
|
406
460
|
const part = `${dest}.try${partSeq}`
|
|
407
461
|
const useTimeout = round === 0 ? timeout : retryTimeout
|
|
462
|
+
// 稀疏只在第一轮试(round 0);"不被支持"或退到第二轮时一律普通 clone(降级语义)。
|
|
463
|
+
const useSparse = wantSparse && round === 0 ? sparse : null
|
|
408
464
|
try { removeDir(part) } catch {}
|
|
409
465
|
// eslint-disable-next-line no-await-in-loop
|
|
410
|
-
const res = await runGitClone(url, part, useTimeout, { spawnFn, killTree, exitWaitMs })
|
|
466
|
+
const res = await runGitClone(url, part, useTimeout, { spawnFn, killTree, exitWaitMs, sparse: useSparse })
|
|
411
467
|
if (res.code === 0) {
|
|
412
468
|
try {
|
|
413
469
|
removeDir(dest)
|
|
414
470
|
renameDir(part, dest)
|
|
415
471
|
try { writeMemo(cand.urlTemplate) } catch {}
|
|
416
|
-
return { result: { url, attempt: sourceIndex + 1, tries: partSeq, dir: dest, source: cand.id, retried: round > 0, deferred: isDeferred } }
|
|
472
|
+
return { result: { url, attempt: sourceIndex + 1, tries: partSeq, dir: dest, source: cand.id, retried: round > 0, deferred: isDeferred, sparse: res.sparse === true, downgradedFromSparse: sparseDowngraded } }
|
|
417
473
|
} catch (error) {
|
|
418
474
|
errors.push({ url, message: `克隆成功但落地失败(${error instanceof Error ? error.message : String(error)})`, dir: part, deferred: isDeferred })
|
|
419
475
|
return null
|
|
@@ -448,6 +504,14 @@ async function gitCloneRepo(repo, dest, source = 'github', timeout = GIT_CLONE_T
|
|
|
448
504
|
})
|
|
449
505
|
return null // 清不掉的残留和这个源绑着,换下一个源(新目录不受影响)
|
|
450
506
|
}
|
|
507
|
+
// ★ 批次 D-②(2026-09-27):稀疏取源码**不被支持**(旧 git 不认 --filter/--sparse,或没有
|
|
508
|
+
// sparse-checkout 子命令)→ **降级到普通 clone**:同一轮的下一轮(更长超时)改用普通 clone,
|
|
509
|
+
// 并把原因如实写进错误清单(不静默降级,用户能在失败/备注里看到"为什么走了整仓 clone")。
|
|
510
|
+
if (round === 0 && typeof res.sparseUnsupported === 'string') {
|
|
511
|
+
sparseDowngraded = true
|
|
512
|
+
errors.push({ url, message: `稀疏取源码不被支持(${res.sparseUnsupported}),已降级到普通 clone 重试`, stderr: res.stderr, sparseUnsupported: res.sparseUnsupported, sparse: true, bytesReceived, dir: part, deferred: isDeferred, ...trashField })
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
451
515
|
if (res.timedOut === true && round === 0 && hasProgress) {
|
|
452
516
|
// ★ 同源、更长超时,重试一次 —— **只对真有进度的源**(0 B 的源再等一次只是把白等拉长)
|
|
453
517
|
errors.push({ url, message: `克隆超时(${useTimeout}ms,本次已收到 ${bytesReceived} B),改用 ${retryTimeout}ms 同源重试`, stderr: res.stderr, timedOut: true, retrying: true, bytesReceived, dir: part, deferred: isDeferred, ...trashField })
|
|
@@ -530,4 +594,4 @@ async function gitCloneRepo(repo, dest, source = 'github', timeout = GIT_CLONE_T
|
|
|
530
594
|
throw new Error(summarizeCloneErrors(errors))
|
|
531
595
|
}
|
|
532
596
|
|
|
533
|
-
export { reposDirCache, getReposDir, setReposDir, listLandedRepos, findLandedRepo, gitCloneRepo, summarizeCloneErrors, orderGitCandidates, readGitSourceMemo, rememberGitSource, killProcessTree, probeSourceAlive, probeSourceAliveDetail, gitInfoRefsUrl, classifyProbeFailure, GIT_STALL_ARGS, GIT_CLONE_TIMEOUT_MS, measureDirBytes, measureProgressBytes }
|
|
597
|
+
export { reposDirCache, getReposDir, setReposDir, listLandedRepos, findLandedRepo, gitCloneRepo, summarizeCloneErrors, orderGitCandidates, readGitSourceMemo, rememberGitSource, killProcessTree, probeSourceAlive, probeSourceAliveDetail, gitInfoRefsUrl, classifyProbeFailure, runGitArgs, runGitClone, sparseUnsupportedReason, GIT_STALL_ARGS, GIT_SPARSE_FLAGS, waitChildExit, GIT_CLONE_TIMEOUT_MS, measureDirBytes, measureProgressBytes }
|
|
@@ -6,6 +6,7 @@ import { dirname, join, basename } from 'node:path'
|
|
|
6
6
|
import { tmpdir } from 'node:os'
|
|
7
7
|
import { appendInsert } from './patch.js'
|
|
8
8
|
import { gitCloneRepo } from './repoland.js'
|
|
9
|
+
import { findPresetDirs, assemblePreset } from './preset-source.js'
|
|
9
10
|
import { deriveEntryId, listEntries } from './runtime.js'
|
|
10
11
|
import { noteChannel } from './git-channel.js'
|
|
11
12
|
import { orderedRegistries, readSources } from './sources.js'
|
|
@@ -36,26 +37,6 @@ function readGitmodules(dir) {
|
|
|
36
37
|
return subs.filter((s) => s.path !== '' && s.url !== '')
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/** 递归找含 preset.yml + agent.cordis.yml 的 agent 预设目录(深度 ≤ maxDepth)。 */
|
|
40
|
-
function findPresetDirs(root, maxDepth = 2) {
|
|
41
|
-
const out = []
|
|
42
|
-
const walk = (dir, depth) => {
|
|
43
|
-
if (depth > maxDepth) return
|
|
44
|
-
let entries = []
|
|
45
|
-
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
46
|
-
if (existsSync(join(dir, 'preset.yml')) && existsSync(join(dir, 'agent.cordis.yml'))) {
|
|
47
|
-
out.push(dir)
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
for (const e of entries) {
|
|
51
|
-
if (!e.isDirectory() || e.name.startsWith('.')) continue
|
|
52
|
-
walk(join(dir, e.name), depth + 1)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
walk(root, 0)
|
|
56
|
-
return out
|
|
57
|
-
}
|
|
58
|
-
|
|
59
40
|
/** 包是否有构建产物(main/module/exports/bin 或 lib/index.js 任一存在)。
|
|
60
41
|
* 排除类型声明(.d.ts)与 package.json 自身——exports 的 "./package.json" 是合法导出但不是运行时入口。 */
|
|
61
42
|
function packageEntryExists(dir, pkg) {
|
|
@@ -296,16 +277,21 @@ async function runSuiteInstallJob(job, ports, deps = {}) {
|
|
|
296
277
|
}
|
|
297
278
|
// c. agent 预设(含 preset.yml + agent.cordis.yml 的目录;预设优先于普通 npm 包——
|
|
298
279
|
// 如 dsh-router-standard 既是 npm 包又带预设目录,install.ps1 意图是复制预设)
|
|
280
|
+
// 2026-09-27(批次 D-③ 改错):装配走 preset-source.js#assemblePreset(**唯一实现**),
|
|
281
|
+
// 与"预设型子包"候选路径共用。旧代码这里是 `rmSync(dest)` + `copyTree` —— **静默覆盖**
|
|
282
|
+
// 用户手上的同名预设;现在改为"先整目录备份、再逐文件合并",并把备份路径与覆盖清单写进报告。
|
|
299
283
|
const presets = findPresetDirs(subDir, 2)
|
|
300
284
|
for (const p of presets) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
285
|
+
const assembled = assemblePreset(p, basename(p))
|
|
286
|
+
report.push({
|
|
287
|
+
component: sub.name,
|
|
288
|
+
type: 'preset',
|
|
289
|
+
ok: assembled.ok === true,
|
|
290
|
+
note: assembled.ok === true ? `${assembled.note}(新建会话时选择)` : `预设装配失败:${assembled.error}`,
|
|
291
|
+
presetPath: assembled.dest ?? null,
|
|
292
|
+
backup: assembled.backup ?? null,
|
|
293
|
+
})
|
|
294
|
+
if (assembled.ok === true) handled = true
|
|
309
295
|
}
|
|
310
296
|
// d. 普通 npm 插件(无 bundle 且无预设/技能)
|
|
311
297
|
if (!handled && pkg && typeof pkg.name === 'string') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.25",
|
|
4
4
|
"description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|