dsh-paoding 0.4.3 → 0.4.5
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/architecture.md +6 -5
- package/docs/architecture_en.md +8 -7
- package/docs/configuration.md +5 -1
- package/docs/configuration_en.md +5 -1
- package/docs/installation.md +4 -3
- package/docs/installation_en.md +4 -3
- package/docs/orchestration.md +2 -2
- package/docs/orchestration_en.md +2 -2
- package/package.json +3 -2
- package/plugins/paoding-config-ui/api-core.mjs +89 -22
- package/plugins/paoding-config-ui/icon.svg +8 -0
- package/plugins/paoding-config-ui/index.mjs +10 -2
- package/plugins/paoding-config-ui/lib/client.js +12 -3
- package/plugins/paoding-config-ui/package.json +7 -2
- package/plugins/paoding-config-ui/runtime-inventory.mjs +22 -0
- package/presets/orchestrator/agent.cordis.yml +7 -3
- package/presets/orchestrator/restrict.mjs +1 -1
- 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
|
@@ -42,7 +42,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
42
42
|
import { getState, refreshState, isStateCacheStale, serializeState, installAssignments, resolveDshHome, discoverWorkspaceCandidates, ensurePresetInstalled, pluginVersion, exportConfigPayload, importConfigContent, listPresets, resolveWorkspacePathByPresetId, deleteWorkspacePreset } from './api-core.mjs'
|
|
43
43
|
import { collectModelRefs } from '../../tools/lib/health.mjs'
|
|
44
44
|
import { detectSkills } from '../../tools/lib/skills.mjs'
|
|
45
|
-
import { collectRuntimeFacts } from './runtime-inventory.mjs'
|
|
45
|
+
import { collectRuntimeFacts, detectHmrOf } from './runtime-inventory.mjs'
|
|
46
46
|
import { getVersionInfo } from '../../tools/lib/version.mjs'
|
|
47
47
|
import { runUpgrade } from '../../tools/lib/upgrade.mjs'
|
|
48
48
|
|
|
@@ -694,7 +694,15 @@ export async function apply(ctx) {
|
|
|
694
694
|
// 200 + error 字段,面板据此静默降级(不打扰使用)。timeoutMs 3000:
|
|
695
695
|
// 检测只是锦上添花,不值得让面板等更久。layout 告知客户端挂载形态
|
|
696
696
|
//(升级确认框按此分流警示文案),见 MOUNT_LAYOUT 注释。
|
|
697
|
-
|
|
697
|
+
// hotReloadable:宿主插件清单里有启用的 dsh-hmr 插件行(0.2.1+ web/
|
|
698
|
+
// desktop 默认)→ 升级后刷新页面即生效,确认框 / 成功 note 换
|
|
699
|
+
// 「刷新即生效」文案;探测失败 / 非 web 部署拿不到运行时 → false,
|
|
700
|
+
// 回落「需重启 DSH」文案。永不抛出(detectHmrOf 自捕)。
|
|
701
|
+
json(res, 200, {
|
|
702
|
+
...(await getVersionInfo({ timeoutMs: 3000 })),
|
|
703
|
+
layout: MOUNT_LAYOUT,
|
|
704
|
+
hotReloadable: detectHmrOf(ctx),
|
|
705
|
+
})
|
|
698
706
|
return
|
|
699
707
|
}
|
|
700
708
|
|
|
@@ -449,8 +449,10 @@ window.__ModuleLoader__.load({
|
|
|
449
449
|
"menu.upgradeConfirm": { zh: "升级", en: "Upgrade" },
|
|
450
450
|
"menu.upgradeDevBody": { zh: "当前为开发 link 直连形态:无法就地升级,请手动执行 dsh plugin add dsh-paoding@latest 切换到 registry 版本。仍要继续尝试吗?", en: "This is a dev link checkout: it cannot be upgraded in place. Run dsh plugin add dsh-paoding@latest manually to switch to the registry version. Continue anyway?" },
|
|
451
451
|
"menu.upgradeBody": { zh: "确定升级到 {v}?升级将通过插件通道原地换版本,完成后需重启 DSH 生效。", en: "Upgrade to {v}? The upgrade swaps the version in place through the plugin channel; restart DSH afterwards to apply it." },
|
|
452
|
+
"menu.upgradeBodyHot": { zh: "确定升级到 {v}?升级将通过插件通道原地换版本,完成后刷新页面即生效(编排预设自动按新版重生成)。", en: "Upgrade to {v}? The upgrade swaps the version in place through the plugin channel; refresh the page afterwards — orchestration presets regenerate automatically." },
|
|
452
453
|
"menu.upToDate": { zh: "已是最新版本", en: "Already up to date" },
|
|
453
454
|
"menu.upgraded": { zh: "✅ 已升级到 {v},重启 DSH 后新版本生效(preset 会自动按新版重生成)", en: "✅ Upgraded to {v} — restart DSH to apply the new version (the preset regenerates itself against it)" },
|
|
455
|
+
"menu.upgradedHot": { zh: "✅ 已升级到 {v},刷新页面即生效(preset 会自动按新版重生成)", en: "✅ Upgraded to {v} — refresh the page to apply (the preset regenerates itself against it)" },
|
|
454
456
|
"menu.errTail": { zh: ":{tail}", en: ": {tail}" },
|
|
455
457
|
"menu.langTip": { zh: "切换界面语言", en: "Switch UI language" },
|
|
456
458
|
};
|
|
@@ -3679,15 +3681,22 @@ window.__ModuleLoader__.load({
|
|
|
3679
3681
|
// 改通用确认弹窗,confirmCtrl 单例见 70 片段)文案按挂载形态分流:
|
|
3680
3682
|
// dev link 直连形态(layout 由服务端 version 路由按包根 .git 判定)无法
|
|
3681
3683
|
// 就地升级,把手动切换命令说在前头;installed(registry 版)原地换版本,
|
|
3682
|
-
//
|
|
3684
|
+
// 按宿主热重载能力(version 路由 hotReloadable:插件清单里有启用的
|
|
3685
|
+
// dsh-hmr 行)再分两级——有则「刷新页面即生效」,无则「重启 DSH 生效」。
|
|
3686
|
+
// 反馈(成功 note / 失败 err,含子进程输出尾部)都收在卡内,不与
|
|
3683
3687
|
// 面板的 toast 反馈混流。升级非破坏性操作,danger 不置位;弹窗本体由
|
|
3684
3688
|
// PageShell 末尾的 ConfirmLayer 渲染(页头与面板发起的确认共用一层)。
|
|
3685
3689
|
function runUpgradeFlow(verInfo, setVerInfo, setUpgBusy, setUpgNote, setUpgErr) {
|
|
3686
3690
|
if (!verInfo || !verInfo.latest) return;
|
|
3687
3691
|
var isDev = verInfo.layout === "dev";
|
|
3692
|
+
// 二级分流:宿主插件清单里有启用的 dsh-hmr 插件行(0.2.1+ web/desktop
|
|
3693
|
+
// 发布 profile 默认带上,version 路由的 hotReloadable 字段)时,插件
|
|
3694
|
+
// 通道原地换版本后刷新页面即生效,无需重启 DSH——确认框与成功 note
|
|
3695
|
+
// 都换「刷新即生效」文案;字段缺失 / 探测失败回落「需重启」文案。
|
|
3696
|
+
var hot = verInfo.hotReloadable === true;
|
|
3688
3697
|
var msg = isDev
|
|
3689
3698
|
? t("menu.upgradeDevBody")
|
|
3690
|
-
: t("menu.upgradeBody", { v: versionTagOf(verInfo.latest) });
|
|
3699
|
+
: t(hot ? "menu.upgradeBodyHot" : "menu.upgradeBody", { v: versionTagOf(verInfo.latest) });
|
|
3691
3700
|
confirmCtrl.ask({
|
|
3692
3701
|
title: t("menu.upgradeTitle"),
|
|
3693
3702
|
body: msg,
|
|
@@ -3702,7 +3711,7 @@ window.__ModuleLoader__.load({
|
|
|
3702
3711
|
var done = versionTagOf((r && r.version) || verInfo.latest);
|
|
3703
3712
|
// 服务端返回了 message 就原文透传(服务端消息不做客户端翻译),
|
|
3704
3713
|
// 没有才落本地化兜底文案。
|
|
3705
|
-
setUpgNote((r && r.message) || t("menu.upgraded", { v: done }));
|
|
3714
|
+
setUpgNote((r && r.message) || t(hot ? "menu.upgradedHot" : "menu.upgraded", { v: done }));
|
|
3706
3715
|
// 重拉版本信息:服务端有 1 小时缓存且本进程挂的还是旧代码,重启前
|
|
3707
3716
|
// 更新卡可能仍在(重启后自然消失),以此处的生效提示为准。
|
|
3708
3717
|
paodingApi("version").then(function (d) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paoding-config-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
7
|
-
"
|
|
7
|
+
"icon": "./icon.svg",
|
|
8
|
+
"description": "庖丁 (dsh-paoding) 可视化配置器 — DSH Web 左侧栏全页配置插件(侧栏入口 + /api/paoding 路由)· Visual config panel for dsh-paoding in the DSH Web sidebar",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18",
|
|
11
|
+
"dsh": ">=0.1.5 <0.3"
|
|
12
|
+
},
|
|
8
13
|
"dsh": {
|
|
9
14
|
"client": {
|
|
10
15
|
"platform": "web",
|
|
@@ -206,6 +206,28 @@ function detectPresetSystemOf(ctx) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
* 宿主热重载能力探测:插件清单里存在 @deepseek-ai/dsh-hmr 插件行(dsh
|
|
211
|
+
* 0.2.1+ 的 web / desktop 发布 profile 默认带上)且已启用,即认为宿主支持
|
|
212
|
+
* 插件热重载——面板升级(插件通道原地换版本)完成后刷新页面即生效,无需
|
|
213
|
+
* 重启 DSH。取数复用 collectPluginEntries 的路径(getService
|
|
214
|
+
* ('pluginInventory') → list() → entries);服务缺席 / 形状异常 / 任何异常
|
|
215
|
+
* 一律 false(安全侧:回落「需重启」文案),永不抛出。
|
|
216
|
+
*/
|
|
217
|
+
export function detectHmrOf(ctx) {
|
|
218
|
+
try {
|
|
219
|
+
const { entries, ok } = collectPluginEntries(ctx, [])
|
|
220
|
+
if (!ok) return false
|
|
221
|
+
// collectPluginEntries 已剔除 enabled===false 的条目,此处再判一次仅为
|
|
222
|
+
// 兑现判定语义(enabled!==false);moduleName / entryId 任一含 'dsh-hmr'
|
|
223
|
+
// 即命中(不同宿主版本两字段的命名习惯有差异,取并集)。
|
|
224
|
+
return entries.some((e) => e.enabled !== false
|
|
225
|
+
&& (e.moduleName?.includes('dsh-hmr') || e.entryId?.includes('dsh-hmr')))
|
|
226
|
+
} catch {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
/**
|
|
210
232
|
* 采集运行时事实(供 collectState 合并 / UI 展示)。
|
|
211
233
|
* 永不抛出:任何异常都会落到 error 里,返回部分结果。
|
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
|
|
19
19
|
# ── identity ────────────────────────────────────────────────────────────────
|
|
20
20
|
|
|
21
|
-
# 编排人设:{{model}}
|
|
21
|
+
# 编排人设:{{model}} 由 agent 的 route 解析;{{cwd}} 禁用(见下)。
|
|
22
|
+
# 注意:作为 agent-preset-registry 选中 preset 挂部署主代理时,0.2.1 的
|
|
23
|
+
# deployment:persona-prefix 段只注册 provider/model——禁用 {{cwd}}(纯装饰,
|
|
24
|
+
# 工作目录已由宿主 env 上下文提供)。
|
|
22
25
|
- id: persona
|
|
23
26
|
name: '@deepseek-ai/dsh-persona'
|
|
24
27
|
config:
|
|
25
28
|
prefix: |-
|
|
26
|
-
You are the orchestrator agent powered by the {{model}} model.
|
|
29
|
+
You are the orchestrator agent powered by the {{model}} model.
|
|
27
30
|
|
|
28
31
|
Decompose the task, delegate to role agents, integrate results.
|
|
29
32
|
- Internal file questions (finding code, references, reading files): solve yourself with glob/grep/read (and codegraph if enabled) — never delegate them.
|
|
@@ -240,11 +243,12 @@
|
|
|
240
243
|
toolName: search_external
|
|
241
244
|
backgroundMode: one-shot
|
|
242
245
|
persona: |-
|
|
243
|
-
You are the external-research agent. Your job is web research only — use web_search and the tavily tools; you never modify files. Search the web, extract key facts, and return a concise structured research summary (findings, sources, open questions).
|
|
246
|
+
You are the external-research agent. Your job is web research only — use web_search, web_fetch and the tavily tools; you never modify files. Search the web, extract key facts, and return a concise structured research summary (findings, sources, open questions).
|
|
244
247
|
On tool failure, do not retry a denied operation: report the failure compactly — what you completed, the exact error, which tool/permission you lack, and your suggested next step — so the orchestrator can re-delegate or adjust.
|
|
245
248
|
toolFilter:
|
|
246
249
|
allow:
|
|
247
250
|
- web_search
|
|
251
|
+
- web_fetch
|
|
248
252
|
- mcp__tavily__tavily_search
|
|
249
253
|
- mcp__tavily__tavily_crawl
|
|
250
254
|
- mcp__tavily__tavily_extract
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* 停用即自动清除。本文件只保留 dsh 核心工具。
|
|
25
25
|
*
|
|
26
26
|
* What the main agent deliberately does NOT see (all costs drop to zero for
|
|
27
|
-
* its requests): web_search, every mcp__tavily__*, write / edit /
|
|
27
|
+
* its requests): web_search, web_fetch, every mcp__tavily__*, write / edit /
|
|
28
28
|
* str_replace_editor, skill (+ its ~1.3k-token catalog), workflow, ralph,
|
|
29
29
|
* ssh_*, mcp__tablepro__*, describe_image, and the unfiltered subagent /
|
|
30
30
|
* subagent_fork delegations.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role tool-surface filter (role-restrict.mjs)
|
|
3
|
+
*
|
|
4
|
+
* Part of the dsh-paoding 编排模式 (orchestrator) preset — one copy is mounted
|
|
5
|
+
* inside every per-role AgentPreset (paoding-role-<toolName>) that the
|
|
6
|
+
* generator emits for DSH 0.2.1+ hosts. It hooks the `system-prompt/assemble`
|
|
7
|
+
* waterfall and narrows the MOUNTING agent's model-facing tool catalog to the
|
|
8
|
+
* role's `config.allow` list (the former inline `toolFilter.allow` of the
|
|
9
|
+
* delegation row, relocated here by the 0.2.1 migration — dsh-tool-subagent
|
|
10
|
+
* rejects inline persona / toolFilter since 0.2.1, so the role surface now
|
|
11
|
+
* lives in the preset's plugin composition instead).
|
|
12
|
+
*
|
|
13
|
+
* Semantics are the MIRROR of the orchestrator restrict.mjs: that one spares
|
|
14
|
+
* delegated children (origin: 'subagent') because it sits in the MAIN agent's
|
|
15
|
+
* preset; this one sits in the role agent's own preset, so it filters
|
|
16
|
+
* unconditionally — no delegated-child escape hatch, and no registration-time
|
|
17
|
+
* deny either (a role allow list may contain 'skill'; the skill directory
|
|
18
|
+
* injection guard runs at registration level and must keep passing).
|
|
19
|
+
*
|
|
20
|
+
* The filter is fail-open: any unexpected error returns the assembly
|
|
21
|
+
* unchanged so a filter bug can never brick a delegated run. It filters the
|
|
22
|
+
* POST-registration assembly (the same timing as restrict.mjs): every allow
|
|
23
|
+
* name has been resolved by the time the waterfall runs, so names that are
|
|
24
|
+
* not currently mounted are simply absent — never an error. Restricting
|
|
25
|
+
* during plugin apply() instead would validate allow names against a
|
|
26
|
+
* partially-mounted registry (MCP tools may register later) and wrongly
|
|
27
|
+
* reject role creation.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
31
|
+
export const name = 'paoding-role-restrict'
|
|
32
|
+
|
|
33
|
+
/** Prompt assembly and the tool registry must exist before this filter runs. */
|
|
34
|
+
export const inject = ['systemPrompt', 'tools']
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default allow list. The generator always injects the role's explicit
|
|
38
|
+
* `config.allow`; this fallback only exists so a hand-mounted instance
|
|
39
|
+
* without config fails open to "no filtering" instead of stripping every
|
|
40
|
+
* tool from the role agent (an empty allow would brick every delegated run).
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_ALLOW = null
|
|
43
|
+
|
|
44
|
+
/** Warn once so a misbehaving filter never spams the log per request. */
|
|
45
|
+
let warned = false
|
|
46
|
+
|
|
47
|
+
export function apply(ctx, config = {}) {
|
|
48
|
+
const allow = config.allow === undefined ? DEFAULT_ALLOW : new Set(config.allow)
|
|
49
|
+
if (allow === null || allow.size === 0) return
|
|
50
|
+
|
|
51
|
+
ctx.on(
|
|
52
|
+
'system-prompt/assemble',
|
|
53
|
+
async (_assembly, context, next) => {
|
|
54
|
+
// Downstream errors propagate untouched (the same contract as the
|
|
55
|
+
// orchestrator restrict.mjs): only this filter's own logic is guarded.
|
|
56
|
+
const assembled = await next()
|
|
57
|
+
try {
|
|
58
|
+
// Unconditional by design: this preset is only ever mounted for role
|
|
59
|
+
// agents (explicit agentPreset selection skips parent inheritance),
|
|
60
|
+
// so whatever agent assembles here IS the role agent.
|
|
61
|
+
const tools = Array.isArray(assembled.tools) ? assembled.tools : []
|
|
62
|
+
const filtered = tools.filter((tool) => allow.has(tool?.name))
|
|
63
|
+
if (!warned) {
|
|
64
|
+
ctx.logger.info(
|
|
65
|
+
`${name}: role agent tool surface ${tools.length} -> ${filtered.length} tools (allow=${[...allow].sort().join(', ')})`,
|
|
66
|
+
)
|
|
67
|
+
warned = true
|
|
68
|
+
}
|
|
69
|
+
return { ...assembled, tools: filtered }
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Fail open: a filter bug must never brick a delegated run.
|
|
72
|
+
ctx.logger.warn(`${name}: filter failed, exposing full catalog: ${String(error)}`)
|
|
73
|
+
return assembled
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{ prepend: true },
|
|
77
|
+
)
|
|
78
|
+
}
|
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
|
|