dsh-paoding 0.4.6 → 0.4.7
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 +1 -1
- package/docs/configuration.md +19 -0
- package/docs/configuration_en.md +19 -0
- package/docs/installation.md +7 -3
- package/docs/installation_en.md +6 -2
- package/package.json +1 -1
- package/plugins/paoding-config-ui/api-core.mjs +58 -6
- package/plugins/paoding-config-ui/index.mjs +8 -5
- package/plugins/paoding-config-ui/lib/base.css +9 -0
- package/plugins/paoding-config-ui/lib/client.js +6 -1
- package/plugins/paoding-config-ui/stats-core.mjs +94 -35
- package/tools/lib/config.mjs +23 -1
- package/tools/lib/doctor.mjs +79 -19
- package/tools/lib/lifecycle.mjs +105 -36
- package/tools/lib/preset-system.mjs +254 -38
- package/tools/lib/state.mjs +24 -9
|
@@ -15,23 +15,39 @@
|
|
|
15
15
|
* 一条声明行的 config.plugins(restrict 行与 delegation-labels 行的本地
|
|
16
16
|
* name 换成对应 .mjs 的绝对 file: URL——声明轨没有「相对 preset 目录」
|
|
17
17
|
* 语义,必须绝对化);
|
|
18
|
-
* 3)
|
|
19
|
-
* readHomePatchRows:对
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* 3) patch 托管块手术 upsertHomePatchRows / stripHomePatchRows /
|
|
19
|
+
* readHomePatchRows:对 patch 文件的 MANAGED 标记块做行级增删查,块外用户
|
|
20
|
+
* 内容一字节不动;托管块损坏(缺结束标记)时拒绝写盘并明确报错,与仓库
|
|
21
|
+
* 「配置不可信时拒绝落盘」口径一致。层感知:patchFilePath 按 opts
|
|
22
|
+
* { layer, profileName } 把写盘落到 home 层($DSH_HOME/cordis.patch.yml)或
|
|
23
|
+
* profile 层($DSH_HOME/profiles/<name>/cordis.patch.yml),缺省 home(全部
|
|
24
|
+
* 既有调用点零改动);层判定见 isCompatShellLike / resolvePatchTargetLayer。
|
|
25
|
+
* 每次写盘后整文件 YAML 校验(validatePatchYaml),解析失败即回滚为写入前
|
|
26
|
+
* 字节并报错——Android compat 壳会重置/压平 home 层 patch(dsh-maoying#1
|
|
27
|
+
* 同款问题),坏文件一旦落盘宿主会在任何插件代码运行前死掉,宁可不写。
|
|
28
|
+
* 4) 层判定与迁移 isCompatShellLike / resolvePatchTargetLayer /
|
|
29
|
+
* reconcilePatchLayers:platform=android 或 dshHome 形如 /data/(user|data)/*
|
|
30
|
+
* 的容器壳 → 默认 profile 层(config patch_layer 键 / DSH_PAODING_PATCH_LAYER
|
|
31
|
+
* 环境变量可显式钉死);目标层写成功后 reconcile 撤掉另一层的托管块,
|
|
32
|
+
* 声明行永不跨层重复。
|
|
33
|
+
* 5) 可疑行探测 findSuspectPresetRows:托管块外扫「- id: <显示名 (英文名)>」
|
|
34
|
+
* 形状的坏行(id 含空白或非 ASCII——preset/package id 不会这样),WARN 级
|
|
35
|
+
* 供 doctor / 启动自愈提示,绝不自动删除。
|
|
36
|
+
* 6) 轨道迁移对账 backfillHomePatchRows / listOrchestratorPresetIds /
|
|
23
37
|
* parseGeneratorMarker / formatGeneratorMarker:宿主只升 dsh 不动插件时
|
|
24
38
|
* (标记版本一致、轨道翻转或 workflow worker 形态换代——升级最常见路径),
|
|
25
39
|
* 按磁盘产物补写缺失声明行而不整盘重生成;生成器标记记「版本 + 轨道 +
|
|
26
40
|
* worker 形态」三行,任一不符即触发自愈(worker 形态换代必须整盘重生成,
|
|
27
41
|
* 见 detectHostVersionTriple / isPtcWorkerHost)。
|
|
28
|
-
* 零运行时依赖(node:child_process / node:fs / node:path / node:url
|
|
42
|
+
* 零运行时依赖(node:child_process / node:fs / node:path / node:url;yaml 校验
|
|
43
|
+
* 经兄弟模块 yaml.mjs 的惰性 loadYaml);被 state 引用。
|
|
29
44
|
*/
|
|
30
45
|
import { spawnSync } from 'node:child_process'
|
|
31
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
46
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
32
47
|
import path from 'node:path'
|
|
33
48
|
import { pathToFileURL } from 'node:url'
|
|
34
|
-
import { warn } from './util.mjs'
|
|
49
|
+
import { warn, PROFILE_NAME_RE } from './util.mjs'
|
|
50
|
+
import { loadYaml, parseYamlSubset, preprocessPatchText } from './yaml.mjs'
|
|
35
51
|
|
|
36
52
|
/** 托管块起始标记:其后到结束标记之间是本工具自动生成的声明行,用户勿改。 */
|
|
37
53
|
export const MANAGED_BEGIN = '# --- dsh-paoding presets (auto-generated; do not edit) ---'
|
|
@@ -290,16 +306,105 @@ export function buildPresetRowText({ presetId, name = null, description = null,
|
|
|
290
306
|
// buildPresetRowText 的 4 空格基准缩进,作 insert 列表元素。块外用户内容除
|
|
291
307
|
// strip 的空行收敛外一字节不动。
|
|
292
308
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
309
|
+
// ── patch 层判定与路径(home / profile 双层)────────────────────────────────
|
|
310
|
+
//
|
|
311
|
+
// Android compat 壳(DeepSeekHarnessCompat)会在每次 boot / repair 时把 home 层
|
|
312
|
+
// $DSH_HOME/cordis.patch.yml 重置回出厂种子,还会把声明行的显示元数据压平成
|
|
313
|
+
// 「- id: 显示名 (英文名)」+ 8 空格键的坏行——宿主在任何插件代码运行前就死于
|
|
314
|
+
// YAML 解析(dsh-maoying#1 同款问题)。对策:这类环境默认把托管块写进 profile
|
|
315
|
+
// 层 $DSH_HOME/profiles/<name>/cordis.patch.yml(壳不重置它,且启动自愈每轮
|
|
316
|
+
// 重写可自愈),并撤掉 home 层的遗留块。
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 兼容壳判定(可注入便于测试):platform === 'android',或 dshHomePath 落在
|
|
320
|
+
* Android 容器路径 ^/data/(user|data)/ 下即 true——两者都是 compat 壳会重写
|
|
321
|
+
* home 层 patch 的环境特征。
|
|
322
|
+
*/
|
|
323
|
+
export function isCompatShellLike({ platform = process.platform, dshHomePath = null } = {}) {
|
|
324
|
+
if (platform === 'android') return true
|
|
325
|
+
return typeof dshHomePath === 'string' && /^\/data\/(user|data)\//.test(dshHomePath)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** 从 argv 解析显式 profile 名:`--profile <name>` 或 `--profile=<name>`。 */
|
|
329
|
+
function profileNameFromArgv(argv) {
|
|
330
|
+
const args = Array.isArray(argv) ? argv : []
|
|
331
|
+
for (let i = 0; i < args.length; i++) {
|
|
332
|
+
const arg = String(args[i])
|
|
333
|
+
if (arg === '--profile') {
|
|
334
|
+
const next = args[i + 1]
|
|
335
|
+
return typeof next === 'string' && next.trim() !== '' ? next.trim() : null
|
|
336
|
+
}
|
|
337
|
+
const m = /^--profile=(.+)$/.exec(arg)
|
|
338
|
+
if (m) return m[1].trim()
|
|
339
|
+
}
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** 'home' | 'profile' 之外的值一律 null(调用方 warn 后回落)。 */
|
|
344
|
+
const normalizeLayerValue = (value) => (value === 'home' || value === 'profile' ? value : null)
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 解析声明行的目标 patch 层。优先级:
|
|
348
|
+
* 配置键 patch_layer('home' | 'profile')> 环境变量 DSH_PAODING_PATCH_LAYER
|
|
349
|
+
* (测试/诊断钩子)> isCompatShellLike(android / 容器路径 → 'profile')>
|
|
350
|
+
* 缺省 'home'。非法值 warn 后按未设置处理。
|
|
351
|
+
* profileName:argv `--profile` > 配置键 profile > 'web';拼进路径前按
|
|
352
|
+
* PROFILE_NAME_RE 白名单校验(同名口径见 util.mjs),非法回落 'web'——
|
|
353
|
+
* profile 名要拼进 patch 文件路径,不能放过路径穿越形状。
|
|
354
|
+
* 返回 { layer, profileName, patchPath, legacyPatchPath };legacyPatchPath 是
|
|
355
|
+
* 另一层的路径(同 profileName 基准),供 reconcilePatchLayers 迁移对账。
|
|
356
|
+
*/
|
|
357
|
+
export function resolvePatchTargetLayer(dshHome, { config = null, argv = process.argv, platform = process.platform } = {}) {
|
|
358
|
+
let layer = normalizeLayerValue(config?.patch_layer)
|
|
359
|
+
if (layer === null && config?.patch_layer !== undefined && config?.patch_layer !== null) {
|
|
360
|
+
warn(`patch_layer: 非法值已忽略(${JSON.stringify(config.patch_layer)}),按自动判定(可选: 'home' / 'profile')`)
|
|
361
|
+
}
|
|
362
|
+
if (layer === null) {
|
|
363
|
+
const fromEnv = normalizeLayerValue(process.env.DSH_PAODING_PATCH_LAYER)
|
|
364
|
+
if (fromEnv === null && process.env.DSH_PAODING_PATCH_LAYER !== undefined && process.env.DSH_PAODING_PATCH_LAYER !== '') {
|
|
365
|
+
warn(`DSH_PAODING_PATCH_LAYER: 非法值已忽略(${JSON.stringify(process.env.DSH_PAODING_PATCH_LAYER)}),按自动判定(可选: 'home' / 'profile')`)
|
|
366
|
+
}
|
|
367
|
+
layer = fromEnv
|
|
368
|
+
}
|
|
369
|
+
if (layer === null) layer = isCompatShellLike({ platform, dshHomePath: dshHome }) ? 'profile' : 'home'
|
|
370
|
+
|
|
371
|
+
let profileName = profileNameFromArgv(argv)
|
|
372
|
+
if (profileName === null && typeof config?.profile === 'string' && config.profile.trim() !== '') profileName = config.profile.trim()
|
|
373
|
+
if (profileName === null) profileName = 'web'
|
|
374
|
+
if (!PROFILE_NAME_RE.test(profileName)) {
|
|
375
|
+
warn(`profile 名 ${JSON.stringify(profileName)} 不合法(首字符字母数字,其余 [A-Za-z0-9._-]),patch 层回落 'web'`)
|
|
376
|
+
profileName = 'web'
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
layer,
|
|
380
|
+
profileName,
|
|
381
|
+
patchPath: patchFilePath(dshHome, { layer, profileName }),
|
|
382
|
+
legacyPatchPath: patchFilePath(dshHome, { layer: layer === 'profile' ? 'home' : 'profile', profileName }),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** 手术 opts { layer?, profileName? } → 规范形状(缺省 home 层,全部旧调用点零改动)。 */
|
|
387
|
+
function patchOptsOf(opts = {}) {
|
|
388
|
+
const layer = opts?.layer === 'profile' ? 'profile' : 'home'
|
|
389
|
+
return { layer, profileName: typeof opts?.profileName === 'string' && opts.profileName !== '' ? opts.profileName : 'web' }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* patch 文件路径:home 层 $DSH_HOME/cordis.patch.yml;profile 层
|
|
394
|
+
* $DSH_HOME/profiles/<profileName>/cordis.patch.yml(父目录在写盘时创建)。
|
|
395
|
+
*/
|
|
396
|
+
function patchFilePath(dshHome, opts = {}) {
|
|
397
|
+
const { layer, profileName } = patchOptsOf(opts)
|
|
398
|
+
return layer === 'profile'
|
|
399
|
+
? path.join(dshHome, 'profiles', profileName, 'cordis.patch.yml')
|
|
400
|
+
: path.join(dshHome, 'cordis.patch.yml')
|
|
296
401
|
}
|
|
297
402
|
|
|
298
|
-
/** 读
|
|
299
|
-
function readPatchLines(dshHome) {
|
|
403
|
+
/** 读 patch 为行数组;文件缺失当空文件。行尾换行符风格随原文件保留。 */
|
|
404
|
+
function readPatchLines(dshHome, opts = {}) {
|
|
300
405
|
let text = ''
|
|
301
406
|
try {
|
|
302
|
-
text = readFileSync(patchFilePath(dshHome), 'utf8')
|
|
407
|
+
text = readFileSync(patchFilePath(dshHome, opts), 'utf8')
|
|
303
408
|
} catch (err) {
|
|
304
409
|
if (err?.code !== 'ENOENT') throw err
|
|
305
410
|
return { lines: [], eol: '\n' }
|
|
@@ -308,9 +413,116 @@ function readPatchLines(dshHome) {
|
|
|
308
413
|
return { lines: text.split(/\r?\n/), eol: text.includes('\r\n') ? '\r\n' : '\n' }
|
|
309
414
|
}
|
|
310
415
|
|
|
311
|
-
function writePatchLines(dshHome, lines, eol) {
|
|
312
|
-
|
|
313
|
-
|
|
416
|
+
function writePatchLines(dshHome, opts, lines, eol) {
|
|
417
|
+
const target = patchFilePath(dshHome, opts)
|
|
418
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
419
|
+
writeFileSync(target, lines.join(eol), 'utf8')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 整文件 YAML 校验(写盘后的回滚判据,doctor 整文件解析检查同源复用):
|
|
424
|
+
* 宿主的 yaml 包($DSH_HOME/node_modules,经 loadYaml 惰性加载)可用时以它为
|
|
425
|
+
* 准——宿主用什么解析我们就用什么验证;!!js 表达式标签按 state.mjs 同款技巧
|
|
426
|
+
* 中和成 true(校验只关心结构合法性,不求值表达式)。yaml 包不可用时退回
|
|
427
|
+
* 内置 patch 子集解析器(lenient,挡不住坏缩进,仅兜 gross 损坏)。
|
|
428
|
+
* 返回 { ok: true } 或 { ok: false, message }(message 含解析器报的行号)。
|
|
429
|
+
*/
|
|
430
|
+
export function validatePatchYaml(dshHome, text) {
|
|
431
|
+
const yamlMod = loadYaml(dshHome)
|
|
432
|
+
if (yamlMod) {
|
|
433
|
+
try {
|
|
434
|
+
yamlMod.parse(String(text).replace(/!!js\s+[^\r\n]*/g, 'true'))
|
|
435
|
+
return { ok: true }
|
|
436
|
+
} catch (err) {
|
|
437
|
+
return { ok: false, message: err?.message ?? String(err) }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
parseYamlSubset(preprocessPatchText(text))
|
|
442
|
+
return { ok: true }
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return { ok: false, message: err?.message ?? String(err) }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 写盘 + 整文件校验 + 失败回滚。写前记下原字节,写后把整文件过
|
|
450
|
+
* validatePatchYaml;解析失败即把原字节原样写回(写入前不存在则删掉残留),
|
|
451
|
+
* 再抛点名文件、解析错误(含行号)与两类可能原因的明确错误——坏文件绝不
|
|
452
|
+
* 留在盘上(compat 壳环境里宿主会在任何插件代码运行前死于解析)。
|
|
453
|
+
*/
|
|
454
|
+
function writePatchLinesValidated(dshHome, opts, lines, eol) {
|
|
455
|
+
const target = patchFilePath(dshHome, opts)
|
|
456
|
+
let before = null
|
|
457
|
+
let readError = null
|
|
458
|
+
try {
|
|
459
|
+
before = readFileSync(target, 'utf8')
|
|
460
|
+
} catch (err) {
|
|
461
|
+
if (err?.code !== 'ENOENT') readError = err
|
|
462
|
+
}
|
|
463
|
+
writePatchLines(dshHome, opts, lines, eol)
|
|
464
|
+
let verdict = { ok: false, message: '写入后文件不可读' }
|
|
465
|
+
if (readError === null) {
|
|
466
|
+
try {
|
|
467
|
+
verdict = validatePatchYaml(dshHome, readFileSync(target, 'utf8'))
|
|
468
|
+
} catch (err) {
|
|
469
|
+
verdict = { ok: false, message: err?.message ?? String(err) }
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
verdict = { ok: false, message: `写入前原文件已不可读(${readError.message}),拒绝覆盖写` }
|
|
473
|
+
}
|
|
474
|
+
if (verdict.ok) return
|
|
475
|
+
// 回滚为写入前的精确字节(写入前不存在 → 删除本次新建的残留)。
|
|
476
|
+
let restoreNote = '文件已回滚为写入前内容,一字节未变'
|
|
477
|
+
try {
|
|
478
|
+
if (before === null) rmSync(target, { force: true })
|
|
479
|
+
else writeFileSync(target, before, 'utf8')
|
|
480
|
+
} catch (restoreErr) {
|
|
481
|
+
restoreNote = `回滚失败(${restoreErr?.message ?? restoreErr}),请从备份/手改恢复后再试`
|
|
482
|
+
}
|
|
483
|
+
const lineHint = /line (\d+)/i.exec(verdict.message)
|
|
484
|
+
throw new Error(
|
|
485
|
+
`写入 ${target} 后 YAML 校验失败,${restoreNote}。解析错误${lineHint ? `(约第 ${lineHint[1]} 行)` : ''}: ` +
|
|
486
|
+
verdict.message.replace(/\s+/g, ' ').trim() +
|
|
487
|
+
'\n可能原因: (a) 文件在写入前已含坏块(compat 壳重写/手改)→ 运行 `dsh-paoding doctor` 检查并清理坏行;' +
|
|
488
|
+
'(b) paoding 自身产物缺陷 → 请报 issue(附该文件内容)',
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 目标层写盘成功后对另一层做迁移对账:撤掉 legacy 层的整个托管块(只撤不补
|
|
494
|
+
* ——声明行永不跨层重复,复用 stripHomePatchRows 的整块摘除逻辑)。目标层为
|
|
495
|
+
* home 时对称撤 profile 层遗留块。返回是否实际撤过。
|
|
496
|
+
*/
|
|
497
|
+
export function reconcilePatchLayers(dshHome, opts = {}) {
|
|
498
|
+
const { layer, profileName } = patchOptsOf(opts)
|
|
499
|
+
return stripHomePatchRows(dshHome, null, { layer: layer === 'profile' ? 'home' : 'profile', profileName })
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 托管块外扫可疑声明行(maoying 坏行形状,dsh-maoying#1):`- id: <值>` 单行
|
|
504
|
+
* 且值含空白或非 ASCII 字符——preset / package id 永远是紧凑 ASCII(如
|
|
505
|
+
* `preset-orchestrator`),「- id: 内阁首辅 (Orchestrator)」这种是显示名被
|
|
506
|
+
* 压平进 id 位的表现。托管块内的行不算(自家声明行有 `- insert:` 包裹 + 4 空
|
|
507
|
+
* 格缩进 id,形状本就不同且在块内);托管块损坏(只有起始标记)时按整文件块
|
|
508
|
+
* 外处理。返回 [{ lineNo, id }](lineNo 1 起)。WARN 级探测,绝不自动删除。
|
|
509
|
+
*/
|
|
510
|
+
export function findSuspectPresetRows(text) {
|
|
511
|
+
const lines = String(text ?? '').split(/\r?\n/)
|
|
512
|
+
let outside = lines.map((_, i) => i)
|
|
513
|
+
try {
|
|
514
|
+
const span = findManagedSpan(lines)
|
|
515
|
+
if (span !== null) outside = outside.filter((i) => i < span.begin || i > span.end)
|
|
516
|
+
} catch {
|
|
517
|
+
/* 托管块损坏:整文件按块外处理 */
|
|
518
|
+
}
|
|
519
|
+
const suspects = []
|
|
520
|
+
for (const i of outside) {
|
|
521
|
+
const m = /^[ \t]*-[ \t]+id:[ \t]*(\S.*\S)[ \t]*$/.exec(lines[i])
|
|
522
|
+
if (!m) continue
|
|
523
|
+
if (/\s/.test(m[1]) || /[^\x00-\x7F]/.test(m[1])) suspects.push({ lineNo: i + 1, id: m[1] })
|
|
524
|
+
}
|
|
525
|
+
return suspects
|
|
314
526
|
}
|
|
315
527
|
|
|
316
528
|
/**
|
|
@@ -388,11 +600,13 @@ function collapseBlankRuns(lines) {
|
|
|
388
600
|
}
|
|
389
601
|
|
|
390
602
|
/**
|
|
391
|
-
* 把若干声明行(buildPresetRowText 产物)行级 upsert 进
|
|
603
|
+
* 把若干声明行(buildPresetRowText 产物)行级 upsert 进 patch 托管块。
|
|
392
604
|
* 托管块不存在则文末新建(非空文件先补尾换行、块前空一行);已存在则块内按
|
|
393
605
|
* rowId 替换 / 追加,块外用户内容一字节不动。托管块损坏时 throw(不写盘)。
|
|
606
|
+
* 尾参 opts = { layer?, profileName? }(缺省 home 层,旧调用点零改动);写盘
|
|
607
|
+
* 后整文件校验、失败回滚(见 writePatchLinesValidated)。
|
|
394
608
|
*/
|
|
395
|
-
export function upsertHomePatchRows(dshHome, rowTexts) {
|
|
609
|
+
export function upsertHomePatchRows(dshHome, rowTexts, opts = {}) {
|
|
396
610
|
const rows = (Array.isArray(rowTexts) ? rowTexts : [])
|
|
397
611
|
.filter((t) => typeof t === 'string' && t.trim() !== '')
|
|
398
612
|
if (rows.length === 0) return
|
|
@@ -407,7 +621,7 @@ export function upsertHomePatchRows(dshHome, rowTexts) {
|
|
|
407
621
|
// 同 rowId 传多条时后者生效(Map 去重,保证替换语义而非重复追加)
|
|
408
622
|
const wantedById = new Map(wanted.map((w) => [w.rowId, w]))
|
|
409
623
|
|
|
410
|
-
const { lines, eol } = readPatchLines(dshHome)
|
|
624
|
+
const { lines, eol } = readPatchLines(dshHome, opts)
|
|
411
625
|
const span = findManagedSpan(lines) // 损坏块 throw,直接中止(未写任何盘)
|
|
412
626
|
if (span !== null) {
|
|
413
627
|
const segments = parseRowSegments(lines.slice(span.begin + 1, span.end))
|
|
@@ -420,7 +634,7 @@ export function upsertHomePatchRows(dshHome, rowTexts) {
|
|
|
420
634
|
if (!segments.some((s) => s.rowId === w.rowId)) merged.push(...w.lines) // 新行追加到块尾
|
|
421
635
|
}
|
|
422
636
|
const next = [...lines.slice(0, span.begin), MANAGED_BEGIN, ...merged, MANAGED_END, ...lines.slice(span.end + 1)]
|
|
423
|
-
|
|
637
|
+
writePatchLinesValidated(dshHome, opts, next, eol)
|
|
424
638
|
return
|
|
425
639
|
}
|
|
426
640
|
// 无托管块:文末新建。非空文件先补尾换行(若缺)。块前空一行不再单独推:
|
|
@@ -432,23 +646,24 @@ export function upsertHomePatchRows(dshHome, rowTexts) {
|
|
|
432
646
|
out.push(MANAGED_BEGIN)
|
|
433
647
|
for (const w of wanted) out.push(...w.lines)
|
|
434
648
|
out.push(MANAGED_END, '')
|
|
435
|
-
|
|
649
|
+
writePatchLinesValidated(dshHome, opts, out, eol)
|
|
436
650
|
}
|
|
437
651
|
|
|
438
652
|
/**
|
|
439
|
-
* 从
|
|
653
|
+
* 从 patch 托管块撤掉指定 rowId 的声明行段;rowIdsOrAll 传 null 删整块
|
|
440
654
|
* (含起止标记与块前自加的空行分隔,用户内容回到插入前的字节形态)。
|
|
441
655
|
* 块不存在 / 指定行都不在场返回 false(未写盘);托管块损坏时 warn 后返回
|
|
442
656
|
* false(撤行是锦上添花的自愈路径,不值得为此中断安装,修复由 upsert 的
|
|
443
|
-
*
|
|
657
|
+
* 明确报错引导)。尾参 opts 同 upsertHomePatchRows;写盘同样过校验回滚。
|
|
444
658
|
*/
|
|
445
|
-
export function stripHomePatchRows(dshHome, rowIdsOrAll = null) {
|
|
659
|
+
export function stripHomePatchRows(dshHome, rowIdsOrAll = null, opts = {}) {
|
|
660
|
+
const target = patchFilePath(dshHome, opts)
|
|
446
661
|
let lines
|
|
447
662
|
let eol
|
|
448
663
|
try {
|
|
449
|
-
({ lines, eol } = readPatchLines(dshHome))
|
|
664
|
+
({ lines, eol } = readPatchLines(dshHome, opts))
|
|
450
665
|
} catch (err) {
|
|
451
|
-
warn(
|
|
666
|
+
warn(`${target} 读取失败,声明行撤除跳过: ${err?.message ?? err}`)
|
|
452
667
|
return false
|
|
453
668
|
}
|
|
454
669
|
let span = null
|
|
@@ -474,7 +689,7 @@ export function stripHomePatchRows(dshHome, rowIdsOrAll = null) {
|
|
|
474
689
|
const next = [...lines.slice(0, start), ...lines.slice(span.end + 1)]
|
|
475
690
|
// 文件撤空(只剩空行)就落空文件,不留一撮换行尾巴
|
|
476
691
|
const empty = next.every((l) => l.trim() === '')
|
|
477
|
-
|
|
692
|
+
writePatchLinesValidated(dshHome, opts, empty ? [] : collapseBlankRuns(next), eol)
|
|
478
693
|
return true
|
|
479
694
|
}
|
|
480
695
|
// 块内还有其余 preset 的行:只摘指定段,标记与块结构保留
|
|
@@ -484,19 +699,20 @@ export function stripHomePatchRows(dshHome, rowIdsOrAll = null) {
|
|
|
484
699
|
if (!removeIds.has(seg.rowId)) merged.push(...trimTrailingBlankLines(seg.lines))
|
|
485
700
|
}
|
|
486
701
|
const next = [...lines.slice(0, span.begin), MANAGED_BEGIN, ...merged, MANAGED_END, ...lines.slice(span.end + 1)]
|
|
487
|
-
|
|
702
|
+
writePatchLinesValidated(dshHome, opts, collapseBlankRuns(next), eol)
|
|
488
703
|
return true
|
|
489
704
|
}
|
|
490
705
|
|
|
491
706
|
/**
|
|
492
|
-
* 读
|
|
707
|
+
* 读 patch 托管块里现有的声明行 [{ rowId, presetId }](presetId 取自声明
|
|
493
708
|
* 行 config 的 id: 值,8 空格缩进;解析不出为 null)。文件缺失 / 无托管块 /
|
|
494
709
|
* 托管块损坏一律返回空数组——调用方只拿它做「块在不在」的判断。
|
|
710
|
+
* 尾参 opts 同 upsertHomePatchRows(layer=profile 时读 profile 层)。
|
|
495
711
|
*/
|
|
496
|
-
export function readHomePatchRows(dshHome) {
|
|
712
|
+
export function readHomePatchRows(dshHome, opts = {}) {
|
|
497
713
|
let lines
|
|
498
714
|
try {
|
|
499
|
-
({ lines } = readPatchLines(dshHome))
|
|
715
|
+
({ lines } = readPatchLines(dshHome, opts))
|
|
500
716
|
} catch {
|
|
501
717
|
return []
|
|
502
718
|
}
|
|
@@ -600,7 +816,7 @@ export function detectAgentRolesFlavor(agentYmlText) {
|
|
|
600
816
|
}
|
|
601
817
|
|
|
602
818
|
/**
|
|
603
|
-
*
|
|
819
|
+
* 轨道迁移对账:把目标层 patch 文件 MANAGED 块里缺失的编排类 preset
|
|
604
820
|
* 声明行按磁盘产物补写回来。只补「行缺失」:行在则零写盘(内容对账是
|
|
605
821
|
* ensurePresetInstalled 的整盘重生成路径,不在这里做)。
|
|
606
822
|
*
|
|
@@ -609,10 +825,10 @@ export function detectAgentRolesFlavor(agentYmlText) {
|
|
|
609
825
|
* 0.2.1 而磁盘还是内联形状——补出去的声明行会让 0.2.1 宿主挂编排 preset 的
|
|
610
826
|
* 新建会话直接被 tool-subagent 守卫拒绝。此时跳过该 preset 并 warn(可整盘
|
|
611
827
|
* 重生成修复),绝不按旧产物写行;反方向(宿主降级、磁盘已是新形状)同理。
|
|
612
|
-
* 守卫经 DSH_PAODING_FAKE_HOST_VERSION
|
|
828
|
+
* 守卫经 DSH_PAODING_FAKE_HOST_VERSION 可测。尾参 opts 同 upsertHomePatchRows。
|
|
613
829
|
*/
|
|
614
|
-
export function backfillHomePatchRows(dshHome, presetIds) {
|
|
615
|
-
const rowsOnDisk = readHomePatchRows(dshHome)
|
|
830
|
+
export function backfillHomePatchRows(dshHome, presetIds, opts = {}) {
|
|
831
|
+
const rowsOnDisk = readHomePatchRows(dshHome, opts)
|
|
616
832
|
const wanted = new Set((Array.isArray(presetIds) ? presetIds : []).filter((id) => ORCHESTRATOR_PRESET_ID_RE.test(id)))
|
|
617
833
|
const hostRolesFlavor = presetRolesFlavorOf(detectHostVersionTriple())
|
|
618
834
|
const rows = []
|
|
@@ -657,10 +873,10 @@ export function backfillHomePatchRows(dshHome, presetIds) {
|
|
|
657
873
|
warn(`声明行补写跳过 ${presetId}(产物不全,可整盘重生成修复): ${err?.message ?? err}`)
|
|
658
874
|
}
|
|
659
875
|
}
|
|
660
|
-
if (rows.length > 0) upsertHomePatchRows(dshHome, rows)
|
|
876
|
+
if (rows.length > 0) upsertHomePatchRows(dshHome, rows, opts)
|
|
661
877
|
// 反向对账:托管块里有行、磁盘上已无对应目录的(用户手删目录等),行同步撤下
|
|
662
878
|
const orphanRowIds = rowsOnDisk.filter((r) => r.presetId !== null && !wanted.has(r.presetId)).map((r) => r.rowId)
|
|
663
|
-
if (orphanRowIds.length > 0) stripHomePatchRows(dshHome, orphanRowIds)
|
|
879
|
+
if (orphanRowIds.length > 0) stripHomePatchRows(dshHome, orphanRowIds, opts)
|
|
664
880
|
return backfilled
|
|
665
881
|
}
|
|
666
882
|
|
package/tools/lib/state.mjs
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* saveConfig,支持按工作区落到 orchestrator-<slug> 独立 preset 目录并把条目
|
|
6
6
|
* 并进配置 workspaces 段;配置读取失败 / 状态不可信时拒绝破坏性落盘,成功后
|
|
7
7
|
* 回收孤儿工作区 preset;并按 presetSystem 双轨收尾——0.1.7+ 声明行轨把声明行
|
|
8
|
-
* upsert
|
|
8
|
+
* upsert 进目标层 patch 托管块(层判定见 preset-system.resolvePatchTargetLayer:
|
|
9
|
+
* Android compat 壳 / patch_layer 钉层落 profile 层,写后 reconcile 撤 home 遗留块),
|
|
10
|
+
* ≤0.1.6 目录扫描轨撤掉残留声明块自愈)。
|
|
9
11
|
* install.mjs 的公共导出(collectState / generateAndInstall)即来自本模块。
|
|
10
12
|
* 依赖 util / yaml / config / host / skills / spans / compose / alloc /
|
|
11
13
|
* workspaces / preset-system 与 node:fs / node:path / node:url;被 cli 与
|
|
@@ -54,6 +56,8 @@ import {
|
|
|
54
56
|
presetRowId,
|
|
55
57
|
readHomePatchRows,
|
|
56
58
|
readPresetDirMeta,
|
|
59
|
+
reconcilePatchLayers,
|
|
60
|
+
resolvePatchTargetLayer,
|
|
57
61
|
stripHomePatchRows,
|
|
58
62
|
upsertHomePatchRows,
|
|
59
63
|
} from './preset-system.mjs'
|
|
@@ -369,6 +373,12 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
|
|
|
369
373
|
throw new Error(`现有配置 ${configFile} 读取失败,已中止(未做任何落盘): ${err.message}`)
|
|
370
374
|
}
|
|
371
375
|
|
|
376
|
+
// 声明行目标 patch 层解析一次(prev 配置在场:patch_layer / profile 键生效;
|
|
377
|
+
// argv 缺省取本进程——Android compat 壳环境自动落 profile 层,见
|
|
378
|
+
// preset-system.resolvePatchTargetLayer 的层判定链)。
|
|
379
|
+
const patchLayerTarget = resolvePatchTargetLayer(dshHome, { config: prev })
|
|
380
|
+
const patchOpts = { layer: patchLayerTarget.layer, profileName: patchLayerTarget.profileName }
|
|
381
|
+
|
|
372
382
|
if (wsPath !== null) {
|
|
373
383
|
// slug 取自磁盘配置的 workspaces 键集合 ∪ 本路径(prev 已在上方统一读取):
|
|
374
384
|
// 整组统一分配才能让同 basename 的冲突方都拿到稳定的 hash 后缀。
|
|
@@ -489,11 +499,13 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
|
|
|
489
499
|
const next = wsPath === null
|
|
490
500
|
? {
|
|
491
501
|
profile: effectiveProfile,
|
|
502
|
+
patch_layer: prev?.patch_layer ?? null,
|
|
492
503
|
...normalizeTargetFields(assignments),
|
|
493
504
|
workspaces: prev?.workspaces ?? {},
|
|
494
505
|
}
|
|
495
506
|
: {
|
|
496
507
|
profile: prev?.profile ?? effectiveProfile,
|
|
508
|
+
patch_layer: prev?.patch_layer ?? null,
|
|
497
509
|
...normalizeTargetFields(prev),
|
|
498
510
|
workspaces: { ...(prev?.workspaces ?? {}), [wsPath]: normalizeTargetFields(assignments) },
|
|
499
511
|
}
|
|
@@ -525,10 +537,10 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
|
|
|
525
537
|
continue
|
|
526
538
|
}
|
|
527
539
|
if (presetSystem === 'declarative') {
|
|
528
|
-
//
|
|
540
|
+
// 声明行轨:目录回收了,目标层 patch 托管块里的对应声明行同步撤下——
|
|
529
541
|
// 目录与行必须同生同灭,否则 0.1.7 宿主会挂出一个指向已删目录的 preset。
|
|
530
542
|
try {
|
|
531
|
-
stripHomePatchRows(dshHome, [presetRowId(name)])
|
|
543
|
+
stripHomePatchRows(dshHome, [presetRowId(name)], patchOpts)
|
|
532
544
|
} catch (err) {
|
|
533
545
|
warn(`孤儿 preset 的声明行撤除失败(${presetRowId(name)}): ${err?.message ?? err}`)
|
|
534
546
|
}
|
|
@@ -538,11 +550,13 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
|
|
|
538
550
|
|
|
539
551
|
// ── 预设安装双轨收尾(wrote=true 才动;dry-run 零副作用)────────────────
|
|
540
552
|
// declarative(0.1.7+):目录扫描机制已删,必须把本 preset 以一条 `- insert:`
|
|
541
|
-
//
|
|
553
|
+
// 声明行写进目标层 patch 托管块才算安装完成(preset 元信息与写 preset.yml 同
|
|
542
554
|
// 源:显示名取 displayName ?? SRC preset.yml 的 name,description / order 取
|
|
543
555
|
// SRC;restrict.mjs / delegation-labels.mjs / role-restrict.mjs 用绝对 file: URL——声明行没有
|
|
544
556
|
// 「相对 preset 目录」语义;role-restrict URL 仅供 0.2.1+ AgentPreset 形态产物的角色面行消费)。
|
|
545
|
-
//
|
|
557
|
+
// 目标层为 profile(compat 壳 / patch_layer 钉层)时,写成功后顺手撤掉 home
|
|
558
|
+
// 层的遗留托管块(reconcilePatchLayers)——声明行永不跨层重复。
|
|
559
|
+
// directory(≤0.1.6):上面落盘即完成;目标层 patch 里若有残留托管声明块
|
|
546
560
|
//(宿主刚从 0.1.7 降级等场景)整块撤下自愈——残留声明行会让 profile 启动失败。
|
|
547
561
|
if (wrote) {
|
|
548
562
|
if (presetSystem === 'declarative') {
|
|
@@ -558,10 +572,11 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
|
|
|
558
572
|
delegationLabelsFileUrl: pathToFileURL(path.join(dstDir, 'delegation-labels.mjs')).href,
|
|
559
573
|
roleRestrictFileUrl: pathToFileURL(path.join(dstDir, 'role-restrict.mjs')).href,
|
|
560
574
|
}),
|
|
561
|
-
])
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
575
|
+
], patchOpts)
|
|
576
|
+
if (patchOpts.layer === 'profile') reconcilePatchLayers(dshHome, patchOpts)
|
|
577
|
+
} else if (readHomePatchRows(dshHome, patchOpts).length > 0) {
|
|
578
|
+
stripHomePatchRows(dshHome, null, patchOpts)
|
|
579
|
+
warn(`${patchLayerTarget.patchPath} 检出 dsh-paoding 声明块,但当前宿主是目录扫描轨(DSH ≤ 0.1.6),已整块撤下(残留声明行会导致 profile 启动失败)`)
|
|
565
580
|
}
|
|
566
581
|
}
|
|
567
582
|
|