@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.
- package/lib/client.js +89 -3
- package/lib/server/domain/compat.js +68 -2
- package/lib/server/domain/framework-cleanup.js +225 -0
- package/lib/server/domain/framework-residuals.js +59 -0
- package/lib/server/domain/patch-composition-audit.js +218 -0
- package/lib/server/domain/patch.js +68 -15
- package/lib/server/domain/preset-audit.js +144 -0
- package/lib/server/domain/preset-declare.js +44 -12
- package/lib/server/domain/preset-rows.js +285 -0
- package/lib/server/domain/presets.js +6 -3
- package/lib/server/infra/exec.js +23 -2
- package/lib/server/infra/framework-root.js +99 -0
- package/lib/server/infra/package-resolve.js +73 -0
- package/lib/server/infra/registry-versions.js +64 -0
- package/lib/server/routes/compat.js +41 -1
- package/lib/server/routes/framework-preflight.js +43 -2
- package/lib/server/routes/index.js +13 -1
- package/lib/server/routes/patch-audit.js +59 -0
- package/lib/server/routes/plugins.js +20 -2
- package/lib/server/routes/preset-audit.js +24 -0
- package/lib/server/routes/state.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// L1 · domain —— patch-composition-audit.js(补丁「行 → 包」可解析性体检;0.5.30 加法)
|
|
2
|
+
//
|
|
3
|
+
// 为什么必须有它(**真机事故 2026-09-28**):
|
|
4
|
+
// 框架 0.2.0-rc.1 移除了 `@deepseek-ai/dsh-workflow-worker-thread`(引擎重构为 `…-workflow-ptc`),
|
|
5
|
+
// 而 `profiles/web/cordis.patch.yml` 里三处行仍指向旧包名。该行解析失败 → 隔离组里
|
|
6
|
+
// `workflowEngine` 就没有提供方 → `dsh-tool-workflow` / `dsh-tool-ralph`(`inject` 含 `workflowEngine`)
|
|
7
|
+
// 永久 waiting → 老会话 resume 失败,真机报错原文:
|
|
8
|
+
// resume failed for session "…": RemoteError: tool-workflow (@deepseek-ai/dsh-tool-workflow):
|
|
9
|
+
// waiting for workflowEngine tool-ralph (@deepseek-ai/dsh-tool-ralph): waiting for workflowEngine
|
|
10
|
+
// 而现有 format-scan / framework-preflight **只扫「生产方源码里的 API 引用」**,
|
|
11
|
+
// 不扫「补丁里引用的包在当前框架树里还在不在」→ 升级前判「无 blocker」,升级后才炸。
|
|
12
|
+
//
|
|
13
|
+
// 本模块只做三件事(纯函数;`resolve` / `existsFile` 全部可注入,便于离线断言):
|
|
14
|
+
// ① 解析补丁行:`- id:` / `name:` / `disabled: true`(嵌套行同样收;缩进语义与 preset-rows.js 同源);
|
|
15
|
+
// ② 分类:解析失败=blocker;**已 disabled** 的解析失败=warning;`file://` 目标缺失=blocker;
|
|
16
|
+
// 子路径(`pkg/sub`)解析失败=warning(父包还在,只是导出变了);
|
|
17
|
+
// ③ 疑似改名建议:同 scope + 公共 token,纯启发式 —— **只提示,绝不自动改写用户补丁**。
|
|
18
|
+
//
|
|
19
|
+
// 行数预算:独立模块,留在 600 行架构硬顶内。
|
|
20
|
+
|
|
21
|
+
const ROW_RE = /^(\s*)-\s*id:\s*(\S+)\s*$/u
|
|
22
|
+
const NAME_RE = /^(\s*)name:\s*(.+?)\s*$/u
|
|
23
|
+
const DISABLED_RE = /^(\s*)disabled:\s*true\s*$/u
|
|
24
|
+
const CONFIG_RE = /^(\s*)config:\s*$/u
|
|
25
|
+
|
|
26
|
+
/** 去引号(单/双);空串视作未提供。 */
|
|
27
|
+
function unquote(raw) {
|
|
28
|
+
const text = String(raw ?? '').trim()
|
|
29
|
+
const quoted = text.length >= 2
|
|
30
|
+
&& ((text.startsWith("'") && text.endsWith("'")) || (text.startsWith('"') && text.endsWith('"')))
|
|
31
|
+
return quoted ? text.slice(1, -1) : text
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 解析补丁文本 → 行数组 `{ id, name, disabled, indent, line }`(CRLF 先归一:真机补丁是 CRLF)。 */
|
|
35
|
+
function parsePatchRows(text) {
|
|
36
|
+
const lines = String(text ?? '').replace(/\r\n/gu, '\n').split('\n')
|
|
37
|
+
const rows = []
|
|
38
|
+
let cur = null
|
|
39
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
40
|
+
const raw = lines[i]
|
|
41
|
+
if (raw.trim() === '' || /^\s*#/u.test(raw)) continue
|
|
42
|
+
const item = raw.match(ROW_RE)
|
|
43
|
+
if (item !== null) {
|
|
44
|
+
cur = { id: unquote(item[2]), indent: item[1].length, name: null, disabled: false, config: false, keys: 0, line: i + 1 }
|
|
45
|
+
rows.push(cur)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (cur === null) continue
|
|
49
|
+
const indent = (raw.match(/^ */u) ?? [''])[0].length
|
|
50
|
+
if (indent < cur.indent) { cur = null; continue }
|
|
51
|
+
const name = raw.match(NAME_RE)
|
|
52
|
+
if (name !== null && indent > cur.indent && cur.name === null) { cur.name = unquote(name[2]); continue }
|
|
53
|
+
if (DISABLED_RE.test(raw) && indent > cur.indent) cur.disabled = true
|
|
54
|
+
if (CONFIG_RE.test(raw) && indent > cur.indent) cur.config = true
|
|
55
|
+
// 行里出现的**任何**键(config / disabled / isolate / …)都说明这是「补丁行」(改已存在条目的配置或开关),
|
|
56
|
+
// 不是「声明行」—— 声明行才需要 name。真机补丁里 `- id: web-ui-pet` + `disabled: true` 就是这种。
|
|
57
|
+
const kv = raw.match(/^(\s*)([A-Za-z_][\w-]*):/u)
|
|
58
|
+
if (kv !== null && indent > cur.indent && kv[2] !== 'id') cur.keys += 1
|
|
59
|
+
}
|
|
60
|
+
return rows
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** scope 名(`@a/b` → `@a`;裸名 → 空串)。 */
|
|
64
|
+
function scopeOf(name) {
|
|
65
|
+
return String(name).startsWith('@') ? String(name).split('/')[0] : ''
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 词根集合:去掉 scope、按 `-`/`_`/`/` 切,丢弃长度 < 3 的碎片。 */
|
|
69
|
+
function tokensOf(name) {
|
|
70
|
+
return String(name)
|
|
71
|
+
.replace(/^@[^/]+\//u, '')
|
|
72
|
+
.split(/[-_/]/u)
|
|
73
|
+
.map((t) => t.toLowerCase())
|
|
74
|
+
.filter((t) => t.length >= 3)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* **真机确证的改名片 —— 建议的权威来源**(每条都有真机错误/发布物背书,同 preset-rows.js 的判据文化)。
|
|
79
|
+
* 表里没有的一律只给「严格启发式候选」,猜不到就不猜(宁缺勿滥)。
|
|
80
|
+
*/
|
|
81
|
+
const KNOWN_RENAMES = [
|
|
82
|
+
{
|
|
83
|
+
from: '@deepseek-ai/dsh-workflow-worker-thread',
|
|
84
|
+
to: '@deepseek-ai/dsh-workflow-ptc',
|
|
85
|
+
since: '0.2.0-rc.1',
|
|
86
|
+
evidence: '2026-09-28 真机:升级 0.2.0-rc.1 后 web profile 老会话 resume 失败'
|
|
87
|
+
+ '(RemoteError: tool-workflow … waiting for workflowEngine);旧包已从 .pnpm store 消失,'
|
|
88
|
+
+ '新引擎 README「最小配置」列出 ptc,且 config.provider 默认 spawn(与旧行一致)',
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
/** 疑似改名建议:① 真机确证表命中即权威返回;② 否则严格启发式(同 scope + 前两词根一致 + 交集 ≥ 3)。
|
|
93
|
+
* 返回 `{ names, source }`,`source: 'known' | 'heuristic' | null`。 */
|
|
94
|
+
function suggestRenames(name, candidates, limit = 3) {
|
|
95
|
+
const known = KNOWN_RENAMES.filter((r) => r.from === name).map((r) => r.to)
|
|
96
|
+
if (known.length > 0) return { names: known.slice(0, limit), source: 'known' }
|
|
97
|
+
const scope = scopeOf(name)
|
|
98
|
+
const want = tokensOf(name)
|
|
99
|
+
const scored = []
|
|
100
|
+
for (const cand of (Array.isArray(candidates) ? candidates : [])) {
|
|
101
|
+
if (cand === name || scopeOf(cand) !== scope) continue
|
|
102
|
+
const have = tokensOf(cand)
|
|
103
|
+
const common = want.filter((t) => have.includes(t)).length
|
|
104
|
+
const headSame = want.length >= 2 && have.length >= 2 && want[0] === have[0] && want[1] === have[1]
|
|
105
|
+
if (!headSame || common < 3) continue
|
|
106
|
+
scored.push({ name: cand, score: common })
|
|
107
|
+
}
|
|
108
|
+
scored.sort((a, b) => (b.score - a.score) || a.name.localeCompare(b.name))
|
|
109
|
+
const names = scored.slice(0, limit).map((s) => s.name)
|
|
110
|
+
return { names, source: names.length > 0 ? 'heuristic' : null }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 分类体检(纯函数)。`resolve(name)` → `{ ok: true, from }` / `{ ok: false, reason }`;
|
|
115
|
+
* `existsFile(name)` → boolean(`file://` 目标存在性,未注入则跳过该项)。
|
|
116
|
+
*/
|
|
117
|
+
function classifyPatchRows(rows, { resolve, existsFile = null, candidates = [] } = {}) {
|
|
118
|
+
const blockers = []
|
|
119
|
+
const warnings = []
|
|
120
|
+
const resolvable = []
|
|
121
|
+
const patchOnly = []
|
|
122
|
+
for (const row of (Array.isArray(rows) ? rows : [])) {
|
|
123
|
+
if (typeof row?.name !== 'string' || row.name === '') {
|
|
124
|
+
// 只改配置的行(`- id: webserver` + `config:`)是**合法的补丁形态**:它在改已存在条目的配置,
|
|
125
|
+
// 没有 name 可言 —— 真机补丁前 30 行全是这种,判红就是误杀(2026-09-28 真机实测抓到)。
|
|
126
|
+
if ((row?.keys ?? 0) > 0) { patchOnly.push(row.id ?? null); continue }
|
|
127
|
+
warnings.push({ code: 'row-without-name', row: row?.id ?? null, line: row?.line ?? null, detail: `行 ${row?.id ?? '?'} 既没有 name 也没有 config`, hint: '补上 name(包名或 file:/// URL),或确认它是配置补丁行' })
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
const name = row.name
|
|
131
|
+
if (/^file:\/\//iu.test(name)) {
|
|
132
|
+
if (existsFile === null) continue
|
|
133
|
+
let present = null
|
|
134
|
+
try { present = existsFile(name) } catch { present = null }
|
|
135
|
+
if (present === false) {
|
|
136
|
+
blockers.push({ code: 'file-target-missing', row: row.id, line: row.line, name, detail: `行 ${row.id} 的 file:// 目标不存在:${name}`, hint: '补齐文件或修正引用(写一行必然加载失败的声明等于把缺陷留给运行期)' })
|
|
137
|
+
}
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (/^\.\.?\//u.test(name) || /^cordis:/iu.test(name)) continue
|
|
141
|
+
const res = (() => { try { return resolve(name) } catch (error) { return { ok: false, reason: String(error?.message ?? error) } } })()
|
|
142
|
+
if (res?.ok === true) { resolvable.push(name); continue }
|
|
143
|
+
const segments = name.split('/')
|
|
144
|
+
const isSubpath = segments.length > (name.startsWith('@') ? 2 : 1)
|
|
145
|
+
const entry = {
|
|
146
|
+
code: isSubpath ? 'subpath-unresolvable' : 'package-unresolvable',
|
|
147
|
+
row: row.id,
|
|
148
|
+
line: row.line,
|
|
149
|
+
name,
|
|
150
|
+
reason: res?.reason ?? null,
|
|
151
|
+
...(() => {
|
|
152
|
+
const s = isSubpath ? { names: [], source: null } : suggestRenames(name, candidates)
|
|
153
|
+
return { suggestions: s.names, suggestionSource: s.source }
|
|
154
|
+
})(),
|
|
155
|
+
detail: isSubpath
|
|
156
|
+
? `行 ${row.id} 的 ${name} 解析不到(父包在,子路径不在?)`
|
|
157
|
+
: `行 ${row.id} 引用的包 ${name} 在当前框架树里解析不到`,
|
|
158
|
+
hint: isSubpath
|
|
159
|
+
? '该子路径可能已改名/移除 —— 请对照当前框架版本的 exports 修正'
|
|
160
|
+
: '该包可能已被移除或改名:先确认新包名再改这一行;**在修好之前,依赖它提供的服务的行会永久 waiting**',
|
|
161
|
+
}
|
|
162
|
+
if (isSubpath) warnings.push(entry)
|
|
163
|
+
else if (row.disabled === true) warnings.push({ ...entry, disabled: true })
|
|
164
|
+
else blockers.push(entry)
|
|
165
|
+
}
|
|
166
|
+
return { blockers, warnings, resolvable, patchOnly }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 组合入口:文本 + 依赖 → 报告。 */
|
|
170
|
+
function auditPatchText(text, deps = {}) {
|
|
171
|
+
const rows = parsePatchRows(text)
|
|
172
|
+
const report = classifyPatchRows(rows, deps)
|
|
173
|
+
return { rows: rows.length, ...report }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 从行里挑出「随框架版本走」的包名(`@deepseek-ai/*`)并带上行号 ——
|
|
178
|
+
* 预检要拿它们去**目标版本的 registry** 上核对(真机事故:0.2.0-rc.1 移除了
|
|
179
|
+
* `@deepseek-ai/dsh-workflow-worker-thread`,而补丁三处仍引用它 → 该行解析失败 →
|
|
180
|
+
* 隔离组里 workflowEngine 没有提供方 → 老会话 resume 失败)。
|
|
181
|
+
* 子路径归一成父包(`pkg/sub` → `pkg`):版本随框架走的是包,不是子路径。
|
|
182
|
+
*/
|
|
183
|
+
function frameworkPackageRows(rows) {
|
|
184
|
+
const map = new Map()
|
|
185
|
+
for (const row of (Array.isArray(rows) ? rows : [])) {
|
|
186
|
+
const name = typeof row?.name === 'string' ? row.name : ''
|
|
187
|
+
if (!name.startsWith('@deepseek-ai/')) continue
|
|
188
|
+
if (/^file:\/\//iu.test(name) || /^\.\.?\//u.test(name)) continue
|
|
189
|
+
const base = name.split('/').slice(0, 2).join('/')
|
|
190
|
+
if (!map.has(base)) map.set(base, [])
|
|
191
|
+
map.get(base).push(row.line ?? null)
|
|
192
|
+
}
|
|
193
|
+
return [...map.entries()].map(([name, lines]) => ({ name, lines }))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 目标版本可用性判定(纯函数):`publishedBy` 是 `Map<name, true|false|null>`
|
|
198
|
+
* (true=目标版本已发布 / false=没有该版本 / null=未知,网络失败或元数据缺失)。
|
|
199
|
+
* **未知一律不判死**(宁可漏报,也不因为查不动就把用户吓住)——只进 `unknown`。
|
|
200
|
+
*/
|
|
201
|
+
function classifyTargetAvailability(entries, { publishedBy, targetVersion, candidates = [] } = {}) {
|
|
202
|
+
const missing = []
|
|
203
|
+
const unknown = []
|
|
204
|
+
for (const entry of (Array.isArray(entries) ? entries : [])) {
|
|
205
|
+
const name = entry?.name
|
|
206
|
+
if (typeof name !== 'string' || name === '') continue
|
|
207
|
+
const state = publishedBy && typeof publishedBy.get === 'function' ? publishedBy.get(name) : null
|
|
208
|
+
if (state === true) continue
|
|
209
|
+
const base = { name, lines: Array.isArray(entry.lines) ? entry.lines : [], targetVersion: targetVersion ?? null }
|
|
210
|
+
if (state === false) missing.push({ ...base, suggestions: suggestRenames(name, candidates).names })
|
|
211
|
+
else unknown.push(base)
|
|
212
|
+
}
|
|
213
|
+
return { missing, unknown }
|
|
214
|
+
}
|
|
215
|
+
export {
|
|
216
|
+
KNOWN_RENAMES, auditPatchText, classifyPatchRows, classifyTargetAvailability, frameworkPackageRows,
|
|
217
|
+
parsePatchRows, scopeOf, suggestRenames, tokensOf, unquote,
|
|
218
|
+
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
5
5
|
import { dirname } from 'node:path'
|
|
6
|
+
import { frameworkBases, isFrameworkModuleName } from '../infra/framework-root.js'
|
|
6
7
|
import { queuedWrite } from '../infra/fsx.js'
|
|
7
8
|
import { escapeRegExp } from '../infra/mask.js'
|
|
8
|
-
import {
|
|
9
|
+
import { makePackageResolver } from '../infra/package-resolve.js'
|
|
9
10
|
|
|
10
11
|
/** 读取补丁文件并扫描:停用块与 insert 行的 id。 */
|
|
11
12
|
async function readPatchState(patchPath) {
|
|
@@ -145,15 +146,39 @@ function parseInsertNames(text) {
|
|
|
145
146
|
/**
|
|
146
147
|
* 补丁安全自愈(服务永不崩机制):
|
|
147
148
|
* ① 核心行被误禁用 → 自动移除禁用块恢复;
|
|
148
|
-
* ② 启用态用户 insert
|
|
149
|
-
*
|
|
149
|
+
* ② 启用态用户 insert 行的模块**确证缺失**(如引用未安装包的行)→ 自动禁用(loader 对缺失模块会致命崩溃)。
|
|
150
|
+
*
|
|
151
|
+
* ★ 2026-09-30 改错(真机事故 2026-09-29 01:05:44):② 过去只用**单基准**
|
|
152
|
+
* `resolvePackageJson(name, profileDir)` 判定"缺失",而框架 0.2.0-rc.1 起 `@deepseek-ai/*` 的实体
|
|
153
|
+
* 全在 `.pnpm` **内部**那层、profile 顶层只剩旧版残影 → 框架**真存在**的内置包
|
|
154
|
+
* `dsh-agent-preset` / `dsh-workflow-ptc` 被判成缺失 → 给它们自动写了 4 个 `disabled: true` 块,
|
|
155
|
+
* 把用户的 preset-router-spec/standard/react 三条全停掉。
|
|
156
|
+
* 现在改为**多基准并集**(profile 目录 + 框架运行时根,见 infra/framework-root.js):
|
|
157
|
+
* 任一基准能解析即算命中 → 不再误判。
|
|
158
|
+
*
|
|
159
|
+
* 语义收紧(三条硬约束):
|
|
160
|
+
* · **确证缺失才禁用**:解析不确定(resolver 报 uncertain / 一个基准都没有 / 解析器抛异常)→ 只报告不写盘;
|
|
161
|
+
* · **框架命名空间 `@deepseek-ai/*` 永不自动禁用**:它们是框架自带包(随版本增删由框架自己管),
|
|
162
|
+
* 写 `disabled: true` 等于用旧判断把新框架的组件停掉 —— 命中一律只报告;
|
|
163
|
+
* · **幂等零写盘**:没有真正需要修复的内容时,一个字节都不写(`healedAt=0`),
|
|
164
|
+
* 且即便算出了新内容也要与原文逐字节比较后才落盘(`GET /state` 每 ≤2min 调一次本函数)。
|
|
165
|
+
* 写盘失败**如实回报**(`written:false` + `writeError`),绝不静默吞掉。
|
|
166
|
+
*
|
|
167
|
+
* return { healed:[], autoDisabled:[], skipped:[], uncertain:[], healedAt, written, writeError }
|
|
168
|
+
* healedAt=0 表示本次无修改。
|
|
150
169
|
*/
|
|
151
|
-
async function healPatchSafety(patchPath) {
|
|
170
|
+
async function healPatchSafety(patchPath, deps = {}) {
|
|
152
171
|
const profileDir = dirname(patchPath)
|
|
172
|
+
const readText = deps.readText ?? ((p) => readFile(p, 'utf8'))
|
|
173
|
+
const writeText = deps.writeText ?? ((p, data) => writeFile(p, data, 'utf8'))
|
|
153
174
|
const { text } = await readPatchState(patchPath)
|
|
154
|
-
let next = text
|
|
155
175
|
const healed = []
|
|
156
176
|
const autoDisabled = []
|
|
177
|
+
const skipped = []
|
|
178
|
+
const uncertain = []
|
|
179
|
+
let next = text
|
|
180
|
+
|
|
181
|
+
// ① 核心行被误禁用 → 恢复(与判据无关,先做)
|
|
157
182
|
for (const id of CORE_PATCH_ROW_IDS) {
|
|
158
183
|
const re = new RegExp(`^- id: ${escapeRegExp(id)}\\s*\\r?\\n {2}disabled: true\\s*\\r?\\n`, 'mu')
|
|
159
184
|
if (re.test(next)) {
|
|
@@ -161,23 +186,51 @@ async function healPatchSafety(patchPath) {
|
|
|
161
186
|
healed.push(id)
|
|
162
187
|
}
|
|
163
188
|
}
|
|
189
|
+
|
|
190
|
+
// ② 启用态 insert 行的模块缺失 → 自动禁用(仅在**确证缺失**时)
|
|
164
191
|
if (healed.length === 0) {
|
|
165
192
|
const insertNames = parseInsertNames(next)
|
|
166
|
-
|
|
193
|
+
const bases = Array.isArray(deps.bases)
|
|
194
|
+
? deps.bases.filter((b) => typeof b === 'string' && b !== '')
|
|
195
|
+
: frameworkBases(deps.baseDir ?? null, profileDir)
|
|
196
|
+
const resolve = deps.resolve ?? (bases.length > 0 ? makePackageResolver(bases) : null)
|
|
167
197
|
for (const [id, moduleName] of insertNames) {
|
|
168
198
|
if (!moduleName || moduleName.startsWith('cordis:')) continue
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
199
|
+
const alreadyDisabled = new RegExp(`^- id: ${escapeRegExp(id)}\\s*\\r?\\n {2}disabled: true\\s*\\r?\\n`, 'mu').test(next)
|
|
200
|
+
if (alreadyDisabled) continue
|
|
201
|
+
// framework 命名空间:随框架版本走,永不自动禁用(只报告)
|
|
202
|
+
if (isFrameworkModuleName(moduleName)) { skipped.push({ id, moduleName, reason: 'framework-owned' }); continue }
|
|
203
|
+
// 没有解析器 / 没有任何基准 → 无法判定,只报告
|
|
204
|
+
if (resolve === null) { uncertain.push({ id, moduleName, reason: 'no-resolution-base' }); continue }
|
|
205
|
+
let res = null
|
|
206
|
+
try { res = resolve(moduleName) } catch (error) { res = { ok: false, uncertain: true, reason: String(error?.message ?? error) } }
|
|
207
|
+
// 兼容两种 resolver 契约:布尔(旧测试桩)/ `{ ok, uncertain, reason }`(infra/package-resolve.js)
|
|
208
|
+
const ok = res === true || res?.ok === true
|
|
209
|
+
if (ok) continue
|
|
210
|
+
if (res?.uncertain === true) { uncertain.push({ id, moduleName, reason: res?.reason ?? 'uncertain' }); continue }
|
|
211
|
+
next = `${next.trimEnd()}\n- id: ${id}\n disabled: true\n`
|
|
212
|
+
autoDisabled.push(id)
|
|
175
213
|
}
|
|
176
214
|
}
|
|
177
|
-
|
|
178
|
-
|
|
215
|
+
|
|
216
|
+
// ③ 幂等零写盘:没有任何需要修复的内容 / 内容与原文逐字节相同 → 一个字节都不写
|
|
217
|
+
if (next === text) {
|
|
218
|
+
return { healed, autoDisabled, skipped, uncertain, healedAt: 0, written: false, writeError: null }
|
|
219
|
+
}
|
|
220
|
+
const payload = sanitizePatchText(next)
|
|
221
|
+
if (payload === text) {
|
|
222
|
+
return { healed, autoDisabled, skipped, uncertain, healedAt: 0, written: false, writeError: null }
|
|
223
|
+
}
|
|
224
|
+
// ④ 写盘失败如实回报(不静默)
|
|
225
|
+
try {
|
|
226
|
+
await writeText(patchPath, payload)
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return { healed, autoDisabled, skipped, uncertain, healedAt: 0, written: false, writeError: String(error?.message ?? error) }
|
|
179
229
|
}
|
|
180
|
-
|
|
230
|
+
// 读回核实:写盘后重新读一次,只有内容真的落盘才算 healedAt
|
|
231
|
+
let verified = false
|
|
232
|
+
try { verified = (await readText(patchPath)) === payload } catch { verified = false }
|
|
233
|
+
return { healed, autoDisabled, skipped, uncertain, healedAt: Date.now(), written: true, writeError: verified ? null : '写盘后读回内容与期望不一致' }
|
|
181
234
|
}
|
|
182
235
|
|
|
183
236
|
/** 从「子包版本对齐」记录解析纯包名(如 '@linxin666/dsh-pet@0.2.1(新装)')。 */
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// L1 · domain —— preset-audit.js(用户预设 ↔ profile 声明行 的跨源对账;0.5.30 加法)
|
|
2
|
+
//
|
|
3
|
+
// 真机背景(2026-09-28):框架 0.1.7-rc.1 起官方移除了磁盘发现,老会话 resume 靠 profile 补丁里的
|
|
4
|
+
// **声明行**;而预设目录里的 `agent.cordis.yml` 会随后续装配被更新(本机 router-spec 在升级前 1 分钟
|
|
5
|
+
// 被改成 v10),补丁里的行却还是旧的(meta 指向 v1)→ 老会话 resume 到错的模块。
|
|
6
|
+
// 真机证据:`profiles/web/cordis.patch.yml` 指向 `router-bootstrap-v1.mjs`,
|
|
7
|
+
// 而 `router-spec/agent.cordis.yml` 自己写的是 `./router-bootstrap-v10.mjs`。
|
|
8
|
+
//
|
|
9
|
+
// 本模块只做**纯对账**(IO 全注入):
|
|
10
|
+
// · parsePresetDeclarations(text) 预设自身声明的 `{ id → 目标 }`(相对引用保留原样)
|
|
11
|
+
// · auditPresetDeclarations({...}) 与补丁里的 file:/// 行逐条比对,输出四类差异
|
|
12
|
+
//
|
|
13
|
+
// 判据(保守、不猜):
|
|
14
|
+
// · **目标按目录归属**匹配 —— 补丁行落在哪个预设目录下就算哪个预设的(router-* 三个预设各有
|
|
15
|
+
// 同名 id `router-bootstrap`,只能靠目录区分,不能靠 id 全局匹配)。
|
|
16
|
+
// · 只报"能证明不一致"的:id 相同但目标不同(陈旧)、预设里有补丁里没有(缺失)、
|
|
17
|
+
// 补丁里有但预设目录里查无此 id(悬空)、任一目标文件不存在(悬空)。
|
|
18
|
+
// · 行**显式 disabled** 的,降级为 warning(用户已经关掉它了,不算错,但要看得见)。
|
|
19
|
+
|
|
20
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
21
|
+
import { dirname, join } from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
import { parsePatchRows } from './patch-composition-audit.js'
|
|
24
|
+
|
|
25
|
+
/** 预设自身声明的模块:`{ id, target, line, disabled }`(只收有 name 的行)。 */
|
|
26
|
+
function parsePresetDeclarations(text) {
|
|
27
|
+
const lines = String(text ?? '').replace(/\r\n/gu, '\n').split('\n')
|
|
28
|
+
const out = []
|
|
29
|
+
let cur = null
|
|
30
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
31
|
+
const raw = lines[i]
|
|
32
|
+
if (raw.trim() === '' || /^\s*#/u.test(raw)) continue
|
|
33
|
+
const item = raw.match(/^(\s*)-\s*id:\s*(\S+)\s*$/u)
|
|
34
|
+
if (item !== null) {
|
|
35
|
+
cur = { id: item[2].replace(/^['"]|['"]$/gu, ''), target: null, line: i + 1, disabled: false, indent: item[1].length }
|
|
36
|
+
out.push(cur)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if (cur === null) continue
|
|
40
|
+
const indent = (raw.match(/^ */u) ?? [''])[0].length
|
|
41
|
+
if (indent <= cur.indent) continue
|
|
42
|
+
const name = raw.match(/^\s*name:\s*(.+?)\s*$/u)
|
|
43
|
+
if (name !== null && cur.target === null) { cur.target = name[1].replace(/^['"]|['"]$/gu, ''); continue }
|
|
44
|
+
if (/^\s*disabled:\s*true\s*$/u.test(raw)) cur.disabled = true
|
|
45
|
+
}
|
|
46
|
+
// 2026-09-28 改错(真机试跑抓到 89 条假"缺失"):预设里的 `name:` 大量是**包名**(`@deepseek-ai/dsh-persona` 等),
|
|
47
|
+
// 它们在补丁里也是包名行、不归"按目录归属"的文件匹配管 —— 只有**文件型声明**(相对/绝对路径)
|
|
48
|
+
// 才会被改写成 `file:///…` 的声明行。这里只收文件型,避免把包名行误判成"声明行缺失"。
|
|
49
|
+
return out.filter((row) => typeof row.target === 'string' && row.target !== '' && /^(\.\.?[\\/]|[A-Za-z]:[\\/]|file:)/u.test(row.target))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 跨源对账(纯函数,IO 注入)。
|
|
54
|
+
* 入参:
|
|
55
|
+
* · presets: `[{ dir, modules: parsePresetDeclarations(text) }]`
|
|
56
|
+
* · patchRows: `[{ id, name, line, disabled }]`(补丁里的行;`name` 可以是 file:/// 也可以是别的)
|
|
57
|
+
* · toLocalPath(url) / joinPath(dir, rel) / exists(path) / dirnameOf(path)(可注入,便于离线断言)
|
|
58
|
+
* 返回 `{ stale, missing, orphan, targetMissing, ok }`。
|
|
59
|
+
*/
|
|
60
|
+
function auditPresetDeclarations({ presets = [], patchRows = [], toLocalPath, joinPath, exists, dirnameOf } = {}) {
|
|
61
|
+
const stale = []
|
|
62
|
+
const missing = []
|
|
63
|
+
const orphan = []
|
|
64
|
+
const targetMissing = []
|
|
65
|
+
const patchFileRows = []
|
|
66
|
+
for (const row of (Array.isArray(patchRows) ? patchRows : [])) {
|
|
67
|
+
if (row === null || typeof row !== 'object') continue
|
|
68
|
+
const name = typeof row.name === 'string' ? row.name : ''
|
|
69
|
+
if (!/^file:\/\//iu.test(name)) continue
|
|
70
|
+
let local = null
|
|
71
|
+
try { local = toLocalPath(name) } catch { local = null }
|
|
72
|
+
if (local === null) continue
|
|
73
|
+
patchFileRows.push({ id: row.id ?? null, local, line: row.line ?? null, disabled: row.disabled === true })
|
|
74
|
+
}
|
|
75
|
+
for (const preset of (Array.isArray(presets) ? presets : [])) {
|
|
76
|
+
if (preset === null || typeof preset !== 'object' || typeof preset.dir !== 'string') continue
|
|
77
|
+
const mine = patchFileRows.filter((row) => dirnameOf(row.local) === preset.dir)
|
|
78
|
+
const declared = Array.isArray(preset.modules) ? preset.modules : []
|
|
79
|
+
for (const mod of declared) {
|
|
80
|
+
const target = joinPath(preset.dir, mod.target)
|
|
81
|
+
const hit = mine.find((row) => row.id === mod.id) ?? null
|
|
82
|
+
if (hit === null) {
|
|
83
|
+
missing.push({ preset: preset.dir, id: mod.id, expected: target, declaredAt: mod.line, disabled: mod.disabled === true })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (hit.local !== target) {
|
|
87
|
+
stale.push({ preset: preset.dir, id: mod.id, declared: target, patchTarget: hit.local, patchLine: hit.line, disabled: hit.disabled })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
let present = null
|
|
91
|
+
try { present = exists(target) } catch { present = null }
|
|
92
|
+
if (present === false) targetMissing.push({ preset: preset.dir, id: mod.id, target, source: 'declaration' })
|
|
93
|
+
}
|
|
94
|
+
const ids = new Set(declared.map((m) => m.id))
|
|
95
|
+
for (const row of mine) {
|
|
96
|
+
if (ids.has(row.id)) continue
|
|
97
|
+
orphan.push({ preset: preset.dir, id: row.id, patchTarget: row.local, patchLine: row.line, disabled: row.disabled })
|
|
98
|
+
let present = null
|
|
99
|
+
try { present = exists(row.local) } catch { present = null }
|
|
100
|
+
if (present === false) targetMissing.push({ preset: preset.dir, id: row.id, target: row.local, source: 'patch' })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const blockers = stale.length + missing.length + orphan.filter((o) => o.disabled !== true).length
|
|
104
|
+
+ targetMissing.length
|
|
105
|
+
return { stale, missing, orphan, targetMissing, blockers, ok: blockers === 0 }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/** IO 收集层(preset-audit 路由与升级前预检共用):扫磁盘预设 + 读补丁行 → 出报告。 */
|
|
110
|
+
function collectPresetAudit({ presetsRoot, patchPath, deps = {} } = {}) {
|
|
111
|
+
const {
|
|
112
|
+
exists = existsSync, readdir = readdirSync, readText = (p) => readFileSync(p, 'utf8'),
|
|
113
|
+
toLocal = (url) => fileURLToPath(url), joinPath = join, dirOf = dirname,
|
|
114
|
+
} = deps
|
|
115
|
+
if (typeof presetsRoot !== 'string' || presetsRoot === '' || !exists(presetsRoot)) {
|
|
116
|
+
return { ok: false, error: '没有用户预设目录(无需体检)', presetCount: 0, stale: [], missing: [], orphan: [], targetMissing: [], blockers: 0 }
|
|
117
|
+
}
|
|
118
|
+
const presets = []
|
|
119
|
+
try {
|
|
120
|
+
for (const entry of readdir(presetsRoot, { withFileTypes: true })) {
|
|
121
|
+
if (!entry.isDirectory()) continue
|
|
122
|
+
const name = entry.name
|
|
123
|
+
if (name.startsWith('.') || name.startsWith('_') || name.includes('.bak')) continue
|
|
124
|
+
const dir = joinPath(presetsRoot, name)
|
|
125
|
+
const file = joinPath(dir, 'agent.cordis.yml')
|
|
126
|
+
if (!exists(file)) continue
|
|
127
|
+
const modules = parsePresetDeclarations(readText(file))
|
|
128
|
+
if (modules.length > 0) presets.push({ dir, modules })
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
131
|
+
let patchRows = []
|
|
132
|
+
try { patchRows = parsePatchRows(readText(patchPath)) } catch { patchRows = [] }
|
|
133
|
+
const report = auditPresetDeclarations({
|
|
134
|
+
presets,
|
|
135
|
+
patchRows: patchRows.map((r) => ({ id: r.id, name: r.name, line: r.line, disabled: r.disabled })),
|
|
136
|
+
toLocalPath: toLocal,
|
|
137
|
+
joinPath,
|
|
138
|
+
exists,
|
|
139
|
+
dirnameOf: dirOf,
|
|
140
|
+
})
|
|
141
|
+
return { ok: report.ok, presetCount: presets.length, ...report }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { auditPresetDeclarations, collectPresetAudit, parsePresetDeclarations }
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
|
|
32
32
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
33
33
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
34
|
-
import { pathToFileURL } from 'node:url'
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
35
35
|
import { queuedWrite } from '../infra/fsx.js'
|
|
36
36
|
import { validateAgentConfig } from './preset-yaml.js'
|
|
37
|
+
import { migratePluginRowKeys, validatePresetPluginRows } from './preset-rows.js'
|
|
37
38
|
|
|
38
39
|
/** 声明行指向的模块名(框架侧预设插件,0.1.7-rc.2 起才有)。 */
|
|
39
40
|
const AGENT_PRESET_MODULE = '@deepseek-ai/dsh-agent-preset'
|
|
@@ -377,14 +378,40 @@ function buildPresetDeclaration(dir, id, opts = {}) {
|
|
|
377
378
|
if (verdict.ok !== true) {
|
|
378
379
|
return { ok: false, reason: 'invalid-composition', detail: `agent.cordis.yml 未通过结构校验:${verdict.problems[0] ?? ''}` }
|
|
379
380
|
}
|
|
380
|
-
|
|
381
|
+
// ① 插件 config 旧键迁移(**加法**:把框架 schema 根本读不到的旧键改成真键;表见 preset-rows.js)。
|
|
382
|
+
// 与框架升级那一步**同一张表**(PRESET_CONFIG_KEY_MIGRATIONS),只是判据挂在**写行那一刻** ——
|
|
383
|
+
// 预设还会经「装预设型子包 / 源码装配 / 手工放文件」落盘,那些路完全不过升级步骤(真机 D-⑦)。
|
|
384
|
+
// 只动唯一命中、且**该行还没有真键**的那一行;版本判据过不去就一个字都不动。
|
|
385
|
+
const migrationVersion = opts.frameworkVersion ?? null
|
|
386
|
+
const migratedKeys = migratePluginRowKeys(composition.text, migrationVersion)
|
|
387
|
+
// ② 写行**之前**的契约校验:行结构 / `file:///` 目标存在性 / 已确证的框架契约。
|
|
388
|
+
// 命中即**如实拒绝写入** —— 绝不往用户 profile 里写一行必然「加载失败」的声明。
|
|
389
|
+
const rowsVerdict = validatePresetPluginRows(migratedKeys.text, {
|
|
390
|
+
checkFiles: true,
|
|
391
|
+
presetDir: dir,
|
|
392
|
+
exists: (localPath) => existsSync(localPath),
|
|
393
|
+
})
|
|
394
|
+
if (rowsVerdict.ok !== true) {
|
|
395
|
+
const first = rowsVerdict.problems[0]
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
reason: first.code === 'file-target-missing' || first.code === 'file-url-unparsable' || first.code === 'relative-target-missing'
|
|
399
|
+
? 'unresolvable-plugin-file'
|
|
400
|
+
: 'invalid-plugin-rows',
|
|
401
|
+
detail: `预设的 plugins 行未通过契约校验:${rowsVerdict.problems.map((p) => p.detail).join(';')}`
|
|
402
|
+
+ `(出路:${first.hint})`,
|
|
403
|
+
problems: rowsVerdict.problems,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const absolutized = absolutizeComposition(migratedKeys.text, dir, opts)
|
|
381
407
|
const compositionText = normalizeComposition(absolutized.text)
|
|
382
408
|
const meta = presetDisplayMeta(dir, presetId)
|
|
383
409
|
const config = { id: presetId, name: meta.name, description: meta.description, order: meta.order }
|
|
384
410
|
const rowId = `${PRESET_ROW_PREFIX}${presetId}`
|
|
385
411
|
return {
|
|
386
412
|
ok: true, id: presetId, rowId, dir, config, compositionText,
|
|
387
|
-
compositionFile: composition.file, rewritten: absolutized.rewritten,
|
|
413
|
+
compositionFile: composition.file, rewritten: absolutized.rewritten, aliased: migratedKeys.migrated,
|
|
414
|
+
migrationNote: migratedKeys.skippedReason,
|
|
388
415
|
rowText: renderPresetRow(config, compositionText),
|
|
389
416
|
}
|
|
390
417
|
}
|
|
@@ -424,16 +451,17 @@ function declarePresetRow(dir, id, opts = {}) {
|
|
|
424
451
|
const {
|
|
425
452
|
patchPath = null, profileDir = null, now = Date.now,
|
|
426
453
|
backup = true, verify = true, toFileUrl = pathToFileURL, exists = existsSync,
|
|
454
|
+
frameworkVersion = null,
|
|
427
455
|
} = opts
|
|
428
|
-
const built = buildPresetDeclaration(dir, id, { toFileUrl, exists })
|
|
456
|
+
const built = buildPresetDeclaration(dir, id, { toFileUrl, exists, frameworkVersion })
|
|
429
457
|
if (built.ok !== true) {
|
|
430
|
-
return { ok: false, status: 'failed', reason: built.reason, detail: built.detail, rowId: null, patchPath: null, backup: null, changed: false, verified: null, rewritten: [], note: built.detail }
|
|
458
|
+
return { ok: false, status: 'failed', reason: built.reason, detail: built.detail, rowId: null, patchPath: null, backup: null, changed: false, verified: null, rewritten: [], aliased: [], problems: built.problems ?? [], note: built.detail }
|
|
431
459
|
}
|
|
432
460
|
const target = resolvePatchPath({ patchPath, profileDir })
|
|
433
461
|
if (target === null) {
|
|
434
462
|
return {
|
|
435
463
|
ok: false, status: 'skipped', reason: 'no-patch-path', rowId: built.rowId, patchPath: null, backup: null,
|
|
436
|
-
changed: false, verified: null, rewritten: built.rewritten, config: built.config,
|
|
464
|
+
changed: false, verified: null, rewritten: built.rewritten, aliased: built.aliased, config: built.config,
|
|
437
465
|
note: `未写入声明行:本次调用没有目标 profile(既没有 patchPath 也没有 profileDir)—— `
|
|
438
466
|
+ `**文件已就位,但当前框架版本(0.1.7-rc.x 起)需要声明行才能显示**;请在目标 profile 的 cordis.patch.yml 里补一行 preset-${built.id}`,
|
|
439
467
|
}
|
|
@@ -443,7 +471,7 @@ function declarePresetRow(dir, id, opts = {}) {
|
|
|
443
471
|
if (!exists(dirname(target))) {
|
|
444
472
|
return {
|
|
445
473
|
ok: false, status: 'failed', reason: 'no-profile-dir', rowId: built.rowId, patchPath: target, backup: null,
|
|
446
|
-
changed: false, verified: null, rewritten: built.rewritten, config: built.config,
|
|
474
|
+
changed: false, verified: null, rewritten: built.rewritten, aliased: built.aliased, config: built.config,
|
|
447
475
|
detail: `目标 profile 目录不存在:${dirname(target)}`,
|
|
448
476
|
note: `未写入声明行:目标 profile 目录不存在(${dirname(target)})—— **文件已就位,但当前框架版本需要声明行才能显示**`,
|
|
449
477
|
}
|
|
@@ -456,7 +484,7 @@ function declarePresetRow(dir, id, opts = {}) {
|
|
|
456
484
|
if (outcome.changed !== true) {
|
|
457
485
|
return {
|
|
458
486
|
ok: true, status: 'unchanged', rowId: built.rowId, patchPath: target, backup: null, changed: false,
|
|
459
|
-
verified: verify === true ? verifyDeclaredRow(before, built) : null, rewritten: built.rewritten, config: built.config,
|
|
487
|
+
verified: verify === true ? verifyDeclaredRow(before, built) : null, rewritten: built.rewritten, aliased: built.aliased, config: built.config,
|
|
460
488
|
removed: outcome.removed,
|
|
461
489
|
// 与 created/updated 用**同一句**核心文案:幂等重装时用户同样要看懂"它在、重启后可选",
|
|
462
490
|
// 只多一句"本次未改动任何字节"(诚实交代用户的文件没被动过)。
|
|
@@ -471,7 +499,7 @@ function declarePresetRow(dir, id, opts = {}) {
|
|
|
471
499
|
} catch (error) {
|
|
472
500
|
return {
|
|
473
501
|
ok: false, status: 'failed', reason: 'backup-failed', rowId: built.rowId, patchPath: target, backup: null,
|
|
474
|
-
changed: false, verified: null, rewritten: built.rewritten, config: built.config,
|
|
502
|
+
changed: false, verified: null, rewritten: built.rewritten, aliased: built.aliased, config: built.config,
|
|
475
503
|
detail: `备份失败(绝不在没备份的情况下改用户补丁):${error instanceof Error ? error.message : String(error)}`,
|
|
476
504
|
note: '未写入声明行:改前备份失败,已放弃写入(补丁文件一个字节都没动)',
|
|
477
505
|
}
|
|
@@ -483,7 +511,7 @@ function declarePresetRow(dir, id, opts = {}) {
|
|
|
483
511
|
} catch (error) {
|
|
484
512
|
return {
|
|
485
513
|
ok: false, status: 'failed', reason: 'write-failed', rowId: built.rowId, patchPath: target, backup: backupPath,
|
|
486
|
-
changed: false, verified: null, rewritten: built.rewritten, config: built.config,
|
|
514
|
+
changed: false, verified: null, rewritten: built.rewritten, aliased: built.aliased, config: built.config,
|
|
487
515
|
detail: `写入失败:${error instanceof Error ? error.message : String(error)}`,
|
|
488
516
|
note: `写入声明行失败(${error instanceof Error ? error.message : String(error)})—— `
|
|
489
517
|
+ '**文件已就位,但当前框架版本(0.1.7-rc.x 起)需要声明行才能显示**',
|
|
@@ -494,7 +522,7 @@ function declarePresetRow(dir, id, opts = {}) {
|
|
|
494
522
|
if (readBack === null) {
|
|
495
523
|
return {
|
|
496
524
|
ok: false, status: 'failed', reason: 'readback-failed', rowId: built.rowId, patchPath: target, backup: backupPath,
|
|
497
|
-
changed: false, verified: null, rewritten: built.rewritten, config: built.config,
|
|
525
|
+
changed: false, verified: null, rewritten: built.rewritten, aliased: built.aliased, config: built.config,
|
|
498
526
|
detail: '写后读回失败(读不到文件)', note: '写入声明行后读回失败:无法确认是否写成,请人工检查该补丁文件',
|
|
499
527
|
}
|
|
500
528
|
}
|
|
@@ -506,10 +534,14 @@ function declarePresetRow(dir, id, opts = {}) {
|
|
|
506
534
|
reason: ok ? null : 'verify-failed',
|
|
507
535
|
detail: ok ? null : `读回核实不通过:${checked.problems.join(';')}`,
|
|
508
536
|
rowId: built.rowId, patchPath: target, backup: backupPath, changed: true,
|
|
509
|
-
verified: checked, rewritten: built.rewritten, config: built.config, removed: outcome.removed,
|
|
537
|
+
verified: checked, rewritten: built.rewritten, aliased: built.aliased, config: built.config, removed: outcome.removed,
|
|
510
538
|
note: ok
|
|
511
539
|
? presetDeclarationSentence(built.rowId, target)
|
|
512
540
|
+ (built.rewritten.length > 0 ? `;composition 里 ${built.rewritten.length} 处相对文件已改写为 file: URL(基准是 profile 目录,不是预设目录)` : '')
|
|
541
|
+
+ (built.aliased.length > 0
|
|
542
|
+
? `;composition 里 ${built.aliased.length} 处插件旧键已按框架 schema 迁移(`
|
|
543
|
+
+ `${built.aliased.map((r) => `${r.pkg} 的 ${r.from} → ${r.to}(第 ${r.line} 行)`).join('、')})`
|
|
544
|
+
: '')
|
|
513
545
|
+ extra
|
|
514
546
|
: `声明行写入后读回核实不通过:${checked.problems.join(';')}(已备份到 ${backupPath},请人工检查)`,
|
|
515
547
|
}
|