dsh-paoding 0.2.2 → 0.3.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.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "paoding-config-ui",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
- "description": "庖丁 (dsh-paoding) 可视化配置器 — DSH Web 设置页集成插件(settings.section 分区 + /api/paoding 路由)",
7
+ "description": "庖丁 (dsh-paoding) 可视化配置器 — DSH Web 左侧栏全页配置插件(侧栏入口 + /api/paoding 路由)",
8
8
  "dsh": {
9
9
  "client": {
10
10
  "platform": "web",
@@ -62,8 +62,9 @@ function collectToolNames(value, out, seen) {
62
62
  } else if (item && typeof item === 'object') {
63
63
  if (typeof item.name === 'string') {
64
64
  pushName(item.name, out, seen)
65
- } else {
66
- // 兜底:{ [key]: def } 形态的对象,按 key/def.name 处理
65
+ } else if (Object.keys(item).length === 1) {
66
+ // 兜底:{ [key]: def } 形态的对象,按 key/def.name 处理——仅接受单键
67
+ // 对象(多键对象没有唯一「工具名」语义,跳过)
67
68
  for (const [k, v] of Object.entries(item)) pushPair(k, v, out, seen)
68
69
  }
69
70
  }
@@ -93,25 +94,15 @@ function getService(ctx, key) {
93
94
  return undefined
94
95
  }
95
96
 
96
- /** 收集一个对象的可调用方法名(沿原型链,去重,限量)。 */
97
- function collectMethodNames(svc) {
98
- const methods = []
99
- for (let proto = Object.getPrototypeOf(svc); proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
100
- for (const m of Object.getOwnPropertyNames(proto)) {
101
- if (m !== 'constructor' && typeof svc[m] === 'function' && !methods.includes(m)) methods.push(m)
102
- }
103
- }
104
- return methods
105
- }
106
-
107
97
  /**
108
98
  * 读取运行时插件清单。返回 { entries, ok }:ok 表示成功拿到 list() 结果
109
99
  * (空清单也算成功,区别于服务不可用)。
110
100
  */
111
101
  function collectPluginEntries(ctx, errors) {
112
102
  const pi = getService(ctx, 'pluginInventory')
113
- if (pi == null || typeof pi.list !== 'function') return { entries: [], ok: false }
114
103
  try {
104
+ // 服务属性访问也纳入 try(cordis 代理 getter 理论上可能抛):兑现「永不抛」契约
105
+ if (pi == null || typeof pi.list !== 'function') return { entries: [], ok: false }
115
106
  const result = pi.list()
116
107
  // list() 官方返回 { entries: [...] };兼容直接返回数组的形态
117
108
  const raw = Array.isArray(result) ? result : Array.isArray(result?.entries) ? result.entries : null
@@ -135,7 +126,7 @@ function collectPluginEntries(ctx, errors) {
135
126
  }
136
127
  return { entries, ok: true }
137
128
  } catch (err) {
138
- errors.push(`pluginInventory.list() threw: ${String(err?.message ?? err)}`)
129
+ errors.push(`pluginInventory.list() failed: ${String(err?.message ?? err)}`)
139
130
  return { entries: [], ok: false }
140
131
  }
141
132
  }
@@ -145,20 +136,26 @@ function collectPluginEntries(ctx, errors) {
145
136
  * 1) t.entries() 若可调用(Map / 迭代器 / 数组 归一化);
146
137
  * 2) t.schemas() 若可调用(返回 [{ name, description, ... }],全局视图 =
147
138
  * 全部已注册工具,含 mcp__<server>__<tool>);
148
- * 3) 在方法名里找含 entries/list/schemas/keys 的可调用方法逐个 try 调用
149
- * (不硬猜 presentAs/visible 这类语义方法);
150
- * 4) 全部失败返回空数组。返回 { names, ok }。
139
+ * 3) 兜底:只试白名单里的只读枚举方法名(entries/schemas/list/keys/
140
+ * listKeys),不做正则泛配(避免误调写方法);
141
+ * 4) 全部失败返回空数组(原因记入 errors)。返回 { names, ok }。
151
142
  */
152
143
  function readRegisteredToolNames(ctx, errors) {
153
144
  const out = []
154
145
  const seen = new Set()
155
146
  const t = getService(ctx, 'tools')
156
- if (t == null) return { names: [], ok: false }
147
+ if (t == null) {
148
+ // 单侧失败也留痕:UI 可区分「无工具」与「tools 服务不可用」
149
+ errors.push('tools service unavailable')
150
+ return { names: [], ok: false }
151
+ }
157
152
  const tried = new Set()
158
153
  const attempt = (method, source) => {
159
- if (typeof t[method] !== 'function' || tried.has(method)) return false
154
+ if (tried.has(method)) return false
160
155
  tried.add(method)
161
156
  try {
157
+ // 方法属性读取也纳入 try(cordis 代理 getter 可能抛)
158
+ if (typeof t[method] !== 'function') return false
162
159
  const result = t[method]()
163
160
  // 拿到可迭代结果即视为“服务可用”(即使为空清单);无结果 / 抛错都跳过
164
161
  if (result != null && typeof result[Symbol.iterator] === 'function') {
@@ -176,14 +173,17 @@ function readRegisteredToolNames(ctx, errors) {
176
173
  let acquired = attempt('entries', 'tools.entries()')
177
174
  if (!acquired) acquired = attempt('schemas', 'tools.schemas()')
178
175
 
179
- // 3) 兜底:在方法列表里找名字含 entries/list/schemas/keys 的函数逐个尝试
176
+ // 3) 兜底:只试白名单里的只读枚举方法名(不做正则泛配);已在 1)/2) 试过
177
+ // 的 entries/schemas 由 tried 去重,不会重复调用
180
178
  if (!acquired) {
181
- const hint = /entries|list|schemas|keys/
182
- for (const m of collectMethodNames(t)) {
183
- if (!hint.test(m)) continue
179
+ for (const m of ['entries', 'schemas', 'list', 'keys', 'listKeys']) {
184
180
  if (attempt(m, `tools.${m}()`)) { acquired = true; break }
185
181
  }
186
182
  }
183
+ // 有服务但枚举口全不可读:同样留痕(区别于「服务可用但工具清单为空」)
184
+ if (!acquired) {
185
+ errors.push('tools service present but no readable enumeration (entries/schemas/list/keys/listKeys)')
186
+ }
187
187
  return { names: out, ok: acquired }
188
188
  }
189
189
 
@@ -197,9 +197,10 @@ export function collectRuntimeFacts(ctx) {
197
197
  const tools = readRegisteredToolNames(ctx, errors)
198
198
 
199
199
  // ok 语义:pluginInventory 与 tools 至少一个服务真正可用即为成功;
200
- // 两个都拿不到才整体失败(UI 据此提示“已回退文件扫描”)。
200
+ // 两个都拿不到才整体失败(UI 据此提示“已回退文件扫描”)。单侧失败的原因
201
+ // 也已各自记入 errors(上方),UI 可区分「无工具」与「tools 服务不可用」。
201
202
  const ok = plugin.ok || tools.ok
202
- if (!ok) errors.unshift('tools service unavailable (pluginInventory/tools both missing)')
203
+ if (!ok) errors.unshift('pluginInventory/tools both unavailable: fell back to file scan')
203
204
 
204
205
  let error = errors.join('; ')
205
206
  if (error.length > MAX_ERROR_LEN) error = error.slice(0, MAX_ERROR_LEN) + '…'
@@ -7,11 +7,11 @@
7
7
  # · orchestrator-restrict → restrict.mjs 在 system-prompt/assemble 瀑布上把
8
8
  # 主 agent 的工具面裁剪为「编排 + 内部搜索」白名单;子 agent(origin:
9
9
  # 'subagent')放行,由各自 toolFilter 决定工具面
10
- # · delegation-* → 3 个角色委派工具(dsh-tool-subagent),子 agent 创建时
10
+ # · delegation-* → 4 个角色委派工具(dsh-tool-subagent,search_internal_deep 为静态委派),子 agent 创建时
11
11
  # 以 per-child tools.restrict() 只保留各自 allow 的工具
12
12
  #
13
- # 工具税(每请求估算):主 agent 全量 ~16.2k → 白名单后 ~5.5k(含 3 个委派工具);
14
- # search_external ~3.0k / design ~3.2k / implement ~3.7k,仅实际委派时发生。
13
+ # 工具税(每请求估算):主 agent 全量 ~16.2k → 白名单后 ~5.5k(含 4 个委派工具);
14
+ # search_external ~3.0k / design ~3.3k / implement ~3.8k,仅实际委派时发生。
15
15
 
16
16
  # ── identity ────────────────────────────────────────────────────────────────
17
17
 
@@ -19,11 +19,11 @@
19
19
  - id: persona
20
20
  name: '@deepseek-ai/dsh-persona'
21
21
  config:
22
- text: |-
22
+ prefix: |-
23
23
  You are the orchestrator agent powered by the {{model}} model. Your working directory is {{cwd}}.
24
24
 
25
25
  Decompose the task, delegate to role agents, integrate results.
26
- - Internal file questions (finding code, references, reading files): solve yourself with glob/grep/read/codegraph — never delegate them.
26
+ - Internal file questions (finding code, references, reading files): solve yourself with glob/grep/read (and codegraph if enabled) — never delegate them.
27
27
  - External research: delegate to search_external.
28
28
  - UI/design work: delegate to design.
29
29
  - Code implementation: delegate to implement.
@@ -43,6 +43,11 @@
43
43
  · aborted → only re-delegate when the cancellation was accidental.
44
44
  · completed but poor output → re-delegate with concrete missing points, or send_message
45
45
  to ask a continuable child to finish the gaps.
46
+ - Continuable roles (background_mode: continuable): when a delegated run fails or
47
+ the output falls short, first send_message in the SAME child session to continue
48
+ and fix in place — resuming keeps the child's accumulated context, while
49
+ re-delegating starts a cold task and repeats finished work. Only re-delegate
50
+ when the child session is gone or the task needs a different role.
46
51
  - If the same task fails twice, stop retrying: report to the user what failed, why,
47
52
  and what you already tried. Never loop a failing delegation.
48
53
  - When re-delegating (fresh task or continuation), always include your integration
@@ -167,6 +172,7 @@
167
172
  - id: tool-subagent-list-agents
168
173
  name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
169
174
 
175
+ # subagent / subagent_fork / workflow-worker-thread / workflow / ralph 仅当用户经 main_agent_extra 显式开启时对主 agent 可达;保留以对齐 standard 全量组合。
170
176
  - id: tool-subagent
171
177
  name: '@deepseek-ai/dsh-tool-subagent'
172
178
  config:
@@ -207,6 +213,7 @@
207
213
  config:
208
214
  provider: spawn
209
215
  toolName: search_external
216
+ backgroundMode: one-shot
210
217
  persona: |-
211
218
  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).
212
219
  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.
@@ -223,12 +230,13 @@
223
230
  - read
224
231
  - ask_user_question
225
232
 
226
- # design:设计(读参考 + 设计技能 + 出 spec,不写最终代码)~3.2k
233
+ # design:设计(读参考 + 设计技能 + 出 spec,不写最终代码)~3.3k
227
234
  - id: delegation-design
228
235
  name: '@deepseek-ai/dsh-tool-subagent'
229
236
  config:
230
237
  provider: spawn
231
238
  toolName: design
239
+ backgroundMode: one-shot
232
240
  persona: |-
233
241
  You are the design agent. You produce UI/UX designs, wireframes, and frontend specs — and may load design skills (frontend-design, html-ppt) via the skill tool. Read reference material first, then deliver a concrete design spec. You do not implement final code.
234
242
  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.
@@ -240,26 +248,27 @@
240
248
  - grep
241
249
  - bash
242
250
  - skill
243
- - write
244
- - edit
251
+ - web_search
245
252
  - ask_user_question
246
253
  - todo_write
247
254
  - job_output
248
255
  - job_list
249
256
  - job_kill
250
257
 
251
- # implement:实现(读上下文 → 外科手术式修改 → 验证)~3.7k
258
+ # implement:实现(读上下文 → 外科手术式修改 → 验证)~3.8k
252
259
  - id: delegation-implement
253
260
  name: '@deepseek-ai/dsh-tool-subagent'
254
261
  config:
255
262
  provider: spawn
256
263
  toolName: implement
264
+ backgroundMode: one-shot
257
265
  persona: |-
258
266
  You are the implementation agent. You write and edit code: read surrounding context first, make surgical changes, then verify (build/test/grep). Report what you changed and how you verified. You do not redesign architecture.
259
267
  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.
260
268
  toolFilter:
261
269
  allow:
262
270
  - read
271
+ - read_image
263
272
  - write
264
273
  - edit
265
274
  - glob
@@ -287,6 +296,7 @@
287
296
  config:
288
297
  provider: spawn
289
298
  toolName: search_internal_deep
299
+ backgroundMode: one-shot
290
300
  persona: |-
291
301
  You are the internal deep-search agent. Your job is repository exploration only — you never modify files and you never use the network. Handle exactly the tasks the orchestrator can't afford in its own context: whole-repo grep dumps, full-file reads beyond a page, cross-directory symbol/definition tracking. Search thoroughly, then return a CONCISE structured summary: what you searched, key files with line numbers, definitions/symbols found, and the takeaway the orchestrator needs to decide next. Never echo large dumps back — condense aggressively; the orchestrator re-delegates or reads specific ranges itself if it needs details.
292
302
  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.
@@ -1,6 +1,7 @@
1
1
  name: 编排模式 (Orchestrator)
2
2
  description: 按角色分工的编排架构——主 agent 只做编排与内部搜索(glob/grep/read/codegraph),
3
- 通过 search_external / design / implement 三个委派工具把联网调研、设计、实现分别交给
4
- 各自只加载必要工具的子 agent;主 agent 不加载任何外部搜索、写文件或技能工具,
3
+ 通过 search_external / design / implement / search_internal_deep 四个委派工具(search_internal_deep
4
+ 为静态委派)把联网调研、设计、实现、内部深搜分别交给各自只加载必要工具的子 agent;
5
+ 主 agent 不加载任何外部搜索、写文件或技能工具,
5
6
  每请求工具税从 ~16.2k tokens 降到 ~5.5k。主 agent 技能目录注入亦归零(实测 ≈1.3k tokens/轮)。
6
7
  order: 5
@@ -10,7 +10,8 @@
10
10
  * would intersect with — and wrongly shrink — those role surfaces.
11
11
  *
12
12
  * What the main agent keeps:
13
- * - orchestration: search_external / design / implement (role delegation),
13
+ * - orchestration: 4 role-delegation tools — search_external / design /
14
+ * implement / search_internal_deep (the last one a static delegation),
14
15
  * send_message / list_agents / interrupt_agent (child management)
15
16
  * - internal search (hot path, zero delegation round-trip): glob / grep /
16
17
  * read / read_image / bash
package/tools/install.mjs CHANGED
@@ -3,18 +3,23 @@
3
3
  * dsh-paoding installer — CLI 入口 + 公共 API 聚合薄层。
4
4
  *
5
5
  * 原单文件(2767 行)的功能实现已按模块拆分到 tools/lib/*.mjs(util / yaml /
6
- * config / host / skills / spans / compose / alloc / wizard / state / cli 模块
7
- * 树);本文件仅保留:
6
+ * config / host / skills / spans / compose / alloc / state / cli / workspaces /
7
+ * version / upgrade 模块树);本文件仅保留:
8
8
  * - 公共导出面(与旧单文件完全一致,供 plugins/paoding-config-ui 消费):
9
- * DEFAULT_MAIN_AGENT_PERSONA_EXTRA / collectState / generateAndInstall;
9
+ * DEFAULT_MAIN_AGENT_PERSONA_EXTRA / collectState / generateAndInstall
10
+ * (外加工作区 slug / preset id 助手 assignSlugs / presetIdOf /
11
+ * sanitizeSlugBase / stableHash);
10
12
  * - 直接运行守卫(CLI entry:import 本模块不触发安装,仅直接执行时经
11
- * lib/cli.mjs 的 main() 运行)。
13
+ * lib/cli.mjs 的 main() 运行;官方安装通道是 dsh plugin add dsh-paoding,
14
+ * 本 CLI 仅作仓库内开发兜底)。
12
15
  */
13
16
  import { pathToFileURL } from 'node:url'
14
17
  import { main } from './lib/cli.mjs'
15
18
 
16
19
  export { DEFAULT_MAIN_AGENT_PERSONA_EXTRA } from './lib/util.mjs'
17
20
  export { collectState, generateAndInstall } from './lib/state.mjs'
21
+ // 工作区 slug / preset id(api-core 的 workspaceMeta 等插件侧消费)。
22
+ export { assignSlugs, presetIdOf, sanitizeSlugBase, stableHash } from './lib/workspaces.mjs'
18
23
 
19
24
  // CLI entry: only run main() when invoked directly (importing this module for
20
25
  // the visual config UI must not trigger an install).
@@ -2,8 +2,10 @@
2
2
  * 分配策略:smartDefaults 按工具名关键词(keywords)给出默认角色分配、在无既有
3
3
  * 配置时作 --auto 的兜底 assignments;baseAssignments 给出「基础模板」(仅 dsh
4
4
  * 内置工具,fresh 非交互安装的默认起点);resolveAssignments 把既有配置 / 建议 /
5
- * 静态基础合成最终 assignments。纯参数计算,仅依赖 util 的 ROLES 常量;
6
- * 被 state(smartDefaults)/ cli(resolveAssignments / baseAssignments)引用。
5
+ * 静态基础合成最终 assignments;autoAssignments 是 --auto 语义的一站式入口
6
+ * (CLI --auto 分支与插件首装 ensurePresetInstalled 共享,勿两边复制粘贴)。
7
+ * 纯参数计算,仅依赖 util 的 ROLES 常量;被 state(smartDefaults)/ cli /
8
+ * plugins/paoding-config-ui(autoAssignments)引用。
7
9
  */
8
10
  import { ROLES } from './util.mjs'
9
11
 
@@ -12,11 +14,15 @@ import { ROLES } from './util.mjs'
12
14
 
13
15
  /**
14
16
  * Default host-tool → role assignment used by --auto without a config file
15
- * and as the wizard's suggested values. codegraph and memory_search stay on
17
+ * and as the suggested values shown in the visual config UI. codegraph and
18
+ * memory_search stay on
16
19
  * the main agent (matching the static preset); other MCP tools are suggested
17
- * by tool-name keywords. 角色条目形状 { model, provider, persona, tools }:
20
+ * by tool-name keywords. 角色条目形状 { model, provider, background_mode, persona, tools }:
18
21
  * model / provider(角色专用模型)在此恒为 null(= 不注入 agentOptions,子 agent
19
- * 继承主 agent 当前模型),仅配置文件 / 向导 4d 显式设置。
22
+ * 继承主 agent 当前模型),仅配置文件 / 配置面板 4d 显式设置;background_mode
23
+ * (roles.<toolName>.background_mode,可续模式)在此恒为 'one-shot'(= 不注入
24
+ * backgroundMode 行,dsh-tool-subagent 缺省即 one-shot),仅配置文件 / 配置面板
25
+ * 显式设置为 'continuable'。
20
26
  */
21
27
  export function smartDefaults(mcpReports, inventory, staticBase) {
22
28
  const roles = {}
@@ -27,7 +33,9 @@ export function smartDefaults(mcpReports, inventory, staticBase) {
27
33
  const tools = [...inventory].filter((name) => name.startsWith(`mcp__${server}__`))
28
34
  byServer.set(server, tools)
29
35
  const assign = (role) => {
30
- roles[role] ??= { model: null, provider: null, persona: null, tools: [...staticBase[role]] }
36
+ // 与 baseAssignments / resolveAssignments 同形状(name 恒 null = 不改名):
37
+ // 缺 name 键会让下游形状断言/序列化层按缺省处理,和 baseAssignments 不一致。
38
+ roles[role] ??= { name: null, model: null, provider: null, background_mode: 'one-shot', persona: null, tools: [...staticBase[role]] }
31
39
  for (const t of tools) if (!roles[role].tools.includes(t)) roles[role].tools.push(t)
32
40
  }
33
41
  if (server === 'codegraph') {
@@ -44,7 +52,8 @@ export function smartDefaults(mcpReports, inventory, staticBase) {
44
52
  }
45
53
  if (inventory.has('memory_search')) extra.push('memory_search')
46
54
  // main_agent_persona_extra 不在此赋值(null 即可):compose 时 null 回落默认常量。
47
- // roles_remove / main_agent_name 不在此赋值([] / null 即可):删除与改名是显式配置。
55
+ // roles_remove / main_agent_display_name 不在此赋值([] / null 即可):删除是
56
+ // 显式配置(显示名同理,null = 默认「编排模式 (Orchestrator)」)。
48
57
  return {
49
58
  roles,
50
59
  roles_remove: [],
@@ -53,7 +62,7 @@ export function smartDefaults(mcpReports, inventory, staticBase) {
53
62
  main_agent_skills: [],
54
63
  main_agent_skills_inline: [],
55
64
  main_agent_persona_extra: null,
56
- main_agent_name: null,
65
+ main_agent_display_name: null,
57
66
  skills: {},
58
67
  }
59
68
  }
@@ -64,19 +73,21 @@ export function keywords(names, pattern) {
64
73
 
65
74
  /**
66
75
  * 基础模板 assignments(fresh 非交互安装的默认起点):每个内置角色保留静态
67
- * preset 的 dsh 基础工具,host/MCP 工具一概不纳入 —— npx 场景先装完可用基础
68
- * 版,host 工具留给用户在 设置 → 庖丁配置(或向导)里显式开启,避免智能默认
69
- * 把检测到的 host 工具静默塞进委派链。形状与 smartDefaults 返回值完全一致,
70
- * 生成层 / 序列化层无需区分来源;staticBase 复制而非别名,防调用方原地改写。
76
+ * preset 的 dsh 基础工具,host/MCP 工具一概不纳入 —— 首装(插件通道启动自愈
77
+ * 或 --auto)先装完可用基础版,host 工具留给用户在 设置 → 庖丁配置 里显式
78
+ * 开启,避免智能默认把检测到的 host 工具静默塞进委派链。形状与 smartDefaults
79
+ * 返回值完全一致,生成层 / 序列化层无需区分来源;staticBase 复制而非别名,
80
+ * 防调用方原地改写。每个内置角色条目显式带 background_mode: 'one-shot',随
81
+ * serializeConfig 恒写出 —— fresh 落盘的配置文件里旋钮可见。
71
82
  */
72
83
  export function baseAssignments(staticBase) {
73
84
  const roles = {}
74
85
  for (const role of ROLES) {
75
86
  // static preset 的角色 allow 自带静态 host 工具名(如 mcp__tavily__*);模板
76
87
  // 一律剔除 mcp__ 前缀名(KNOWN_HOST_PLUGINS 工具不在 staticBase 里,无需再滤),
77
- // 保证落盘的配置文件只含 dsh 基础工具 —— host 工具全部经 UI/向导显式开启。
88
+ // 保证落盘的配置文件只含 dsh 基础工具 —— host 工具全部经 UI 显式开启。
78
89
  const baseTools = (staticBase[role] ?? []).filter((name) => !name.startsWith('mcp__'))
79
- roles[role] = { name: null, model: null, provider: null, persona: null, tools: [...baseTools] }
90
+ roles[role] = { name: null, model: null, provider: null, background_mode: 'one-shot', persona: null, tools: [...baseTools] }
80
91
  }
81
92
  return {
82
93
  roles,
@@ -86,18 +97,20 @@ export function baseAssignments(staticBase) {
86
97
  main_agent_skills: [],
87
98
  main_agent_skills_inline: [],
88
99
  main_agent_persona_extra: null,
89
- main_agent_name: null,
100
+ main_agent_display_name: null,
90
101
  skills: {},
91
102
  }
92
103
  }
93
104
 
94
105
  /**
95
106
  * Resolve the final assignments from the existing config, smart defaults, or
96
- * the interactive wizard. Returns { roles, roles_remove, main_agent_extra,
107
+ * the visual config UI. Returns { roles, roles_remove, main_agent_extra,
97
108
  * main_agent_remove, main_agent_skills, main_agent_skills_inline,
98
- * main_agent_persona_extra, main_agent_name, skills };roles 条目形状
99
- * { name, model, provider, persona, tools }(model / provider = 角色专用模型,
100
- * 缺省 null = 不注入 agentOptions)。
109
+ * main_agent_persona_extra, main_agent_display_name, skills };
110
+ * roles 条目形状
111
+ * { name, model, provider, background_mode, persona, tools }(model / provider =
112
+ * 角色专用模型,缺省 null = 不注入 agentOptions;background_mode = 角色可续模式,
113
+ * 缺省 'one-shot' = 不注入 backgroundMode 行)。
101
114
  */
102
115
  export function resolveAssignments(existing, suggested, staticBase) {
103
116
  if (existing) {
@@ -111,28 +124,46 @@ export function resolveAssignments(existing, suggested, staticBase) {
111
124
  name: role.name ?? null,
112
125
  model: role.model ?? null,
113
126
  provider: role.provider ?? null,
127
+ // 缺键(老配置 / UI 手工构造的半成品对象)回落 'one-shot';非法值由
128
+ // 生成层按「非 'continuable' 即 one-shot」口径兜底。
129
+ background_mode: role.background_mode ?? 'one-shot',
114
130
  persona: role.persona ?? null,
115
- tools: role.tools ?? (suggested.roles[toolName]?.tools ?? staticBase[toolName] ?? []),
131
+ // 回落的 suggested/staticBase 数组包一层复制,防与来源别名后一处
132
+ // 原地 push 污染另一处(如 smartDefaults 的 assign 复用 suggested)。
133
+ tools: role.tools ?? [...(suggested.roles[toolName]?.tools ?? staticBase[toolName] ?? [])],
116
134
  }
117
135
  }
118
136
  return {
119
137
  roles,
120
138
  roles_remove: existing.roles_remove ?? [],
121
- // main_agent_extra 回填语义:配置文件里「显式写了键」(含显式空列表,见
122
- // normalizeConfig.has_main_agent_extra)一律尊重 —— 显式 [] = 用户明确不
123
- // 要任何 host 工具,不得被智能默认悄悄回填;只有键完全缺失(老配置,无
124
- // 该标记)才按旧行为用 suggested.main_agent_extra 兜底。
125
- main_agent_extra:
126
- existing.has_main_agent_extra || existing.main_agent_extra.length > 0
127
- ? existing.main_agent_extra
128
- : suggested.main_agent_extra,
139
+ // main_agent_extra 回填语义:配置文件里「显式写了键」(has_main_agent_extra
140
+ // 标记,含显式空列表)一律尊重 —— 显式 [] = 用户明确不要任何 host 工具,
141
+ // 不得被智能默认悄悄回填;键缺失(老配置 / UI 手工构造的半成品对象)才用
142
+ // suggested.main_agent_extra 兜底。直接按 has 标记判定:原 `has || extra.length > 0`
143
+ // 的后半支在 has=true 时恒为假(短路先命中),且对手造输入(缺
144
+ // main_agent_extra 键)会抛 TypeError,一并去掉。
145
+ main_agent_extra: existing.has_main_agent_extra
146
+ ? existing.main_agent_extra ?? []
147
+ : suggested.main_agent_extra,
129
148
  main_agent_remove: existing.main_agent_remove ?? [],
130
149
  main_agent_skills: existing.main_agent_skills ?? [],
131
150
  main_agent_skills_inline: existing.main_agent_skills_inline ?? [],
132
151
  main_agent_persona_extra: existing.main_agent_persona_extra ?? null,
133
- main_agent_name: existing.main_agent_name ?? null,
152
+ main_agent_display_name: existing.main_agent_display_name ?? null,
134
153
  skills: existing.skills,
135
154
  }
136
155
  }
137
156
  return suggested
138
157
  }
158
+
159
+ /**
160
+ * --auto 语义的 assignments 一站式构造(CLI main 的 --auto 分支与插件首装
161
+ * ensurePresetInstalled 共用,抽在此处防两边逻辑漂移):既有配置按配置合成
162
+ * (不变);fresh(无配置)缺省基础模板(仅 dsh 基础工具,host/MCP 工具留给
163
+ * Web 配置器显式开启),suggest=true 时改用智能默认自动纳入检测到的 host 工具。
164
+ * 返回值形状与 resolveAssignments 一致,可直接传 generateAndInstall。
165
+ */
166
+ export function autoAssignments({ existing = null, suggested, staticBase, suggest = false } = {}) {
167
+ if (existing) return resolveAssignments(existing, suggested, staticBase)
168
+ return suggest ? suggested : baseAssignments(staticBase)
169
+ }