dsh-paoding 0.4.3 → 0.4.4
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 +3 -3
- package/README_en.md +3 -3
- package/docs/installation.md +4 -3
- package/docs/installation_en.md +4 -3
- package/package.json +1 -1
- package/plugins/paoding-config-ui/api-core.mjs +89 -22
- package/plugins/paoding-config-ui/package.json +1 -1
- package/presets/orchestrator/agent.cordis.yml +5 -2
- package/presets/orchestrator/role-restrict.mjs +78 -0
- package/tools/lib/compose.mjs +286 -2
- package/tools/lib/doctor.mjs +44 -4
- package/tools/lib/preset-system.mjs +193 -32
- package/tools/lib/state.mjs +18 -10
- package/tools/lib/util.mjs +4 -2
package/tools/lib/compose.mjs
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
* workflow worker 行按宿主版本生成期适配(adaptWorkflowWorkerRow:≥0.1.6 把
|
|
32
32
|
* 0.1.5-only 的 workflow-worker-thread 条目换成 workflow-ptc;探测不到版本
|
|
33
33
|
* 保守保留源行,产物零 diff 性质随之保留)。
|
|
34
|
+
* 角色委派行也按宿主版本生成期适配(adaptPresetRoleRows:≥0.2.1 把内联
|
|
35
|
+
* persona / toolFilter 的角色行瘦身为 agentPreset 选择子预设——dsh-tool-subagent
|
|
36
|
+
* 0.2.1 起 apply() 硬拒 persona / toolFilter 键——并追加 paoding-role-<toolName>
|
|
37
|
+
* AgentPreset 声明行(dsh-persona config.prefix 带角色人设 + ./role-restrict.mjs
|
|
38
|
+
* 行带工具面 allow);fork provider 不支持 agentPreset,fork 角色 persona/
|
|
39
|
+
* toolFilter 丢弃并 warn;< 0.2.1 / 探测不到版本保守保留源形状)。
|
|
34
40
|
* 自定义角色另有三道防护/自动化:toolName 保留名校验(assertCustomToolName,
|
|
35
41
|
* 内置角色 / restrictBase / subagent* / mcp__ 前缀 / YAML 字面量一律 throw);
|
|
36
42
|
* tools 为空时拒绝安装零工具角色(renderCustomRoleBlocks 直接 throw,防
|
|
@@ -44,7 +50,7 @@
|
|
|
44
50
|
import { DEFAULT_MAIN_AGENT_PERSONA_EXTRA, ROLES, warn } from './util.mjs'
|
|
45
51
|
import { normalizeModelRef, yamlScalar } from './config.mjs'
|
|
46
52
|
import { isHostDependent } from './host.mjs'
|
|
47
|
-
import { isPtcWorkerHost } from './preset-system.mjs'
|
|
53
|
+
import { isPresetRoleHost, isPtcWorkerHost } from './preset-system.mjs'
|
|
48
54
|
import { expandToolPatterns } from './glob.mjs'
|
|
49
55
|
|
|
50
56
|
// ── workflow worker 行的宿主版本适配 ─────────────────────────────────────────
|
|
@@ -83,6 +89,280 @@ export function adaptWorkflowWorkerRow(text, hostVersionTriple) {
|
|
|
83
89
|
return text.replace(WORKER_THREAD_BLOCK, WORKER_PTC_BLOCK)
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
// ── 角色委派行宿主版本适配(DSH 0.2.1+ AgentPreset 形态)────────────────────
|
|
93
|
+
|
|
94
|
+
/** 角色 AgentPreset 声明行的 id 前缀(paoding-role-<toolName>,与 deleg 行的
|
|
95
|
+
* agentPreset: 键互指)。前缀本身保证 id 是干净标量,toolName 无需再过引号。 */
|
|
96
|
+
export const ROLE_PRESET_ID_PREFIX = 'paoding-role-'
|
|
97
|
+
/** 角色 AgentPreset 的 order 分配:base + step × 序号,内置在前、自定义随后。 */
|
|
98
|
+
const ROLE_PRESET_BASE_ORDER = 100
|
|
99
|
+
const ROLE_PRESET_ORDER_STEP = 10
|
|
100
|
+
/** 角色面行(preset plugins 内)的本地插件名,buildPresetRowText 据此绝对化。 */
|
|
101
|
+
const ROLE_RESTRICT_ROW_NAME = './role-restrict.mjs'
|
|
102
|
+
|
|
103
|
+
/** 子预设声明行前的说明注释(生成产物自解释;风格照抄源模板段注释)。 */
|
|
104
|
+
const ROLE_PRESET_HEADER = [
|
|
105
|
+
'# ── 角色子预设(DSH 0.2.1+ agentPreset 形态)──────────────────────────────',
|
|
106
|
+
'# 0.2.1 起 dsh-tool-subagent 拒绝内联 persona / toolFilter(apply 时抛',
|
|
107
|
+
'# "persona is no longer supported"),角色人设与工具面改挂各角色的',
|
|
108
|
+
'# AgentPreset(spawn provider 支持 agentPreset 选择子预设)。显式选择时子',
|
|
109
|
+
'# agent 不继承父 preset、也不会自动获得 standard 基础工具,所以每个角色',
|
|
110
|
+
'# preset 的 plugins 是完整组合:standard 基础行(下方 shell/filesystem/jobs/',
|
|
111
|
+
'# skills/goals/plan/compaction 与 remaining 各节的工具注册行,与主 preset',
|
|
112
|
+
'# 逐字同源照抄)+ 该角色 persona + role-restrict 裁面——只放 persona 与',
|
|
113
|
+
'# restrict 的子预设会让子 agent 一个工具都没有:',
|
|
114
|
+
'# · standard 基础行 → 与 preset-orchestrator 同源,只注册不裁面;',
|
|
115
|
+
'# MCP 工具(tavily 等)由宿主全局注册,无需 preset 行,allow 引用即可',
|
|
116
|
+
'# · persona → dsh-persona 的 config.prefix(角色人设原样搬运)',
|
|
117
|
+
'# · role-restrict → role-restrict.mjs 在 system-prompt/assemble 瀑布上',
|
|
118
|
+
'# 无条件按 config.allow 裁剪子 agent 工具面(与主 agent restrict.mjs',
|
|
119
|
+
'# 语义相反:无 delegated-child 放行守卫;assemble 期全部名字已注册,',
|
|
120
|
+
'# fail-open;注册级不做 deny,role allow 含 skill 时技能目录注入守卫',
|
|
121
|
+
'# 才能放行)',
|
|
122
|
+
'# · fork provider 不支持 agentPreset——fork 角色的人设/工具面被丢弃,',
|
|
123
|
+
'# 子 agent 按宿主缺省组合启动(生成期已 warn)',
|
|
124
|
+
].join('\n')
|
|
125
|
+
|
|
126
|
+
/** 内置角色(含静态的 search_internal_deep)的缺省显示名(无 roles.<toolName>.name
|
|
127
|
+
* 配置时子预设的 name 落这里;自定义角色退到 persona 首行职责句)。 */
|
|
128
|
+
const ROLE_DISPLAY_NAME_DEFAULTS = {
|
|
129
|
+
search_external: '外部调研',
|
|
130
|
+
design: '设计',
|
|
131
|
+
implement: '实现',
|
|
132
|
+
search_internal_deep: '全仓深度探索',
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 剥掉 yamlScalar 落盘的成对引号('123' / "x" → 123 / x);其余原样返回。 */
|
|
136
|
+
function unquoteYamlScalar(text) {
|
|
137
|
+
const s = String(text ?? '').trim()
|
|
138
|
+
if (s.length >= 2 && ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"')))) {
|
|
139
|
+
return s.slice(1, -1)
|
|
140
|
+
}
|
|
141
|
+
return s
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 块标量内容区结束下标扫描(appendToMainPersona 的结构扫描原样复用):首内容行
|
|
146
|
+
* 定缩进,到第一条比内容缩进浅的非空行为止。
|
|
147
|
+
*/
|
|
148
|
+
function blockScalarEnd(text, contentStart) {
|
|
149
|
+
const firstLineEnd = text.indexOf('\n', contentStart)
|
|
150
|
+
const firstLine = text.slice(contentStart, firstLineEnd === -1 ? text.length : firstLineEnd)
|
|
151
|
+
const indent = (firstLine.match(/^[ \t]*/) ?? [''])[0].length
|
|
152
|
+
let cursor = contentStart
|
|
153
|
+
let end = contentStart
|
|
154
|
+
while (cursor < text.length) {
|
|
155
|
+
const lineEnd = text.indexOf('\n', cursor)
|
|
156
|
+
const line = text.slice(cursor, lineEnd === -1 ? text.length : lineEnd)
|
|
157
|
+
if (line.trim() !== '' && (line.match(/^[ \t]*/) ?? [''])[0].length < indent) break
|
|
158
|
+
end = lineEnd === -1 ? text.length : lineEnd + 1
|
|
159
|
+
cursor = end
|
|
160
|
+
}
|
|
161
|
+
return end
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 解析一条旧形(内联 persona / toolFilter)角色委派块:头部 provider / toolName、
|
|
166
|
+
* persona 块标量文本、toolFilter.allow 名单与瘦身 drop span [dropStart, dropEnd)
|
|
167
|
+
*(从 `persona:` 行行首到最后一条 allow 条目行行尾——恰是 0.2.1 形状下要删掉/替换
|
|
168
|
+
* 的整段)。块内无 persona 行(已是新形 / 已瘦身的 fork 块 / 非角色行)返回 null;
|
|
169
|
+
* 有 persona 而缺 toolName / allow 属模板漂移,硬错误(与其余 marker 缺失口径一致)。
|
|
170
|
+
*/
|
|
171
|
+
function parseInlineRoleBlock(text, itemStart, blockEnd) {
|
|
172
|
+
const personaMarker = ' persona: |-'
|
|
173
|
+
const personaKeyIdx = text.indexOf(personaMarker, itemStart)
|
|
174
|
+
if (personaKeyIdx === -1 || personaKeyIdx >= blockEnd) return null
|
|
175
|
+
const head = text.slice(itemStart, personaKeyIdx)
|
|
176
|
+
const toolNameM = /\n toolName: ([^\n]*)/.exec(head)
|
|
177
|
+
if (!toolNameM) throw new Error('角色委派块缺 toolName 行(0.2.1 角色行适配无法定位 8 空格缩进的 "toolName: ")')
|
|
178
|
+
const providerM = /\n provider: ([^\n]*)/.exec(head)
|
|
179
|
+
// persona 块标量:key 行之后 `|-` + 换行 + 缩进内容(缩进剥皮走 personaDefaultText)。
|
|
180
|
+
const dashStart = personaKeyIdx + personaMarker.length // 指向 `|-` 之后的换行
|
|
181
|
+
const contentStart = dashStart + 1
|
|
182
|
+
const personaEnd = blockScalarEnd(text, contentStart)
|
|
183
|
+
const personaText = personaDefaultText(text.slice(dashStart, personaEnd))
|
|
184
|
+
// toolFilter.allow:10 空格键行 + 12 空格条目行,扫到第一条非条目行为止
|
|
185
|
+
//(下一块的注释行 / 空行天然截断,drop span 不吞下一块内容)。
|
|
186
|
+
const allowKeyIdx = text.indexOf('\n allow:', personaEnd)
|
|
187
|
+
if (allowKeyIdx === -1 || allowKeyIdx >= blockEnd) {
|
|
188
|
+
throw new Error('角色委派块缺 toolFilter.allow(0.2.1 角色行适配要求内联 allow 可定位)')
|
|
189
|
+
}
|
|
190
|
+
const names = []
|
|
191
|
+
let cursor = allowKeyIdx + '\n allow:'.length + 1 // 首条目行行首
|
|
192
|
+
let end = cursor
|
|
193
|
+
while (cursor < text.length && cursor < blockEnd) {
|
|
194
|
+
const lineEnd = text.indexOf('\n', cursor)
|
|
195
|
+
const line = text.slice(cursor, lineEnd === -1 ? text.length : lineEnd)
|
|
196
|
+
const m = /^ - (.*)$/.exec(line)
|
|
197
|
+
if (!m) break
|
|
198
|
+
names.push(unquoteYamlScalar(m[1]))
|
|
199
|
+
end = lineEnd === -1 ? text.length : lineEnd + 1
|
|
200
|
+
cursor = end
|
|
201
|
+
}
|
|
202
|
+
if (names.length === 0) {
|
|
203
|
+
throw new Error('角色委派块的 toolFilter.allow 为空(生成器拒绝零工具角色,空名单属模板漂移)')
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
toolName: unquoteYamlScalar(toolNameM[1]),
|
|
207
|
+
provider: providerM ? unquoteYamlScalar(providerM[1]) : 'spawn',
|
|
208
|
+
personaText,
|
|
209
|
+
names,
|
|
210
|
+
dropStart: personaKeyIdx,
|
|
211
|
+
dropEnd: end,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 渲染一条角色 AgentPreset 声明行(agent.cordis.yml 顶层行形状,0 缩进起排):
|
|
216
|
+
* config.plugins = dsh-persona(config.prefix 带角色人设)+ role-restrict.mjs
|
|
217
|
+
* (config.allow 带角色工具面)+ standard 基础行(baseRowsBlock,已归一化并
|
|
218
|
+
* 预缩进到条目层级——0.2.1 显式选择 preset 时子 agent 不继承父 preset,角色
|
|
219
|
+
* preset 必须自带完整基础组合)。persona 文本渲染沿用 renderPersona(12 空格
|
|
220
|
+
* 内容缩进 = plugins 内 config.prefix 的块标量缩进)。 */
|
|
221
|
+
function renderRolePresetRow({ presetId, displayName, order, personaText, allowNames, baseRowsBlock }) {
|
|
222
|
+
return (
|
|
223
|
+
[
|
|
224
|
+
`- id: ${presetId}`,
|
|
225
|
+
` name: '@deepseek-ai/dsh-agent-preset'`,
|
|
226
|
+
' config:',
|
|
227
|
+
` id: ${presetId}`,
|
|
228
|
+
` name: ${yamlScalar(displayName)}`,
|
|
229
|
+
` order: ${order}`,
|
|
230
|
+
' plugins:',
|
|
231
|
+
' - id: persona',
|
|
232
|
+
` name: '@deepseek-ai/dsh-persona'`,
|
|
233
|
+
' config:',
|
|
234
|
+
' prefix: |-',
|
|
235
|
+
...String(personaText).split('\n').map((l) => (l === '' ? '' : ` ${l}`)),
|
|
236
|
+
' - id: role-restrict',
|
|
237
|
+
` name: ${ROLE_RESTRICT_ROW_NAME}`,
|
|
238
|
+
' config:',
|
|
239
|
+
' allow:',
|
|
240
|
+
...allowNames.map((n) => ` - ${yamlScalar(n)}`),
|
|
241
|
+
].join('\n') + '\n' + baseRowsBlock + '\n'
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 从生成产物抽取角色 preset 共用的 standard 基础行片段:shell 分节注释行 →
|
|
247
|
+
* 「delegation and workflows」分节注释行之前(工具注册 + planning/compaction
|
|
248
|
+
* 组),加 remaining-marker 注释行之后到文尾(ask-user / todo / web)。两段
|
|
249
|
+
* 都在委派层之外,恰好排除了编排层四行(persona / agent-instructions /
|
|
250
|
+
* orchestrator-restrict / delegation-labels)与 delegation 组(含 worker 行)。
|
|
251
|
+
* 分节注释缺失属模板漂移——只放 persona+restrict 的角色 preset 会让子 agent
|
|
252
|
+
* 零工具,硬错误优于静默产出废 preset。返回已归一(去首尾空行)并整体 +6
|
|
253
|
+
* 缩进到 plugins 条目层级的文本块(两段之间一个空行)。
|
|
254
|
+
*/
|
|
255
|
+
function extractRoleBaseRows(out) {
|
|
256
|
+
const shellIdx = out.indexOf('# ── shell')
|
|
257
|
+
const delegIdx = out.indexOf('# ── delegation and workflows')
|
|
258
|
+
const markerIdx = out.indexOf('# ── remaining model-facing rows')
|
|
259
|
+
if (shellIdx === -1 || delegIdx === -1 || markerIdx === -1 || delegIdx < shellIdx || markerIdx < delegIdx) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
'角色子预设 standard 基础行定位失败(模板漂移:缺 "# ── shell" / ' +
|
|
262
|
+
'"# ── delegation and workflows" / "# ── remaining model-facing rows" 分节注释)',
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
const slice2Start = out.indexOf('\n', markerIdx) + 1 // 跳过 marker 注释行本身
|
|
266
|
+
const normalize = (text) => {
|
|
267
|
+
const lines = text.replace(/\n+$/, '').split('\n')
|
|
268
|
+
while (lines.length > 0 && lines[0].trim() === '') lines.shift()
|
|
269
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
|
|
270
|
+
return lines.map((l) => (l === '' ? '' : ` ${l}`)).join('\n')
|
|
271
|
+
}
|
|
272
|
+
return `${normalize(out.slice(shellIdx, delegIdx))}\n\n${normalize(out.slice(slice2Start))}`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 角色委派行宿主版本适配(≥0.2.1 走 AgentPreset 形态,见 isPresetRoleHost):
|
|
277
|
+
* 1) 扫全部 4 空格缩进的 `- id: delegation-<rowId>` 行(内置三块 +
|
|
278
|
+
* search_internal_deep 静态块 + 自定义角色块;delegation-labels 等顶层行
|
|
279
|
+
* 缩进不匹配);块边界 = 下一行首 / '# ── remaining model-facing rows' / 文尾。
|
|
280
|
+
* 2) 逐块 parseInlineRoleBlock;无 persona 行的块(已适配 / 非旧形)原样跳过
|
|
281
|
+
* (幂等:重复调用零改动)。
|
|
282
|
+
* 3) 瘦身:块内 [persona 行首, allow 末条目行尾) 整段替换为
|
|
283
|
+
* ` agentPreset: paoding-role-<toolName>`(其余键 provider / toolName /
|
|
284
|
+
* agentOptions / backgroundMode 原样保留);fork provider 不支持 agentPreset
|
|
285
|
+
* (capabilities 仅 spawn 声明)→ 整段删除 + warn(0.2.1 守卫禁止内联回退)。
|
|
286
|
+
* 4) 非瘦身块逐个渲染成 paoding-role-<toolName> AgentPreset 声明行,插到
|
|
287
|
+
* '# ── remaining model-facing rows' 注释之前(自定义角色块之后,块间空行
|
|
288
|
+
* 风格与全文一致)。声明行 plugins 除 persona + role-restrict 外照抄
|
|
289
|
+
* standard 基础行(extractRoleBaseRows:0.2.1 显式选择 preset 时子 agent
|
|
290
|
+
* 不继承父 preset,角色 preset 必须是完整组合)。
|
|
291
|
+
* < 0.2.1 / 探测不到版本:原样返回(产物零 diff 不变量)。hostPrerelease 可选
|
|
292
|
+
*(0.2.1 的 prerelease 再分辨:守卫 0.2.1-rc.2 落地,rc.0/rc.1 保持内联形状,
|
|
293
|
+
* 见 isPresetRoleHost)。调用点在 composeGenerated 全部 edits 应用之后,与
|
|
294
|
+
* span 偏移零交互(同 adaptWorkflowWorkerRow)。
|
|
295
|
+
*/
|
|
296
|
+
export function adaptPresetRoleRows(text, assignments, hostVersionTriple, hostPrerelease) {
|
|
297
|
+
if (!isPresetRoleHost(hostVersionTriple, hostPrerelease)) return text
|
|
298
|
+
const rowRe = /^ - id: (delegation-\S+)[ \t]*$/gm
|
|
299
|
+
const starts = []
|
|
300
|
+
let m
|
|
301
|
+
while ((m = rowRe.exec(text)) !== null) starts.push(m.index)
|
|
302
|
+
if (starts.length === 0) return text
|
|
303
|
+
const tailIdx = text.indexOf('# ── remaining model-facing rows')
|
|
304
|
+
const blocks = []
|
|
305
|
+
for (let i = 0; i < starts.length; i++) {
|
|
306
|
+
const blockEnd = i + 1 < starts.length ? starts[i + 1] : tailIdx === -1 ? text.length : tailIdx
|
|
307
|
+
const parsed = parseInlineRoleBlock(text, starts[i], blockEnd)
|
|
308
|
+
if (parsed !== null) blocks.push(parsed)
|
|
309
|
+
}
|
|
310
|
+
if (blocks.length === 0) return text // 全部已是新形:幂等零改动
|
|
311
|
+
// 子预设顺序(order 分配 + 声明行落盘序):内置三角色(ROLES 序,在场的)→
|
|
312
|
+
// search_internal_deep 静态 → 自定义角色(文档序);同一块只出现一次。
|
|
313
|
+
const ordered = []
|
|
314
|
+
for (const roleName of [...ROLES, 'search_internal_deep']) {
|
|
315
|
+
const block = blocks.find((b) => b.toolName === roleName)
|
|
316
|
+
if (block !== undefined) ordered.push(block)
|
|
317
|
+
}
|
|
318
|
+
for (const block of blocks) {
|
|
319
|
+
if (!ordered.includes(block)) ordered.push(block)
|
|
320
|
+
}
|
|
321
|
+
const displayNameOf = (toolName, personaText) => {
|
|
322
|
+
const raw = assignments?.roles?.[toolName]?.name
|
|
323
|
+
if (typeof raw === 'string' && raw.trim() !== '') return raw.trim()
|
|
324
|
+
if (ROLE_DISPLAY_NAME_DEFAULTS[toolName] !== undefined) return ROLE_DISPLAY_NAME_DEFAULTS[toolName]
|
|
325
|
+
return firstLine(personaText)
|
|
326
|
+
}
|
|
327
|
+
const chunks = []
|
|
328
|
+
const dropEdits = []
|
|
329
|
+
// standard 基础行片段从原文提取即可:两段切片(shell→delegation 注释行之间、
|
|
330
|
+
// remaining marker→文尾)都在委派层之外,瘦身编辑不落在切片内。
|
|
331
|
+
const baseRowsBlock = extractRoleBaseRows(text)
|
|
332
|
+
for (let idx = 0; idx < ordered.length; idx++) {
|
|
333
|
+
const block = ordered[idx]
|
|
334
|
+
if (block.provider === 'fork') {
|
|
335
|
+
warn(
|
|
336
|
+
`roles.${block.toolName}: start_mode: 'fork' 不支持 agentPreset(仅 spawn provider 支持),` +
|
|
337
|
+
'0.2.1+ 宿主上该角色的 persona 与 toolFilter 已丢弃——子 agent 将按宿主缺省组合启动',
|
|
338
|
+
)
|
|
339
|
+
dropEdits.push({ start: block.dropStart, end: block.dropEnd, text: '' })
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
const presetId = `${ROLE_PRESET_ID_PREFIX}${block.toolName}`
|
|
343
|
+
const order = ROLE_PRESET_BASE_ORDER + ROLE_PRESET_ORDER_STEP * idx
|
|
344
|
+
chunks.push(
|
|
345
|
+
renderRolePresetRow({
|
|
346
|
+
presetId,
|
|
347
|
+
displayName: displayNameOf(block.toolName, block.personaText),
|
|
348
|
+
order,
|
|
349
|
+
personaText: block.personaText,
|
|
350
|
+
allowNames: block.names,
|
|
351
|
+
baseRowsBlock,
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
dropEdits.push({ start: block.dropStart, end: block.dropEnd, text: ` agentPreset: ${presetId}\n` })
|
|
355
|
+
}
|
|
356
|
+
let out = text
|
|
357
|
+
dropEdits.sort((a, b) => b.start - a.start)
|
|
358
|
+
for (const edit of dropEdits) out = out.slice(0, edit.start) + edit.text + out.slice(edit.end)
|
|
359
|
+
if (chunks.length === 0) return out
|
|
360
|
+
const insertAt = out.indexOf('# ── remaining model-facing rows')
|
|
361
|
+
if (insertAt === -1) throw new Error('remaining model-facing rows marker not found')
|
|
362
|
+
const presetBlock = ROLE_PRESET_HEADER + '\n\n' + chunks.join('\n')
|
|
363
|
+
return out.slice(0, insertAt) + presetBlock + '\n' + out.slice(insertAt)
|
|
364
|
+
}
|
|
365
|
+
|
|
86
366
|
// ── restrict.mjs main-agent allow base ──────────────────────────────────────
|
|
87
367
|
|
|
88
368
|
/**
|
|
@@ -733,7 +1013,7 @@ export function filterUsableTools(tools, presetUniverse, inventory) {
|
|
|
733
1013
|
* block / restrict config.allow injection) plus the per-role diff info for
|
|
734
1014
|
* the report.
|
|
735
1015
|
*/
|
|
736
|
-
export function composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas = {}, hostVersionTriple = null) {
|
|
1016
|
+
export function composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas = {}, hostVersionTriple = null, hostPrerelease = undefined) {
|
|
737
1017
|
const skillsMap = assignments.skills ?? {}
|
|
738
1018
|
const customToolNames = Object.keys(assignments.roles).filter((n) => !ROLES.includes(n))
|
|
739
1019
|
// roles_remove:被删除的内置角色不产 allow/persona edits(委派行整条删除时避免重叠),
|
|
@@ -1016,6 +1296,10 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
1016
1296
|
: String(personaExtraRaw)
|
|
1017
1297
|
if (personaExtra !== '') out = appendToMainPersona(out, '\n\n' + personaExtra)
|
|
1018
1298
|
|
|
1299
|
+
// 角色委派行宿主版本适配(≥0.2.1:内联 persona/toolFilter → agentPreset 子预设
|
|
1300
|
+
// + paoding-role-* 声明行;fork 角色丢弃并 warn;见 adaptPresetRoleRows)。
|
|
1301
|
+
out = adaptPresetRoleRows(out, assignments, hostVersionTriple, hostPrerelease)
|
|
1302
|
+
|
|
1019
1303
|
return { text: out, roleResults }
|
|
1020
1304
|
}
|
|
1021
1305
|
|
package/tools/lib/doctor.mjs
CHANGED
|
@@ -26,12 +26,14 @@ import { assignSlugs, presetIdOf } from './workspaces.mjs'
|
|
|
26
26
|
import {
|
|
27
27
|
MANAGED_BEGIN,
|
|
28
28
|
MANAGED_END,
|
|
29
|
-
|
|
29
|
+
detectAgentRolesFlavor,
|
|
30
|
+
detectHostVersionInfo,
|
|
30
31
|
detectPresetSystem,
|
|
31
32
|
isDeclarativeTriple,
|
|
32
33
|
isPtcWorkerHost,
|
|
33
34
|
listOrchestratorPresetIds,
|
|
34
35
|
parseGeneratorMarker,
|
|
36
|
+
presetRolesFlavorOf,
|
|
35
37
|
readHomePatchRows,
|
|
36
38
|
} from './preset-system.mjs'
|
|
37
39
|
import { healthCheck } from './health.mjs'
|
|
@@ -108,7 +110,9 @@ export async function runDoctor({ dshHome, configFile, profile = null, state = n
|
|
|
108
110
|
|
|
109
111
|
// ── 2. 预设安装轨道 vs 宿主版本 ───────────────────────────────────────────
|
|
110
112
|
const track = state?.presetSystem ?? (await detectPresetSystem({ dshHome }).catch(() => null))
|
|
111
|
-
const
|
|
113
|
+
const hostInfo = detectHostVersionInfo()
|
|
114
|
+
const hostTriple = hostInfo?.triple ?? null
|
|
115
|
+
const hostPrerelease = hostInfo?.prerelease ?? null
|
|
112
116
|
if (state === null && stateError !== null) {
|
|
113
117
|
add('skip', '预设安装轨道', `检测管线不可用(${stateError}),跳过轨道核对`)
|
|
114
118
|
} else if (hostTriple === null) {
|
|
@@ -175,9 +179,35 @@ export async function runDoctor({ dshHome, configFile, profile = null, state = n
|
|
|
175
179
|
} else {
|
|
176
180
|
add('warn', '工作区预设目录', `孤儿目录(配置 workspaces 已无对应条目):${orphans.join('、')}——下次「保存并应用」会自动回收,不用可手动删除`)
|
|
177
181
|
}
|
|
182
|
+
// 形态漂移(基础预设标记对账覆盖不到的暗角):工作区产物的角色行形态与宿主
|
|
183
|
+
// 所需不符时,挂该预设的新建会话会被宿主拒绝(0.2.1 守卫拒内联 persona /
|
|
184
|
+
// toolFilter;反方向降级同理)。启动自愈会自动换代,这里只报事实。宿主版本
|
|
185
|
+
// 探测不到不猜(与第 5 节 worker/角色行口径一致)。
|
|
186
|
+
if (hostTriple !== null && configUsable && dirs.length > 0) {
|
|
187
|
+
const wantedRoles = presetRolesFlavorOf(hostTriple, hostPrerelease)
|
|
188
|
+
const stale = []
|
|
189
|
+
for (const id of dirs) {
|
|
190
|
+
if (orphans.includes(id)) continue
|
|
191
|
+
try {
|
|
192
|
+
const flavor = detectAgentRolesFlavor(readFileSync(path.join(dshHome, '.agent-presets', id, 'agent.cordis.yml'), 'utf8'))
|
|
193
|
+
if (flavor !== null && flavor !== wantedRoles) stale.push(id)
|
|
194
|
+
} catch { /* 产物读不到:缺失/损坏归孤儿与漂移检查之外的异常,不在此判 */ }
|
|
195
|
+
}
|
|
196
|
+
if (stale.length > 0) {
|
|
197
|
+
add(
|
|
198
|
+
'warn',
|
|
199
|
+
'工作区预设目录',
|
|
200
|
+
`角色行形态与宿主所需 ${wantedRoles} 不符:${stale.join('、')}` +
|
|
201
|
+
(wantedRoles === 'agent-preset'
|
|
202
|
+
? '(DSH 0.2.1+ 拒绝内联 persona/toolFilter,挂这些预设的新建会话会报错)'
|
|
203
|
+
: '') +
|
|
204
|
+
'——重启 DSH 触发自愈自动换代,或对各工作区重新「保存并应用」',
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
// ── 5. 版本标记对账(.generator-version:版本 + 轨道 + worker
|
|
210
|
+
// ── 5. 版本标记对账(.generator-version:版本 + 轨道 + worker/角色形态)──────
|
|
181
211
|
const presetDir = path.join(dshHome, '.agent-presets', 'orchestrator')
|
|
182
212
|
if (!existsSync(presetDir)) {
|
|
183
213
|
add('skip', '版本标记', '基础预设未安装(无 orchestrator 目录):首装自动化或「保存并应用」会生成')
|
|
@@ -198,13 +228,23 @@ export async function runDoctor({ dshHome, configFile, profile = null, state = n
|
|
|
198
228
|
if (marker.track !== null && track !== null && marker.track !== track) {
|
|
199
229
|
parts.push(`标记轨道 ${TRACK_LABELS[marker.track] ?? marker.track} 与当前 ${trackLabel(track)} 不符(下次启动自动迁移)`)
|
|
200
230
|
}
|
|
201
|
-
// worker
|
|
231
|
+
// worker / 角色行形态只与探测得到的宿主版本比(探测不到不猜,保守不报)。
|
|
202
232
|
if (marker.worker !== null && hostTriple !== null) {
|
|
203
233
|
const workerNow = isPtcWorkerHost(hostTriple) ? 'ptc' : 'worker-thread'
|
|
204
234
|
if (marker.worker !== workerNow) {
|
|
205
235
|
parts.push(`标记 worker 形态 ${marker.worker} 与宿主所需 ${workerNow} 不符(下次启动整盘重生成)`)
|
|
206
236
|
}
|
|
207
237
|
}
|
|
238
|
+
if (marker.roles !== null && hostTriple !== null) {
|
|
239
|
+
const rolesNow = presetRolesFlavorOf(hostTriple, hostPrerelease)
|
|
240
|
+
if (marker.roles !== rolesNow) {
|
|
241
|
+
parts.push(
|
|
242
|
+
rolesNow === 'agent-preset'
|
|
243
|
+
? `标记角色行形态 ${marker.roles} 与宿主所需 agent-preset 不符(DSH 0.2.1+ 拒绝内联 persona/toolFilter,下次启动整盘重生成)`
|
|
244
|
+
: `标记角色行形态 ${marker.roles} 与宿主所需 inline 不符(下次启动整盘重生成)`,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
208
248
|
if (parts.length === 0) {
|
|
209
249
|
add('ok', '版本标记', `标记 ${marker.version}(${marker.track ?? '轨道未知'})与当前生成器一致`)
|
|
210
250
|
} else {
|