dsh-agent-toolkit 0.1.0 → 0.2.2

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/lib/index.d.ts CHANGED
@@ -47,6 +47,21 @@ interface BotsModuleConfig {
47
47
  errorDetailMaxChars: number;
48
48
  }
49
49
  //#endregion
50
+ //#region src/agents/team-preset.d.ts
51
+ /** 本功能的可调配置(Config schema 在 ../index.ts)。 */
52
+ interface AgentTeamPresetConfig {
53
+ /** 总开关:false 时启动不生成/刷新 preset。 */
54
+ enabled: boolean;
55
+ /** 生成的 preset id(即目录名)。 */
56
+ id: string;
57
+ /** 源 preset id,读其 composition 做派生。 */
58
+ source: string;
59
+ /** preset.yml 的显示名。 */
60
+ name: string;
61
+ /** preset.yml 的描述。 */
62
+ description: string;
63
+ }
64
+ //#endregion
50
65
  //#region src/index.d.ts
51
66
  declare const name = "dsh-agent-toolkit";
52
67
  declare const inject: string[];
@@ -62,6 +77,7 @@ interface Config {
62
77
  provider: string;
63
78
  toolName: string;
64
79
  feishu: BotsModuleConfig;
80
+ agentTeamPreset: AgentTeamPresetConfig;
65
81
  }
66
82
  /** layers/rules 的 schemastery schema 照归档 prompt-stack/src/index.ts:21-41 逐字段平移(含 overrides transform hack)。 */
67
83
  declare const Config: z<unknown, Config>;
package/lib/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { z as z$1 } from "zod";
3
3
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
4
- import { readFile, readdir } from "node:fs/promises";
5
- import { join } from "node:path";
4
+ import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
+ import { join, resolve } from "node:path";
6
6
  import yaml from "js-yaml";
7
- import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
+ import { expandHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
8
8
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
9
10
  import { existsSync } from "node:fs";
10
11
  import { SessionId } from "@deepseek-ai/dsh-session";
11
12
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
12
13
  import * as lark from "@larksuiteoapi/node-sdk";
13
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
14
14
  import { randomBytes, randomUUID } from "node:crypto";
15
15
  import { setupUsage } from "@dsh-agent-toolkit/token-usage";
16
16
  //#region src/agents/store.ts
@@ -60,7 +60,42 @@ function migrateAgentRecord(record) {
60
60
  } : rest;
61
61
  }
62
62
  //#endregion
63
+ //#region src/channels/basic-tools.ts
64
+ const BASIC_TOOLS = [
65
+ {
66
+ id: "@deepseek-ai/dsh-persona",
67
+ config: { text: "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}." }
68
+ },
69
+ {
70
+ id: "@deepseek-ai/dsh-agent-instructions",
71
+ config: { maxBytes: 65536 }
72
+ },
73
+ ...process.platform === "win32" ? [{ id: "@deepseek-ai/dsh-tool-pwsh" }] : [{ id: "@deepseek-ai/dsh-tool-bash" }],
74
+ { id: "@deepseek-ai/dsh-tool-fs" },
75
+ {
76
+ id: "@deepseek-ai/dsh-tool-fs-search",
77
+ config: { sampleOverCapGlobResults: false }
78
+ }
79
+ ];
80
+ /** 原生工具名(白名单 UI 与存量迁移用):与 BASIC_TOOLS 挂载插件注册的工具名一一对应。
81
+ * 名字来源(摘自 deepseek-harness 源码):dsh-tool-pwsh/dsh-tool-bash → 'pwsh'/'bash'(平台互斥);
82
+ * dsh-tool-fs → 'read'/'write'/'edit'/'read_image';dsh-tool-fs-search → 'glob'/'grep'。
83
+ * 这些工具 scoped 挂载在 agentCtx,不出现在顶层 ctx.tools.schemas(),故需显式常量。 */
84
+ const NATIVE_TOOL_NAMES = [
85
+ process.platform === "win32" ? "pwsh" : "bash",
86
+ "read",
87
+ "write",
88
+ "edit",
89
+ "read_image",
90
+ "glob",
91
+ "grep"
92
+ ];
93
+ //#endregion
63
94
  //#region src/agents/builtin.ts
95
+ /** 内置保底 Agent 记录:main + explorer(只读白名单)/ general(不限制)。 */
96
+ /** explorer 默认白名单:原生工具去掉写类(write/edit)。shell 名平台互斥(win32=pwsh、其余=bash),
97
+ * 必须从 NATIVE_TOOL_NAMES 派生不可写死——宿主 tools.restrict 对未知名响亮失败。 */
98
+ const EXPLORER_READONLY_ALLOW = NATIVE_TOOL_NAMES.filter((n) => n !== "write" && n !== "edit");
64
99
  const BUILTIN_AGENTS = [
65
100
  {
66
101
  id: "main",
@@ -74,7 +109,8 @@ const BUILTIN_AGENTS = [
74
109
  persona: `你是代码库探索员。快速定位与任务相关的文件与符号,回答关于代码结构、
75
110
  调用关系、实现位置的问题。你只读不写:不修改任何文件、不运行有副作用的命令。
76
111
  输出结论清单,每条附文件路径与行号;信息不足时说明缺口,不要猜测。`,
77
- builtin: true
112
+ builtin: true,
113
+ tools: { allow: [...EXPLORER_READONLY_ALLOW] }
78
114
  },
79
115
  {
80
116
  id: "general",
@@ -97,20 +133,16 @@ const RoleYamlSchema = z$1.object({
97
133
  persona: z$1.string().min(1),
98
134
  provider: z$1.string().optional(),
99
135
  model: z$1.string().optional(),
100
- tools: z$1.object({
101
- allow: z$1.array(z$1.string()).optional(),
102
- deny: z$1.array(z$1.string()).optional()
103
- }).optional()
136
+ tools: z$1.object({ allow: z$1.array(z$1.string()).optional() }).optional()
104
137
  });
105
138
  /**
106
139
  * 解析校验单个角色 YAML 文件并转成 AgentRecord。
107
140
  * @param text - 文件内容。
108
141
  * @param source - 用于错误信息的来源名(通常是文件路径)。
109
142
  * @param fileName - 文件名(去 .yml),name 省略时的取值;显式 name 须与它一致。
110
- * @param warn - 非致命丢弃(如 tools.deny)的通知通道。
111
143
  * @throws YAML 语法错误、结构非法、name 与文件名不一致、id 非法、tools 空。
112
144
  */
113
- function parseRoleYaml(text, source, fileName, warn) {
145
+ function parseRoleYaml(text, source, fileName) {
114
146
  let parsed;
115
147
  try {
116
148
  parsed = yaml.load(text);
@@ -127,8 +159,7 @@ function parseRoleYaml(text, source, fileName, warn) {
127
159
  const id = raw.name ?? fileName;
128
160
  if (raw.name !== void 0 && raw.name !== fileName) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 name "${raw.name}" 与文件名 "${fileName}" 不一致(省略 name 即取文件名)`);
129
161
  if (!AGENT_ID_RE.test(id)) throw new Error(`dsh-agent-toolkit: 角色 id "${id}" 非法(${source}):只允许小写字母、数字、-,且以小写字母开头`);
130
- if (hasTools && raw.tools !== void 0 && (raw.tools.allow?.length ?? 0) === 0 && (raw.tools.deny?.length ?? 0) === 0) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 tools 为空:allow/deny 至少配一个`);
131
- if (raw.tools?.deny !== void 0 && raw.tools.deny.length > 0) warn?.(`dsh-agent-toolkit: 角色文件 ${source} 的 tools.deny 已忽略(注册表仅支持 allow 白名单)`);
162
+ if (hasTools && raw.tools !== void 0 && (raw.tools.allow?.length ?? 0) === 0) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 tools 为空:allow 至少配一个(不需要限制请整段省略 tools)`);
132
163
  const model = raw.provider !== void 0 && raw.model !== void 0 ? {
133
164
  provider: raw.provider,
134
165
  model: raw.model
@@ -186,7 +217,7 @@ async function importRolesYaml(ctx, rolesDir) {
186
217
  return result;
187
218
  }
188
219
  for (const ref of refs) try {
189
- const record = parseRoleYaml(await readFile(ref.path, "utf8"), ref.path, ref.fileName, ctx.warn);
220
+ const record = parseRoleYaml(await readFile(ref.path, "utf8"), ref.path, ref.fileName);
190
221
  await ctx.agents.put(record.id, record);
191
222
  result.imported++;
192
223
  } catch (error) {
@@ -200,47 +231,17 @@ async function markImported(ctx) {
200
231
  await ctx.meta.put(ROLES_YAML_IMPORTED_KEY, { value: "1" });
201
232
  }
202
233
  //#endregion
203
- //#region src/channels/basic-tools.ts
204
- const BASIC_TOOLS = [
205
- {
206
- id: "@deepseek-ai/dsh-persona",
207
- config: { text: "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}." }
208
- },
209
- {
210
- id: "@deepseek-ai/dsh-agent-instructions",
211
- config: { maxBytes: 65536 }
212
- },
213
- ...process.platform === "win32" ? [{ id: "@deepseek-ai/dsh-tool-pwsh" }] : [{ id: "@deepseek-ai/dsh-tool-bash" }],
214
- { id: "@deepseek-ai/dsh-tool-fs" },
215
- {
216
- id: "@deepseek-ai/dsh-tool-fs-search",
217
- config: { sampleOverCapGlobResults: false }
218
- }
219
- ];
220
- /** 原生工具名(白名单 UI 与存量迁移用):与 BASIC_TOOLS 挂载插件注册的工具名一一对应。
221
- * 名字来源(摘自 deepseek-harness 源码):dsh-tool-pwsh/dsh-tool-bash → 'pwsh'/'bash'(平台互斥);
222
- * dsh-tool-fs → 'read'/'write'/'edit'/'read_image';dsh-tool-fs-search → 'glob'/'grep'。
223
- * 这些工具 scoped 挂载在 agentCtx,不出现在顶层 ctx.tools.schemas(),故需显式常量。 */
224
- const NATIVE_TOOL_NAMES = [
225
- process.platform === "win32" ? "pwsh" : "bash",
226
- "read",
227
- "write",
228
- "edit",
229
- "read_image",
230
- "glob",
231
- "grep"
232
- ];
233
- //#endregion
234
234
  //#region src/agents/registry.ts
235
235
  /** tools.allow 一次性并入原生工具名的 meta 表标记键。 */
236
236
  const TOOLS_NATIVE_MIGRATED_KEY = "tools_native_migrated";
237
+ /** explorer 只读白名单一次性并入的 meta 表标记键。 */
238
+ const EXPLORER_READONLY_MIGRATED_KEY = "explorer_readonly_migrated";
237
239
  /**
238
- * 打开 dsh_agent_toolkit 域 → 缺 main/explorer/general 时种入内置 → 首启 YAML 导入 →
239
- * 构建内存缓存。域由 apply 统一 open(storage-domain 同名单开),此处只消费表句柄。
240
+ * 打开 dsh_agent_toolkit 域 → 首启 YAML 导入 → 旧记录迁移(promptLayers/原生并入/explorer 只读)→
241
+ * 缺 main/explorer/general 时种入内置 → 构建内存缓存。域由 apply 统一 open(storage-domain 同名单开),此处只消费表句柄。
240
242
  */
241
243
  async function createRegistry(warn, tables) {
242
244
  const { agents, meta } = tables;
243
- await seedBuiltins(agents);
244
245
  await importRolesYaml({
245
246
  agents,
246
247
  meta,
@@ -260,6 +261,15 @@ async function createRegistry(warn, tables) {
260
261
  if (next !== record) await agents.put(id, next);
261
262
  }
262
263
  if (!nativeMigrated) await meta.put(TOOLS_NATIVE_MIGRATED_KEY, { value: "1" });
264
+ if (!(meta.get("explorer_readonly_migrated") !== void 0)) {
265
+ const explorer = agents.get("explorer");
266
+ if (explorer !== void 0 && explorer.tools === void 0) await agents.put("explorer", {
267
+ ...explorer,
268
+ tools: { allow: [...EXPLORER_READONLY_ALLOW] }
269
+ });
270
+ await meta.put(EXPLORER_READONLY_MIGRATED_KEY, { value: "1" });
271
+ }
272
+ await seedBuiltins(agents);
263
273
  const cache = /* @__PURE__ */ new Map();
264
274
  for (const [id, record] of agents.entries()) cache.set(id, record);
265
275
  const listeners = /* @__PURE__ */ new Set();
@@ -1192,7 +1202,7 @@ function withPartialText(error, output) {
1192
1202
  return text.length === 0 ? error : `${error}\n成员中断前的部分产出:\n${text}`;
1193
1203
  }
1194
1204
  /** 收集并释放一次前台运行;dispose 失败不掩盖独立的结果失败。 */
1195
- async function settleForegroundRun(run, roleId) {
1205
+ async function settleForegroundRun(run, roleId, route) {
1196
1206
  const childSessionId = String(run.id);
1197
1207
  const [execution] = await Promise.allSettled([run.result.then((result) => {
1198
1208
  const error = stopReasonError(result);
@@ -1202,7 +1212,11 @@ async function settleForegroundRun(run, roleId) {
1202
1212
  role: roleId,
1203
1213
  runId: String(run.id),
1204
1214
  childSessionId,
1205
- output: result.output
1215
+ output: result.output,
1216
+ ...route !== void 0 ? {
1217
+ provider: route.provider,
1218
+ model: route.model
1219
+ } : {}
1206
1220
  };
1207
1221
  })]);
1208
1222
  const [disposal] = await Promise.allSettled([Promise.resolve().then(() => run.dispose())]);
@@ -1269,7 +1283,9 @@ function createDelegateTool(toolName, deps) {
1269
1283
  type: "array",
1270
1284
  required: true,
1271
1285
  items: { type: "json" }
1272
- }
1286
+ },
1287
+ provider: { type: "string" },
1288
+ model: { type: "string" }
1273
1289
  }
1274
1290
  },
1275
1291
  render: (_args, value) => [{
@@ -1279,7 +1295,11 @@ function createDelegateTool(toolName, deps) {
1279
1295
  presentationMeta: (_args, value) => ({
1280
1296
  role: value.role,
1281
1297
  runId: value.runId,
1282
- childSessionId: value.childSessionId
1298
+ childSessionId: value.childSessionId,
1299
+ ...typeof value.provider === "string" && typeof value.model === "string" ? {
1300
+ provider: value.provider,
1301
+ model: value.model
1302
+ } : {}
1283
1303
  })
1284
1304
  },
1285
1305
  isConcurrencySafe: () => true,
@@ -1312,7 +1332,19 @@ function createDelegateTool(toolName, deps) {
1312
1332
  } } : {},
1313
1333
  ...role.tools !== void 0 ? { toolFilter: { allow: [...role.tools.allow] } } : {}
1314
1334
  };
1315
- return settleForegroundRun(await deps.startRun(deps.provider, request), role.id);
1335
+ const route = (role.model !== void 0 && role.model.provider !== "" && role.model.model !== "" ? role.model : void 0) ?? (typeof parent.options.provider === "string" && parent.options.provider !== "" && typeof parent.options.model === "string" && parent.options.model !== "" ? {
1336
+ provider: parent.options.provider,
1337
+ model: parent.options.model
1338
+ } : void 0);
1339
+ const parentSessionId = String(parent.session.id);
1340
+ if (route !== void 0) deps.active.set(parentSessionId, role.id, route);
1341
+ try {
1342
+ const run = await deps.startRun(deps.provider, request);
1343
+ if (route !== void 0) await deps.recordRoute(String(run.id), route).catch(() => void 0);
1344
+ return await settleForegroundRun(run, role.id, route);
1345
+ } finally {
1346
+ if (route !== void 0) deps.active.delete(parentSessionId, role.id);
1347
+ }
1316
1348
  }
1317
1349
  });
1318
1350
  }
@@ -1326,7 +1358,7 @@ const TEAM_SECTION_ORDER = 116.6;
1326
1358
  * 工具随 provider 在场与否挂载/摘除。名册来自注册表(main 排除),工具与提示段都经
1327
1359
  * 闭包读取 registry,UI 改角色后新会话即生效。
1328
1360
  */
1329
- function setupDelegate(ctx, config, registry) {
1361
+ function setupDelegate(ctx, config, registry, channels) {
1330
1362
  const { provider, toolName } = config;
1331
1363
  let disposeTool;
1332
1364
  let providerFailed = false;
@@ -1339,7 +1371,9 @@ function setupDelegate(ctx, config, registry) {
1339
1371
  roster: () => registry.list(),
1340
1372
  provider,
1341
1373
  buildPersona: (role) => buildAgentPersona({ rules: config.rules }, role, role.model),
1342
- startRun: (pr, request) => ctx.subagents.start(pr, request)
1374
+ startRun: (pr, request) => ctx.subagents.start(pr, request),
1375
+ active: channels.active,
1376
+ recordRoute: channels.recordRoute
1343
1377
  }));
1344
1378
  };
1345
1379
  ctx.on("subagent/provider-added", (p) => {
@@ -1372,6 +1406,86 @@ function setupDelegate(ctx, config, registry) {
1372
1406
  });
1373
1407
  }
1374
1408
  //#endregion
1409
+ //#region src/delegate/active.ts
1410
+ function createActiveRoutes() {
1411
+ const map = /* @__PURE__ */ new Map();
1412
+ const key = (sessionId, roleId) => `${sessionId}:${roleId}`;
1413
+ return {
1414
+ set: (sessionId, roleId, route) => {
1415
+ map.set(key(sessionId, roleId), route);
1416
+ },
1417
+ get: (sessionId, roleId) => map.get(key(sessionId, roleId)),
1418
+ delete: (sessionId, roleId) => {
1419
+ map.delete(key(sessionId, roleId));
1420
+ }
1421
+ };
1422
+ }
1423
+ //#endregion
1424
+ //#region src/delegate/routes.ts
1425
+ /** 委派路由持久存储域:子会话头部 chip 的数据源(schema 与 domain 布局的单一来源在本文件)。 */
1426
+ const DelegationRouteRecordSchema = z$1.object({
1427
+ provider: z$1.string(),
1428
+ model: z$1.string(),
1429
+ at: z$1.number()
1430
+ });
1431
+ const delegationRoutesDomain = defineDomain({
1432
+ name: "dsh_agent_toolkit_routes",
1433
+ version: 1,
1434
+ tables: { routes: domainTable(DelegationRouteRecordSchema) }
1435
+ });
1436
+ //#endregion
1437
+ //#region src/delegate/api.ts
1438
+ function createDelegateApiHandler(deps) {
1439
+ return async (req, res) => {
1440
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1441
+ const sub = url.pathname.replace(/^\/dsh-agent-toolkit\/api/, "") || "/";
1442
+ if (req.method !== "GET") {
1443
+ json$1(res, 405, { error: "method not allowed" });
1444
+ return;
1445
+ }
1446
+ if (sub === "/delegate/active") {
1447
+ const route = deps.active.get(url.searchParams.get("session") ?? "", url.searchParams.get("role") ?? "");
1448
+ if (route === void 0) {
1449
+ json$1(res, 404, { error: "not found" });
1450
+ return;
1451
+ }
1452
+ json$1(res, 200, {
1453
+ provider: route.provider,
1454
+ model: route.model
1455
+ });
1456
+ return;
1457
+ }
1458
+ if (sub === "/delegate/route") {
1459
+ const record = deps.routes.get(url.searchParams.get("session") ?? "");
1460
+ if (record === void 0) {
1461
+ json$1(res, 404, { error: "not found" });
1462
+ return;
1463
+ }
1464
+ json$1(res, 200, {
1465
+ provider: record.provider,
1466
+ model: record.model
1467
+ });
1468
+ return;
1469
+ }
1470
+ json$1(res, 404, { error: "not found" });
1471
+ };
1472
+ }
1473
+ /**
1474
+ * 注册 delegate 路由(恒启用,与 agents 同策略)。webServer 为可选服务:
1475
+ * 缺席时经 registerOptionalRoutes 惰性不注册。prefix 先于 /api 兜底前缀命中。
1476
+ */
1477
+ function setupDelegateApi(ctx, deps) {
1478
+ const handler = createDelegateApiHandler(deps);
1479
+ registerOptionalRoutes(ctx, (webCtx) => {
1480
+ const unregister = webCtx.webServer.register({
1481
+ kind: "prefix",
1482
+ path: "/dsh-agent-toolkit/api/delegate",
1483
+ handler
1484
+ });
1485
+ return () => unregister();
1486
+ });
1487
+ }
1488
+ //#endregion
1375
1489
  //#region src/agents/api.ts
1376
1490
  function createAgentsApiHandler(deps) {
1377
1491
  return async (req, res) => {
@@ -1498,6 +1612,59 @@ function setupAgentsApi(ctx, deps) {
1498
1612
  });
1499
1613
  }
1500
1614
  //#endregion
1615
+ //#region src/agents/create-command.ts
1616
+ /** 拼装 /create-agent 的引导文本(模型面契约,文案为规格锁定内容)。 */
1617
+ function buildCreateAgentGuidance(input) {
1618
+ const lines = [
1619
+ "# 交互式创建 Agent 团队成员",
1620
+ "",
1621
+ "## 工作流(三步)",
1622
+ "1. 澄清需求:需求不明确时用 ask_user_question 向用户提问,整个流程提问总次数不超过 5 次,不重复问已确认的信息;",
1623
+ "2. 生成推荐并请用户确认:推荐 id / name / description / persona / tools 五个字段(id 是团队内唯一标识,name 是显示名,description 是职责一句话描述,persona 是系统提示词个性段,tools 是工具白名单);",
1624
+ "3. 迭代:用户有修改意见时按意见修订名称、描述、个性和工具后再次确认,直到用户明确确认。"
1625
+ ];
1626
+ if (input.requirement !== "") lines.push("", "## 用户初始需求", `用户已在命令中提供初始需求:「${input.requirement}」。请据此减少提问轮次,仅就不明确的点提问。`);
1627
+ lines.push("", "## 现有 Agent id(不可复用)", input.agentIds.join(", "), "id 规则:小写字母开头,仅含小写字母/数字/连字符([a-z0-9-]),最长 32 字符。", "", "## 可用工具清单", `原生工具:${NATIVE_TOOL_NAMES.join(", ")}`, `全局工具:${input.globalTools.join(", ")}`, "省略 tools 字段表示不限制(Agent 可使用全部工具)。一旦给出白名单,该 Agent 只有列出的工具可用:通常应保留原生工具,否则失去读文件/搜索/执行命令等基本能力(最终取舍按需求判断,如只读角色可去掉 write/edit)。");
1628
+ if (input.origin === void 0) lines.push("", "## 落库", "当前宿主无 web 服务,无法自动落库。用户确认推荐后,请把最终配置完整输出给用户,并提示其打开 Agents 面板按推荐内容手动创建。");
1629
+ else lines.push("", "## 落库(用户明确确认后执行)", "用你的 shell 工具调用 Agents 面板同一 HTTP 端点完成创建:", `1. 先 GET ${input.origin}/dsh-agent-toolkit/api/agents 复核所选 id 仍未被占用;`, `2. 再 PUT ${input.origin}/dsh-agent-toolkit/api/agents/<id>,请求体为 JSON(不要在 body 中携带 id 或 builtin 字段):`, " {\"name\":\"...\",\"description\":\"...\",\"persona\":\"...\",\"tools\":{\"allow\":[\"...\"]}}", " (description/persona/tools 均可省略;省略 tools 表示不限制)", "3. curl 示例(Windows 的 pwsh 里用 curl.exe):", ` curl.exe -s -X PUT "${input.origin}/dsh-agent-toolkit/api/agents/<id>" -H "Content-Type: application/json" -d "{\\"name\\":\\"...\\"}"`, `4. 返回 200 后必须再 GET ${input.origin}/dsh-agent-toolkit/api/agents,在返回列表中找到该 id 的记录,把它的 name/description/persona/tools 关键字段展示给用户,作为落库证据;`, "5. 落库成功后告知用户可在 Agents 面板查看、并可被 team_delegate 委派;任一步返回 4xx 则把错误信息展示给用户,修正后重试。");
1630
+ return lines.join("\n");
1631
+ }
1632
+ /**
1633
+ * 注册 /create-agent。webServer 为可选服务按仓库规则经 ctx.get 读取(不进 inject),
1634
+ * 缺席(headless/CLI)时 origin 为 undefined,引导文本落库节降级为手动创建指引。
1635
+ *
1636
+ * 命令结果(command/done)是 log-only、不进模型:引导文本必须经 agent.followup
1637
+ * 投递为 user 消息驱动主 Agent(与 channels/inbound.ts 同一机制),命令卡只回执短文案。
1638
+ */
1639
+ function setupCreateAgentCommand(ctx, deps) {
1640
+ ctx.commands.register({
1641
+ name: "create-agent",
1642
+ description: "交互式创建 Agent 团队成员:访谈澄清需求 → 推荐配置 → 确认后经面板 API 落库",
1643
+ input: { hint: "初始需求描述,可空" },
1644
+ handler: ({ rawInput, agent }) => {
1645
+ const webServer = ctx.get("webServer");
1646
+ const origin = webServer === void 0 ? void 0 : `http://127.0.0.1:${webServer.port}`;
1647
+ const text = buildCreateAgentGuidance({
1648
+ requirement: rawInput.trim(),
1649
+ agentIds: deps.registry.list().map((agent) => agent.id),
1650
+ globalTools: deps.listTools(),
1651
+ origin
1652
+ });
1653
+ agent.followup(createUserMessage({
1654
+ content: [{
1655
+ type: "text",
1656
+ text
1657
+ }],
1658
+ source: { kind: "user" }
1659
+ }));
1660
+ return {
1661
+ kind: "success",
1662
+ text: "已向主 Agent 发出创建 Agent 引导,请继续对话完成访谈与确认。"
1663
+ };
1664
+ }
1665
+ });
1666
+ }
1667
+ //#endregion
1501
1668
  //#region src/channels/agent-setup.ts
1502
1669
  /** bot 会话角色 persona 的 scoped 段名:与全局 persona 层同名,scoped 注册即 shadow 覆盖主 Agent persona。 */
1503
1670
  const TOOLKIT_PERSONA_SECTION = "prompt-stack:persona";
@@ -3027,6 +3194,125 @@ function setupBots(ctx, config, deps) {
3027
3194
  });
3028
3195
  }
3029
3196
  //#endregion
3197
+ //#region src/agents/team-preset.ts
3198
+ /**
3199
+ * Agent 团队 preset 自动生成:派生宿主当前 shipped standard composition,
3200
+ * 文本级禁用 subagent 工具族 4 个行,写入首个 trust=user 的 preset root。
3201
+ * 设计:docs/superpowers/specs/2026-09-02-agent-team-preset-design.md
3202
+ */
3203
+ /** 禁用目标行:覆盖与 team_delegate 竞争/配套的 5 个模型可见工具所属的 4 个行。 */
3204
+ const TEAM_PRESET_DISABLED_ROWS = [
3205
+ "tool-subagent",
3206
+ "tool-subagent-fork",
3207
+ "tool-subagent-control",
3208
+ "tool-subagent-list-agents"
3209
+ ];
3210
+ /**
3211
+ * 文本级锚点改写:对 4 个目标行各插入一行 `disabled: true`(缩进 = 锚点缩进 + 2 空格)。
3212
+ * 锚点是整行精确匹配 `- id: <行id>`(忽略首尾空白),防 tool-subagent 误中
3213
+ * tool-subagent-fork / tool-subagent-control 前缀;锚点所属块内(锚点行之后、
3214
+ * 首个缩进 <= 锚点缩进的非空行之前)已有 `disabled:` 键则跳过——既幂等,也避免
3215
+ * 与宿主已有的 `disabled: !!js ...` 撞出 YAML 重复键。锚点缺失 warn 并跳过该锚点,
3216
+ * 其余照常。除插入行外文本逐字节不变。
3217
+ */
3218
+ function disableSubagentRows(source, warn) {
3219
+ const lines = source.split("\n");
3220
+ for (const row of TEAM_PRESET_DISABLED_ROWS) {
3221
+ const anchor = `- id: ${row}`;
3222
+ const index = lines.findIndex((line) => line.trim() === anchor);
3223
+ if (index === -1) {
3224
+ warn(`dsh-agent-toolkit: agent-team preset 锚点行 "${anchor}" 在源 composition 中缺失,已跳过`);
3225
+ continue;
3226
+ }
3227
+ const indent = lines[index].length - lines[index].trimStart().length;
3228
+ let hasDisabled = false;
3229
+ for (let i = index + 1; i < lines.length; i += 1) {
3230
+ const line = lines[i];
3231
+ if (line.trim() === "") continue;
3232
+ if (line.length - line.trimStart().length <= indent) break;
3233
+ if (/^\s*disabled\s*:/.test(line)) {
3234
+ hasDisabled = true;
3235
+ break;
3236
+ }
3237
+ }
3238
+ if (hasDisabled) continue;
3239
+ lines.splice(index + 1, 0, `${" ".repeat(indent + 2)}disabled: true`);
3240
+ }
3241
+ return lines.join("\n");
3242
+ }
3243
+ const COMPOSITION_FILE = "agent.cordis.yml";
3244
+ const METADATA_FILE = "preset.yml";
3245
+ /**
3246
+ * 生成目录的归属标记:无此标记的同名目录视为用户手工 preset,不覆盖。
3247
+ * 标记必须在 mkdir 后最先写入(marker-first):这样写 composition/metadata 失败留下的是
3248
+ * 带标记的半成品目录,下次启动会被正常重写自愈;若最后才写标记,则半成品目录无标记、
3249
+ * 会被误判为用户手工 preset 而永久卡死不覆盖。
3250
+ */
3251
+ const MARKER_FILE = ".generated-by";
3252
+ const MARKER_CONTENT = "dsh-agent-toolkit";
3253
+ /** 镜像宿主 PRESET_ID:preset id 即目录名,正则白名单是路径逃逸的 containment 边界。 */
3254
+ const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
3255
+ const GENERATED_HEADER = "# 本文件由 dsh-agent-toolkit 自动生成,勿手改(每次启动重写)。\n";
3256
+ /**
3257
+ * 启动时生成/刷新 agent-team preset。所有失败路径 warn 降级,不影响插件其余功能。
3258
+ * 不设为默认 preset、卸载不删目录(可能有会话在用;composition 不引用 toolkit 行,
3259
+ * 残留 preset 自身仍可用)。每次启动重写:standing mount 按文件代际,重写只影响新会话。
3260
+ */
3261
+ async function setupAgentTeamPreset(ctx, config) {
3262
+ if (!config.enabled) return;
3263
+ const warn = (msg) => {
3264
+ ctx.logger.warn(msg);
3265
+ };
3266
+ const agentPresets = ctx.get("agentPresets", false);
3267
+ if (agentPresets === void 0) return;
3268
+ if (!PRESET_ID.test(config.id)) {
3269
+ warn(`dsh-agent-toolkit: agentTeamPreset.id "${config.id}" 不是合法 preset id,跳过 agent-team 生成`);
3270
+ return;
3271
+ }
3272
+ let source;
3273
+ try {
3274
+ source = await agentPresets.read(config.source);
3275
+ } catch (error) {
3276
+ warn(`dsh-agent-toolkit: 读取源 preset "${config.source}" 失败,跳过 agent-team 生成:${error instanceof Error ? error.message : String(error)}`);
3277
+ return;
3278
+ }
3279
+ const root = agentPresets.roots.find((r) => r.trust === "user");
3280
+ if (root === void 0) {
3281
+ warn("dsh-agent-toolkit: preset roots 中无 trust=user 的目录,跳过 agent-team 生成");
3282
+ return;
3283
+ }
3284
+ const composition = GENERATED_HEADER + disableSubagentRows(source, warn);
3285
+ const dir = join(resolve(expandHomePath(root.path)), config.id);
3286
+ try {
3287
+ const markerPath = join(dir, MARKER_FILE);
3288
+ let dirExists = true;
3289
+ try {
3290
+ await access(dir);
3291
+ } catch {
3292
+ dirExists = false;
3293
+ }
3294
+ if (dirExists) {
3295
+ let marked = false;
3296
+ try {
3297
+ marked = (await readFile(markerPath, "utf8")).trim() === MARKER_CONTENT;
3298
+ } catch {}
3299
+ if (!marked) {
3300
+ warn(`dsh-agent-toolkit: ${dir} 已存在且非本插件生成,不覆盖,跳过 agent-team 生成`);
3301
+ return;
3302
+ }
3303
+ }
3304
+ await mkdir(dir, { recursive: true });
3305
+ await writeFile(markerPath, `${MARKER_CONTENT}\n`, "utf8");
3306
+ await writeFile(join(dir, COMPOSITION_FILE), composition, "utf8");
3307
+ await writeFile(join(dir, METADATA_FILE), yaml.dump({
3308
+ name: config.name,
3309
+ description: config.description
3310
+ }, { lineWidth: -1 }), "utf8");
3311
+ } catch (error) {
3312
+ warn(`dsh-agent-toolkit: 写入 agent-team preset 失败(${dir}):${error instanceof Error ? error.message : String(error)}`);
3313
+ }
3314
+ }
3315
+ //#endregion
3030
3316
  //#region src/index.ts
3031
3317
  const name = "dsh-agent-toolkit";
3032
3318
  const inject = [
@@ -3081,6 +3367,19 @@ const Config = z.object({
3081
3367
  registerAppTimeoutMs: 6e5,
3082
3368
  processingReactionEmoji: "OneSecond",
3083
3369
  errorDetailMaxChars: 500
3370
+ }),
3371
+ agentTeamPreset: z.object({
3372
+ enabled: z.boolean().default(true),
3373
+ id: z.string().default("agent-team"),
3374
+ source: z.string().default("standard"),
3375
+ name: z.string().default("Agent 团队"),
3376
+ description: z.string().default("Agent 团队模式:禁用原生 subagent 工具族,委派统一走 team_delegate 团队角色")
3377
+ }).default({
3378
+ enabled: true,
3379
+ id: "agent-team",
3380
+ source: "standard",
3381
+ name: "Agent 团队",
3382
+ description: "Agent 团队模式:禁用原生 subagent 工具族,委派统一走 team_delegate 团队角色"
3084
3383
  })
3085
3384
  });
3086
3385
  async function apply(ctx, config) {
@@ -3107,14 +3406,30 @@ async function apply(ctx, config) {
3107
3406
  source: layerSource,
3108
3407
  rules: config.rules
3109
3408
  });
3409
+ const routesTable = (await openDomainSafely(ctx, delegationRoutesDomain, warn)).table("routes");
3410
+ const activeRoutes = createActiveRoutes();
3110
3411
  setupDelegate(ctx, {
3111
3412
  provider: config.provider,
3112
3413
  toolName: config.toolName,
3113
3414
  rules: config.rules
3114
- }, registry);
3415
+ }, registry, {
3416
+ active: activeRoutes,
3417
+ recordRoute: async (childSessionId, route) => {
3418
+ await routesTable.put(childSessionId, {
3419
+ provider: route.provider,
3420
+ model: route.model,
3421
+ at: Date.now()
3422
+ });
3423
+ }
3424
+ });
3425
+ setupDelegateApi(ctx, {
3426
+ active: activeRoutes,
3427
+ routes: routesTable
3428
+ });
3429
+ const listTools = () => ctx.tools.schemas().map((s) => s.name);
3115
3430
  setupAgentsApi(ctx, {
3116
3431
  registry,
3117
- listTools: () => ctx.tools.schemas().map((s) => s.name),
3432
+ listTools,
3118
3433
  listProviders: () => ctx.llm.listProviders().map(({ id, name }) => ({
3119
3434
  id,
3120
3435
  name
@@ -3124,6 +3439,10 @@ async function apply(ctx, config) {
3124
3439
  name
3125
3440
  })))
3126
3441
  });
3442
+ setupCreateAgentCommand(ctx, {
3443
+ registry,
3444
+ listTools
3445
+ });
3127
3446
  setupPromptLayersApi(ctx, {
3128
3447
  source: layerSource,
3129
3448
  rules: config.rules,
@@ -3142,6 +3461,7 @@ async function apply(ctx, config) {
3142
3461
  };
3143
3462
  }
3144
3463
  });
3464
+ await setupAgentTeamPreset(ctx, config.agentTeamPreset);
3145
3465
  if (config.modules.feishu) setupBots(ctx, config.feishu, { registry });
3146
3466
  if (config.modules.usage) setupUsage(ctx, { timezone: config.timezone }, name);
3147
3467
  }