@noob-stupid/dsh-plugin-console 0.5.28 → 0.5.30

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.
@@ -7,17 +7,20 @@
7
7
  // 「一发消息就报错、会话整体不可用」。包级适配门(peerDependencies / 版本号)看不见这类
8
8
  // 运行期契约破坏,所以必须在**升级前**单独预检、先把本地适配好再装。
9
9
 
10
- import { readFileSync } from 'node:fs'
10
+ import { existsSync, readFileSync } from 'node:fs'
11
11
  import { dirname, join } from 'node:path'
12
12
  import { isFrameworkOwnedPackage } from '../domain/compat.js'
13
+ import { classifyTargetAvailability, frameworkPackageRows, parsePatchRows } from '../domain/patch-composition-audit.js'
14
+ import { collectPresetAudit } from '../domain/preset-audit.js'
13
15
  import { loadContractRules, summarizeContractRules } from '../domain/contract-rules.js'
14
16
  import { discoverFormatContract, summarizeContract } from '../domain/format-contract.js'
15
17
  import { applyFormatPatch, collectProducerTargets, scanProducerFiles } from '../domain/format-scan.js'
16
18
  import { currentFrameworkVersion } from '../domain/framework.js'
17
19
  import { listEntries } from '../domain/runtime.js'
18
20
  import { curlText, fetchJsonUrl } from '../infra/http.js'
21
+ import { checkPublishedVersions, makeVersionProbe } from '../infra/registry-versions.js'
19
22
  import { sendError, sendJson } from '../infra/httpd.js'
20
- import { baseDirOf, dshHome, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
23
+ import { baseDirOf, dshHome, findPatchPath, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
21
24
 
22
25
  const REGISTRY = 'https://registry.npmmirror.com'
23
26
 
@@ -109,6 +112,42 @@ async function routeFrameworkPreflight(req, res, rc) {
109
112
  const rules = contract?.rules ?? { producerKindRequired: false, forbiddenKinds: ['plugin'], pluginPrefix: 'plugin:', renames: {} }
110
113
  const scan = scanProducerFiles({ targets, rules, targetVersion })
111
114
 
115
+ // 补丁 ↔ 目标版本核对(2026-09-28 加法;真机事故:0.2.0-rc.1 移除了
116
+ // `@deepseek-ai/dsh-workflow-worker-thread`,补丁三处仍引用 → 升级后 `workflowEngine`
117
+ // 没有提供方 → `dsh-tool-workflow`/`dsh-tool-ralph` 永久 waiting → 老会话 resume 失败)。
118
+ // 只核对**随框架版本走**的包(`@deepseek-ai/*`):目标版本还没装到本机,只有 registry 元数据能提前看见。
119
+ // 网络不可用/查询失败一律记 `unknown`,**绝不判死**。
120
+ let patchTarget = { checked: 0, missing: [], unknown: [], skipped: null, patchPath: null }
121
+ try {
122
+ const patchPath = findPatchPath(rc.ctx)
123
+ patchTarget.patchPath = patchPath
124
+ if (patchPath === null || !existsSync(patchPath)) {
125
+ patchTarget.skipped = '找不到 profile 补丁文件(cordis.patch.yml)'
126
+ } else {
127
+ const rows = parsePatchRows(readFileSync(patchPath, 'utf8'))
128
+ const fwRows = frameworkPackageRows(rows)
129
+ if (fwRows.length === 0) patchTarget.skipped = '补丁里没有随框架版本走的包(@deepseek-ai/*)'
130
+ else if (!networkAllowed()) patchTarget.skipped = '已跳过网络(DSH_TEST_SKIP_NETWORK=1)'
131
+ else {
132
+ const probe = makeVersionProbe({ registry: REGISTRY, fetchJson: (url, timeoutMs) => fetchJsonUrl(url, timeoutMs) })
133
+ const results = await checkPublishedVersions(fwRows, { probe, version: targetVersion })
134
+ const publishedBy = new Map(results.map((row) => [row.name, row.published]))
135
+ const classified = classifyTargetAvailability(fwRows, { publishedBy, targetVersion })
136
+ patchTarget = { ...patchTarget, checked: results.length, missing: classified.missing, unknown: classified.unknown, skipped: null }
137
+ }
138
+ }
139
+ } catch (error) {
140
+ patchTarget.skipped = '核对失败:' + String(error?.message ?? error)
141
+ }
142
+ // 预设声明行体检(2026-09-28 加法):升级前就能看见"补丁里的声明行已陈旧/缺失"。
143
+ // 真机事故:预设自身被改成 v10、补丁行还指向 v1 → 老会话 resume 到错的模块,且没有任何地方会报。
144
+ // 只读、不阻塞:作为 presetAudit 字段随预检一起下发(blockers>0 时由面板提示先修再升)。
145
+ let presetAudit = null
146
+ try {
147
+ presetAudit = collectPresetAudit({ presetsRoot: join(dshHome(), '.agent-presets'), patchPath: findPatchPath(rc.ctx) })
148
+ } catch (error) {
149
+ presetAudit = { ok: false, error: String(error?.message ?? error), blockers: 0, stale: [], missing: [], orphan: [], targetMissing: [] }
150
+ }
112
151
  sendJson(res, 200, {
113
152
  ok: true,
114
153
  targetVersion,
@@ -122,6 +161,8 @@ async function routeFrameworkPreflight(req, res, rc) {
122
161
  rules: summarizeContractRules(loadContractRules()),
123
162
  roots: roots.map((r) => ({ root: r.root, kind: r.kind, moduleName: r.moduleName ?? null })),
124
163
  scan,
164
+ patchTarget,
165
+ presetAudit,
125
166
  })
126
167
  }
127
168
 
@@ -15,7 +15,9 @@ import { readBody, sendError } from '../infra/httpd.js'
15
15
  import { routeAiConsent, routeAiEmpowerCancel, routeAiEmpowerList, routeAiEmpowerPlan, routeAiEmpowerRun, routeAiEmpowerStatus } from './ai.js'
16
16
  import { routeRunSuggested } from './actions.js'
17
17
  import { routeComponentAutostart, routeComponentStart, routeComponentStatus, routeComponentStop, routeComponents, routeRepoClone, routeRepoLandConfig, routeRepoList, routeRepoOpen, routeRepoRemove } from './components.js'
18
- import { routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled } from './compat.js'
18
+ import { routeCompatAck, routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled } from './compat.js'
19
+ import { routePatchAuditGet } from './patch-audit.js'
20
+ import { routePresetAuditGet } from './preset-audit.js'
19
21
  import { routeFrameworkUpgrade } from './framework-upgrade.js'
20
22
  import { routeFrameworkPreflight, routeFrameworkPreflightPatch } from './framework-preflight.js'
21
23
  import { routeCheckUpdate, routeCompatGate, routeFrameworkCheck, routeFrameworkRelaunch, routeFrameworkRollback, routeFrameworkStatusClear, routeFrameworkUpgradeStatusGet, routeRestart } from './framework.js'
@@ -37,6 +39,15 @@ const ROUTES_EARLY = [
37
39
  { methods: ['GET'], path: `${ROUTE_PREFIX}/gitee-oauth-callback`, handler: routeGiteeOauthCallbackGet },
38
40
  { methods: ['GET'], path: `${ROUTE_PREFIX}/skills-installed`, handler: routeSkillsInstalledGet }, { methods: ['GET'], path: `${ROUTE_PREFIX}/framework-upgrade-status`, handler: routeFrameworkUpgradeStatusGet },
39
41
  { methods: ['GET'], path: `${ROUTE_PREFIX}/compat-status`, handler: routeCompatStatusGet },
42
+ // 只读体检(2026-09-30 改错:这两条**必须**住在守卫之前)。
43
+ // 原实现把它们登记在守卫之后的 ROUTES 表里 → GET 在守卫处就被 405 掉,永远到不了 handler。
44
+ // 真机现场(0.5.29 活体实例):`GET /plugin-console/preset-audit` → 405「不支持的方法」,
45
+ // `POST` 同路径 → 404「未知接口」—— 两句都由 handle() 自己发出,说明**查表根本没参与**。
46
+ // 判据:405 守卫 `if (method !== 'POST' && !(method === 'GET' && GET_COMPAT.has(pathname)))`
47
+ // 跑在 ROUTES 查表之前,只放行 POST(外加 GET_COMPAT 白名单)→
48
+ // **任何声明 GET 的路由都必须登记在 ROUTES_EARLY**。该判据已固化进 tests/test-route-inventory.mjs。
49
+ { methods: ['GET'], path: `${ROUTE_PREFIX}/patch-audit`, handler: routePatchAuditGet },
50
+ { methods: ['GET'], path: `${ROUTE_PREFIX}/preset-audit`, handler: routePresetAuditGet },
40
51
  { methods: ['POST'], path: `${ROUTE_PREFIX}/framework-relaunch`, handler: routeFrameworkRelaunch },
41
52
 
42
53
  ]
@@ -98,6 +109,7 @@ const ROUTES = [
98
109
  { methods: ['POST'], path: `${ROUTE_PREFIX}/framework-status-clear`, handler: routeFrameworkStatusClear },
99
110
  { methods: ['POST'], path: `${ROUTE_PREFIX}/compat-mode`, handler: routeCompatMode },
100
111
  { methods: ['POST'], path: `${ROUTE_PREFIX}/compat-stamp`, handler: routeCompatStamp },
112
+ { methods: ['POST'], path: `${ROUTE_PREFIX}/compat-ack`, handler: routeCompatAck },
101
113
  { methods: ['POST'], path: `${ROUTE_PREFIX}/declare-installed`, handler: routeDeclareInstalled },
102
114
  { methods: ['POST'], path: `${ROUTE_PREFIX}/framework-rollback`, handler: routeFrameworkRollback },
103
115
  { methods: ['POST'], path: `${ROUTE_PREFIX}/restart`, handler: routeRestart },
@@ -0,0 +1,59 @@
1
+ // L2 · routes —— 补丁体检(GET /plugin-console/patch-audit;0.5.30 加法)
2
+ //
3
+ // 真机事故 2026-09-28:框架 0.2.0-rc.1 移除了 `@deepseek-ai/dsh-workflow-worker-thread`(引擎改为 ptc),
4
+ // 而 profile 补丁三处行仍指向旧包 → 该行解析失败 → 隔离组里 `workflowEngine` 没有提供方 →
5
+ // `dsh-tool-workflow` / `dsh-tool-ralph` 永久 waiting → 老会话 resume 失败
6
+ // (RemoteError: tool-workflow … waiting for workflowEngine)。
7
+ //
8
+ // 本路由把 domain/patch-composition-audit.js 的**纯判据**接到真实环境(真补丁文件 + 真 Node 解析器),
9
+ // 并且**只报告、不代改**:改名建议来自「真机确证表」(KNOWN_RENAMES),猜不到的只列候选 ——
10
+ // 绝不自动改写用户补丁(同 preset-rows.js 的判据文化:宁缺勿滥、不猜)。
11
+
12
+ import { existsSync, readFileSync } from 'node:fs'
13
+ import { dirname, join } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { auditPatchText } from '../domain/patch-composition-audit.js'
16
+ import { frameworkBases } from '../infra/framework-root.js'
17
+ import { sendError, sendJson } from '../infra/httpd.js'
18
+ import { listAvailablePackages, makePackageResolver } from '../infra/package-resolve.js'
19
+ import { baseDirOf, findPatchPath } from '../infra/paths.js'
20
+
21
+ // 框架运行时根的取法已收敛到 infra/framework-root.js(0.5.30 改错:**同一份判据**必须同时供
22
+ // 只读体检与本文件、以及 domain/patch.js 的自愈使用 —— 各写一份的话,框架下次再挪目录只会有一处被修好;
23
+ // 2026-09-29 真机事故正是"自愈那处没跟上")。
24
+
25
+ /** profile 补丁 → 体检报告(只读;不写任何文件)。 */
26
+ async function routePatchAuditGet(req, res, rc) {
27
+ const ctx = rc.ctx
28
+ let patchPath = null
29
+ try { patchPath = findPatchPath(ctx) } catch {}
30
+ if (patchPath === null || !existsSync(patchPath)) {
31
+ sendError(res, 404, '找不到 profile 补丁文件(cordis.patch.yml)—— 无法体检')
32
+ return
33
+ }
34
+ const profileDir = dirname(patchPath)
35
+ let baseDir = null
36
+ try { baseDir = baseDirOf(rc?.deps?.baseUrl ?? rc?.ctx?.baseUrl ?? 'file:///') } catch { baseDir = null }
37
+ const fwBases = frameworkBases(baseDir, profileDir)
38
+ const bases = [profileDir, ...fwBases]
39
+ const resolve = makePackageResolver(bases)
40
+ const candidates = listAvailablePackages([join(profileDir, 'node_modules'), ...fwBases])
41
+ const text = readFileSync(patchPath, 'utf8')
42
+ const report = auditPatchText(text, {
43
+ resolve,
44
+ existsFile: (url) => { try { return existsSync(fileURLToPath(url)) } catch { return false } },
45
+ candidates,
46
+ })
47
+ sendJson(res, 200, {
48
+ ok: true,
49
+ patchPath,
50
+ bases,
51
+ rows: report.rows,
52
+ blockerCount: report.blockers.length,
53
+ warningCount: report.warnings.length,
54
+ blockers: report.blockers,
55
+ warnings: report.warnings,
56
+ })
57
+ }
58
+
59
+ export { routePatchAuditGet }
@@ -5,6 +5,8 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
5
5
  import { dirname, join } from 'node:path'
6
6
  import { createRequire } from 'node:module'
7
7
  import { checkPluginFrameworkCompat, probePluginImport, readCompatPending, rowIdModuleMap, writeCompatPending } from '../domain/compat.js'
8
+ import { cleanFrameworkBackups, cleanFrameworkTopLevel, readRollbackReferenced, reportFrameworkEntities, resolveFrameworkLayout } from '../domain/framework-cleanup.js'
9
+ import { currentFrameworkVersion } from '../domain/framework.js'
8
10
  import { pnpmRemove } from '../domain/install-job.js'
9
11
  import { curlManualInstall, readExtraBundleOwners } from '../domain/install.js'
10
12
  import { disableEntry, enableEntry, readPatchState, removeDisableBlock, removeInsertRow } from '../domain/patch.js'
@@ -17,7 +19,7 @@ import { orderedRegistries, readSources } from '../domain/sources.js'
17
19
  import { fetchJsonUrl } from '../infra/http.js'
18
20
  import { sendError, sendJson } from '../infra/httpd.js'
19
21
  import { removeDirVerifiedAsync } from '../infra/fsx.js'
20
- import { dshHome, findPatchPath, pluginRoot, resolvePackageJson, rowIdOf } from '../infra/paths.js'
22
+ import { baseDirOf, dshHome, findPatchPath, pluginRoot, resolvePackageJson, rowIdOf } from '../infra/paths.js'
21
23
  import { installJobs } from '../state.js'
22
24
 
23
25
  /** 插件控制台自身的包名(撤销分支禁止删自己;@deepseek-ai/* 由通用护栏拒绝)。 */
@@ -504,7 +506,23 @@ async function routeCleanResiduals(req, res, rc) {
504
506
  }
505
507
  }
506
508
  } catch {}
507
- const payload = { ok: failed.length === 0, removed, kept, failed, count: removed.length }
509
+ // ── scope 扩展(2026-09-28 加法):framework / frameworkBackups / pnpmEntities ──
510
+ // 默认(不传 scope)与以前**完全一致**:只清插件侧残留。framework 两档必须显式 confirm:true,
511
+ // 否则只出报告(dryRun)—— 高风险动作不许"点一下全干"。
512
+ const scope = Array.isArray(body?.scope) ? body.scope.filter((s) => typeof s === 'string' && s !== '') : ['plugin']
513
+ const extra = {}
514
+ if (scope.some((s) => s !== 'plugin')) {
515
+ const dryRun = body?.confirm !== true
516
+ let runningVersion = null
517
+ try { runningVersion = currentFrameworkVersion(ctx) } catch {}
518
+ const layout = resolveFrameworkLayout({ baseDir: baseDirOf(ctx?.baseUrl ?? 'file:///'), profileDir })
519
+ if (scope.includes('framework')) extra.framework = await cleanFrameworkTopLevel({ topScopeDir: layout.topScopeDir, dshLinkPath: layout.dshLinkPath, runningVersion, relink: body?.relink !== false, dryRun, stamp: Date.now() })
520
+ if (scope.includes('frameworkBackups')) extra.frameworkBackups = await cleanFrameworkBackups({ backupsRoot: join(dshHome(), 'plugin-console', 'framework-backups'), referenced: readRollbackReferenced(), keep: Number.isFinite(body?.keep) ? body.keep : 1, dryRun })
521
+ if (scope.includes('pnpmEntities')) extra.pnpmEntities = reportFrameworkEntities({ pnpmDir: layout.pnpmDir, runningVersion })
522
+ extra.scope = scope
523
+ extra.dryRun = dryRun
524
+ }
525
+ const payload = { ok: failed.length === 0, removed, kept, failed, count: removed.length, ...extra }
508
526
  if (failed.length > 0) {
509
527
  const detail = failed.slice(0, 3).map((f) => `${f.name}(${f.error ?? '未知原因'})`).join(';')
510
528
  const keptNote = kept.length > 0 ? `;已在用插件 ${kept.length} 个(保留不删)` : ''
@@ -0,0 +1,24 @@
1
+ // L2 · routes —— 预设声明行体检(GET /plugin-console/preset-audit;0.5.30 加法)
2
+ //
3
+ // 真机背景:框架 0.1.7-rc.1 起预设靠 profile 补丁里的**声明行**发现;预设目录自身后来被改成 v10,
4
+ // 而补丁里的行还指向 v1 → 老会话 resume 到错的模块(而且没有任何地方会报)。本路由**只读**出报告:
5
+ // 陈旧(stale)/ 缺失(missing)/ 悬空(orphan)/ 目标不存在(targetMissing)。
6
+ import { existsSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+ import { collectPresetAudit } from '../domain/preset-audit.js'
9
+ import { sendError, sendJson } from '../infra/httpd.js'
10
+ import { dshHome, findPatchPath } from '../infra/paths.js'
11
+
12
+ async function routePresetAuditGet(req, res, rc) {
13
+ let patchPath = null
14
+ try { patchPath = findPatchPath(rc.ctx) } catch {}
15
+ if (patchPath === null || !existsSync(patchPath)) {
16
+ sendError(res, 404, '找不到 profile 补丁文件(cordis.patch.yml)—— 无法体检')
17
+ return
18
+ }
19
+ const presetsRoot = join(dshHome(), '.agent-presets')
20
+ const report = collectPresetAudit({ presetsRoot, patchPath })
21
+ sendJson(res, 200, { ok: true, patchPath, presetsRoot, ...report })
22
+ }
23
+
24
+ export { routePresetAuditGet }
@@ -28,7 +28,9 @@ async function routeStateGet(req, res, rc) {
28
28
  const readExtraBundleRows = rc.deps.readExtraBundleRows
29
29
  const patchPath = findPatchPath(ctx)
30
30
  const patch = await readPatchState(patchPath)
31
- // 补丁安全自愈(核心行误禁用恢复 / 缺失模块行自动禁用)——每 2 分钟最多跑一次
31
+ // 补丁安全自愈(核心行误禁用恢复 / **确证**缺失模块行自动禁用)——每 2 分钟最多跑一次。
32
+ // 2026-09-30 改错:① 无真正需要修复的内容时零写盘(避免每 2 分钟无谓重写用户补丁);
33
+ // ② 写盘失败 / 解析不确定 / 框架自带包 → 如实回报(skipped / uncertain / writeError),不静默。
32
34
  let patchHeal = null
33
35
  try {
34
36
  if (patchHealAt === null || Date.now() - patchHealAt > 120000) {
@@ -38,7 +40,11 @@ async function routeStateGet(req, res, rc) {
38
40
  patchHeal = patchHealReport
39
41
  }
40
42
  if (patchHeal !== null) setPatchHealReport(patchHeal)
41
- } catch {}
43
+ } catch (error) {
44
+ // 自愈本身抛异常也要如实进 patchHeal(过去这里 catch {} 静默吞掉,面板看不出任何异常)
45
+ patchHeal = { healed: [], autoDisabled: [], skipped: [], uncertain: [], healedAt: 0, written: false, writeError: String(error?.message ?? error) }
46
+ setPatchHealReport(patchHeal)
47
+ }
42
48
  const extraRows = await readExtraBundleRows(dirname(patchPath))
43
49
  const compatPending = readCompatPending()
44
50
  const compatPendingRows = new Set((compatPending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => p.rowId))
@@ -91,7 +97,7 @@ async function routeStateGet(req, res, rc) {
91
97
  const selfPkg = JSON.parse(readFileSync(join(pluginRoot(), 'package.json'), 'utf8'))
92
98
  selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
93
99
  } catch {}
94
- sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0 }, gating: gatingSummary(ctx, compatPending, entries), 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, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null })) }, compatGate, 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 })) })
100
+ sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries), 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, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null })) }, compatGate, 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 })) })
95
101
  return
96
102
  }
97
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.28",
3
+ "version": "0.5.30",
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",