dsh-paoding 0.2.2 → 0.3.1
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 +27 -32
- package/README_en.md +27 -32
- package/bin/dsh-paoding.mjs +73 -11
- package/docs/architecture.md +41 -34
- package/docs/architecture_en.md +65 -42
- package/docs/configuration.md +159 -65
- package/docs/configuration_en.md +161 -66
- package/docs/images/1.png +0 -0
- package/docs/installation.md +100 -237
- package/docs/installation_en.md +112 -252
- package/docs/orchestration.md +34 -38
- package/docs/orchestration_en.md +34 -38
- package/package.json +8 -5
- package/plugins/paoding-config-ui/api-core.mjs +215 -18
- package/plugins/paoding-config-ui/index.mjs +414 -154
- package/plugins/paoding-config-ui/lib/base.css +1083 -42
- package/plugins/paoding-config-ui/lib/client.js +2266 -252
- package/plugins/paoding-config-ui/package.json +2 -2
- package/plugins/paoding-config-ui/runtime-inventory.mjs +27 -26
- package/presets/orchestrator/agent.cordis.yml +19 -9
- package/presets/orchestrator/preset.yml +3 -2
- package/presets/orchestrator/restrict.mjs +2 -1
- package/tools/install.mjs +9 -4
- package/tools/lib/alloc.mjs +60 -29
- package/tools/lib/cli.mjs +127 -278
- package/tools/lib/compose.mjs +261 -95
- package/tools/lib/config.mjs +266 -86
- package/tools/lib/host.mjs +55 -24
- package/tools/lib/skills.mjs +72 -45
- package/tools/lib/state.mjs +172 -40
- package/tools/lib/upgrade.mjs +170 -0
- package/tools/lib/util.mjs +7 -0
- package/tools/lib/version.mjs +233 -0
- package/tools/lib/workspaces.mjs +74 -0
- package/tools/lib/yaml.mjs +92 -15
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6.png +0 -0
- package/install.sh +0 -31
- package/tools/lib/wizard.mjs +0 -516
package/tools/lib/compose.mjs
CHANGED
|
@@ -6,19 +6,28 @@
|
|
|
6
6
|
* (renderCustomRoleBlocks / firstLine / insertCustomRoles)、roles_remove 委派行
|
|
7
7
|
* span 删除(delegationRowSpan / composeMainPersonaEdit)、角色专用模型注入
|
|
8
8
|
* (injectRoleAgentOptions,roles.<toolName>.model / .provider → 内置角色委派块
|
|
9
|
-
* 的 agentOptions
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* 的 agentOptions 子块)、角色可续模式改写/兜底注入(injectRoleBackgroundMode,
|
|
10
|
+
* roles.<toolName>.background_mode = 'continuable' → 内置角色委派块的
|
|
11
|
+
* backgroundMode 行:源模板已预置该行时原位改写其值,块内无该行时在 toolName
|
|
12
|
+
* 行行尾兜底注入;'one-shot' 不动源行 —— 源模板四块各预置
|
|
13
|
+
* `backgroundMode: one-shot`(可见性),全默认产物与源零 diff)、
|
|
14
|
+
* restrict config.allow 注入(computeRestrictAllow /
|
|
15
|
+
* injectRestrictAllow / filterUsableTools)与整文 compose(composeGenerated)。
|
|
16
|
+
* allow 过滤走白名单语义(usableWith / presetUniverse ∪ inventory,见
|
|
17
|
+
* filterUsableTools 上方说明):手写进配置、既不在 preset 自带工具面也不在
|
|
18
|
+
* 检测库存的名字一律从 allow 丢弃,不再「非 host 依赖名直通」。
|
|
19
|
+
* 自定义角色另有三道防护/自动化:toolName 保留名校验(assertCustomToolName,
|
|
20
|
+
* 内置角色 / restrictBase / subagent* / mcp__ 前缀 / YAML 字面量一律 throw);
|
|
21
|
+
* tools 为空时拒绝安装零工具角色(renderCustomRoleBlocks 直接 throw,防
|
|
22
|
+
* `toolFilter.allow:` 空白名单);并把角色 persona 首行职责句自动追加为主
|
|
23
|
+
* agent persona 委派行(composeMainPersonaEdit 第 4 参 customBullets →
|
|
24
|
+
* `- <职责>: delegate to <toolName>.`),否则主 agent 永远不会委派给自定义角色。
|
|
16
25
|
* 依赖 util(ROLES / warn / DEFAULT_MAIN_AGENT_PERSONA_EXTRA)、config
|
|
17
|
-
* (
|
|
18
|
-
* (isHostDependent);被
|
|
26
|
+
* (normalizeModelRef / yamlScalar)与 host
|
|
27
|
+
* (isHostDependent);被 state 引用。
|
|
19
28
|
*/
|
|
20
29
|
import { DEFAULT_MAIN_AGENT_PERSONA_EXTRA, ROLES, warn } from './util.mjs'
|
|
21
|
-
import {
|
|
30
|
+
import { normalizeModelRef, yamlScalar } from './config.mjs'
|
|
22
31
|
import { isHostDependent } from './host.mjs'
|
|
23
32
|
|
|
24
33
|
// ── restrict.mjs main-agent allow base ──────────────────────────────────────
|
|
@@ -31,11 +40,62 @@ import { isHostDependent } from './host.mjs'
|
|
|
31
40
|
export function extractMainAgentAllow(restrictSrc) {
|
|
32
41
|
const match = restrictSrc.match(/const MAIN_AGENT_ALLOW = new Set\(\[([\s\S]*?)\]\)/)
|
|
33
42
|
if (!match) throw new Error('MAIN_AGENT_ALLOW block not found in restrict.mjs')
|
|
34
|
-
|
|
43
|
+
// 先按行剥 // 注释再提取引号串:注释里的撇号(如 don't)会被当成工具名的
|
|
44
|
+
// 引号对,产生幽灵工具名(allow 段常带中英文注释)。
|
|
45
|
+
const body = match[1]
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map((line) => line.replace(/\/\/.*$/, ''))
|
|
48
|
+
.join('\n')
|
|
49
|
+
const names = [...body.matchAll(/'([^']+)'/g)].map((m) => m[1])
|
|
35
50
|
if (names.length === 0) throw new Error('MAIN_AGENT_ALLOW block is empty in restrict.mjs')
|
|
36
51
|
return names
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
// ── shared helpers ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** 名字可用性判定(allow 过滤多处共用,白名单语义):名字必须落在 preset 自带
|
|
57
|
+
* 工具面(presetUniverse = restrict.mjs 主 agent 白名单 ∪ 源 preset 各角色 allow)
|
|
58
|
+
* 或检测清单 inventory 之中,两者都不在 → 不可用(插件停用后的 mnemon_*、手写
|
|
59
|
+
* 错名等不再「非 host 依赖名直通」,与文档「配置意图 ∩ 实际启用工具」口径对齐);
|
|
60
|
+
* host 依赖名(mcp__* / 已知插件工具)即便在 presetUniverse 里也必须真的被检测
|
|
61
|
+
* 到才保留。 */
|
|
62
|
+
function usableWith(name, presetUniverse, inventory) {
|
|
63
|
+
if (inventory.has(name)) return true
|
|
64
|
+
return !isHostDependent(name) && presetUniverse.has(name)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 角色专用模型归一(自定义块渲染与内置角色注入两处共用):model / provider 过
|
|
68
|
+
* normalizeModelRef;仅 model 非 null 才有效,provider-alone warn 后置 null。 */
|
|
69
|
+
function normalizeRoleModel(toolName, rawModel, rawProvider) {
|
|
70
|
+
const model = normalizeModelRef(rawModel, `roles.${toolName}.model`)
|
|
71
|
+
let provider = normalizeModelRef(rawProvider, `roles.${toolName}.provider`)
|
|
72
|
+
if (model === null && provider !== null) {
|
|
73
|
+
warn(`roles.${toolName}.provider: '${provider}' 仅在同时配置 model 时生效,已忽略`)
|
|
74
|
+
provider = null
|
|
75
|
+
}
|
|
76
|
+
return { model, provider }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 自定义角色 toolName 保留名校验:与内置角色(ROLES)、restrict.mjs 主 agent
|
|
81
|
+
* allow 白名单(restrictBase)、顶层 subagent / subagent_fork 撞名的 toolName
|
|
82
|
+
* 会静默遮蔽既有工具面;mcp__ 前缀闯 host MCP 工具名空间;true/false/null 等
|
|
83
|
+
* YAML 字面量会让生成的 `toolName: <名>` 行解析成布尔/null —— 一律在生成期
|
|
84
|
+
* throw(错误信息带角色名),宁可拒绝安装也不静默产出坏 preset。
|
|
85
|
+
*/
|
|
86
|
+
export function assertCustomToolName(toolName, restrictBase = []) {
|
|
87
|
+
const reserved = new Set([...ROLES, ...restrictBase, 'subagent', 'subagent_fork'])
|
|
88
|
+
if (reserved.has(toolName)) {
|
|
89
|
+
throw new Error(`role "${toolName}": toolName 与保留名冲突(内置角色 / restrict 主 agent 白名单 / subagent*),请换一个名字`)
|
|
90
|
+
}
|
|
91
|
+
if (String(toolName).startsWith('mcp__')) {
|
|
92
|
+
throw new Error(`role "${toolName}": toolName 不得使用 mcp__ 前缀(与 host MCP 工具名空间冲突)`)
|
|
93
|
+
}
|
|
94
|
+
if (/^(true|false|null|~)$/i.test(toolName)) {
|
|
95
|
+
throw new Error(`role "${toolName}": toolName 是 YAML 字面量,生成的委派行会被解析成布尔/null,请换一个名字`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
39
99
|
/** Render a persona block scalar with a given content indent. */
|
|
40
100
|
export function renderPersona(text, indent) {
|
|
41
101
|
const pad = ' '.repeat(indent)
|
|
@@ -85,14 +145,16 @@ export function replaceRoleIdentity(text, displayName) {
|
|
|
85
145
|
* Render the delegation block(s) for custom roles, matching the existing
|
|
86
146
|
* delegation-* indentation (4-space list items).
|
|
87
147
|
*/
|
|
88
|
-
export function renderCustomRoleBlocks(roles, skillsMap) {
|
|
148
|
+
export function renderCustomRoleBlocks(roles, skillsMap, restrictBase = []) {
|
|
89
149
|
const custom = Object.entries(roles).filter(([toolName]) => !ROLES.includes(toolName))
|
|
90
150
|
if (custom.length === 0) return ''
|
|
91
151
|
const chunks = []
|
|
92
152
|
for (const [toolName, role] of custom) {
|
|
93
|
-
// 零工具防护(与 composeGenerated
|
|
94
|
-
// `tools:` 空列表)会生成
|
|
95
|
-
//
|
|
153
|
+
// 保留名校验(见 assertCustomToolName)+ 零工具防护(与 composeGenerated
|
|
154
|
+
// 内置角色循环同款):tools 为空(缺省或 `tools:` 空列表)会生成
|
|
155
|
+
// `toolFilter.allow:`(YAML null),子 agent 创建后零工具——宁可拒绝安装,
|
|
156
|
+
// 也不静默生成一个干不了活的角色。
|
|
157
|
+
assertCustomToolName(toolName, restrictBase)
|
|
96
158
|
if ((role.tools ?? []).length === 0) {
|
|
97
159
|
throw new Error(`role "${toolName}" would end up with an empty toolFilter.allow — refusing to install a zero-tool role.`)
|
|
98
160
|
}
|
|
@@ -101,16 +163,14 @@ export function renderCustomRoleBlocks(roles, skillsMap) {
|
|
|
101
163
|
toolName,
|
|
102
164
|
skillsMap,
|
|
103
165
|
)
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
provider = null
|
|
113
|
-
}
|
|
166
|
+
// 角色专用模型与可续模式(roles.<toolName>.model / .provider / .background_mode):
|
|
167
|
+
// 自定义角色与内置角色同形状(provider: spawn / toolName / agentOptions? /
|
|
168
|
+
// backgroundMode / persona / toolFilter)。先归一再判 provider-alone(防 UI /
|
|
169
|
+
// 手工构造的 assignments 绕过规范化);仅 model 非 null 才插 agentOptions 子块,
|
|
170
|
+
// provider-alone 仅 warn 忽略。backgroundMode 行恒写(与内置块源模板预置口径
|
|
171
|
+
// 一致:行始终可见),continuable → continuable,one-shot/缺省/异常值 →
|
|
172
|
+
// one-shot(与 config 解析回落口径一致)。
|
|
173
|
+
const { model, provider } = normalizeRoleModel(toolName, role.model ?? null, role.provider ?? null)
|
|
114
174
|
// agentOptions 子行(键序固定 provider 在前、model 在后,与 dsh-tool-subagent
|
|
115
175
|
// Config 字段序一致);model 缺省时整块不出现。
|
|
116
176
|
const agentOptionsLines = []
|
|
@@ -119,6 +179,9 @@ export function renderCustomRoleBlocks(roles, skillsMap) {
|
|
|
119
179
|
if (provider !== null) agentOptionsLines.push(` provider: ${yamlScalar(provider)}`)
|
|
120
180
|
agentOptionsLines.push(` model: ${yamlScalar(model)}`)
|
|
121
181
|
}
|
|
182
|
+
// backgroundMode 行恒写(键序固定在 agentOptions? 之后、persona 之前,与内置
|
|
183
|
+
// 角色委派块的预置/注入落点键序一致);非 continuable 一律落 one-shot。
|
|
184
|
+
const backgroundModeLines = [` backgroundMode: ${role.background_mode === 'continuable' ? 'continuable' : 'one-shot'}`]
|
|
122
185
|
chunks.push(
|
|
123
186
|
[
|
|
124
187
|
` # ${toolName}:自定义角色(${firstLine(role.persona || '')})`,
|
|
@@ -126,13 +189,16 @@ export function renderCustomRoleBlocks(roles, skillsMap) {
|
|
|
126
189
|
` name: '@deepseek-ai/dsh-tool-subagent'`,
|
|
127
190
|
' config:',
|
|
128
191
|
' provider: spawn',
|
|
129
|
-
|
|
192
|
+
// toolName / 工具条目过 yamlScalar:纯数字、YAML 字面量形态等歧义名
|
|
193
|
+
// 必须带引号落盘,否则委派行会被解析成数字/布尔/null。
|
|
194
|
+
` toolName: ${yamlScalar(toolName)}`,
|
|
130
195
|
...agentOptionsLines,
|
|
196
|
+
...backgroundModeLines,
|
|
131
197
|
' persona: |-',
|
|
132
198
|
...persona.split('\n').map((l) => ` ${l}`),
|
|
133
199
|
' toolFilter:',
|
|
134
200
|
' allow:',
|
|
135
|
-
...(role.tools ?? []).map((t) => ` - ${t}`),
|
|
201
|
+
...(role.tools ?? []).map((t) => ` - ${yamlScalar(t)}`),
|
|
136
202
|
].join('\n'),
|
|
137
203
|
)
|
|
138
204
|
}
|
|
@@ -146,21 +212,21 @@ export function firstLine(text) {
|
|
|
146
212
|
|
|
147
213
|
/**
|
|
148
214
|
* Compute the orchestrator-restrict config.allow: restrict.mjs base, minus
|
|
149
|
-
*
|
|
150
|
-
* role toolNames, minus main_agent_remove entries and
|
|
151
|
-
* toolNames (roles_remove). Returns null when the result
|
|
152
|
-
* injection needed) — but only when no builtin role was
|
|
153
|
-
* roles_remove is non-empty the explicit allow MUST be injected,
|
|
154
|
-
* static restrict.mjs MAIN_AGENT_ALLOW would keep delegating to
|
|
155
|
-
* role (删除不生效).
|
|
215
|
+
* names outside the preset universe and detection inventory, plus main-agent
|
|
216
|
+
* extra tools and custom role toolNames, minus main_agent_remove entries and
|
|
217
|
+
* removed builtin role toolNames (roles_remove). Returns null when the result
|
|
218
|
+
* equals the base (no injection needed) — but only when no builtin role was
|
|
219
|
+
* removed: once roles_remove is non-empty the explicit allow MUST be injected,
|
|
220
|
+
* otherwise the static restrict.mjs MAIN_AGENT_ALLOW would keep delegating to
|
|
221
|
+
* the deleted role (删除不生效).
|
|
156
222
|
*/
|
|
157
|
-
export function computeRestrictAllow(base, inventory, mainAgentExtra, mainAgentRemove, customToolNames, removedRoleNames = []) {
|
|
223
|
+
export function computeRestrictAllow(base, presetUniverse, inventory, mainAgentExtra, mainAgentRemove, customToolNames, removedRoleNames = []) {
|
|
158
224
|
const kept = []
|
|
159
225
|
for (const name of base) {
|
|
160
|
-
if (
|
|
226
|
+
if (usableWith(name, presetUniverse, inventory)) kept.push(name)
|
|
161
227
|
}
|
|
162
228
|
for (const name of mainAgentExtra) {
|
|
163
|
-
if (!kept.includes(name) && (
|
|
229
|
+
if (!kept.includes(name) && usableWith(name, presetUniverse, inventory)) kept.push(name)
|
|
164
230
|
}
|
|
165
231
|
for (const name of customToolNames) {
|
|
166
232
|
if (!kept.includes(name)) kept.push(name)
|
|
@@ -187,7 +253,8 @@ export function injectRestrictAllow(srcText, allowList) {
|
|
|
187
253
|
* used to inline the selected skills' full SKILL.md bodies (composeGenerated,
|
|
188
254
|
* the main-agent skills step) and to append the user-editable persona tail
|
|
189
255
|
* (main_agent_persona_extra). Locates the block structurally (same scan as
|
|
190
|
-
* locatePersonaBlocks' regex branch): marker row → `
|
|
256
|
+
* locatePersonaBlocks' regex branch): marker row → `prefix: |-` (legacy
|
|
257
|
+
* `text: |-`) → first content
|
|
191
258
|
* line sets the indent (6 in the source preset) → block ends at the first
|
|
192
259
|
* non-empty line indented shallower than the content. Replaces the span
|
|
193
260
|
* [dashStart, end) with the original content + suffixText re-rendered as a
|
|
@@ -197,9 +264,17 @@ export function appendToMainPersona(srcText, suffixText) {
|
|
|
197
264
|
const marker = `- id: persona\n name: '@deepseek-ai/dsh-persona'`
|
|
198
265
|
const idx = srcText.indexOf(marker)
|
|
199
266
|
if (idx === -1) throw new Error('main agent persona block (`- id: persona` / @deepseek-ai/dsh-persona) not found')
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
267
|
+
// Persona config scalar: `prefix: |-` per the current dsh-persona schema
|
|
268
|
+
// (required `prefix`); `text: |-` is the legacy pre-rename name, still
|
|
269
|
+
// located so old files compose unchanged.
|
|
270
|
+
const prefixStart = srcText.indexOf('prefix: |-', idx)
|
|
271
|
+
const textStart = prefixStart === -1 ? srcText.indexOf('text: |-', idx) : -1
|
|
272
|
+
if (prefixStart === -1 && textStart === -1) {
|
|
273
|
+
throw new Error('main agent persona block: `prefix: |-` (or legacy `text: |-`) scalar not found')
|
|
274
|
+
}
|
|
275
|
+
const keyStart = prefixStart !== -1 ? prefixStart : textStart
|
|
276
|
+
const keyLen = prefixStart !== -1 ? 'prefix: '.length : 'text: '.length
|
|
277
|
+
const dashStart = keyStart + keyLen // points at the `|` of `|-`
|
|
203
278
|
const contentStart = dashStart + '|-'.length + 1
|
|
204
279
|
const firstLineEnd = srcText.indexOf('\n', contentStart)
|
|
205
280
|
const firstLine = srcText.slice(contentStart, firstLineEnd === -1 ? srcText.length : firstLineEnd)
|
|
@@ -223,7 +298,9 @@ export function insertCustomRoles(srcText, customBlock) {
|
|
|
223
298
|
const marker = '# ── remaining model-facing rows'
|
|
224
299
|
const idx = srcText.indexOf(marker)
|
|
225
300
|
if (idx === -1) throw new Error('remaining model-facing rows marker not found')
|
|
226
|
-
|
|
301
|
+
// 自定义块与 marker 注释之间补一个空行,与全文「块间空行分隔」的风格一致
|
|
302
|
+
// (customBlock 自带行尾换行;marker 前原有的空行留在 customBlock 之前)。
|
|
303
|
+
return srcText.slice(0, idx) + customBlock + '\n' + srcText.slice(idx)
|
|
227
304
|
}
|
|
228
305
|
|
|
229
306
|
/**
|
|
@@ -280,22 +357,17 @@ export function delegationRowSpan(srcText, role) {
|
|
|
280
357
|
}
|
|
281
358
|
|
|
282
359
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* 零宽 span edit { start, end, text }(start = end = toolName 行行尾换行符下标;
|
|
286
|
-
* 原换行符保留在 text 之后,故 text 不带尾换行)。子块键序固定 provider 在前、
|
|
287
|
-
* model 在后(与 dsh-tool-subagent Config 字段序一致),provider 缺省时不出该行。
|
|
288
|
-
* 块定位:`- id: delegation-<rowId>`(rowId = 角色名 `_` 换 `-`,找不到 throw);
|
|
360
|
+
* 内置角色委派块边界定位(roleToolNameLineEnd / injectRoleBackgroundMode 共用):
|
|
361
|
+
* 块定位 `- id: delegation-<rowId>`(rowId = 角色名 `_` 换 `-`,找不到 throw);
|
|
289
362
|
* 块结束 = min(其后首个 `\n - id: delegation-` 下标, '# ── remaining
|
|
290
|
-
* model-facing rows' 注释下标, srcText.length)
|
|
291
|
-
*
|
|
292
|
-
* 应用。
|
|
363
|
+
* model-facing rows' 注释下标, srcText.length),保证块内行搜索不越界命中
|
|
364
|
+
* 别的块。errorKey 用于把报错归因到具体配置键(model / background_mode)。
|
|
293
365
|
*/
|
|
294
|
-
|
|
366
|
+
function roleDelegationBlockBounds(srcText, role, errorKey) {
|
|
295
367
|
const rowId = role.replace(/_/g, '-') // 委派行 id 用连字符(toolName 用下划线)
|
|
296
368
|
const marker = `- id: delegation-${rowId}`
|
|
297
369
|
const idx = srcText.indexOf(marker)
|
|
298
|
-
if (idx === -1) throw new Error(`委派行缺失(roles.${role}
|
|
370
|
+
if (idx === -1) throw new Error(`委派行缺失(roles.${role}.${errorKey} 引用了 '${role}',但源文本里找不到 "${marker}")`)
|
|
299
371
|
const nextIdx = srcText.indexOf('\n - id: delegation-', idx + marker.length)
|
|
300
372
|
const tailIdx = srcText.indexOf('# ── remaining model-facing rows')
|
|
301
373
|
// 块结束取三者最小(无下一条/无尾注释时以 srcText.length 兜底)。
|
|
@@ -304,13 +376,41 @@ export function injectRoleAgentOptions(srcText, role, model, provider) {
|
|
|
304
376
|
tailIdx === -1 ? srcText.length : tailIdx,
|
|
305
377
|
srcText.length,
|
|
306
378
|
)
|
|
379
|
+
return { idx, blockEnd }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 定位内置角色委派块内 8 空格缩进的 `toolName:` 行行尾换行符下标
|
|
384
|
+
* (injectRoleAgentOptions / injectRoleBackgroundMode 兜底注入共用的插入点):
|
|
385
|
+
* 块边界见 roleDelegationBlockBounds,toolName 搜索限定块内不越界命中别的块。
|
|
386
|
+
* errorKey 用于把报错归因到具体配置键(model / background_mode)。
|
|
387
|
+
*/
|
|
388
|
+
function roleToolNameLineEnd(srcText, role, errorKey) {
|
|
389
|
+
const { idx, blockEnd } = roleDelegationBlockBounds(srcText, role, errorKey)
|
|
307
390
|
const toolNameMarker = '\n toolName: ' // 8 空格缩进 = config 键层
|
|
308
391
|
const toolNameIdx = srcText.indexOf(toolNameMarker, idx)
|
|
309
392
|
if (toolNameIdx === -1 || toolNameIdx >= blockEnd) {
|
|
310
|
-
throw new Error(`委派块缺 toolName 行(roles.${role}
|
|
393
|
+
throw new Error(`委派块缺 toolName 行(roles.${role}.${errorKey}:"${marker}" 块内找不到 8 空格缩进的 "toolName: " 行)`)
|
|
311
394
|
}
|
|
312
395
|
// 插入点 = toolName 行行尾换行符的下标;零宽插入后该换行符仍在 text 之后。
|
|
313
396
|
const lineEnd = srcText.indexOf('\n', toolNameIdx + 1)
|
|
397
|
+
if (lineEnd === -1) {
|
|
398
|
+
// 与其余 marker 缺失同款哨兵:不静默错位(-1 会把注入行插到文首)。
|
|
399
|
+
throw new Error(`委派块 toolName 行缺换行符(roles.${role}.${errorKey}:"${marker}" 块的 toolName 行是最后一行且无换行,无法定位插入点)`)
|
|
400
|
+
}
|
|
401
|
+
return lineEnd
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 角色专用模型注入(roles.<toolName>.model / .provider 生效路径):在 role(内置
|
|
406
|
+
* 角色名,下划线)的既有委派块内、`toolName:` 行行尾插入 agentOptions 子块,返回
|
|
407
|
+
* 零宽 span edit { start, end, text }(start = end = toolName 行行尾换行符下标;
|
|
408
|
+
* 原换行符保留在 text 之后,故 text 不带尾换行)。子块键序固定 provider 在前、
|
|
409
|
+
* model 在后(与 dsh-tool-subagent Config 字段序一致),provider 缺省时不出该行。
|
|
410
|
+
* 返回坐标为源文本原坐标,与 allow/persona/删除 edits 一起按 start 降序应用。
|
|
411
|
+
*/
|
|
412
|
+
export function injectRoleAgentOptions(srcText, role, model, provider) {
|
|
413
|
+
const lineEnd = roleToolNameLineEnd(srcText, role, 'model')
|
|
314
414
|
const text =
|
|
315
415
|
'\n' +
|
|
316
416
|
[
|
|
@@ -322,30 +422,81 @@ export function injectRoleAgentOptions(srcText, role, model, provider) {
|
|
|
322
422
|
}
|
|
323
423
|
|
|
324
424
|
/**
|
|
325
|
-
*
|
|
425
|
+
* 角色可续模式改写/兜底注入(roles.<toolName>.background_mode = 'continuable'
|
|
426
|
+
* 生效路径)。源模板的四个内置角色委派块各预置一行 `backgroundMode: one-shot`
|
|
427
|
+
* (产品口径:旋钮在源里可见,值 = dsh-tool-subagent 缺省),本函数语义因此是
|
|
428
|
+
* 「改写或兜底注入」,块内 span 查找既有行,杜绝任何情况下同块出现两行
|
|
429
|
+
* backgroundMode:
|
|
430
|
+
* - 块内已有该行(源模板预置 / 手工加过):整行原位改写为规范的
|
|
431
|
+
* `backgroundMode: continuable`(位置不动 → 键序 toolName → agentOptions? →
|
|
432
|
+
* backgroundMode 天然成立);该行值已是 continuable 时返回 null(无改动,
|
|
433
|
+
* 与其它「无差异不产 edit」口径一致)。
|
|
434
|
+
* - 块内无该行(防手工删掉的老配置/异常源):兜底在 `toolName:` 行行尾零宽
|
|
435
|
+
* 注入一行(与 injectRoleAgentOptions 同锚点,两者并存靠收集序定键序 ——
|
|
436
|
+
* edits 按 start 降序稳定排序,同 start 保持收集序,composeGenerated 先收集
|
|
437
|
+
* 本 edit 再收集 agentOptions edit,先收集者居后)。
|
|
438
|
+
* 返回 span edit { start, end, text }(坐标为源文本原坐标,与 allow/persona/
|
|
439
|
+
* 删除 edits 一起按 start 降序应用;改写路径 start/end = 既有行行首/行尾,
|
|
440
|
+
* 兜底路径 start = end = toolName 行行尾换行符下标),无任何改动时返回 null。
|
|
441
|
+
* 调用方(composeGenerated)仅对 'continuable' 调本函数;'one-shot'/缺省不调
|
|
442
|
+
* 用 —— 源预置行原样保留,全默认配置的生成产物与源逐字节一致。手工构造的
|
|
443
|
+
* assignments 绕过规范化时按同口径兜底(非 continuable 即不调用)。
|
|
444
|
+
*/
|
|
445
|
+
export function injectRoleBackgroundMode(srcText, role) {
|
|
446
|
+
const { idx, blockEnd } = roleDelegationBlockBounds(srcText, role, 'background_mode')
|
|
447
|
+
// 块内 span 查既有行:8 空格缩进(config 键层,与 roleToolNameLineEnd 的
|
|
448
|
+
// toolName 定位同口径),行首带 \n 保证不误匹配块首部分行或 persona 内容行。
|
|
449
|
+
const bgMarker = '\n backgroundMode:'
|
|
450
|
+
const bgIdx = srcText.indexOf(bgMarker, idx)
|
|
451
|
+
if (bgIdx !== -1 && bgIdx < blockEnd) {
|
|
452
|
+
const lineStart = bgIdx + 1 // 跳过行首 \n,定位到行首
|
|
453
|
+
const nextNl = srcText.indexOf('\n', lineStart)
|
|
454
|
+
const lineEnd = nextNl === -1 ? srcText.length : nextNl
|
|
455
|
+
const line = srcText.slice(lineStart, lineEnd)
|
|
456
|
+
const value = line.slice(line.indexOf(':') + 1).trim()
|
|
457
|
+
if (value === 'continuable') return null // 已是目标值:不动(保持零 diff)
|
|
458
|
+
// 整行原位改写(缩进一并规范成 8 空格),不新增行 → 不可能重复键。
|
|
459
|
+
return { start: lineStart, end: lineEnd, text: ' backgroundMode: continuable' }
|
|
460
|
+
}
|
|
461
|
+
// 兜底:块内无该行(手工删掉的老配置 / 异常源)→ 沿用 toolName 行行尾注入。
|
|
462
|
+
const lineEnd = roleToolNameLineEnd(srcText, role, 'background_mode')
|
|
463
|
+
return { start: lineEnd, end: lineEnd, text: '\n backgroundMode: continuable' }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 主 persona 整块改造(删委派 bullet + 自定义角色自动委派行),返回一个
|
|
326
468
|
* span edit { start, end, text }(start/end 为源文本原坐标、随其它 edits 降序应用),
|
|
327
469
|
* 无任何改动时返回 null(产物零 diff)。结构扫描与 appendToMainPersona 相同:
|
|
328
|
-
* `- id: persona` 行 → `text: |-`
|
|
329
|
-
* - 改名:main_agent_name 非空时,把身份行 label 换成它(正则不中则 warn 不崩);
|
|
470
|
+
* `- id: persona` 行 → `prefix: |-`(旧名 `text: |-` 兼容)→ 首内容行定缩进 → 浅缩进行收尾。
|
|
330
471
|
* - 删 bullet:对每个 removedRoles 角色,删掉 content 中 trimmed 匹配
|
|
331
472
|
* `^- .*delegate to <role>\.?$` 的行(限定行首 `- `,不误删 SOP 段落);
|
|
332
|
-
* - 自定义角色自动委派行:customBullets(第
|
|
473
|
+
* - 自定义角色自动委派行:customBullets(第 3 参,元素 { toolName, duty })逐个
|
|
333
474
|
* 渲染成 `- <duty>: delegate to <toolName>.`(duty 去掉尾部 `。`/`.`/`!`/`!`
|
|
334
475
|
* 与空白),插到 body 中最后一个匹配 `^- .*delegate to \S+\.?$` 的行之后;
|
|
335
476
|
* 一条都没有时退到匹配 `^Decompose the task` 的行之后;仍没有则退到首行之后。
|
|
336
477
|
* 其余文本与空行结构保持不变——没有这行指引,主 agent persona 就不含自定义
|
|
337
478
|
* 角色,主 agent 永远不会委派给它。
|
|
479
|
+
* 身份行(You are the …)不可改名:编排主 agent 的 persona 身份语义不属于
|
|
480
|
+
* 可配置项(preset 显示名归 main_agent_display_name,见 state.mjs)。
|
|
338
481
|
* 返回值经 renderPersona 以块标量整体回填;技能行/extra 追加在 edits 应用后由
|
|
339
482
|
* appendToMainPersona 基于已改造文本追加,顺序天然正确。
|
|
340
483
|
*/
|
|
341
|
-
export function composeMainPersonaEdit(srcText,
|
|
342
|
-
if (
|
|
484
|
+
export function composeMainPersonaEdit(srcText, removedRoles, customBullets = []) {
|
|
485
|
+
if (removedRoles.length === 0 && customBullets.length === 0) return null
|
|
343
486
|
const marker = `- id: persona\n name: '@deepseek-ai/dsh-persona'`
|
|
344
487
|
const idx = srcText.indexOf(marker)
|
|
345
488
|
if (idx === -1) throw new Error('main agent persona block (`- id: persona` / @deepseek-ai/dsh-persona) not found')
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
489
|
+
// Persona config scalar: `prefix: |-` per the current dsh-persona schema
|
|
490
|
+
// (required `prefix`); `text: |-` is the legacy pre-rename name, still
|
|
491
|
+
// located so old files compose unchanged.
|
|
492
|
+
const prefixStart = srcText.indexOf('prefix: |-', idx)
|
|
493
|
+
const textStart = prefixStart === -1 ? srcText.indexOf('text: |-', idx) : -1
|
|
494
|
+
if (prefixStart === -1 && textStart === -1) {
|
|
495
|
+
throw new Error('main agent persona block: `prefix: |-` (or legacy `text: |-`) scalar not found')
|
|
496
|
+
}
|
|
497
|
+
const keyStart = prefixStart !== -1 ? prefixStart : textStart
|
|
498
|
+
const keyLen = prefixStart !== -1 ? 'prefix: '.length : 'text: '.length
|
|
499
|
+
const dashStart = keyStart + keyLen // points at the `|` of `|-`
|
|
349
500
|
const contentStart = dashStart + '|-'.length + 1
|
|
350
501
|
const firstLineEnd = srcText.indexOf('\n', contentStart)
|
|
351
502
|
const firstLine = srcText.slice(contentStart, firstLineEnd === -1 ? srcText.length : firstLineEnd)
|
|
@@ -361,17 +512,6 @@ export function composeMainPersonaEdit(srcText, mainAgentName, removedRoles, cus
|
|
|
361
512
|
}
|
|
362
513
|
if (end === contentStart) throw new Error('main agent persona block: no content lines found')
|
|
363
514
|
let body = personaDefaultText(srcText.slice(dashStart, end))
|
|
364
|
-
if (mainAgentName) {
|
|
365
|
-
// 身份行 label 替换:仅匹配行首的 "You are the … agent powered by the {{model}} model."
|
|
366
|
-
const identityRe = /^You are the [^\n]*?agent powered by the \{\{model\}\} model\./
|
|
367
|
-
const match = body.match(identityRe)
|
|
368
|
-
if (!match) {
|
|
369
|
-
warn('main_agent_name: 主 persona 身份行未匹配,agent.cordis.yml 保留默认身份行')
|
|
370
|
-
} else {
|
|
371
|
-
const renamed = `You are the ${mainAgentName} agent powered by the {{model}} model.`
|
|
372
|
-
body = body.slice(0, match.index) + renamed + body.slice(match.index + match[0].length)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
515
|
if (removedRoles.length > 0) {
|
|
376
516
|
// 删委派 bullet:行首 `- ` 且含 `delegate to <role>`;SOP 段落等不命中。
|
|
377
517
|
const dropRe = removedRoles.map((role) => new RegExp(`^- .*delegate to ${role}\\.?$`))
|
|
@@ -407,9 +547,10 @@ export function composeMainPersonaEdit(srcText, mainAgentName, removedRoles, cus
|
|
|
407
547
|
return { start: dashStart, end, text: renderPersona(body, indent) }
|
|
408
548
|
}
|
|
409
549
|
|
|
410
|
-
/** Keep only
|
|
411
|
-
|
|
412
|
-
|
|
550
|
+
/** Keep only names that fall inside the preset universe or the detection
|
|
551
|
+
* inventory (see usableWith); everything else is dropped from the allow list. */
|
|
552
|
+
export function filterUsableTools(tools, presetUniverse, inventory) {
|
|
553
|
+
return tools.filter((name) => usableWith(name, presetUniverse, inventory))
|
|
413
554
|
}
|
|
414
555
|
|
|
415
556
|
/**
|
|
@@ -422,16 +563,25 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
422
563
|
const skillsMap = assignments.skills ?? {}
|
|
423
564
|
const customToolNames = Object.keys(assignments.roles).filter((n) => !ROLES.includes(n))
|
|
424
565
|
// roles_remove:被删除的内置角色不产 allow/persona edits(委派行整条删除时避免重叠),
|
|
425
|
-
// 主 persona bullet 与 restrict allow 也据此裁剪。
|
|
566
|
+
// 主 persona bullet 与 restrict allow 也据此裁剪。persona 身份行不可改名
|
|
567
|
+
// (preset 显示名归 main_agent_display_name,见 state.mjs)。
|
|
426
568
|
const removedRoles = ROLES.filter((role) => (assignments.roles_remove ?? []).includes(role))
|
|
427
|
-
|
|
569
|
+
|
|
570
|
+
// presetUniverse = restrict.mjs 主 agent 白名单 ∪ 源 preset 各角色 allow 自带面。
|
|
571
|
+
// 生成期「名字可用」白名单的前半边(后半边是检测清单 inventory):手写进配置、
|
|
572
|
+
// 既不在 preset 自带面也不在检测库存的名字(停用插件残留的 mnemon_*、错名等)
|
|
573
|
+
// 不再直通,一律从 allow 丢弃——与文档「配置意图 ∩ 实际启用工具」口径对齐。
|
|
574
|
+
const presetUniverse = new Set(restrictBase)
|
|
575
|
+
for (const block of blocks.values()) {
|
|
576
|
+
for (const name of block.names) presetUniverse.add(name)
|
|
577
|
+
}
|
|
428
578
|
|
|
429
579
|
// Role allow results (default roles only; custom roles are rendered fresh).
|
|
430
580
|
const roleResults = []
|
|
431
581
|
for (const role of ROLES) {
|
|
432
582
|
if (removedRoles.includes(role)) continue // 已删除角色:不产 roleResults / allow edits
|
|
433
583
|
const intent = assignments.roles[role]?.tools ?? blocks.get(role).names
|
|
434
|
-
const filtered =
|
|
584
|
+
const filtered = filterUsableTools(intent, presetUniverse, inventory)
|
|
435
585
|
if (filtered.length === 0) {
|
|
436
586
|
throw new Error(`role "${role}" would end up with an empty toolFilter.allow — refusing to install a zero-tool role.`)
|
|
437
587
|
}
|
|
@@ -456,7 +606,10 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
456
606
|
// persona delegate bullet / skills 分配键一律不受影响(不产对应 edit)。
|
|
457
607
|
for (const role of ROLES) {
|
|
458
608
|
if (removedRoles.includes(role)) continue // 已删除角色:persona 随委派行整条删除
|
|
459
|
-
|
|
609
|
+
// 空串与 null 同语义(= 走默认 persona):?? 口径会把 '' 当「已自定义」
|
|
610
|
+
// 而生成空 persona 块,统一成 || 判定(与 renderCustomRoleBlocks 的
|
|
611
|
+
// `persona ? ... : 默认` 口径一致)。
|
|
612
|
+
const configured = assignments.roles[role]?.persona || null
|
|
460
613
|
const displayNameRaw = assignments.roles[role]?.name ?? null
|
|
461
614
|
// 空串/null 视为无;字符串取 trim 值(与 config 端 normalizeRoleName 同语义,
|
|
462
615
|
// 防 UI / 手工构造的 assignments 绕过规范化)。
|
|
@@ -505,7 +658,7 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
505
658
|
const span = delegationRowSpan(srcText, role)
|
|
506
659
|
edits.push({ start: span.start, end: span.end, text: '' })
|
|
507
660
|
}
|
|
508
|
-
// 主 persona
|
|
661
|
+
// 主 persona 改造(删委派 bullet + 自定义角色自动委派行):无任何改动时
|
|
509
662
|
// 不产 edit(产物零 diff)。自定义角色取 persona 首行职责句生成 delegate 行
|
|
510
663
|
// (firstLine 对空 persona 返回占位「自定义角色」,中文占位可直接用)——没有这
|
|
511
664
|
// 行指引,主 agent persona 就不含该角色,永远不会委派给它。
|
|
@@ -513,30 +666,42 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
513
666
|
toolName: n,
|
|
514
667
|
duty: firstLine(assignments.roles[n]?.persona || ''),
|
|
515
668
|
}))
|
|
516
|
-
const personaEdit = composeMainPersonaEdit(srcText,
|
|
669
|
+
const personaEdit = composeMainPersonaEdit(srcText, removedRoles, customBullets)
|
|
517
670
|
if (personaEdit !== null) edits.push(personaEdit)
|
|
518
671
|
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
672
|
+
// 角色专用模型 / 可续模式注入(roles.<toolName>.model / .provider → 委派块
|
|
673
|
+
// agentOptions;roles.<toolName>.background_mode = 'continuable' → 委派块
|
|
674
|
+
// backgroundMode 行改写/兜底注入):与 roles_remove 委派行删除 edits 同段收集。
|
|
675
|
+
// 被删角色不注入(委派行已整条删除,模型 / background_mode 配置 warn 忽略);
|
|
676
|
+
// 仅 model 非 null 才注入 agentOptions,provider-alone 仅 warn 忽略(先 normalize
|
|
677
|
+
// 再判断,防手工构造的 assignments 绕过规范化);background_mode 仅 'continuable'
|
|
678
|
+
// 才调 injectRoleBackgroundMode('one-shot'/缺省不调 —— 源模板四块各预置的
|
|
679
|
+
// `backgroundMode: one-shot` 原样保留,全默认配置生成产物与源逐字节一致)。
|
|
680
|
+
// 键序与收集序:源块已有该行时走改写路径(edit start = 既有行行首,位于
|
|
681
|
+
// toolName 行行尾之后 → 降序应用先改写该行,agentOptions 随后插在更前的
|
|
682
|
+
// toolName 行行尾,键序 toolName → agentOptions? → backgroundMode 自然成立);
|
|
683
|
+
// 源块无该行时走兜底注入路径,与 agentOptions 同锚点(start 相等)→ backgroundMode
|
|
684
|
+
// edit 必须先于 agentOptions edit 收集(先收集者居后),与自定义块键序一致。
|
|
523
685
|
for (const role of ROLES) {
|
|
524
686
|
const rawModel = assignments.roles[role]?.model ?? null
|
|
525
687
|
const rawProvider = assignments.roles[role]?.provider ?? null
|
|
688
|
+
const rawBackgroundMode = assignments.roles[role]?.background_mode ?? null
|
|
526
689
|
if (removedRoles.includes(role)) {
|
|
527
690
|
const model = normalizeModelRef(rawModel, `roles.${role}.model`)
|
|
528
691
|
const provider = normalizeModelRef(rawProvider, `roles.${role}.provider`)
|
|
529
692
|
if (model !== null || provider !== null) {
|
|
530
693
|
warn(`roles.${role}.model: 该角色已在 roles_remove 中删除,模型配置被忽略`)
|
|
531
694
|
}
|
|
695
|
+
if (rawBackgroundMode === 'continuable') {
|
|
696
|
+
warn(`roles.${role}.background_mode: 该角色已在 roles_remove 中删除,background_mode 配置被忽略`)
|
|
697
|
+
}
|
|
532
698
|
continue
|
|
533
699
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
warn(`roles.${role}.provider: '${provider}' 仅在同时配置 model 时生效,已忽略`)
|
|
538
|
-
provider = null
|
|
700
|
+
if (rawBackgroundMode === 'continuable') {
|
|
701
|
+
const backgroundModeEdit = injectRoleBackgroundMode(srcText, role)
|
|
702
|
+
if (backgroundModeEdit !== null) edits.push(backgroundModeEdit)
|
|
539
703
|
}
|
|
704
|
+
const { model, provider } = normalizeRoleModel(role, rawModel, rawProvider)
|
|
540
705
|
if (model !== null) edits.push(injectRoleAgentOptions(srcText, role, model, provider))
|
|
541
706
|
}
|
|
542
707
|
// 注入 edit(零宽,位于 toolName 行行尾)与该块的 allow edit / persona edit
|
|
@@ -548,7 +713,7 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
548
713
|
edits.sort((a, b) => b.start - a.start)
|
|
549
714
|
for (const edit of edits) out = out.slice(0, edit.start) + edit.text + out.slice(edit.end)
|
|
550
715
|
|
|
551
|
-
const customBlock = renderCustomRoleBlocks(assignments.roles, skillsMap)
|
|
716
|
+
const customBlock = renderCustomRoleBlocks(assignments.roles, skillsMap, restrictBase)
|
|
552
717
|
if (customBlock !== '') out = insertCustomRoles(out, customBlock)
|
|
553
718
|
|
|
554
719
|
const mainAgentRemove = assignments.main_agent_remove ?? []
|
|
@@ -556,8 +721,8 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
556
721
|
// (undetected host tools, typos, names never present) and skip them.
|
|
557
722
|
if (mainAgentRemove.length > 0) {
|
|
558
723
|
const removable = new Set(
|
|
559
|
-
[...restrictBase, ...(assignments.main_agent_extra ?? []), ...customToolNames].filter(
|
|
560
|
-
(n
|
|
724
|
+
[...restrictBase, ...(assignments.main_agent_extra ?? []), ...customToolNames].filter((n) =>
|
|
725
|
+
usableWith(n, presetUniverse, inventory),
|
|
561
726
|
),
|
|
562
727
|
)
|
|
563
728
|
for (const name of mainAgentRemove) {
|
|
@@ -566,6 +731,7 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
566
731
|
}
|
|
567
732
|
const restrictAllow = computeRestrictAllow(
|
|
568
733
|
restrictBase,
|
|
734
|
+
presetUniverse,
|
|
569
735
|
inventory,
|
|
570
736
|
assignments.main_agent_extra ?? [],
|
|
571
737
|
mainAgentRemove,
|