dsh-paoding 0.3.9 → 0.4.0
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 +6 -1
- package/README_en.md +6 -1
- package/bin/dsh-paoding.mjs +23 -0
- package/docs/architecture.md +4 -3
- package/docs/architecture_en.md +6 -4
- package/docs/configuration.md +85 -12
- package/docs/configuration_en.md +88 -13
- package/docs/installation.md +18 -6
- package/docs/installation_en.md +18 -6
- package/docs/orchestration.md +4 -0
- package/docs/orchestration_en.md +4 -0
- package/package.json +1 -1
- package/plugins/paoding-config-ui/api-core.mjs +107 -4
- package/plugins/paoding-config-ui/index.mjs +93 -28
- package/plugins/paoding-config-ui/lib/client.js +941 -237
- package/plugins/paoding-config-ui/package.json +1 -1
- package/presets/orchestrator/agent.cordis.yml +13 -0
- package/presets/orchestrator/delegation-labels.mjs +80 -0
- package/tools/lib/cli.mjs +91 -13
- package/tools/lib/compose.mjs +103 -16
- package/tools/lib/doctor.mjs +245 -0
- package/tools/lib/glob.mjs +76 -0
- package/tools/lib/health.mjs +298 -0
- package/tools/lib/preset-system.mjs +29 -11
- package/tools/lib/state.mjs +24 -6
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
# · orchestrator-restrict → restrict.mjs 在 system-prompt/assemble 瀑布上把
|
|
8
8
|
# 主 agent 的工具面裁剪为「编排 + 内部搜索」白名单;子 agent(origin:
|
|
9
9
|
# 'subagent')放行,由各自 toolFilter 决定工具面
|
|
10
|
+
# · delegation-labels → delegation-labels.mjs 在 tools/pre-execute 瀑布上把
|
|
11
|
+
# 命中角色的委派调用 description 原地改写为「角色名 · 原任务短语」,任务
|
|
12
|
+
# 管理页 / 子代理目录树 / 后台任务列表的标签统一带角色名
|
|
10
13
|
# · delegation-* → 4 个角色委派工具(dsh-tool-subagent,search_internal_deep 为静态委派),子 agent 创建时
|
|
11
14
|
# 以 per-child tools.restrict() 只保留各自 allow 的工具
|
|
12
15
|
#
|
|
@@ -68,6 +71,16 @@
|
|
|
68
71
|
- id: orchestrator-restrict
|
|
69
72
|
name: ./restrict.mjs
|
|
70
73
|
|
|
74
|
+
# ── 委派标签角色名前缀 ───────────────────────────────────────────────────────
|
|
75
|
+
# delegation-labels.mjs 挂 tools/pre-execute 瀑布,把命中角色的委派调用
|
|
76
|
+
# description 原地改写为「角色名 · 原任务短语」——子会话 label 持久化后,任务
|
|
77
|
+
# 管理页、会话头子代理目录树与后台任务列表统一显示角色名。roles 由生成器按
|
|
78
|
+
# roles.<toolName>.name 填充;空对象 = 没有角色配了名字,插件不注册任何处理器。
|
|
79
|
+
- id: delegation-labels
|
|
80
|
+
name: ./delegation-labels.mjs
|
|
81
|
+
config:
|
|
82
|
+
roles: {} # 生成器按 roles.<toolName>.name 填充
|
|
83
|
+
|
|
71
84
|
# ── shell ───────────────────────────────────────────────────────────────────
|
|
72
85
|
|
|
73
86
|
- id: tool-bash
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 委派子代理标签带角色名(delegation-labels.mjs)
|
|
3
|
+
*
|
|
4
|
+
* Part of the dsh-paoding 编排模式 (orchestrator) preset. 主 agent 委派角色
|
|
5
|
+
* (implement / design / search_external 及自定义角色)时,委派工具调用的
|
|
6
|
+
* `description` 参数会成为子会话的持久 label——任务管理页(better-sidebar)、
|
|
7
|
+
* 原生会话头的子代理目录树、后台任务列表展示的都是它。编排模型随手写的
|
|
8
|
+
* description 只是一句任务短语,扫一眼分不清哪条子代理是谁在干活;本插件在
|
|
9
|
+
* `tools/pre-execute` 瀑布上把命中角色的委派调用改写成 `角色名 · 原任务短语`
|
|
10
|
+
* (如「民工码农 · 修复标签显示」),三处展示面统一带上角色名。
|
|
11
|
+
*
|
|
12
|
+
* 机制(提示词约束做不到的确定性改写):
|
|
13
|
+
* dsh-tools 调度器在每次工具执行前跑 tools/pre-execute 瀑布,随后把同一个
|
|
14
|
+
* exec.arguments 对象原样交给工具本体。dsh-tool-subagent 的子会话 label、
|
|
15
|
+
* jobs.start 的 job label、startContinuable 的 label 三者都取自
|
|
16
|
+
* args.description,所以在这里原地改写该字段,下游一并生效,并随子会话
|
|
17
|
+
* descriptor 持久化。处理完必须 `return await next()` 放行——本插件只管
|
|
18
|
+
* 标签,永不拦截委派。
|
|
19
|
+
*
|
|
20
|
+
* config.roles 由生成器按 dsh-paoding.config.yml 的 roles.<toolName>.name 填充
|
|
21
|
+
* (toolName → 显示名):没配名字的角色不进映射、标签保持原样;全空时本插件
|
|
22
|
+
* 不注册任何处理器。幂等:已带 `角色名 · ` 前缀的 description 不重复加前缀
|
|
23
|
+
* (同一次委派重复过瀑布也不会双写)。
|
|
24
|
+
*
|
|
25
|
+
* 失败面:整段 try/catch 包住,任何异常只 warn 一次后照常放行——标签出错顶多
|
|
26
|
+
* 退回旧观感,绝不能拦掉一次委派。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
30
|
+
export const name = 'orchestrator-delegation-labels'
|
|
31
|
+
|
|
32
|
+
/** The waterfall rides on ctx events; tools guarantees the registry exists first. */
|
|
33
|
+
export const inject = ['tools']
|
|
34
|
+
|
|
35
|
+
/** 角色名与任务短语之间的缺省分隔符。 */
|
|
36
|
+
const DEFAULT_SEPARATOR = ' · '
|
|
37
|
+
|
|
38
|
+
/** 标签改写出错只提醒一次,避免每次委派都刷一条日志。 */
|
|
39
|
+
let warned = false
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {object} ctx - cordis context(事件瀑布 / logger)。
|
|
43
|
+
* @param {{ separator?: string, roles?: Record<string, string> }} config
|
|
44
|
+
* 生成器写入的角色显示名映射与可选分隔符。
|
|
45
|
+
*/
|
|
46
|
+
export function apply(ctx, config = {}) {
|
|
47
|
+
const separator =
|
|
48
|
+
typeof config.separator === 'string' && config.separator !== '' ? config.separator : DEFAULT_SEPARATOR
|
|
49
|
+
// 只收非空显示名;一个名字都没有就什么都不注册(无事可做,零开销)。
|
|
50
|
+
const displayNames = new Map()
|
|
51
|
+
for (const [toolName, displayName] of Object.entries(config.roles ?? {})) {
|
|
52
|
+
if (typeof toolName !== 'string' || toolName === '') continue
|
|
53
|
+
if (typeof displayName !== 'string' || displayName.trim() === '') continue
|
|
54
|
+
displayNames.set(toolName, displayName.trim())
|
|
55
|
+
}
|
|
56
|
+
if (displayNames.size === 0) return
|
|
57
|
+
|
|
58
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
59
|
+
try {
|
|
60
|
+
const toolName = exec?.name
|
|
61
|
+
if (typeof toolName === 'string' && displayNames.has(toolName)) {
|
|
62
|
+
const args = exec.arguments
|
|
63
|
+
if (args !== null && typeof args === 'object') {
|
|
64
|
+
const prefix = `${displayNames.get(toolName)}${separator}`
|
|
65
|
+
const description = args.description
|
|
66
|
+
// 非空字符串且未带前缀才改写(幂等:再次进瀑布不双写前缀)。
|
|
67
|
+
if (typeof description === 'string' && description.trim() !== '' && !description.startsWith(prefix)) {
|
|
68
|
+
args.description = `${prefix}${description}`
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!warned) {
|
|
74
|
+
warned = true
|
|
75
|
+
ctx.logger?.warn?.(`${name}: 标签前缀注入失败,本次按原 description 放行(后续不再提醒): ${String(error)}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return await next()
|
|
79
|
+
})
|
|
80
|
+
}
|
package/tools/lib/cli.mjs
CHANGED
|
@@ -8,17 +8,21 @@
|
|
|
8
8
|
* 自带的 Web 配置器(侧栏底部「庖丁配置」入口)。
|
|
9
9
|
*
|
|
10
10
|
* 依赖 util(ROLES / SRC_DIR / PROFILE_NAME_RE)、config(normalizeRoleName)、
|
|
11
|
-
* host(removedReason)、state(collectState /
|
|
11
|
+
* host(removedReason)、state(PRESET_ASIS_FILES / collectState /
|
|
12
12
|
* generateAndInstall)、alloc(autoAssignments,--auto 语义与插件首装共享)、
|
|
13
|
-
*
|
|
13
|
+
* health(captureWarnings / healthCheck,--dry-run 报告的结构化体检)、
|
|
14
|
+
* doctor(runDoctor,doctor 子命令的只读体检)、version(getVersionInfo,
|
|
15
|
+
* main 收尾的新版提示)以及 node:os / node:path。
|
|
14
16
|
*/
|
|
15
17
|
import { homedir } from 'node:os'
|
|
16
18
|
import path from 'node:path'
|
|
17
19
|
import { PROFILE_NAME_RE, ROLES, SRC_DIR } from './util.mjs'
|
|
18
20
|
import { normalizeRoleName } from './config.mjs'
|
|
19
21
|
import { removedReason } from './host.mjs'
|
|
20
|
-
import { collectState, generateAndInstall } from './state.mjs'
|
|
22
|
+
import { PRESET_ASIS_FILES, collectState, generateAndInstall } from './state.mjs'
|
|
21
23
|
import { autoAssignments } from './alloc.mjs'
|
|
24
|
+
import { captureWarnings, healthCheck } from './health.mjs'
|
|
25
|
+
import { runDoctor } from './doctor.mjs'
|
|
22
26
|
import { getVersionInfo } from './version.mjs'
|
|
23
27
|
|
|
24
28
|
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
@@ -35,6 +39,7 @@ export function printHelp() {
|
|
|
35
39
|
|
|
36
40
|
Usage:
|
|
37
41
|
node tools/install.mjs [options]
|
|
42
|
+
node tools/install.mjs doctor [options]
|
|
38
43
|
|
|
39
44
|
Options:
|
|
40
45
|
--profile <name> Profile whose patch layer is scanned (default: web).
|
|
@@ -47,13 +52,19 @@ Options:
|
|
|
47
52
|
--auto Non-interactive: apply the config file when present,
|
|
48
53
|
otherwise apply the base template (--suggest seeds smart
|
|
49
54
|
defaults on a fresh install instead).
|
|
50
|
-
--dry-run Print the detection report
|
|
51
|
-
without writing anything; exit 0.
|
|
55
|
+
--dry-run Print the detection report, the generated allow lists and
|
|
56
|
+
the config health check without writing anything; exit 0.
|
|
52
57
|
--suggest Fresh install without a config file: seed smart defaults
|
|
53
58
|
from the detected host/MCP tools instead of the base
|
|
54
59
|
template (host tools are folded in automatically).
|
|
55
60
|
--help Show this help and exit.
|
|
56
61
|
|
|
62
|
+
Subcommands:
|
|
63
|
+
doctor Read-only health check of the local install (config file,
|
|
64
|
+
preset track vs host version, managed patch rows, orphan
|
|
65
|
+
workspace presets, config drift, version marker) and a
|
|
66
|
+
summary; exit 1 only on error-level findings.
|
|
67
|
+
|
|
57
68
|
Environment:
|
|
58
69
|
DSH_HOME Directory holding cordis.patch.yml etc. (default: $HOME/.dsh)
|
|
59
70
|
|
|
@@ -73,10 +84,15 @@ function assertProfileName(value) {
|
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
export function parseArgs(argv) {
|
|
76
|
-
const opts = { profile: null, patches: [], dryRun: false, help: false, auto: false, config: undefined, suggest: false }
|
|
87
|
+
const opts = { command: 'install', profile: null, patches: [], dryRun: false, help: false, auto: false, config: undefined, suggest: false }
|
|
77
88
|
for (let i = 0; i < argv.length; i++) {
|
|
78
89
|
const arg = argv[i]
|
|
79
|
-
if (arg === '
|
|
90
|
+
if (arg === 'doctor') {
|
|
91
|
+
// 子命令:doctor = 只读体检;缺省 install(原 installer 主流程)。位置参数
|
|
92
|
+
// 只认这一个,其余仍按未知选项报错;--help / --profile / --config 等照常
|
|
93
|
+
// 解析,doctor 分支共用。
|
|
94
|
+
opts.command = 'doctor'
|
|
95
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
80
96
|
opts.help = true
|
|
81
97
|
} else if (arg === '--dry-run') {
|
|
82
98
|
opts.dryRun = true
|
|
@@ -130,7 +146,7 @@ export function parseArgs(argv) {
|
|
|
130
146
|
|
|
131
147
|
// ── report & install ────────────────────────────────────────────────────────
|
|
132
148
|
|
|
133
|
-
export function printReport({ scanned, mcpReports, pluginReports, roleResults, dstDir, dryRun, staleNote, mainAgent }) {
|
|
149
|
+
export function printReport({ scanned, mcpReports, pluginReports, roleResults, dstDir, dryRun, staleNote, mainAgent, health = null }) {
|
|
134
150
|
const pad = (s, n) => String(s).padEnd(n)
|
|
135
151
|
console.log('dsh-paoding installer — orchestrator preset (host-aware tool filtering)')
|
|
136
152
|
console.log(` preset source : ${SRC_DIR}`)
|
|
@@ -188,6 +204,21 @@ export function printReport({ scanned, mcpReports, pluginReports, roleResults, d
|
|
|
188
204
|
} else {
|
|
189
205
|
console.log(' removed: none — every host-dependent allow entry is enabled')
|
|
190
206
|
}
|
|
207
|
+
if (health !== null) {
|
|
208
|
+
// 配置体检小节(--dry-run 报告专属;health 为 null 时不打印,--auto 输出
|
|
209
|
+
// 保持原样)。条目结构与面板 preview/apply 响应的 warnings 字段同源
|
|
210
|
+
//(tools/lib/health.mjs healthCheck)。
|
|
211
|
+
console.log()
|
|
212
|
+
console.log('config health (体检):')
|
|
213
|
+
if (health.length === 0) {
|
|
214
|
+
console.log(' ✔ 未发现问题')
|
|
215
|
+
} else {
|
|
216
|
+
for (const h of health) {
|
|
217
|
+
const mark = h.level === 'error' ? '✘' : h.level === 'warn' ? '⚠' : 'ℹ'
|
|
218
|
+
console.log(` ${mark} ${h.code} — ${h.message}`)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
191
222
|
if (staleNote) {
|
|
192
223
|
console.log('\nnote: 检测到 patch 配置在本次安装后有过修改,改动后请重跑 node tools/install.mjs --auto')
|
|
193
224
|
}
|
|
@@ -196,7 +227,7 @@ export function printReport({ scanned, mcpReports, pluginReports, roleResults, d
|
|
|
196
227
|
`${dryRun ? '[dry-run] would write' : 'wrote'} ${path.join(dstDir, 'agent.cordis.yml')}` +
|
|
197
228
|
(dryRun ? '' : ' (allow lists rewritten)'),
|
|
198
229
|
)
|
|
199
|
-
console.log(`${dryRun ? '[dry-run] would copy' : 'copied'}
|
|
230
|
+
console.log(`${dryRun ? '[dry-run] would copy' : 'copied'} ${PRESET_ASIS_FILES.join(', ')} as-is`)
|
|
200
231
|
if (!dryRun) {
|
|
201
232
|
console.log()
|
|
202
233
|
console.log('next steps:')
|
|
@@ -211,6 +242,30 @@ export function printReport({ scanned, mcpReports, pluginReports, roleResults, d
|
|
|
211
242
|
console.log(`uninstall: rm -rf "${dstDir}"`)
|
|
212
243
|
}
|
|
213
244
|
|
|
245
|
+
/** doctor 子命令的人读报告:每行「✔/✘/⚠/– 项目 — 一句说明」,结尾汇总。
|
|
246
|
+
* 渲染与检查本体分离(tools/lib/doctor.mjs 只产结构化 items,便于测试)。 */
|
|
247
|
+
export function printDoctorReport(report, { dshHome, configFile }) {
|
|
248
|
+
const marks = { ok: '✔', error: '✘', warn: '⚠', skip: '–' }
|
|
249
|
+
console.log('dsh-paoding doctor — 安装体检(只读,不改任何文件)')
|
|
250
|
+
console.log(` DSH_HOME : ${dshHome}`)
|
|
251
|
+
console.log(` 配置文件 : ${configFile}`)
|
|
252
|
+
console.log()
|
|
253
|
+
for (const { status, label, detail } of report.items) {
|
|
254
|
+
console.log(`${marks[status] ?? '·'} ${label} — ${detail}`)
|
|
255
|
+
}
|
|
256
|
+
const count = (status) => report.items.filter((i) => i.status === status).length
|
|
257
|
+
const errors = count('error')
|
|
258
|
+
const warns = count('warn')
|
|
259
|
+
console.log()
|
|
260
|
+
if (errors > 0) {
|
|
261
|
+
console.log(`汇总: ${report.items.length} 项检查,${errors} 项错误(✘)${warns > 0 ? `、${warns} 项警告(⚠)` : ''}——请先处理 ✘ 再使用;详细说明见 docs/installation.md 排查节`)
|
|
262
|
+
} else if (warns > 0) {
|
|
263
|
+
console.log(`汇总: ${report.items.length} 项检查,无错误;${warns} 项警告(⚠)不影响使用,可按各行说明处理`)
|
|
264
|
+
} else {
|
|
265
|
+
console.log(`汇总: ${report.items.length} 项检查全部正常`)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
214
269
|
// ── main ────────────────────────────────────────────────────────────────────
|
|
215
270
|
|
|
216
271
|
/**
|
|
@@ -232,6 +287,16 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
|
|
|
232
287
|
return 0
|
|
233
288
|
}
|
|
234
289
|
|
|
290
|
+
// doctor 子命令:只读体检,不走 --auto 门禁(门禁仅拦会动盘的安装主流程),
|
|
291
|
+
// 也不用 collectState 的全量检测——runDoctor 内部按项自行探测与降级。
|
|
292
|
+
if (opts.command === 'doctor') {
|
|
293
|
+
const dshHome = process.env.DSH_HOME || path.join(homedir(), '.dsh')
|
|
294
|
+
const configFile = opts.config || path.join(dshHome, 'dsh-paoding.config.yml')
|
|
295
|
+
const report = await runDoctor({ dshHome, configFile, profile: opts.profile })
|
|
296
|
+
printDoctorReport(report, { dshHome, configFile })
|
|
297
|
+
return report.hasErrors ? 1 : 0
|
|
298
|
+
}
|
|
299
|
+
|
|
235
300
|
// 无 --auto 直接拒绝(交互模式已删):--dry-run 只读不动盘,放行作检测预览。
|
|
236
301
|
// 门禁放在 collectState 之前:被拒路径不做任何检测 / 握手,快速失败。
|
|
237
302
|
if (!opts.auto && !opts.dryRun) {
|
|
@@ -281,12 +346,24 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
|
|
|
281
346
|
// saveConfigOnWrite:fresh 非交互安装把基础模板落进配置文件(显式
|
|
282
347
|
// main_agent_extra: [] 锚定「未开启 host 工具」);existing 路径绝不覆写
|
|
283
348
|
// 用户配置文件。
|
|
349
|
+
// --dry-run 顺路做一次结构化体检(captureWarnings 抓生成期告警 + healthCheck,
|
|
350
|
+
// 产物 dryRunResult 直喂体检零重跑;models 缺省 null,CLI 不做模型目录发现):
|
|
351
|
+
// health 条目由 printReport 的新小节打印。--auto 路径不体检,输出保持原样。
|
|
284
352
|
let composed
|
|
353
|
+
let healthItems = null
|
|
285
354
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
355
|
+
if (opts.dryRun) {
|
|
356
|
+
const [result, generationWarnings] = captureWarnings(() =>
|
|
357
|
+
generateAndInstall(state, assignments, { dryRun: true, saveConfigOnWrite: false }),
|
|
358
|
+
)
|
|
359
|
+
composed = result
|
|
360
|
+
healthItems = healthCheck({ state, assignments, dryRunResult: composed, generationWarnings }).items
|
|
361
|
+
} else {
|
|
362
|
+
composed = generateAndInstall(state, assignments, {
|
|
363
|
+
dryRun: false,
|
|
364
|
+
saveConfigOnWrite: !existing,
|
|
365
|
+
})
|
|
366
|
+
}
|
|
290
367
|
} catch (err) {
|
|
291
368
|
console.error(`error: ${err.message}`)
|
|
292
369
|
return 1
|
|
@@ -307,6 +384,7 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
|
|
|
307
384
|
extra: assignments.main_agent_extra ?? [],
|
|
308
385
|
removed: assignments.main_agent_remove ?? [],
|
|
309
386
|
},
|
|
387
|
+
health: healthItems,
|
|
310
388
|
})
|
|
311
389
|
if (!existing && !opts.dryRun) {
|
|
312
390
|
// fresh 非交互安装:提示基础模板已落盘、host/MCP 工具去哪补(--suggest 可
|
package/tools/lib/compose.mjs
CHANGED
|
@@ -16,10 +16,18 @@
|
|
|
16
16
|
* 行行尾兜底注入;'one-shot' 不动源行 —— 源模板四块各预置
|
|
17
17
|
* `backgroundMode: one-shot`(可见性),全默认产物与源零 diff)、
|
|
18
18
|
* restrict config.allow 注入(computeRestrictAllow /
|
|
19
|
-
* injectRestrictAllow / filterUsableTools
|
|
19
|
+
* injectRestrictAllow / filterUsableTools)、委派标签角色名映射注入
|
|
20
|
+
* (injectDelegationLabelRoles:roles.<toolName>.name → delegation-labels 行的
|
|
21
|
+
* config.roles,运行期给委派调用的 description 加「角色名 · 」前缀)与整文
|
|
22
|
+
* compose(composeGenerated)。
|
|
20
23
|
* allow 过滤走白名单语义(usableWith / presetUniverse ∪ inventory,见
|
|
21
24
|
* filterUsableTools 上方说明):手写进配置、既不在 preset 自带工具面也不在
|
|
22
25
|
* 检测库存的名字一律从 allow 丢弃,不再「非 host 依赖名直通」。
|
|
26
|
+
* 工具条目支持 `*` / `?` 通配(expandToolPatterns,见 glob.mjs):主 agent 侧
|
|
27
|
+
* (computeRestrictAllow 的 base / mainAgentExtra / customToolNames 三路)与角色
|
|
28
|
+
* 侧(内置角色 intent、自定义角色 tools)都在白名单过滤**之前**先对候选集展开
|
|
29
|
+
* 为具体名 —— 语义叠加不替代,展开后的名字仍走既有 usableWith / 直通过滤;
|
|
30
|
+
* 产物只落具体名,运行时 toolFilter 语义不变。
|
|
23
31
|
* workflow worker 行按宿主版本生成期适配(adaptWorkflowWorkerRow:≥0.1.6 把
|
|
24
32
|
* 0.1.5-only 的 workflow-worker-thread 条目换成 workflow-ptc;探测不到版本
|
|
25
33
|
* 保守保留源行,产物零 diff 性质随之保留)。
|
|
@@ -30,13 +38,14 @@
|
|
|
30
38
|
* agent persona 委派行(composeMainPersonaEdit 第 4 参 customBullets →
|
|
31
39
|
* `- <职责>: delegate to <toolName>.`),否则主 agent 永远不会委派给自定义角色。
|
|
32
40
|
* 依赖 util(ROLES / warn / DEFAULT_MAIN_AGENT_PERSONA_EXTRA)、config
|
|
33
|
-
* (normalizeModelRef / yamlScalar)与 host
|
|
41
|
+
* (normalizeModelRef / yamlScalar)、glob(expandToolPatterns)与 host
|
|
34
42
|
* (isHostDependent);被 state 引用。
|
|
35
43
|
*/
|
|
36
44
|
import { DEFAULT_MAIN_AGENT_PERSONA_EXTRA, ROLES, warn } from './util.mjs'
|
|
37
45
|
import { normalizeModelRef, yamlScalar } from './config.mjs'
|
|
38
46
|
import { isHostDependent } from './host.mjs'
|
|
39
47
|
import { isPtcWorkerHost } from './preset-system.mjs'
|
|
48
|
+
import { expandToolPatterns } from './glob.mjs'
|
|
40
49
|
|
|
41
50
|
// ── workflow worker 行的宿主版本适配 ─────────────────────────────────────────
|
|
42
51
|
|
|
@@ -217,18 +226,25 @@ export function replaceRoleIdentity(text, displayName) {
|
|
|
217
226
|
/**
|
|
218
227
|
* Render the delegation block(s) for custom roles, matching the existing
|
|
219
228
|
* delegation-* indentation (4-space list items).
|
|
229
|
+
* expandCandidates(可选,Set/数组):传入时角色 tools 先过通配符展开(与内置
|
|
230
|
+
* 角色同一候选集口径,见 composeGenerated),展开后的具体名直接进块内 allow ——
|
|
231
|
+
* 自定义角色的 allow 不走 usableWith(角色名本身就不在既有工具面里),展开是
|
|
232
|
+
* 纯加法,不过滤;模式零匹配被丢弃后 tools 若变空,走下方既有的零工具 throw
|
|
233
|
+
*(拒绝安装零工具角色,warn 已提示是哪个模式没匹配上)。缺省不展开(直接调用
|
|
234
|
+
* 方/测试的旧签名行为不变)。
|
|
220
235
|
*/
|
|
221
|
-
export function renderCustomRoleBlocks(roles, skillsMap, restrictBase = []) {
|
|
236
|
+
export function renderCustomRoleBlocks(roles, skillsMap, restrictBase = [], expandCandidates = null) {
|
|
222
237
|
const custom = Object.entries(roles).filter(([toolName]) => !ROLES.includes(toolName))
|
|
223
238
|
if (custom.length === 0) return ''
|
|
224
239
|
const chunks = []
|
|
225
240
|
for (const [toolName, role] of custom) {
|
|
226
241
|
// 保留名校验(见 assertCustomToolName)+ 零工具防护(与 composeGenerated
|
|
227
|
-
// 内置角色循环同款):tools 为空(缺省或 `tools:`
|
|
228
|
-
// `toolFilter.allow:`(YAML null),子 agent
|
|
229
|
-
//
|
|
242
|
+
// 内置角色循环同款):tools 为空(缺省或 `tools:` 空列表,或传入候选集时
|
|
243
|
+
// 通配符展开后零命中)会生成 `toolFilter.allow:`(YAML null),子 agent
|
|
244
|
+
// 创建后零工具——宁可拒绝安装,也不静默生成一个干不了活的角色。
|
|
230
245
|
assertCustomToolName(toolName, restrictBase)
|
|
231
|
-
|
|
246
|
+
const tools = expandCandidates === null ? (role.tools ?? []) : expandToolPatterns(role.tools ?? [], expandCandidates)
|
|
247
|
+
if (tools.length === 0) {
|
|
232
248
|
throw new Error(`role "${toolName}" would end up with an empty toolFilter.allow — refusing to install a zero-tool role.`)
|
|
233
249
|
}
|
|
234
250
|
const persona = withSkillGuidance(
|
|
@@ -278,7 +294,7 @@ export function renderCustomRoleBlocks(roles, skillsMap, restrictBase = []) {
|
|
|
278
294
|
...persona.split('\n').map((l) => ` ${l}`),
|
|
279
295
|
' toolFilter:',
|
|
280
296
|
' allow:',
|
|
281
|
-
...
|
|
297
|
+
...tools.map((t) => ` - ${yamlScalar(t)}`),
|
|
282
298
|
].join('\n'),
|
|
283
299
|
)
|
|
284
300
|
}
|
|
@@ -299,16 +315,23 @@ export function firstLine(text) {
|
|
|
299
315
|
* removed: once roles_remove is non-empty the explicit allow MUST be injected,
|
|
300
316
|
* otherwise the static restrict.mjs MAIN_AGENT_ALLOW would keep delegating to
|
|
301
317
|
* the deleted role (删除不生效).
|
|
318
|
+
* 三路入参(base / mainAgentExtra / customToolNames)先过通配符展开(候选集 =
|
|
319
|
+
* presetUniverse ∪ inventory,与 usableWith 判定用的同一集合),展开后的名字仍
|
|
320
|
+
* 走各自既有的过滤口径(base / extra 过 usableWith,customToolNames 直通收)——
|
|
321
|
+
* 语义叠加不替代。base 与 customToolNames 实际都是精确名(restrict.mjs 源名单 /
|
|
322
|
+
* assignments.roles 的键),展开为恒等变换,零风险;mainAgentExtra 是手写配置
|
|
323
|
+
* 条目,通配符的主战场。零通配时展开是同引用恒等,默认路径输出逐字节不变。
|
|
302
324
|
*/
|
|
303
325
|
export function computeRestrictAllow(base, presetUniverse, inventory, mainAgentExtra, mainAgentRemove, customToolNames, removedRoleNames = []) {
|
|
326
|
+
const expandCandidates = new Set([...presetUniverse, ...inventory])
|
|
304
327
|
const kept = []
|
|
305
|
-
for (const name of base) {
|
|
328
|
+
for (const name of expandToolPatterns(base, expandCandidates)) {
|
|
306
329
|
if (usableWith(name, presetUniverse, inventory)) kept.push(name)
|
|
307
330
|
}
|
|
308
|
-
for (const name of mainAgentExtra) {
|
|
331
|
+
for (const name of expandToolPatterns(mainAgentExtra, expandCandidates)) {
|
|
309
332
|
if (!kept.includes(name) && usableWith(name, presetUniverse, inventory)) kept.push(name)
|
|
310
333
|
}
|
|
311
|
-
for (const name of customToolNames) {
|
|
334
|
+
for (const name of expandToolPatterns(customToolNames, expandCandidates)) {
|
|
312
335
|
if (!kept.includes(name)) kept.push(name)
|
|
313
336
|
}
|
|
314
337
|
const removed = new Set([...(mainAgentRemove ?? []), ...(removedRoleNames ?? [])])
|
|
@@ -328,6 +351,45 @@ export function injectRestrictAllow(srcText, allowList) {
|
|
|
328
351
|
return srcText.slice(0, insertAt) + block + srcText.slice(insertAt)
|
|
329
352
|
}
|
|
330
353
|
|
|
354
|
+
/**
|
|
355
|
+
* 委派标签角色名映射注入(delegation-labels 行的 config.roles):把
|
|
356
|
+
* delegation-labels.mjs 模板行的 `roles: {}` 占位改写为实际映射
|
|
357
|
+
* (toolName → 显示名,键值均过 yamlScalar 落安全标量)。运行期
|
|
358
|
+
* delegation-labels.mjs 据此在 tools/pre-execute 瀑布上给委派调用的
|
|
359
|
+
* description 加「角色名 · 」前缀(子会话 label / job label 的持久展示名)。
|
|
360
|
+
* 无任何名字时直接返回原文(产物与源零 diff,插件侧拿到空映射也不注册
|
|
361
|
+
* 处理器);找不到 delegation-labels 行或 `roles: {}` 占位属模板漂移,硬
|
|
362
|
+
* 错误(与 injectRestrictAllow 的失败口径一致)。键按字典序落盘,同一配置
|
|
363
|
+
* 反复生成产物稳定。
|
|
364
|
+
*/
|
|
365
|
+
export function injectDelegationLabelRoles(srcText, roleNames) {
|
|
366
|
+
const entries = Object.entries(roleNames ?? {})
|
|
367
|
+
.filter(
|
|
368
|
+
([toolName, displayName]) =>
|
|
369
|
+
typeof toolName === 'string' && toolName !== '' && typeof displayName === 'string' && displayName.trim() !== '',
|
|
370
|
+
)
|
|
371
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
372
|
+
if (entries.length === 0) return srcText
|
|
373
|
+
const marker = '- id: delegation-labels'
|
|
374
|
+
const idx = srcText.indexOf(marker)
|
|
375
|
+
if (idx === -1) throw new Error('delegation-labels row not found')
|
|
376
|
+
// 行块边界 = 下一条顶层 `- id: ` 行(本块只有 id / name / config.roles 三件),
|
|
377
|
+
// 占位匹配限定在块内,防误吞后续行的同名键。
|
|
378
|
+
const nextRow = srcText.indexOf('\n- id: ', idx + marker.length)
|
|
379
|
+
const blockEnd = nextRow === -1 ? srcText.length : nextRow
|
|
380
|
+
const rolesIdx = srcText.indexOf('roles: {}', idx)
|
|
381
|
+
if (rolesIdx === -1 || rolesIdx >= blockEnd) {
|
|
382
|
+
throw new Error('delegation-labels row: `roles: {}` placeholder not found')
|
|
383
|
+
}
|
|
384
|
+
const lineStart = srcText.lastIndexOf('\n', rolesIdx - 1) + 1
|
|
385
|
+
const lineEnd = srcText.indexOf('\n', rolesIdx)
|
|
386
|
+
const rendered = entries
|
|
387
|
+
.map(([toolName, displayName]) => ` ${yamlScalar(toolName)}: ${yamlScalar(displayName.trim())}`)
|
|
388
|
+
.join('\n')
|
|
389
|
+
const end = lineEnd === -1 ? srcText.length : lineEnd
|
|
390
|
+
return srcText.slice(0, lineStart) + ` roles:\n${rendered}` + srcText.slice(end)
|
|
391
|
+
}
|
|
392
|
+
|
|
331
393
|
/**
|
|
332
394
|
* Append text to the main-agent persona (`- id: persona` row) block scalar,
|
|
333
395
|
* used to inline the selected skills' full SKILL.md bodies (composeGenerated,
|
|
@@ -688,11 +750,18 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
688
750
|
for (const name of block.names) presetUniverse.add(name)
|
|
689
751
|
}
|
|
690
752
|
|
|
753
|
+
// 通配符展开候选集 = presetUniverse ∪ inventory(与 usableWith 判定用的同一
|
|
754
|
+
// 集合):内置角色 intent / 自定义角色 tools / main_agent_extra 三处的 `*` /
|
|
755
|
+
// `?` 模式条目都先对它展开成具体名,再进各自既有的过滤(语义叠加不替代)。
|
|
756
|
+
const expandCandidates = new Set([...presetUniverse, ...inventory])
|
|
757
|
+
|
|
691
758
|
// Role allow results (default roles only; custom roles are rendered fresh).
|
|
692
759
|
const roleResults = []
|
|
693
760
|
for (const role of ROLES) {
|
|
694
761
|
if (removedRoles.includes(role)) continue // 已删除角色:不产 roleResults / allow edits
|
|
695
|
-
|
|
762
|
+
// 意图先过通配符展开再进白名单过滤;roleResults 的 intent/removed 都记展开
|
|
763
|
+
// 后的具体名(报告口径「配了几个 → 留几个」对模式条目才有意义)。
|
|
764
|
+
const intent = expandToolPatterns(assignments.roles[role]?.tools ?? blocks.get(role).names, expandCandidates)
|
|
696
765
|
const filtered = filterUsableTools(intent, presetUniverse, inventory)
|
|
697
766
|
if (filtered.length === 0) {
|
|
698
767
|
throw new Error(`role "${role}" would end up with an empty toolFilter.allow — refusing to install a zero-tool role.`)
|
|
@@ -844,17 +913,23 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
844
913
|
// workflow worker 行宿主版本适配(≥0.1.6 换 workflow-ptc;见 adaptWorkflowWorkerRow)。
|
|
845
914
|
out = adaptWorkflowWorkerRow(out, hostVersionTriple)
|
|
846
915
|
|
|
847
|
-
|
|
916
|
+
// 自定义角色块:tools 里的通配符条目按同一候选集展开成具体名再进 allow
|
|
917
|
+
//(第 4 参见 renderCustomRoleBlocks)。
|
|
918
|
+
const customBlock = renderCustomRoleBlocks(assignments.roles, skillsMap, restrictBase, expandCandidates)
|
|
848
919
|
if (customBlock !== '') out = insertCustomRoles(out, customBlock)
|
|
849
920
|
|
|
850
921
|
const mainAgentRemove = assignments.main_agent_remove ?? []
|
|
851
922
|
// Warn about removals that are not part of the main agent's current allow
|
|
852
923
|
// (undetected host tools, typos, names never present) and skip them.
|
|
924
|
+
// main_agent_extra 贡献先过通配符展开(与 computeRestrictAllow 的 allow 口径
|
|
925
|
+
// 一致),否则 extra 里的模式条目会让精确名的 remove 误报「不在当前 allow」。
|
|
853
926
|
if (mainAgentRemove.length > 0) {
|
|
854
927
|
const removable = new Set(
|
|
855
|
-
[
|
|
856
|
-
|
|
857
|
-
|
|
928
|
+
[
|
|
929
|
+
...restrictBase,
|
|
930
|
+
...expandToolPatterns(assignments.main_agent_extra ?? [], expandCandidates),
|
|
931
|
+
...customToolNames,
|
|
932
|
+
].filter((n) => usableWith(n, presetUniverse, inventory)),
|
|
858
933
|
)
|
|
859
934
|
for (const name of mainAgentRemove) {
|
|
860
935
|
if (!removable.has(name)) warn(`main_agent_remove: '${name}' 不在主 agent 当前 allow(未检测或不存在),已跳过`)
|
|
@@ -874,6 +949,18 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
|
|
|
874
949
|
}
|
|
875
950
|
if (restrictAllow !== null) out = injectRestrictAllow(out, restrictAllow)
|
|
876
951
|
|
|
952
|
+
// 委派标签角色名映射(delegation-labels 行的 config.roles):roles.<toolName>.name
|
|
953
|
+
// 的第二处生效点——内置与自定义角色一律收(配了名字才进映射,标签才带前缀);
|
|
954
|
+
// roles_remove 删除的角色委派行已整条不存在,跳过;全空则产物与源零 diff,
|
|
955
|
+
// 插件侧对空映射也不注册任何处理器。
|
|
956
|
+
const delegationLabelRoles = {}
|
|
957
|
+
for (const [toolName, role] of Object.entries(assignments.roles)) {
|
|
958
|
+
if (removedRoles.includes(toolName)) continue
|
|
959
|
+
const name = typeof role?.name === 'string' && role.name.trim() !== '' ? role.name.trim() : null
|
|
960
|
+
if (name !== null) delegationLabelRoles[toolName] = name
|
|
961
|
+
}
|
|
962
|
+
out = injectDelegationLabelRoles(out, delegationLabelRoles)
|
|
963
|
+
|
|
877
964
|
// Main-agent skills (dual mode, mirrors the roles' skill soft guidance):
|
|
878
965
|
// - main_agent_skills (soft): one compact "# skill: <name> — <description>"
|
|
879
966
|
// row + "Read full rules: <file>" per skill appended to the main-agent
|