@noob-stupid/dsh-plugin-console 0.5.26 → 0.5.27
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.
|
@@ -16,6 +16,7 @@ import { classifyInstallFailure, retryPnpmOnce } from './install-diagnose.js'
|
|
|
16
16
|
import { jobLog } from './jobs.js'
|
|
17
17
|
import { gatePrivateRoot } from './repo-size.js'
|
|
18
18
|
import { tryPresetSourceChannel } from './preset-source.js'
|
|
19
|
+
import { settlePresetOutcome } from './preset-in-package.js'
|
|
19
20
|
import { alreadyInstalledTarget, verifyInstalledEntry } from './install-verify.js'
|
|
20
21
|
import { applyNpmNameHint, expandSubpackageCandidates, fetchRepoPackage, fetchRepoPackageEx, fetchSubpackageNames, npmNameHintForRepo, packageProbeErrorText, subpackageCandidates } from './market.js'
|
|
21
22
|
import { declareProfileDependency } from './manifest.js'
|
|
@@ -409,10 +410,9 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
409
410
|
lastError = attempt.lastError
|
|
410
411
|
if (installedName !== null) break
|
|
411
412
|
}
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
if (job.presetDone === true) { job.status = 'done'; job.stage = 'done'; job.candidateDone = true; job.packageName = job.candidateName ?? job.packageName; return }
|
|
413
|
+
// 预设收口(D-③ 源码装配成功 / D-⑥ 落地物里带预设):能按预设收口就直接 return —— 见 preset-in-package.js
|
|
414
|
+
// (这两条路都**没有 node_modules 插件**,写补丁行/声明依赖会让补丁行指向无入口模块 → 启动崩溃)。
|
|
415
|
+
if (await settlePresetOutcome({ job, installedName, profileDir, repo: job.repo, deps: sourceDeps }) === true) return
|
|
416
416
|
if (installedName === null) {
|
|
417
417
|
if (job.diagnosis == null) job.diagnosis = classifyInstallFailure(lastError?.message) // 分类提示兜底(加法)
|
|
418
418
|
// 本地 AI 兜底会调用模型 API、产生费用:挂起等待用户明确同意后再执行
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// L1 · domain —— preset-in-package.js(批次 D-⑥:**预设随包分发** —— 装出来的包体里带 agent.cordis.yml 时按预设装配)
|
|
2
|
+
//
|
|
3
|
+
// 真机事故(2026-09-27,官方桌面端实例、0.5.25 实测,卡片"子包"列表第三件的等价调用):
|
|
4
|
+
// POST /install {repo:'yjh051108/dsh-routing-suite', packageName:'dsh-router-standard'}
|
|
5
|
+
// → release 通道按包名反查到**它自己的仓库** yjh051108/dsh-router-standard 的 release v0.3.0 资产
|
|
6
|
+
// (dsh-router-standard-0.3.0.tgz)→ 装成 node_modules/dsh-router-standard@0.3.0。
|
|
7
|
+
// 而那个包体里**没有 main/exports、也没有 dsh.bundle**,只有 `preset/`(三个预设目录)+ docs/。
|
|
8
|
+
// 于是出现两个后果(都在用户的验收点上):
|
|
9
|
+
// ① 预设**一个字节都没进** `~/.dsh/.agent-presets` → 用户依然用不上(预设才是他要的东西);
|
|
10
|
+
// ② 却照样 `appendInsert` 了一行 `- insert: {id: dsh-router-standard}` → 注册一个**加载不了的模块行**
|
|
11
|
+
// (2026-09-06「装 dsh-desktop 后服务崩」事故同族:补丁行指向的模块没有入口)。
|
|
12
|
+
// 根因是判据只按**仓库**(有没有 .gitmodules / 是不是预设型子包名)分类,没按**落地物**分类:
|
|
13
|
+
// 预设可以随包分发(registry tarball / release 资产 / git 规格三条路都会发生),
|
|
14
|
+
// 而"包里带 agent.cordis.yml"是**装完之后一定能看到**的事实。
|
|
15
|
+
//
|
|
16
|
+
// 本模块补上缺的那一步:**任何通道装成功之后、写补丁行/声明依赖之前**,先看落地物里有没有预设;
|
|
17
|
+
// 有 → 走 preset-install.js#assemblePreset(**复用**既有装配:只补不覆盖 / 校验 / 备份 / 读回核实),
|
|
18
|
+
// 并按 `presetDone` 收口 —— **绝不写补丁行、绝不声明依赖**(预设不是 npm 插件)。
|
|
19
|
+
//
|
|
20
|
+
// 与 preset-source.js#tryPresetSourceChannel 的分工:那条通道是"按包名的通道都失败后,从仓库源码取";
|
|
21
|
+
// 本模块是"包已经装下来了,落地物里带预设"。两条路共用同一份装配实现与同一份"预设目录"判据。
|
|
22
|
+
|
|
23
|
+
import { existsSync } from 'node:fs'
|
|
24
|
+
import { basename, join } from 'node:path'
|
|
25
|
+
import { findPresetDirs, isPresetDir } from './preset-source.js'
|
|
26
|
+
import { assemblePreset, recordPresetSource, suggestedOverwritePresetAction } from './preset-install.js'
|
|
27
|
+
import { noteChannel } from './git-channel.js'
|
|
28
|
+
import { dshHome } from '../infra/paths.js'
|
|
29
|
+
|
|
30
|
+
/** 扫一个已装包的目录树找预设(深度 ≤2,与套装路径 / 候选源码路径**同一份判据**)。
|
|
31
|
+
* 返回预设目录绝对路径数组(有序、去重);包里没有预设时返回空数组。 */
|
|
32
|
+
function findCarriedPresets(packageDir, maxDepth = 2) {
|
|
33
|
+
if (typeof packageDir !== 'string' || packageDir === '' || !existsSync(packageDir)) return []
|
|
34
|
+
const found = findPresetDirs(packageDir, maxDepth)
|
|
35
|
+
// 兜底:包根自己就是预设(有的作者把 agent.cordis.yml 直接放在包根)
|
|
36
|
+
if (found.length === 0 && isPresetDir(packageDir)) found.push(packageDir)
|
|
37
|
+
return [...new Set(found)]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 已装包的目录(支持 scoped 包名)。找不到返回 null。 */
|
|
41
|
+
function installedPackageDir(profileDir, name) {
|
|
42
|
+
const dir = join(profileDir, 'node_modules', ...String(name ?? '').split('/'))
|
|
43
|
+
return existsSync(dir) ? dir : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 「落地物里带预设」的唯一入口(install-job 在装成功后、appendInsert/declareProfileDependency 之前调用)。
|
|
48
|
+
* 返回 { handled, installed, presets, note, error }:
|
|
49
|
+
* · handled=false → 这个包里没有预设,调用方照旧按普通插件收口(行为一个字不变);
|
|
50
|
+
* · installed=true → 已按预设装配并**由本函数把 job 收口**(status/stage/presetDone/presetNote…),
|
|
51
|
+
* 调用方必须直接 return,**不要**再写补丁行/声明依赖。
|
|
52
|
+
* 副作用与既有预设通道一致:`job.presetInstalled / job.presetSource / job.presetNote / channelNotes`,
|
|
53
|
+
* 并调用 recordPresetSource 记下来源(这样面板上的「覆盖该预设」能按记录重新取源码)。
|
|
54
|
+
*/
|
|
55
|
+
async function installPresetsCarriedByPackage({ job, name, profileDir, repo = null, deps = {} } = {}) {
|
|
56
|
+
const packageName = String(name ?? '').trim()
|
|
57
|
+
if (packageName === '' || profileDir === null || profileDir === undefined) return { handled: false, installed: false, error: null }
|
|
58
|
+
const packageDir = deps.packageDir ?? installedPackageDir(profileDir, packageName)
|
|
59
|
+
if (packageDir === null) return { handled: false, installed: false, error: null }
|
|
60
|
+
const presets = findCarriedPresets(packageDir, Number.isFinite(deps.maxDepth) ? deps.maxDepth : 2)
|
|
61
|
+
if (presets.length === 0) return { handled: false, installed: false, error: null }
|
|
62
|
+
|
|
63
|
+
const presetsRoot = deps.presetsRoot ?? join(dshHome(), '.agent-presets')
|
|
64
|
+
noteChannel(job, `装出来的包 ${packageName} 里带着预设(${presets.map((p) => basename(p)).join('、')}):按预设装配,不注册为插件(它没有插件入口)`)
|
|
65
|
+
const limit = Number.isFinite(deps.limit) ? deps.limit : 8
|
|
66
|
+
const reports = []
|
|
67
|
+
for (const p of presets.slice(0, limit)) {
|
|
68
|
+
// eslint-disable-next-line no-await-in-loop
|
|
69
|
+
const report = assemblePreset(p, basename(p), {
|
|
70
|
+
presetsRoot,
|
|
71
|
+
overwrite: deps.overwrite === true, // 默认**只补不覆盖**(与 0.5.26 的语义一致)
|
|
72
|
+
...(deps.now === undefined ? {} : { now: deps.now }),
|
|
73
|
+
})
|
|
74
|
+
reports.push({ ...report, srcDir: p })
|
|
75
|
+
}
|
|
76
|
+
const okReports = reports.filter((r) => r.ok === true)
|
|
77
|
+
const failed = reports.filter((r) => r.ok !== true)
|
|
78
|
+
// 同名冲突(默认只补不覆盖时的正常结局):某个预设的现有文件被跳过
|
|
79
|
+
const conflicted = okReports.filter((r) => (r.skipped?.length ?? 0) > 0).map((r) => r.name)
|
|
80
|
+
const summary = reports.map((r) => ({
|
|
81
|
+
name: r.name, dest: r.dest ?? null, ok: r.ok === true, error: r.error ?? null,
|
|
82
|
+
added: r.added?.length ?? 0, skipped: r.skipped?.length ?? 0, overwritten: r.overwritten?.length ?? 0,
|
|
83
|
+
identical: r.identical?.length ?? 0, backup: r.backup ?? null, bytes: r.bytes ?? 0,
|
|
84
|
+
marker: r.ok === true ? 'agent.cordis.yml' : null, carriedBy: packageName,
|
|
85
|
+
}))
|
|
86
|
+
if (job !== null && job !== undefined) {
|
|
87
|
+
job.presetInstalled = summary
|
|
88
|
+
job.presetSource = { repo, packageName, mode: 'in-package', root: packageDir, presets: reports.map((r) => r.name) }
|
|
89
|
+
// 同名冲突(只补不覆盖时的正常结局)→ 面板给**显式**的「覆盖该预设」动作(复用既有动作构造器)
|
|
90
|
+
if (conflicted.length > 0) {
|
|
91
|
+
job.presetOverwriteCandidates = conflicted
|
|
92
|
+
job.suggestedAction = suggestedOverwritePresetAction({
|
|
93
|
+
presetName: conflicted[0],
|
|
94
|
+
repo,
|
|
95
|
+
reason: `已存在同名预设 ${conflicted.join('、')}:默认只补不覆盖(现有文件一个字节都没动)。要用仓库版本覆盖请点这个动作`,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
job.presetNote = `预设已装配:${okReports.map((r) => r.name).join('、') || '(无)'}`
|
|
99
|
+
+ `(落盘 ${presetsRoot};**新建会话时选择**)`
|
|
100
|
+
+ `;来源:包 ${packageName} 自带的预设目录(该包没有插件入口,已按预设装配、未注册为插件)`
|
|
101
|
+
+ (conflicted.length > 0 ? `;同名预设 ${conflicted.join('、')} 默认只补不覆盖(缺的文件已补、现有文件未改动),如需覆盖请点「覆盖该预设」` : '')
|
|
102
|
+
+ (failed.length > 0 ? `;${failed.length} 个预设未装配:${failed.map((r) => `${r.name}(${r.error})`).join('、')}` : '')
|
|
103
|
+
job.presetDone = okReports.length > 0
|
|
104
|
+
if (okReports.length > 0) {
|
|
105
|
+
job.status = 'done'
|
|
106
|
+
job.stage = 'done'
|
|
107
|
+
job.candidateDone = true
|
|
108
|
+
job.packageName = packageName
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 记下来源(面板的「覆盖该预设」要按记录重新取源码):repo 为空时记 null,动作侧会如实告知"没有源码出处"
|
|
112
|
+
try {
|
|
113
|
+
recordPresetSource(
|
|
114
|
+
okReports.map((r) => ({ name: r.name, repo, candidateName: packageName, subdir: null, sparse: false, dest: r.dest })),
|
|
115
|
+
deps.now === undefined ? {} : { now: deps.now },
|
|
116
|
+
)
|
|
117
|
+
} catch {}
|
|
118
|
+
if (okReports.length === 0) {
|
|
119
|
+
return { handled: true, installed: false, presets: summary, error: new Error(`包 ${packageName} 自带预设,但装配全部失败:${failed.map((r) => `${r.name}(${r.error})`).join('、')}`) }
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
handled: true,
|
|
123
|
+
installed: true,
|
|
124
|
+
presets: summary,
|
|
125
|
+
conflicted,
|
|
126
|
+
note: job?.presetNote ?? null,
|
|
127
|
+
error: failed.length > 0 ? new Error(`${failed.length} 个预设未装配:${failed.map((r) => `${r.name}(${r.error})`).join('、')}`) : null,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 装成功后(或预设源码通道成功后)的**唯一收口**(install-job 只调这一行,它贴着 600 行架构硬顶):
|
|
132
|
+
* · `job.presetDone`(源码通道已装配好)→ 把 job 收口并返回 true;
|
|
133
|
+
* · 落地物里带预设 → 走 installPresetsCarriedByPackage 装配,成功则收口并返回 true;
|
|
134
|
+
* · 其余 → 返回 false(调用方照旧按普通插件收口,行为一个字不变)。 */
|
|
135
|
+
async function settlePresetOutcome({ job, installedName, profileDir, repo = null, deps = {} } = {}) {
|
|
136
|
+
if (job !== null && job !== undefined && job.presetDone === true) {
|
|
137
|
+
job.status = 'done'
|
|
138
|
+
job.stage = 'done'
|
|
139
|
+
job.candidateDone = true
|
|
140
|
+
job.packageName = job.candidateName ?? job.packageName
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
if (typeof installedName !== 'string' || installedName === '') return false
|
|
144
|
+
const carried = await installPresetsCarriedByPackage({ job, name: installedName, profileDir, repo, deps })
|
|
145
|
+
return carried.installed === true
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { findCarriedPresets, installedPackageDir, installPresetsCarriedByPackage, settlePresetOutcome }
|
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.27",
|
|
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",
|