dsh-agent-toolkit 0.2.8 → 0.2.9
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 +1 -1
- package/lib/client.js +1674 -209
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +17 -2
- package/lib/index.js +1858 -243
- package/lib/index.js.map +1 -1
- package/package.json +6 -1
package/lib/index.js
CHANGED
|
@@ -5,14 +5,15 @@ import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import yaml from "js-yaml";
|
|
7
7
|
import { expandHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
8
|
-
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
|
+
import { RUN_CODE_NAME, defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
|
+
import { bindScopeParent, createScope, scopeOf } from "@deepseek-ai/dsh-scope";
|
|
9
10
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
11
|
import { existsSync } from "node:fs";
|
|
11
12
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
12
13
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
13
14
|
import * as lark from "@larksuiteoapi/node-sdk";
|
|
14
15
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
15
|
-
import {
|
|
16
|
+
import { Cron } from "croner";
|
|
16
17
|
import { setupUsage } from "@dsh-agent-toolkit/token-usage";
|
|
17
18
|
//#region src/agents/store.ts
|
|
18
19
|
/** agents 注册表存储域声明:记录 schema + domain 布局的单一来源。 */
|
|
@@ -34,7 +35,8 @@ const AgentRecordSchema = z$1.object({
|
|
|
34
35
|
model: z$1.string()
|
|
35
36
|
}).optional(),
|
|
36
37
|
tools: z$1.object({ allow: z$1.array(z$1.string()).min(1) }).optional(),
|
|
37
|
-
builtin: z$1.boolean().optional()
|
|
38
|
+
builtin: z$1.boolean().optional(),
|
|
39
|
+
visibleInTeam: z$1.boolean().optional()
|
|
38
40
|
});
|
|
39
41
|
/** domain 名/表名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
|
|
40
42
|
const agentToolkitDomain = defineDomain({
|
|
@@ -60,12 +62,16 @@ function migrateAgentRecord(record) {
|
|
|
60
62
|
persona
|
|
61
63
|
} : rest;
|
|
62
64
|
}
|
|
65
|
+
/** 团队可见性判定:缺省(undefined)与 true 均可见;仅显式 false 隐藏。 */
|
|
66
|
+
function isTeamVisible(role) {
|
|
67
|
+
return role.visibleInTeam !== false;
|
|
68
|
+
}
|
|
63
69
|
//#endregion
|
|
64
70
|
//#region src/channels/basic-tools.ts
|
|
65
71
|
const BASIC_TOOLS = [
|
|
66
72
|
{
|
|
67
73
|
id: "@deepseek-ai/dsh-persona",
|
|
68
|
-
config: { text: "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}." }
|
|
74
|
+
config: { text: "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}. If you need information or a decision from the user, ask directly in your reply and wait for their next message." }
|
|
69
75
|
},
|
|
70
76
|
{
|
|
71
77
|
id: "@deepseek-ai/dsh-agent-instructions",
|
|
@@ -78,10 +84,10 @@ const BASIC_TOOLS = [
|
|
|
78
84
|
config: { sampleOverCapGlobResults: false }
|
|
79
85
|
}
|
|
80
86
|
];
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
87
|
+
/** 内置工具名常量(兜底名单):agentPresets 缺席或 standing 枚举失败时,Agents 面板名册
|
|
88
|
+
* 与存量迁移回退到这份常量。语义已降级为"兜底",不再承诺是完整原生工具面——完整面 =
|
|
89
|
+
* 团队 preset 动态枚举(agents/tool-catalog.ts)。explorer 只读白名单仍从本常量派生
|
|
90
|
+
* (刻意的最小集,不追求完整)。 */
|
|
85
91
|
const NATIVE_TOOL_NAMES = [
|
|
86
92
|
process.platform === "win32" ? "pwsh" : "bash",
|
|
87
93
|
"read",
|
|
@@ -93,10 +99,50 @@ const NATIVE_TOOL_NAMES = [
|
|
|
93
99
|
];
|
|
94
100
|
//#endregion
|
|
95
101
|
//#region src/agents/builtin.ts
|
|
96
|
-
/** 内置保底 Agent 记录:main + explorer
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
/** 内置保底 Agent 记录:main + explorer(只读白名单 10 个)/ general(preset 面全量 20 个、禁二级委派)。 */
|
|
103
|
+
const SHELL_NAME = process.platform === "win32" ? "pwsh" : "bash";
|
|
104
|
+
/** 旧版 explorer 默认白名单(5 个):存量条件式迁移的等值比对基准。
|
|
105
|
+
* 独立于 EXPLORER_READONLY_ALLOW 保留旧派生式——比对基准不可随新名单漂移。
|
|
106
|
+
* shell 名平台互斥(win32=pwsh、其余=bash),必须从 NATIVE_TOOL_NAMES 派生不可写死。 */
|
|
107
|
+
const LEGACY_EXPLORER_ALLOW = NATIVE_TOOL_NAMES.filter((n) => n !== "write" && n !== "edit");
|
|
108
|
+
/** explorer 默认白名单(10 个):只读基础五件(shell/read/read_image/glob/grep,旧派生不变)
|
|
109
|
+
* + preset 面只读安全五件(web_search/todo_write/job_list/job_output/skill——skill 加载的是
|
|
110
|
+
* 指令文本,本身只读,用户决策默认可用)。
|
|
111
|
+
* 编排类(ralph/workflow)、写文件类(write/edit)、job_kill、ask_user_question、
|
|
112
|
+
* goal 三件套、exit_plan_mode 不进只读名单。 */
|
|
113
|
+
const EXPLORER_READONLY_ALLOW = [
|
|
114
|
+
...LEGACY_EXPLORER_ALLOW,
|
|
115
|
+
"web_search",
|
|
116
|
+
"todo_write",
|
|
117
|
+
"job_list",
|
|
118
|
+
"job_output",
|
|
119
|
+
"skill"
|
|
120
|
+
];
|
|
121
|
+
/** general 默认白名单(20 个):agent-team preset standing 面全量(2026-09-08 实测枚举),
|
|
122
|
+
* 不含 team_delegate(禁二级委派)与 run_code(宿主 Code Mode 保留名,restrict 拒收)。
|
|
123
|
+
* 静态名单:preset 面日后新增工具不自动进入,需人工再梳理(见 spec 非目标)。 */
|
|
124
|
+
const GENERAL_ALLOW = [
|
|
125
|
+
SHELL_NAME,
|
|
126
|
+
"read",
|
|
127
|
+
"write",
|
|
128
|
+
"edit",
|
|
129
|
+
"read_image",
|
|
130
|
+
"glob",
|
|
131
|
+
"grep",
|
|
132
|
+
"todo_write",
|
|
133
|
+
"web_search",
|
|
134
|
+
"ask_user_question",
|
|
135
|
+
"skill",
|
|
136
|
+
"exit_plan_mode",
|
|
137
|
+
"job_list",
|
|
138
|
+
"job_output",
|
|
139
|
+
"job_kill",
|
|
140
|
+
"create_goal",
|
|
141
|
+
"get_goal",
|
|
142
|
+
"update_goal",
|
|
143
|
+
"ralph",
|
|
144
|
+
"workflow"
|
|
145
|
+
];
|
|
100
146
|
const BUILTIN_AGENTS = [
|
|
101
147
|
{
|
|
102
148
|
id: "main",
|
|
@@ -120,7 +166,8 @@ const BUILTIN_AGENTS = [
|
|
|
120
166
|
persona: `你是通用执行员。按任务书独立完成多步骤工作,可以读写文件、运行命令。
|
|
121
167
|
动手前先阅读相关 AGENTS.md 并遵循项目约定;完成后运行与改动相关的检查
|
|
122
168
|
(测试/类型检查)验证改动,并在最终输出中报告验证结果。`,
|
|
123
|
-
builtin: true
|
|
169
|
+
builtin: true,
|
|
170
|
+
tools: { allow: [...GENERAL_ALLOW] }
|
|
124
171
|
}
|
|
125
172
|
];
|
|
126
173
|
//#endregion
|
|
@@ -237,11 +284,15 @@ async function markImported(ctx) {
|
|
|
237
284
|
const TOOLS_NATIVE_MIGRATED_KEY = "tools_native_migrated";
|
|
238
285
|
/** explorer 只读白名单一次性并入的 meta 表标记键。 */
|
|
239
286
|
const EXPLORER_READONLY_MIGRATED_KEY = "explorer_readonly_migrated";
|
|
287
|
+
/** tools.allow 一次性并入「preset 面 − 原生常量」差集的 meta 表标记键。 */
|
|
288
|
+
const TOOLS_PRESET_MIGRATED_KEY = "tools_preset_catalog_migrated";
|
|
289
|
+
/** 内置角色工具名单重选(explorer 5→10 / general 无→20)一次性迁移的 meta 表标记键。 */
|
|
290
|
+
const BUILTIN_TOOLS_RECATALOG_MIGRATED_KEY = "builtin_tools_recatalog_migrated";
|
|
240
291
|
/**
|
|
241
|
-
* 打开 dsh_agent_toolkit 域 → 首启 YAML 导入 → 旧记录迁移(promptLayers/原生并入/explorer
|
|
292
|
+
* 打开 dsh_agent_toolkit 域 → 首启 YAML 导入 → 旧记录迁移(promptLayers/原生并入/preset 差集并入/explorer 只读/内置名单重选)→
|
|
242
293
|
* 缺 main/explorer/general 时种入内置 → 构建内存缓存。域由 apply 统一 open(storage-domain 同名单开),此处只消费表句柄。
|
|
243
294
|
*/
|
|
244
|
-
async function createRegistry(warn, tables) {
|
|
295
|
+
async function createRegistry(warn, tables, listPresetTools) {
|
|
245
296
|
const { agents, meta } = tables;
|
|
246
297
|
await importRolesYaml({
|
|
247
298
|
agents,
|
|
@@ -262,6 +313,25 @@ async function createRegistry(warn, tables) {
|
|
|
262
313
|
if (next !== record) await agents.put(id, next);
|
|
263
314
|
}
|
|
264
315
|
if (!nativeMigrated) await meta.put(TOOLS_NATIVE_MIGRATED_KEY, { value: "1" });
|
|
316
|
+
if (meta.get("tools_preset_catalog_migrated") === void 0 && listPresetTools !== void 0) {
|
|
317
|
+
let extra;
|
|
318
|
+
try {
|
|
319
|
+
extra = (await listPresetTools()).filter((n) => !NATIVE_TOOL_NAMES.includes(n));
|
|
320
|
+
} catch {
|
|
321
|
+
extra = void 0;
|
|
322
|
+
}
|
|
323
|
+
if (extra !== void 0 && extra.length > 0) {
|
|
324
|
+
for (const [id, record] of agents.entries()) {
|
|
325
|
+
if (record.builtin === true || record.tools === void 0) continue;
|
|
326
|
+
const missing = extra.filter((n) => !record.tools.allow.includes(n));
|
|
327
|
+
if (missing.length > 0) await agents.put(id, {
|
|
328
|
+
...record,
|
|
329
|
+
tools: { allow: [...record.tools.allow, ...missing] }
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
await meta.put(TOOLS_PRESET_MIGRATED_KEY, { value: "1" });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
265
335
|
if (!(meta.get("explorer_readonly_migrated") !== void 0)) {
|
|
266
336
|
const explorer = agents.get("explorer");
|
|
267
337
|
if (explorer !== void 0 && explorer.tools === void 0) await agents.put("explorer", {
|
|
@@ -270,6 +340,24 @@ async function createRegistry(warn, tables) {
|
|
|
270
340
|
});
|
|
271
341
|
await meta.put(EXPLORER_READONLY_MIGRATED_KEY, { value: "1" });
|
|
272
342
|
}
|
|
343
|
+
if (meta.get("builtin_tools_recatalog_migrated") === void 0) {
|
|
344
|
+
const explorer = agents.get("explorer");
|
|
345
|
+
const legacyShapes = [LEGACY_EXPLORER_ALLOW, [
|
|
346
|
+
...LEGACY_EXPLORER_ALLOW,
|
|
347
|
+
"write",
|
|
348
|
+
"edit"
|
|
349
|
+
]];
|
|
350
|
+
if (explorer?.builtin === true && explorer.tools !== void 0 && legacyShapes.some((shape) => explorer.tools.allow.length === shape.length && explorer.tools.allow.every((n, i) => n === shape[i]))) await agents.put("explorer", {
|
|
351
|
+
...explorer,
|
|
352
|
+
tools: { allow: [...EXPLORER_READONLY_ALLOW] }
|
|
353
|
+
});
|
|
354
|
+
const general = agents.get("general");
|
|
355
|
+
if (general?.builtin === true && general.tools === void 0) await agents.put("general", {
|
|
356
|
+
...general,
|
|
357
|
+
tools: { allow: [...GENERAL_ALLOW] }
|
|
358
|
+
});
|
|
359
|
+
await meta.put(BUILTIN_TOOLS_RECATALOG_MIGRATED_KEY, { value: "1" });
|
|
360
|
+
}
|
|
273
361
|
await seedBuiltins(agents);
|
|
274
362
|
const cache = /* @__PURE__ */ new Map();
|
|
275
363
|
for (const [id, record] of agents.entries()) cache.set(id, record);
|
|
@@ -319,6 +407,27 @@ async function seedBuiltins(agents) {
|
|
|
319
407
|
for (const builtin of BUILTIN_AGENTS) if (agents.get(builtin.id) === void 0) await agents.put(builtin.id, builtin);
|
|
320
408
|
}
|
|
321
409
|
//#endregion
|
|
410
|
+
//#region src/agents/tool-catalog.ts
|
|
411
|
+
function createToolCatalog(ctx, presetId) {
|
|
412
|
+
return {
|
|
413
|
+
async listPresetTools() {
|
|
414
|
+
const presets = ctx.get("agentPresets", false);
|
|
415
|
+
if (presets === void 0) return [...NATIVE_TOOL_NAMES];
|
|
416
|
+
try {
|
|
417
|
+
const key = await presets.standingKeyFor(presetId);
|
|
418
|
+
const global = new Set(ctx.tools.schemas().map((s) => s.name));
|
|
419
|
+
return ctx.tools.schemas(key).map((s) => s.name).filter((n) => !global.has(n) && n !== RUN_CODE_NAME).sort();
|
|
420
|
+
} catch (error) {
|
|
421
|
+
ctx.logger.warn(`dsh-agent-toolkit: 枚举 preset "${presetId}" 工具面失败,回退内置常量:${error instanceof Error ? error.message : String(error)}`);
|
|
422
|
+
return [...NATIVE_TOOL_NAMES];
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
listGlobalTools() {
|
|
426
|
+
return ctx.tools.schemas().map((s) => s.name).filter((n) => n !== RUN_CODE_NAME);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
322
431
|
//#region src/shared/storage.ts
|
|
323
432
|
function openDomainSafely(ctx, domain, warn, beforeClose) {
|
|
324
433
|
const ready = ctx.storageDomain.open(domain);
|
|
@@ -1313,10 +1422,19 @@ function createDelegateTool(toolName, deps) {
|
|
|
1313
1422
|
async execute(args, exec) {
|
|
1314
1423
|
const parent = exec.agent;
|
|
1315
1424
|
if (!parent) throw new Error("team_delegate 需要调用方 agent(exec.agent 为空)");
|
|
1316
|
-
const roster = deps.roster().filter((r) => r.id !== "main");
|
|
1425
|
+
const roster = deps.roster().filter((r) => r.id !== "main" && isTeamVisible(r));
|
|
1317
1426
|
const role = roster.find((r) => r.id === args.role);
|
|
1318
1427
|
if (!role) throw new Error(`未知角色 "${args.role}"。可用角色:${roster.map((r) => r.id).join(", ")}`);
|
|
1319
1428
|
const persona = deps.buildPersona(role);
|
|
1429
|
+
let toolFilter;
|
|
1430
|
+
if (role.tools !== void 0) {
|
|
1431
|
+
const visible = new Set(deps.visibleSurface(parent).filter((n) => n !== RUN_CODE_NAME));
|
|
1432
|
+
const effective = role.tools.allow.filter((n) => visible.has(n));
|
|
1433
|
+
const dropped = role.tools.allow.filter((n) => !visible.has(n));
|
|
1434
|
+
if (dropped.length > 0) deps.warn(`dsh-agent-toolkit: 角色 ${role.id} 白名单含本会话不可见工具,委派时忽略:${dropped.join(", ")}`);
|
|
1435
|
+
if (effective.length === 0) throw new Error(`dsh-agent-toolkit: 角色 ${role.id} 工具白名单求交后为空(原 ${role.tools.allow.length} 个均不可见):${role.tools.allow.join(", ")}`);
|
|
1436
|
+
toolFilter = { allow: effective };
|
|
1437
|
+
}
|
|
1320
1438
|
const request = {
|
|
1321
1439
|
label: `role:${role.id}: ${args.description}`,
|
|
1322
1440
|
prompt: [{
|
|
@@ -1331,7 +1449,7 @@ function createDelegateTool(toolName, deps) {
|
|
|
1331
1449
|
provider: role.model.provider,
|
|
1332
1450
|
model: role.model.model
|
|
1333
1451
|
} } : {},
|
|
1334
|
-
...
|
|
1452
|
+
...toolFilter !== void 0 ? { toolFilter } : {}
|
|
1335
1453
|
};
|
|
1336
1454
|
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 !== "" ? {
|
|
1337
1455
|
provider: parent.options.provider,
|
|
@@ -1360,7 +1478,7 @@ const TEAM_SECTION_ORDER = 116.6;
|
|
|
1360
1478
|
*/
|
|
1361
1479
|
function teamSectionText(toolName, roles, toolVisible) {
|
|
1362
1480
|
if (!toolVisible) return "";
|
|
1363
|
-
return `你有一组可委派的成员:用 ${toolName} 把自包含的子任务委派给合适的成员,成员结果会作为工具返回值回到本对话。\n可用成员:\n${roles.filter((r) => r.id !== "main").map((r) => `${r.id}: ${r.description ?? r.name}`).join("\n")}`;
|
|
1481
|
+
return `你有一组可委派的成员:用 ${toolName} 把自包含的子任务委派给合适的成员,成员结果会作为工具返回值回到本对话。\n可用成员:\n${roles.filter((r) => r.id !== "main" && isTeamVisible(r)).map((r) => `${r.id}: ${r.description ?? r.name}`).join("\n")}`;
|
|
1364
1482
|
}
|
|
1365
1483
|
/**
|
|
1366
1484
|
* 挂载 team_delegate 工具与函数式团队提示段。
|
|
@@ -1383,7 +1501,11 @@ function setupDelegate(ctx, config, registry, channels) {
|
|
|
1383
1501
|
buildPersona: (role) => buildAgentPersona({ rules: config.rules }, role, role.model),
|
|
1384
1502
|
startRun: (pr, request) => ctx.subagents.start(pr, request),
|
|
1385
1503
|
active: channels.active,
|
|
1386
|
-
recordRoute: channels.recordRoute
|
|
1504
|
+
recordRoute: channels.recordRoute,
|
|
1505
|
+
visibleSurface: (agent) => agent.ctx.tools.schemas(scopeOf(agent.ctx)).map((s) => s.name),
|
|
1506
|
+
warn: (msg) => {
|
|
1507
|
+
ctx.logger.warn(msg);
|
|
1508
|
+
}
|
|
1387
1509
|
}));
|
|
1388
1510
|
};
|
|
1389
1511
|
ctx.on("subagent/provider-added", (p) => {
|
|
@@ -1504,7 +1626,7 @@ function createAgentsApiHandler(deps) {
|
|
|
1504
1626
|
}
|
|
1505
1627
|
if (sub === "/tools" && method === "GET") {
|
|
1506
1628
|
json$1(res, 200, {
|
|
1507
|
-
|
|
1629
|
+
preset: await deps.listPresetTools(),
|
|
1508
1630
|
global: deps.listTools()
|
|
1509
1631
|
});
|
|
1510
1632
|
return;
|
|
@@ -1631,7 +1753,7 @@ function buildCreateAgentGuidance(input) {
|
|
|
1631
1753
|
"3. 迭代:用户有修改意见时按意见修订名称、描述、个性和工具后再次确认,直到用户明确确认。"
|
|
1632
1754
|
];
|
|
1633
1755
|
if (input.requirement !== "") lines.push("", "## 用户初始需求", `用户已在命令中提供初始需求:「${input.requirement}」。请据此减少提问轮次,仅就不明确的点提问。`);
|
|
1634
|
-
lines.push("", "## 现有 Agent id(不可复用)", input.agentIds.join(", "), "id 规则:小写字母开头,仅含小写字母/数字/连字符([a-z0-9-]),最长 32 字符。", "", "## 可用工具清单",
|
|
1756
|
+
lines.push("", "## 现有 Agent id(不可复用)", input.agentIds.join(", "), "id 规则:小写字母开头,仅含小写字母/数字/连字符([a-z0-9-]),最长 32 字符。", "", "## 可用工具清单", `团队 preset 工具:${input.presetTools.join(", ")}`, `全局工具:${input.globalTools.join(", ")}`, "省略 tools 字段表示不限制(Agent 可使用全部工具)。一旦给出白名单,该 Agent 只有列出的工具可用:通常应保留 read/glob/grep/shell 等基础工具,否则失去读文件/搜索/执行命令等基本能力(最终取舍按需求判断,如只读角色可去掉 write/edit)。");
|
|
1635
1757
|
if (input.origin === void 0) lines.push("", "## 落库", "当前宿主无 web 服务,无法自动落库。用户确认推荐后,请把最终配置完整输出给用户,并提示其打开 Agents 面板按推荐内容手动创建。");
|
|
1636
1758
|
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 则把错误信息展示给用户,修正后重试。");
|
|
1637
1759
|
return lines.join("\n");
|
|
@@ -1648,12 +1770,13 @@ function setupCreateAgentCommand(ctx, deps) {
|
|
|
1648
1770
|
name: "create-agent",
|
|
1649
1771
|
description: "交互式创建 Agent 团队成员:访谈澄清需求 → 推荐配置 → 确认后经面板 API 落库",
|
|
1650
1772
|
input: { hint: "初始需求描述,可空" },
|
|
1651
|
-
handler: ({ rawInput, agent }) => {
|
|
1773
|
+
handler: async ({ rawInput, agent }) => {
|
|
1652
1774
|
const webServer = ctx.get("webServer");
|
|
1653
1775
|
const origin = webServer === void 0 ? void 0 : `http://127.0.0.1:${webServer.port}`;
|
|
1654
1776
|
const text = buildCreateAgentGuidance({
|
|
1655
1777
|
requirement: rawInput.trim(),
|
|
1656
1778
|
agentIds: deps.registry.list().map((agent) => agent.id),
|
|
1779
|
+
presetTools: await deps.listPresetTools(),
|
|
1657
1780
|
globalTools: deps.listTools(),
|
|
1658
1781
|
origin
|
|
1659
1782
|
});
|
|
@@ -1682,8 +1805,8 @@ const TOOLKIT_PERSONA_SECTION = "prompt-stack:persona";
|
|
|
1682
1805
|
* (global + 祖先 scope 层),own 层(agentCtx 直接挂载)的名字既不可 restrict 也
|
|
1683
1806
|
* 不受 restrict 影响,直接挂进 agentCtx 的白名单会抛 unknown global tools。
|
|
1684
1807
|
*/
|
|
1685
|
-
async function setupAgentScope(agentCtx, hooks,
|
|
1686
|
-
await
|
|
1808
|
+
async function setupAgentScope(agentCtx, hooks, joiner) {
|
|
1809
|
+
await joiner.join(agentCtx);
|
|
1687
1810
|
if (hooks.persona !== void 0) agentCtx.systemPrompt.section({
|
|
1688
1811
|
name: TOOLKIT_PERSONA_SECTION,
|
|
1689
1812
|
order: 10,
|
|
@@ -1694,10 +1817,56 @@ async function setupAgentScope(agentCtx, hooks, toolsScope) {
|
|
|
1694
1817
|
order: section.order,
|
|
1695
1818
|
text: section.text
|
|
1696
1819
|
});
|
|
1697
|
-
if (hooks.tools !== void 0)
|
|
1820
|
+
if (hooks.tools !== void 0) {
|
|
1821
|
+
const visible = new Set(agentCtx.tools.schemas(scopeOf(agentCtx)).map((s) => s.name).filter((n) => n !== RUN_CODE_NAME));
|
|
1822
|
+
const effective = hooks.tools.filter((n) => visible.has(n));
|
|
1823
|
+
const dropped = hooks.tools.filter((n) => !visible.has(n));
|
|
1824
|
+
if (dropped.length > 0) agentCtx.logger.warn(`dsh-agent-toolkit: 工具白名单含本会话不可见工具,已忽略:${dropped.join(", ")}`);
|
|
1825
|
+
if (effective.length === 0) throw new Error(`dsh-agent-toolkit: 工具白名单求交后为空(原 ${hooks.tools.length} 个均不可见):${hooks.tools.join(", ")}`);
|
|
1826
|
+
agentCtx.tools.restrict({ allow: effective });
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
//#endregion
|
|
1830
|
+
//#region src/channels/agents-port.ts
|
|
1831
|
+
function createAgentsPort(ctx, joiner, ownedSessions) {
|
|
1832
|
+
function adaptAgent(handle) {
|
|
1833
|
+
const { agent } = handle;
|
|
1834
|
+
return {
|
|
1835
|
+
sessionId: String(agent.id),
|
|
1836
|
+
followup: (message) => agent.followup(message),
|
|
1837
|
+
cancel: () => agent.cancel({ kind: "user" }),
|
|
1838
|
+
whenIdle: () => agent.whenIdle()
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
return {
|
|
1842
|
+
async create(input) {
|
|
1843
|
+
ownedSessions?.add(input.sessionId);
|
|
1844
|
+
return adaptAgent(await ctx.agents.create({
|
|
1845
|
+
sessionId: SessionId(input.sessionId),
|
|
1846
|
+
meta: { cwd: input.cwd },
|
|
1847
|
+
...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
|
|
1848
|
+
setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks, joiner)
|
|
1849
|
+
}));
|
|
1850
|
+
},
|
|
1851
|
+
async resume(input) {
|
|
1852
|
+
ownedSessions?.add(input.sessionId);
|
|
1853
|
+
return adaptAgent(await ctx.agents.resume({
|
|
1854
|
+
resumeSessionId: SessionId(input.sessionId),
|
|
1855
|
+
...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
|
|
1856
|
+
setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks, joiner)
|
|
1857
|
+
}));
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1698
1860
|
}
|
|
1699
1861
|
//#endregion
|
|
1700
1862
|
//#region src/channels/feishu/api.ts
|
|
1863
|
+
/** 从 lark SDK 抛出的 axios 错误提取飞书业务错误码(无则 undefined)。 */
|
|
1864
|
+
function feishuErrorCode(error) {
|
|
1865
|
+
const data = error?.response?.data;
|
|
1866
|
+
if (typeof data !== "object" || data === null) return void 0;
|
|
1867
|
+
const code = data.code;
|
|
1868
|
+
return typeof code === "number" ? code : void 0;
|
|
1869
|
+
}
|
|
1701
1870
|
/** 附件服务接受的图片媒体类型(与宿主 attachment v1 一致)。 */
|
|
1702
1871
|
const IMAGE_MEDIA_TYPES = [
|
|
1703
1872
|
"image/png",
|
|
@@ -1973,27 +2142,10 @@ const STATUS_FINAL = {
|
|
|
1973
2142
|
error: "❌ 输出出错",
|
|
1974
2143
|
cancelled: "⏹ 已取消"
|
|
1975
2144
|
};
|
|
1976
|
-
/**
|
|
1977
|
-
const
|
|
2145
|
+
/** 拆卡定格状态行文案(旧卡内容已接续到下一张卡片)。 */
|
|
2146
|
+
const STATUS_CONTINUED = "📦 内容较长,已接续到下一张卡片";
|
|
1978
2147
|
/** 单卡组件数安全上限(飞书硬上限 200;面板按 2 计:面板 + 内嵌 markdown)。 */
|
|
1979
2148
|
const CARD_ELEMENT_LIMIT = 190;
|
|
1980
|
-
/** 按 UTF-8 字节上限截头(保留头部),不劈开多字节字符与代理对。 */
|
|
1981
|
-
function sliceByBytes(text, maxBytes) {
|
|
1982
|
-
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1983
|
-
let lo = 0;
|
|
1984
|
-
let hi = text.length;
|
|
1985
|
-
while (lo < hi) {
|
|
1986
|
-
const mid = Math.ceil((lo + hi) / 2);
|
|
1987
|
-
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
1988
|
-
else hi = mid - 1;
|
|
1989
|
-
}
|
|
1990
|
-
let cut = lo;
|
|
1991
|
-
if (cut > 0) {
|
|
1992
|
-
const code = text.charCodeAt(cut - 1);
|
|
1993
|
-
if (code >= 55296 && code <= 56319) cut -= 1;
|
|
1994
|
-
}
|
|
1995
|
-
return text.slice(0, cut);
|
|
1996
|
-
}
|
|
1997
2149
|
/** 按 UTF-8 字节上限截尾(保留尾部),不劈开多字节字符与代理对;截断时头部加省略标记。 */
|
|
1998
2150
|
function sliceTailByBytes(text, maxBytes) {
|
|
1999
2151
|
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
@@ -2013,8 +2165,29 @@ function sliceTailByBytes(text, maxBytes) {
|
|
|
2013
2165
|
}
|
|
2014
2166
|
return PROCESS_OMITTED + text.slice(cut);
|
|
2015
2167
|
}
|
|
2168
|
+
/** JSON 串内内容的转义后字节数(去首尾引号);卡片 DSL 真实字节记账用。 */
|
|
2169
|
+
function escapedLen(s) {
|
|
2170
|
+
return Buffer.byteLength(JSON.stringify(s), "utf8") - 2;
|
|
2171
|
+
}
|
|
2172
|
+
/** 按转义后字节上限截头(保留头部),不劈开多字节字符与代理对。 */
|
|
2173
|
+
function sliceByEscapedBytes(text, maxBytes) {
|
|
2174
|
+
if (escapedLen(text) <= maxBytes) return text;
|
|
2175
|
+
let lo = 0;
|
|
2176
|
+
let hi = text.length;
|
|
2177
|
+
while (lo < hi) {
|
|
2178
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
2179
|
+
if (escapedLen(text.slice(0, mid)) <= maxBytes) lo = mid;
|
|
2180
|
+
else hi = mid - 1;
|
|
2181
|
+
}
|
|
2182
|
+
let cut = lo;
|
|
2183
|
+
if (cut > 0) {
|
|
2184
|
+
const code = text.charCodeAt(cut - 1);
|
|
2185
|
+
if (code >= 55296 && code <= 56319) cut -= 1;
|
|
2186
|
+
}
|
|
2187
|
+
return text.slice(0, cut);
|
|
2188
|
+
}
|
|
2016
2189
|
/** 新卡:仅状态行的流式卡;段后续经插入组件 API 动态加入。 */
|
|
2017
|
-
function buildCardJson() {
|
|
2190
|
+
function buildCardJson(printStep) {
|
|
2018
2191
|
return JSON.stringify({
|
|
2019
2192
|
schema: "2.0",
|
|
2020
2193
|
config: {
|
|
@@ -2022,7 +2195,7 @@ function buildCardJson() {
|
|
|
2022
2195
|
summary: { content: "生成中…" },
|
|
2023
2196
|
streaming_config: {
|
|
2024
2197
|
print_frequency_ms: { default: 70 },
|
|
2025
|
-
print_step: { default:
|
|
2198
|
+
print_step: { default: printStep },
|
|
2026
2199
|
print_strategy: "fast"
|
|
2027
2200
|
}
|
|
2028
2201
|
},
|
|
@@ -2065,30 +2238,56 @@ const initialStreamState = () => ({
|
|
|
2065
2238
|
carry: void 0
|
|
2066
2239
|
});
|
|
2067
2240
|
/** 把段序列的新增部分同步到卡片;新段 insert 到状态行之前,尾段增长走元素 update,满卡关流开续卡。 */
|
|
2068
|
-
function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
2241
|
+
function planSync(state, segments, maxBytes, processMaxBytes, printStep) {
|
|
2069
2242
|
const ops = [];
|
|
2070
2243
|
let { cardId, seq, cardBytes, cardElements, segCounter, closedSegCount, tail, carry } = state;
|
|
2071
|
-
|
|
2072
|
-
|
|
2244
|
+
/** 约定:先改规划局部变量再 push——commit 捕获该 op 完成后的状态快照。 */
|
|
2245
|
+
const push = (op) => {
|
|
2246
|
+
const snap = {
|
|
2247
|
+
cardId,
|
|
2248
|
+
seq,
|
|
2249
|
+
cardBytes,
|
|
2250
|
+
cardElements,
|
|
2251
|
+
segCounter,
|
|
2252
|
+
closedSegCount,
|
|
2253
|
+
tail,
|
|
2254
|
+
carry
|
|
2255
|
+
};
|
|
2073
2256
|
ops.push({
|
|
2074
|
-
|
|
2075
|
-
|
|
2257
|
+
op,
|
|
2258
|
+
commit: () => ({ ...snap })
|
|
2076
2259
|
});
|
|
2077
|
-
|
|
2260
|
+
};
|
|
2261
|
+
const ensureCard = () => {
|
|
2262
|
+
if (cardId !== null) return;
|
|
2263
|
+
const cardJson = buildCardJson(printStep);
|
|
2078
2264
|
cardId = PENDING_CARD_ID;
|
|
2079
2265
|
seq = 0;
|
|
2080
|
-
cardBytes =
|
|
2266
|
+
cardBytes = Buffer.byteLength(cardJson, "utf8");
|
|
2081
2267
|
cardElements = 1;
|
|
2268
|
+
push({
|
|
2269
|
+
type: "create",
|
|
2270
|
+
cardJson
|
|
2271
|
+
});
|
|
2272
|
+
push({ type: "send" });
|
|
2082
2273
|
};
|
|
2083
2274
|
const closeCard = () => {
|
|
2084
2275
|
seq += 1;
|
|
2085
|
-
|
|
2086
|
-
type: "
|
|
2087
|
-
|
|
2276
|
+
push({
|
|
2277
|
+
type: "update",
|
|
2278
|
+
elementId: STATUS_ELEMENT_ID,
|
|
2279
|
+
content: STATUS_CONTINUED,
|
|
2088
2280
|
sequence: seq
|
|
2089
2281
|
});
|
|
2282
|
+
seq += 1;
|
|
2090
2283
|
cardId = null;
|
|
2091
2284
|
tail = void 0;
|
|
2285
|
+
push({
|
|
2286
|
+
type: "settings",
|
|
2287
|
+
streaming: false,
|
|
2288
|
+
sequence: seq,
|
|
2289
|
+
summary: STATUS_CONTINUED
|
|
2290
|
+
});
|
|
2092
2291
|
};
|
|
2093
2292
|
let i = tail?.segIndex ?? closedSegCount;
|
|
2094
2293
|
while (i < segments.length) {
|
|
@@ -2098,35 +2297,35 @@ function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
|
2098
2297
|
const elementContent = seg.kind === "text" ? content.slice(base) : content;
|
|
2099
2298
|
if (tail !== void 0 && tail.segIndex === i) {
|
|
2100
2299
|
if (elementContent !== tail.shownText) {
|
|
2101
|
-
const delta =
|
|
2300
|
+
const delta = escapedLen(elementContent) - escapedLen(tail.shownText);
|
|
2102
2301
|
if (cardBytes + delta <= maxBytes) {
|
|
2103
2302
|
seq += 1;
|
|
2104
|
-
ops.push({
|
|
2105
|
-
type: "update",
|
|
2106
|
-
elementId: tail.elementId,
|
|
2107
|
-
content: elementContent,
|
|
2108
|
-
sequence: seq
|
|
2109
|
-
});
|
|
2110
2303
|
cardBytes += delta;
|
|
2111
2304
|
tail = {
|
|
2112
2305
|
...tail,
|
|
2113
2306
|
shownText: elementContent
|
|
2114
2307
|
};
|
|
2308
|
+
push({
|
|
2309
|
+
type: "update",
|
|
2310
|
+
elementId: tail.elementId,
|
|
2311
|
+
content: elementContent,
|
|
2312
|
+
sequence: seq
|
|
2313
|
+
});
|
|
2115
2314
|
} else if (seg.kind === "text") {
|
|
2116
|
-
const piece =
|
|
2315
|
+
const piece = sliceByEscapedBytes(elementContent, escapedLen(tail.shownText) + (maxBytes - cardBytes));
|
|
2117
2316
|
if (piece.length > tail.shownText.length) {
|
|
2118
2317
|
seq += 1;
|
|
2119
|
-
|
|
2318
|
+
cardBytes += escapedLen(piece) - escapedLen(tail.shownText);
|
|
2319
|
+
tail = {
|
|
2320
|
+
...tail,
|
|
2321
|
+
shownText: piece
|
|
2322
|
+
};
|
|
2323
|
+
push({
|
|
2120
2324
|
type: "update",
|
|
2121
2325
|
elementId: tail.elementId,
|
|
2122
2326
|
content: piece,
|
|
2123
2327
|
sequence: seq
|
|
2124
2328
|
});
|
|
2125
|
-
cardBytes += Buffer.byteLength(piece, "utf8") - Buffer.byteLength(tail.shownText, "utf8");
|
|
2126
|
-
tail = {
|
|
2127
|
-
...tail,
|
|
2128
|
-
shownText: piece
|
|
2129
|
-
};
|
|
2130
2329
|
}
|
|
2131
2330
|
carry = {
|
|
2132
2331
|
segIndex: i,
|
|
@@ -2147,21 +2346,17 @@ function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
|
2147
2346
|
continue;
|
|
2148
2347
|
}
|
|
2149
2348
|
if (seg.kind === "process") {
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2349
|
+
const elementId = `seg_${segCounter + 1}`;
|
|
2350
|
+
const elementJson = buildSegmentJson("process", elementId, elementContent);
|
|
2351
|
+
const elBytes = Buffer.byteLength(elementJson, "utf8");
|
|
2352
|
+
if (cardId !== null && (cardBytes + elBytes > maxBytes || cardElements + 2 > CARD_ELEMENT_LIMIT)) {
|
|
2152
2353
|
closeCard();
|
|
2153
2354
|
continue;
|
|
2154
2355
|
}
|
|
2155
2356
|
ensureCard();
|
|
2156
2357
|
segCounter += 1;
|
|
2157
|
-
const elementId = `seg_${segCounter}`;
|
|
2158
2358
|
seq += 1;
|
|
2159
|
-
|
|
2160
|
-
type: "insert",
|
|
2161
|
-
elementJson: buildSegmentJson("process", elementId, elementContent),
|
|
2162
|
-
sequence: seq
|
|
2163
|
-
});
|
|
2164
|
-
cardBytes += windowBytes;
|
|
2359
|
+
cardBytes += elBytes;
|
|
2165
2360
|
cardElements += 2;
|
|
2166
2361
|
tail = {
|
|
2167
2362
|
segIndex: i,
|
|
@@ -2169,6 +2364,11 @@ function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
|
2169
2364
|
base: 0,
|
|
2170
2365
|
shownText: elementContent
|
|
2171
2366
|
};
|
|
2367
|
+
push({
|
|
2368
|
+
type: "insert",
|
|
2369
|
+
elementJson,
|
|
2370
|
+
sequence: seq
|
|
2371
|
+
});
|
|
2172
2372
|
} else {
|
|
2173
2373
|
if (elementContent.length === 0) {
|
|
2174
2374
|
closedSegCount = i + 1;
|
|
@@ -2181,17 +2381,14 @@ function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
|
2181
2381
|
continue;
|
|
2182
2382
|
}
|
|
2183
2383
|
ensureCard();
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2384
|
+
const elementId = `seg_${segCounter + 1}`;
|
|
2385
|
+
const overhead = Buffer.byteLength(buildSegmentJson("text", elementId, ""), "utf8");
|
|
2386
|
+
const piece = sliceByEscapedBytes(elementContent, maxBytes - cardBytes - overhead);
|
|
2387
|
+
if (piece.length === 0) throw new Error(`cardMaxBytes=${maxBytes} 过小,扣基础卡与元素开销后连一个字符都容纳不了`);
|
|
2186
2388
|
segCounter += 1;
|
|
2187
|
-
const
|
|
2389
|
+
const elementJson = buildSegmentJson("text", elementId, piece);
|
|
2188
2390
|
seq += 1;
|
|
2189
|
-
|
|
2190
|
-
type: "insert",
|
|
2191
|
-
elementJson: buildSegmentJson("text", elementId, piece),
|
|
2192
|
-
sequence: seq
|
|
2193
|
-
});
|
|
2194
|
-
cardBytes += Buffer.byteLength(piece, "utf8");
|
|
2391
|
+
cardBytes += Buffer.byteLength(elementJson, "utf8");
|
|
2195
2392
|
cardElements += 1;
|
|
2196
2393
|
tail = {
|
|
2197
2394
|
segIndex: i,
|
|
@@ -2199,6 +2396,11 @@ function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
|
2199
2396
|
base,
|
|
2200
2397
|
shownText: piece
|
|
2201
2398
|
};
|
|
2399
|
+
push({
|
|
2400
|
+
type: "insert",
|
|
2401
|
+
elementJson,
|
|
2402
|
+
sequence: seq
|
|
2403
|
+
});
|
|
2202
2404
|
if (piece.length < elementContent.length) {
|
|
2203
2405
|
carry = {
|
|
2204
2406
|
segIndex: i,
|
|
@@ -2214,19 +2416,21 @@ function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
|
2214
2416
|
closedSegCount = i + 1;
|
|
2215
2417
|
i += 1;
|
|
2216
2418
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
carry
|
|
2227
|
-
},
|
|
2228
|
-
ops
|
|
2419
|
+
const snap = {
|
|
2420
|
+
cardId,
|
|
2421
|
+
seq,
|
|
2422
|
+
cardBytes,
|
|
2423
|
+
cardElements,
|
|
2424
|
+
segCounter,
|
|
2425
|
+
closedSegCount,
|
|
2426
|
+
tail,
|
|
2427
|
+
carry
|
|
2229
2428
|
};
|
|
2429
|
+
ops.push({
|
|
2430
|
+
op: { type: "noop" },
|
|
2431
|
+
commit: () => ({ ...snap })
|
|
2432
|
+
});
|
|
2433
|
+
return { ops };
|
|
2230
2434
|
}
|
|
2231
2435
|
/** 定格:先 update 状态行(流式还开着),再关闭 + summary。 */
|
|
2232
2436
|
function planFinalize(state, status) {
|
|
@@ -2256,6 +2460,10 @@ async function withRetry(fn, attempts = 3, baseDelayMs = 300) {
|
|
|
2256
2460
|
}
|
|
2257
2461
|
throw lastError;
|
|
2258
2462
|
}
|
|
2463
|
+
/** 卡片输出异常时的用户提示(仅真实废弃一张已发卡时发送)。 */
|
|
2464
|
+
const ABANDON_NOTICE = "⚠️ 卡片输出异常,已在新卡片继续;如有内容缺失请重发。";
|
|
2465
|
+
/** 单次 flush 内连续废弃换卡的上限(防异常死循环;超限抛给出站链日志)。 */
|
|
2466
|
+
const MAX_ABANDON_PER_FLUSH = 3;
|
|
2259
2467
|
var FeishuReplyHandle = class {
|
|
2260
2468
|
api;
|
|
2261
2469
|
chatId;
|
|
@@ -2265,6 +2473,7 @@ var FeishuReplyHandle = class {
|
|
|
2265
2473
|
segments = [];
|
|
2266
2474
|
tail = Promise.resolve();
|
|
2267
2475
|
timer;
|
|
2476
|
+
planQueued = false;
|
|
2268
2477
|
finalized = false;
|
|
2269
2478
|
constructor(api, chatId, tunables, log) {
|
|
2270
2479
|
this.api = api;
|
|
@@ -2272,7 +2481,6 @@ var FeishuReplyHandle = class {
|
|
|
2272
2481
|
this.tunables = tunables;
|
|
2273
2482
|
this.log = log;
|
|
2274
2483
|
}
|
|
2275
|
-
/** 惰性建卡:无文本输出的 turn 不产生空卡片。 */
|
|
2276
2484
|
beginTurn() {
|
|
2277
2485
|
return Promise.resolve();
|
|
2278
2486
|
}
|
|
@@ -2297,49 +2505,204 @@ var FeishuReplyHandle = class {
|
|
|
2297
2505
|
}
|
|
2298
2506
|
this.flush();
|
|
2299
2507
|
await this.tail;
|
|
2508
|
+
const hadCard = this.state.cardId !== null;
|
|
2300
2509
|
const { ops } = planFinalize(this.state, status);
|
|
2301
|
-
this.enqueue(() =>
|
|
2302
|
-
|
|
2510
|
+
this.enqueue(async () => {
|
|
2511
|
+
for (const op of ops) if (await this.execOne({
|
|
2512
|
+
op,
|
|
2513
|
+
commit: (s) => s
|
|
2514
|
+
}) === "abandoned") return;
|
|
2515
|
+
});
|
|
2516
|
+
if (!hadCard && detail !== void 0) this.enqueue(() => withRetry(() => this.api.sendText(this.chatId, detail)).then(() => void 0));
|
|
2303
2517
|
await this.tail;
|
|
2304
2518
|
}
|
|
2305
2519
|
notice(text) {
|
|
2306
2520
|
this.enqueue(() => withRetry(() => this.api.sendText(this.chatId, text)).then(() => void 0));
|
|
2307
2521
|
return this.tail.then(() => void 0);
|
|
2308
2522
|
}
|
|
2523
|
+
/**
|
|
2524
|
+
* 规划入串行链:planSync 在执行点读最新已确认状态与最新 segments,
|
|
2525
|
+
* 在飞期间到达的 flush 只标位不重复规划(杜绝重复建卡/重复 insert)。
|
|
2526
|
+
*/
|
|
2309
2527
|
flush() {
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
this.
|
|
2313
|
-
|
|
2528
|
+
if (this.planQueued) return;
|
|
2529
|
+
this.planQueued = true;
|
|
2530
|
+
this.enqueue(async () => {
|
|
2531
|
+
this.planQueued = false;
|
|
2532
|
+
const { ops } = planSync(this.state, this.segments, this.tunables.cardMaxBytes, this.tunables.processMaxBytes, this.tunables.cardPrintStep);
|
|
2533
|
+
await this.exec(ops);
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
/** 逐 op 确认执行;遇废弃从已确认状态重新规划续写(上限 MAX_ABANDON_PER_FLUSH 次)。 */
|
|
2537
|
+
async exec(ops) {
|
|
2538
|
+
let pending = ops;
|
|
2539
|
+
for (let attempts = 0;; attempts++) {
|
|
2540
|
+
let abandoned = false;
|
|
2541
|
+
for (const planned of pending) if (await this.execOne(planned) === "abandoned") {
|
|
2542
|
+
abandoned = true;
|
|
2543
|
+
break;
|
|
2544
|
+
}
|
|
2545
|
+
if (!abandoned) return;
|
|
2546
|
+
if (attempts >= MAX_ABANDON_PER_FLUSH) throw new Error("卡片连续废弃超限,本批输出放弃(下一 flush 继续)");
|
|
2547
|
+
pending = planSync(this.state, this.segments, this.tunables.cardMaxBytes, this.tunables.processMaxBytes, this.tunables.cardPrintStep).ops;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* 单个 op:成功(或 insert 300301 视同成功)才 commit;
|
|
2552
|
+
* 失败按错误码分类:流式超时重激活重放 / 200860 废弃续写 / 未知错误 seq+2 重放一次。
|
|
2553
|
+
*/
|
|
2554
|
+
async execOne(planned) {
|
|
2555
|
+
const { op } = planned;
|
|
2556
|
+
if (op.type === "noop") {
|
|
2557
|
+
this.commit(planned);
|
|
2558
|
+
return "ok";
|
|
2559
|
+
}
|
|
2560
|
+
const liveCard = this.state.cardId !== null && this.state.cardId !== "__pending__";
|
|
2561
|
+
try {
|
|
2562
|
+
await this.invokeThenCommit(planned);
|
|
2563
|
+
return "ok";
|
|
2564
|
+
} catch (error) {
|
|
2565
|
+
if (!liveCard) throw error;
|
|
2566
|
+
const code = feishuErrorCode(error);
|
|
2567
|
+
if (op.type === "insert" && code === 300301) {
|
|
2568
|
+
this.commit(planned);
|
|
2569
|
+
return "ok";
|
|
2570
|
+
}
|
|
2571
|
+
if (code === 200850 || code === 200510) {
|
|
2572
|
+
if (await this.reactivate()) try {
|
|
2573
|
+
await this.invokeThenCommit(planned, this.state.seq + 1);
|
|
2574
|
+
return "ok";
|
|
2575
|
+
} catch {
|
|
2576
|
+
return this.abandon("流式超时重激活后重放失败", true);
|
|
2577
|
+
}
|
|
2578
|
+
return this.abandon("流式超时且重激活失败", true);
|
|
2579
|
+
}
|
|
2580
|
+
if (code === 200860) return this.abandon("卡片超出平台大小上限", false);
|
|
2581
|
+
try {
|
|
2582
|
+
const retrySeq = (op.type === "insert" || op.type === "update" || op.type === "settings" ? op.sequence : this.state.seq) + 2;
|
|
2583
|
+
await this.invokeThenCommit(planned, retrySeq);
|
|
2584
|
+
return "ok";
|
|
2585
|
+
} catch (retryError) {
|
|
2586
|
+
if (op.type === "insert" && feishuErrorCode(retryError) === 300301) {
|
|
2587
|
+
this.commit(planned);
|
|
2588
|
+
return "ok";
|
|
2589
|
+
}
|
|
2590
|
+
return this.abandon("卡片操作重放失败", true);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* 执行 API 并在成功后 commit;seqOverride 用于重放(create 的真实 cardId 在此覆盖进状态)。
|
|
2596
|
+
* 带序号 op 的发送/提交序号一律取 max(本次序号, 已确认 seq+1):重放/重激活会推高已确认 seq,
|
|
2597
|
+
* 批内后续 op 的规划序号可能落后,照发会以非递增序号碰撞(平台可能视作幂等 no-op 静默丢 op →
|
|
2598
|
+
* 定格/关流 op 消失 → 卡死「输出中」)。
|
|
2599
|
+
*/
|
|
2600
|
+
async invokeThenCommit(planned, seqOverride) {
|
|
2601
|
+
const effectiveSeq = effectiveSeqOf(planned.op, seqOverride, this.state.seq);
|
|
2602
|
+
const op = withSeq(planned.op, effectiveSeq);
|
|
2603
|
+
if (op.type === "create") {
|
|
2604
|
+
const id = await withRetry(() => this.api.createCard(op.cardJson));
|
|
2605
|
+
this.state = {
|
|
2606
|
+
...planned.commit(this.state),
|
|
2607
|
+
cardId: id
|
|
2608
|
+
};
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
if (op.type === "send") await withRetry(() => this.api.sendCardMessage(this.chatId, this.state.cardId));
|
|
2612
|
+
else if (op.type === "insert") await this.api.insertElement(this.state.cardId, op.elementJson, STATUS_ELEMENT_ID, op.sequence);
|
|
2613
|
+
else if (op.type === "update") await this.api.updateCardElement(this.state.cardId, op.elementId, op.content, op.sequence);
|
|
2614
|
+
else if (op.type === "settings") await this.api.setCardStreaming(this.state.cardId, op.streaming, op.sequence, op.summary);
|
|
2615
|
+
this.commit(planned, effectiveSeq);
|
|
2616
|
+
}
|
|
2617
|
+
commit(planned, seqOverride) {
|
|
2618
|
+
const next = planned.commit(this.state);
|
|
2619
|
+
const current = this.state.cardId;
|
|
2620
|
+
const cardId = next.cardId === "__pending__" && current !== null && current !== "__pending__" ? current : next.cardId;
|
|
2621
|
+
this.state = seqOverride !== void 0 ? {
|
|
2622
|
+
...next,
|
|
2623
|
+
seq: seqOverride,
|
|
2624
|
+
cardId
|
|
2625
|
+
} : {
|
|
2626
|
+
...next,
|
|
2627
|
+
seq: Math.max(next.seq, this.state.seq),
|
|
2628
|
+
cardId
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
/** 流式超时后的官方恢复路径:settings 重设 streaming_mode:true(占一个 sequence)。 */
|
|
2632
|
+
async reactivate() {
|
|
2633
|
+
const { cardId, seq } = this.state;
|
|
2634
|
+
if (cardId === null || cardId === "__pending__") return false;
|
|
2635
|
+
try {
|
|
2636
|
+
await this.api.setCardStreaming(cardId, true, seq + 1);
|
|
2637
|
+
this.state = {
|
|
2638
|
+
...this.state,
|
|
2639
|
+
seq: seq + 1
|
|
2640
|
+
};
|
|
2641
|
+
return true;
|
|
2642
|
+
} catch {
|
|
2643
|
+
return false;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* 废弃当前卡:尽力关流 → cardId 归零、尾段回卷为 carry → 调用方重新规划续写。
|
|
2648
|
+
* reshowTail=true(未知失败,op 可能已执行)时从尾段 base 重演(少量重复优于丢失);
|
|
2649
|
+
* reshowTail=false(200860 确定性未应用)时跳过已显示部分(零重复)。
|
|
2650
|
+
*/
|
|
2651
|
+
async abandon(reason, reshowTail) {
|
|
2652
|
+
const { cardId, tail, seq } = this.state;
|
|
2653
|
+
const hadRealCard = cardId !== null && cardId !== "__pending__";
|
|
2654
|
+
if (hadRealCard) await this.api.setCardStreaming(cardId, false, seq + 1).catch(() => void 0);
|
|
2655
|
+
if (tail !== void 0) {
|
|
2656
|
+
const base = this.segments[tail.segIndex]?.kind === "process" ? 0 : reshowTail ? tail.base : tail.base + tail.shownText.length;
|
|
2657
|
+
this.state = {
|
|
2658
|
+
...this.state,
|
|
2659
|
+
cardId: null,
|
|
2660
|
+
tail: void 0,
|
|
2661
|
+
closedSegCount: tail.segIndex,
|
|
2662
|
+
carry: {
|
|
2663
|
+
segIndex: tail.segIndex,
|
|
2664
|
+
base
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
} else this.state = {
|
|
2668
|
+
...this.state,
|
|
2669
|
+
cardId: null
|
|
2670
|
+
};
|
|
2671
|
+
this.log(`[project-bot] 卡片输出异常(${reason}),已废弃当前卡并在新卡继续`);
|
|
2672
|
+
if (hadRealCard) await withRetry(() => this.api.sendText(this.chatId, ABANDON_NOTICE)).catch(() => void 0);
|
|
2673
|
+
return "abandoned";
|
|
2314
2674
|
}
|
|
2315
2675
|
enqueue(task) {
|
|
2316
2676
|
this.tail = this.tail.then(task).catch((error) => {
|
|
2317
|
-
if (this.state.cardId === "__pending__") {
|
|
2318
|
-
const t = this.state.tail;
|
|
2319
|
-
this.state = {
|
|
2320
|
-
...this.state,
|
|
2321
|
-
cardId: null,
|
|
2322
|
-
...t !== void 0 ? {
|
|
2323
|
-
tail: void 0,
|
|
2324
|
-
carry: {
|
|
2325
|
-
segIndex: t.segIndex,
|
|
2326
|
-
base: t.base
|
|
2327
|
-
},
|
|
2328
|
-
closedSegCount: t.segIndex
|
|
2329
|
-
} : {}
|
|
2330
|
-
};
|
|
2331
|
-
}
|
|
2332
2677
|
this.log(`[project-bot] 卡片操作失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2333
2678
|
});
|
|
2334
2679
|
}
|
|
2335
|
-
async exec(ops) {
|
|
2336
|
-
for (const op of ops) if (op.type === "create") this.state.cardId = await withRetry(() => this.api.createCard(op.cardJson));
|
|
2337
|
-
else if (op.type === "send") await withRetry(() => this.api.sendCardMessage(this.chatId, this.state.cardId));
|
|
2338
|
-
else if (op.type === "insert") await withRetry(() => this.api.insertElement(this.state.cardId, op.elementJson, STATUS_ELEMENT_ID, op.sequence));
|
|
2339
|
-
else if (op.type === "update") await withRetry(() => this.api.updateCardElement(this.state.cardId, op.elementId, op.content, op.sequence));
|
|
2340
|
-
else await withRetry(() => this.api.setCardStreaming(this.state.cardId, op.streaming, op.sequence, op.summary));
|
|
2341
|
-
}
|
|
2342
2680
|
};
|
|
2681
|
+
/** 重放用:仅带 sequence 语义的 op 替换序号(create/send 无序号)。 */
|
|
2682
|
+
function withSeq(op, sequence) {
|
|
2683
|
+
if (sequence === void 0) return op;
|
|
2684
|
+
if (op.type === "insert") return {
|
|
2685
|
+
...op,
|
|
2686
|
+
sequence
|
|
2687
|
+
};
|
|
2688
|
+
if (op.type === "update") return {
|
|
2689
|
+
...op,
|
|
2690
|
+
sequence
|
|
2691
|
+
};
|
|
2692
|
+
if (op.type === "settings") return {
|
|
2693
|
+
...op,
|
|
2694
|
+
sequence
|
|
2695
|
+
};
|
|
2696
|
+
return op;
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* 带序号 op 的生效 sequence(create/send 返回 undefined = 不占序号):
|
|
2700
|
+
* 重放/重激活推高已确认 seq 后,规划或重放序号落后时抬升到已确认 seq+1,保证单调不碰撞。
|
|
2701
|
+
*/
|
|
2702
|
+
function effectiveSeqOf(op, override, confirmedSeq) {
|
|
2703
|
+
if (op.type !== "insert" && op.type !== "update" && op.type !== "settings") return void 0;
|
|
2704
|
+
return Math.max(override ?? op.sequence, confirmedSeq + 1);
|
|
2705
|
+
}
|
|
2343
2706
|
/** 「处理中」表情:加上后返回删除 disposer;加/删失败都静默(表情残留无害)。 */
|
|
2344
2707
|
function makeAck(api, messageId, emojiType) {
|
|
2345
2708
|
return async () => {
|
|
@@ -2354,6 +2717,119 @@ function makeAck(api, messageId, emojiType) {
|
|
|
2354
2717
|
};
|
|
2355
2718
|
}
|
|
2356
2719
|
//#endregion
|
|
2720
|
+
//#region src/channels/approval/feishu.ts
|
|
2721
|
+
const STATUS_TEXT = {
|
|
2722
|
+
allowed: "✅ 已允许",
|
|
2723
|
+
rejected: "❌ 已拒绝",
|
|
2724
|
+
cancelled: "⏹ 已取消"
|
|
2725
|
+
};
|
|
2726
|
+
function bodyMarkdown(prompt) {
|
|
2727
|
+
const lines = [`**Bot**:${prompt.botName}`, `**工具**:\`${prompt.toolName}\``];
|
|
2728
|
+
if (prompt.reason !== void 0) lines.push(`**理由**:${prompt.reason}`);
|
|
2729
|
+
return lines.join("\n");
|
|
2730
|
+
}
|
|
2731
|
+
function button(text, type, key, decision) {
|
|
2732
|
+
return {
|
|
2733
|
+
tag: "button",
|
|
2734
|
+
text: {
|
|
2735
|
+
tag: "plain_text",
|
|
2736
|
+
content: text
|
|
2737
|
+
},
|
|
2738
|
+
type,
|
|
2739
|
+
behaviors: [{
|
|
2740
|
+
type: "callback",
|
|
2741
|
+
value: {
|
|
2742
|
+
key,
|
|
2743
|
+
decision
|
|
2744
|
+
}
|
|
2745
|
+
}]
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
/** 审批卡(带按钮);summary 供会话列表/推送预览。 */
|
|
2749
|
+
function buildApprovalCardJson(prompt) {
|
|
2750
|
+
return JSON.stringify({
|
|
2751
|
+
schema: "2.0",
|
|
2752
|
+
config: { summary: { content: `权限申请:${prompt.toolName}` } },
|
|
2753
|
+
header: {
|
|
2754
|
+
title: {
|
|
2755
|
+
tag: "plain_text",
|
|
2756
|
+
content: "权限申请"
|
|
2757
|
+
},
|
|
2758
|
+
template: "orange"
|
|
2759
|
+
},
|
|
2760
|
+
body: { elements: [{
|
|
2761
|
+
tag: "markdown",
|
|
2762
|
+
content: bodyMarkdown(prompt)
|
|
2763
|
+
}, {
|
|
2764
|
+
tag: "action",
|
|
2765
|
+
actions: [button("允许", "primary", prompt.key, "allow"), button("拒绝", "danger", prompt.key, "reject")]
|
|
2766
|
+
}] }
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
/** 终态卡(无按钮):定格审批结果;operatorName 仅允许/拒绝时有。 */
|
|
2770
|
+
function buildApprovalFinalCardJson(prompt, status, operatorName) {
|
|
2771
|
+
const statusLine = STATUS_TEXT[status] + (operatorName !== void 0 ? ` · ${operatorName}` : "");
|
|
2772
|
+
const template = status === "allowed" ? "green" : status === "rejected" ? "red" : "grey";
|
|
2773
|
+
return JSON.stringify({
|
|
2774
|
+
schema: "2.0",
|
|
2775
|
+
config: { summary: { content: `权限申请${STATUS_TEXT[status].slice(2).trim()}:${prompt.toolName}` } },
|
|
2776
|
+
header: {
|
|
2777
|
+
title: {
|
|
2778
|
+
tag: "plain_text",
|
|
2779
|
+
content: `权限申请 · ${statusLine}`
|
|
2780
|
+
},
|
|
2781
|
+
template
|
|
2782
|
+
},
|
|
2783
|
+
body: { elements: [{
|
|
2784
|
+
tag: "markdown",
|
|
2785
|
+
content: bodyMarkdown(prompt)
|
|
2786
|
+
}] }
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
/** 飞书审批能力:present = 建卡 + 发消息;finalize = replaceCard 定格(sequence 从 1 起,create/send 不占)。 */
|
|
2790
|
+
var FeishuApprovalPresenter = class {
|
|
2791
|
+
api;
|
|
2792
|
+
log;
|
|
2793
|
+
constructor(api, log) {
|
|
2794
|
+
this.api = api;
|
|
2795
|
+
this.log = log;
|
|
2796
|
+
}
|
|
2797
|
+
async present(prompt) {
|
|
2798
|
+
const cardId = await withRetry(() => this.api.createCard(buildApprovalCardJson(prompt)));
|
|
2799
|
+
await withRetry(() => this.api.sendCardMessage(prompt.chatId, cardId));
|
|
2800
|
+
return { finalize: async (status, operatorName) => {
|
|
2801
|
+
try {
|
|
2802
|
+
await withRetry(() => this.api.replaceCard(cardId, buildApprovalFinalCardJson(prompt, status, operatorName), 1));
|
|
2803
|
+
} catch (error) {
|
|
2804
|
+
this.log(`[project-bot] 审批卡片定格失败(card ${cardId}):${error instanceof Error ? error.message : String(error)}`);
|
|
2805
|
+
}
|
|
2806
|
+
} };
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
//#endregion
|
|
2810
|
+
//#region src/channels/feishu/card-action.ts
|
|
2811
|
+
/** card.action.trigger 回调解析:SDK normalizeCardAction → 渠道无关 CardActionInput;toast 应答帧负载。 */
|
|
2812
|
+
/** raw → CardActionInput;normalize 失败(畸形/缺字段)→ undefined。 */
|
|
2813
|
+
function toCardActionInput(raw) {
|
|
2814
|
+
if (raw === null || typeof raw !== "object") return void 0;
|
|
2815
|
+
const evt = lark.normalizeCardAction(raw);
|
|
2816
|
+
if (evt === null) return void 0;
|
|
2817
|
+
return {
|
|
2818
|
+
chatId: evt.chatId,
|
|
2819
|
+
operatorOpenId: evt.operator.openId,
|
|
2820
|
+
...evt.operator.name !== void 0 ? { operatorName: evt.operator.name } : {},
|
|
2821
|
+
value: evt.action.value
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
/** WS 应答帧负载:handler 返回值经 WSClient 回传(lib/index.js handleEventData:truthy result → respPayload.data)。 */
|
|
2825
|
+
function toastResponse(ack) {
|
|
2826
|
+
if (ack?.toast === void 0) return void 0;
|
|
2827
|
+
return { toast: {
|
|
2828
|
+
type: "info",
|
|
2829
|
+
content: ack.toast
|
|
2830
|
+
} };
|
|
2831
|
+
}
|
|
2832
|
+
//#endregion
|
|
2357
2833
|
//#region src/channels/feishu/index.ts
|
|
2358
2834
|
/** 飞书渠道:WSClient 长连接收事件 → 解析 → 核心;出站走 FeishuReplyHandle。 */
|
|
2359
2835
|
const feishuChannel = {
|
|
@@ -2383,6 +2859,12 @@ const feishuChannel = {
|
|
|
2383
2859
|
reply,
|
|
2384
2860
|
ackProcessing: makeAck(api, parsed.messageId, tunables.processingReactionEmoji)
|
|
2385
2861
|
});
|
|
2862
|
+
},
|
|
2863
|
+
"card.action.trigger": (raw) => {
|
|
2864
|
+
if (io.onCardAction === void 0) return void 0;
|
|
2865
|
+
const action = toCardActionInput(raw);
|
|
2866
|
+
if (action === void 0) return void 0;
|
|
2867
|
+
return toastResponse(io.onCardAction(action)) ?? void 0;
|
|
2386
2868
|
}
|
|
2387
2869
|
});
|
|
2388
2870
|
const ws = new lark.WSClient({
|
|
@@ -2392,6 +2874,7 @@ const feishuChannel = {
|
|
|
2392
2874
|
});
|
|
2393
2875
|
await ws.start({ eventDispatcher: dispatcher });
|
|
2394
2876
|
return {
|
|
2877
|
+
approval: new FeishuApprovalPresenter(api, log),
|
|
2395
2878
|
close: () => {
|
|
2396
2879
|
ws.close({ force: true });
|
|
2397
2880
|
return Promise.resolve();
|
|
@@ -2607,6 +3090,7 @@ var Inbound = class {
|
|
|
2607
3090
|
return;
|
|
2608
3091
|
}
|
|
2609
3092
|
rt.inflight = { ack: void 0 };
|
|
3093
|
+
rt.reply = msg.reply;
|
|
2610
3094
|
rt.inflight.ack = await msg.ackProcessing().catch(() => void 0) ?? void 0;
|
|
2611
3095
|
const imageRefs = [];
|
|
2612
3096
|
if (msg.loadImages !== void 0) {
|
|
@@ -2647,6 +3131,26 @@ function hooksOf(bot) {
|
|
|
2647
3131
|
};
|
|
2648
3132
|
}
|
|
2649
3133
|
//#endregion
|
|
3134
|
+
//#region src/channels/role-assembly.ts
|
|
3135
|
+
/** 角色 persona 的 scoped 段名(与 Router 既有产出一致,逐字段不可改)。 */
|
|
3136
|
+
const ROLE_PERSONA_SECTION = "dsh-agent-toolkit:agent:persona";
|
|
3137
|
+
/** 角色形态创作期注入:persona 非空 → 单 section(order 0);tools 白名单存在 → 带上(由 setupAgentScope restrict)。 */
|
|
3138
|
+
function roleHooks(role) {
|
|
3139
|
+
const sections = role.persona === void 0 || role.persona.trim().length === 0 ? [] : [{
|
|
3140
|
+
name: ROLE_PERSONA_SECTION,
|
|
3141
|
+
order: 0,
|
|
3142
|
+
text: role.persona
|
|
3143
|
+
}];
|
|
3144
|
+
return {
|
|
3145
|
+
...sections.length > 0 ? { sections } : {},
|
|
3146
|
+
...role.tools !== void 0 ? { tools: role.tools.allow } : {}
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
/** 角色模型:自配优先,缺省回退宿主默认模型(与 Router 语义一致)。 */
|
|
3150
|
+
function roleAgentOptions(role, defaultModel) {
|
|
3151
|
+
return role.model ?? defaultModel();
|
|
3152
|
+
}
|
|
3153
|
+
//#endregion
|
|
2650
3154
|
//#region src/channels/router.ts
|
|
2651
3155
|
/** 绑定路由:(botId, chatId) → 长期会话;create / resume / reset。 */
|
|
2652
3156
|
/** 发起人提示段名:bot 会话声明来源渠道与发起人 open_id。 */
|
|
@@ -2674,21 +3178,22 @@ var Router = class {
|
|
|
2674
3178
|
this.registry = registry;
|
|
2675
3179
|
this.injectSender = injectSender;
|
|
2676
3180
|
}
|
|
2677
|
-
/**
|
|
3181
|
+
/**
|
|
3182
|
+
* 取(或建/恢复)该 chat 的会话 runtime。存量活跃会话保持其 reply——运行中 turn 的出站
|
|
3183
|
+
* 必须留在原句柄收尾,reply 刷新由 Inbound 在 in-flight 准入通过后执行;
|
|
3184
|
+
* retiring(已 cancel、收尾中)的会话不可复用,重绑窗口内恢复时 resume + adopt 重建。
|
|
3185
|
+
*/
|
|
2678
3186
|
async ensure(bot, chatId, reply, userId) {
|
|
2679
3187
|
const bound = this.bindings.get(bot.id, chatId);
|
|
2680
3188
|
if (bound !== void 0) {
|
|
2681
3189
|
const existing = this.sessions.get(bound);
|
|
2682
|
-
if (existing !== void 0)
|
|
2683
|
-
existing.reply = reply;
|
|
2684
|
-
return existing;
|
|
2685
|
-
}
|
|
3190
|
+
if (existing !== void 0 && !existing.retiring) return existing;
|
|
2686
3191
|
const agent = await this.agents.resume({
|
|
2687
3192
|
sessionId: bound,
|
|
2688
3193
|
...this.resolveSession(bot, userId)
|
|
2689
3194
|
});
|
|
2690
3195
|
await this.attach(bot.project, bound);
|
|
2691
|
-
return this.adopt(bot.id, chatId, bound, agent, reply);
|
|
3196
|
+
return this.adopt(bot.id, chatId, userId, bound, agent, reply);
|
|
2692
3197
|
}
|
|
2693
3198
|
const sessionId = randomUUID();
|
|
2694
3199
|
const agent = await this.agents.create({
|
|
@@ -2698,7 +3203,7 @@ var Router = class {
|
|
|
2698
3203
|
});
|
|
2699
3204
|
await this.bindings.set(bot.id, chatId, sessionId);
|
|
2700
3205
|
await this.attach(bot.project, sessionId);
|
|
2701
|
-
return this.adopt(bot.id, chatId, sessionId, agent, reply);
|
|
3206
|
+
return this.adopt(bot.id, chatId, userId, sessionId, agent, reply);
|
|
2702
3207
|
}
|
|
2703
3208
|
/** attach 失败仅告警(会话降级为未分组),不阻塞消息处理。 */
|
|
2704
3209
|
async attach(cwd, sessionId) {
|
|
@@ -2737,49 +3242,130 @@ var Router = class {
|
|
|
2737
3242
|
hooks: this.withSenderSection(hooksOf(bot), bot, userId)
|
|
2738
3243
|
};
|
|
2739
3244
|
}
|
|
2740
|
-
const sections = role.persona === void 0 || role.persona.trim().length === 0 ? [] : [{
|
|
2741
|
-
name: "dsh-agent-toolkit:agent:persona",
|
|
2742
|
-
order: 0,
|
|
2743
|
-
text: role.persona
|
|
2744
|
-
}];
|
|
2745
3245
|
return {
|
|
2746
|
-
agentOptions: role
|
|
2747
|
-
hooks: this.withSenderSection(
|
|
2748
|
-
...sections.length > 0 ? { sections } : {},
|
|
2749
|
-
...role.tools !== void 0 ? { tools: role.tools.allow } : {}
|
|
2750
|
-
}, bot, userId)
|
|
3246
|
+
agentOptions: roleAgentOptions(role, this.defaultModel),
|
|
3247
|
+
hooks: this.withSenderSection(roleHooks(role), bot, userId)
|
|
2751
3248
|
};
|
|
2752
3249
|
}
|
|
2753
|
-
/** /new
|
|
3250
|
+
/** /new:取消旧会话;等 turn/end 落定(旧卡在旧句柄 finalize)后再摘出 sessions。 */
|
|
2754
3251
|
async reset(bot, chatId, reply, userId) {
|
|
2755
3252
|
const bound = this.bindings.get(bot.id, chatId);
|
|
2756
3253
|
if (bound !== void 0) {
|
|
2757
|
-
this.sessions.get(bound)
|
|
2758
|
-
this.
|
|
3254
|
+
const old = this.sessions.get(bound);
|
|
3255
|
+
if (old !== void 0) this.retire(bound, old);
|
|
2759
3256
|
await this.bindings.delete(bot.id, chatId);
|
|
2760
3257
|
}
|
|
2761
3258
|
return this.ensure(bot, chatId, reply, userId);
|
|
2762
3259
|
}
|
|
3260
|
+
/** 取消会话并等出站链落定后摘出 sessions(让在飞 turn 的 turn/end 正常 finalize 旧卡)。 */
|
|
3261
|
+
retire(sessionId, rt) {
|
|
3262
|
+
rt.retiring = true;
|
|
3263
|
+
rt.agent.cancel();
|
|
3264
|
+
(async () => {
|
|
3265
|
+
await rt.agent.whenIdle().catch(() => void 0);
|
|
3266
|
+
await rt.tail.catch(() => void 0);
|
|
3267
|
+
if (this.sessions.get(sessionId) === rt) this.sessions.delete(sessionId);
|
|
3268
|
+
})();
|
|
3269
|
+
}
|
|
2763
3270
|
lookup(botId, chatId) {
|
|
2764
3271
|
const bound = this.bindings.get(botId, chatId);
|
|
2765
3272
|
return bound === void 0 ? void 0 : this.sessions.get(bound);
|
|
2766
3273
|
}
|
|
2767
|
-
adopt(botId, chatId, sessionId, agent, reply) {
|
|
3274
|
+
adopt(botId, chatId, userId, sessionId, agent, reply) {
|
|
2768
3275
|
const rt = {
|
|
2769
3276
|
botId,
|
|
2770
3277
|
chatId,
|
|
2771
3278
|
sessionId,
|
|
3279
|
+
initiatorOpenId: userId,
|
|
2772
3280
|
agent,
|
|
2773
3281
|
reply,
|
|
2774
3282
|
inflight: void 0,
|
|
2775
3283
|
tail: Promise.resolve(),
|
|
2776
|
-
turn: void 0
|
|
3284
|
+
turn: void 0,
|
|
3285
|
+
retiring: false
|
|
2777
3286
|
};
|
|
2778
3287
|
this.sessions.set(sessionId, rt);
|
|
2779
3288
|
return rt;
|
|
2780
3289
|
}
|
|
2781
3290
|
};
|
|
2782
3291
|
//#endregion
|
|
3292
|
+
//#region src/channels/approval/center.ts
|
|
3293
|
+
/** 审批中心(渠道无关):自有 bot 会话的 approval ask → 渠道审批卡片挂起 → 卡片回调 resolve。
|
|
3294
|
+
* spec: docs/superpowers/specs/2026-09-08-feishu-approval-card-design.md(飞书独占 / 仅发起人 / 不超时)。 */
|
|
3295
|
+
var ApprovalCenter = class {
|
|
3296
|
+
sessions;
|
|
3297
|
+
channelFor;
|
|
3298
|
+
warn;
|
|
3299
|
+
newId;
|
|
3300
|
+
pending = /* @__PURE__ */ new Map();
|
|
3301
|
+
constructor(sessions, channelFor, warn, newId = randomUUID) {
|
|
3302
|
+
this.sessions = sessions;
|
|
3303
|
+
this.channelFor = channelFor;
|
|
3304
|
+
this.warn = warn;
|
|
3305
|
+
this.newId = newId;
|
|
3306
|
+
}
|
|
3307
|
+
async handleRequest(req) {
|
|
3308
|
+
const sessionId = String(req.agent.session.id);
|
|
3309
|
+
const rt = this.sessions.get(sessionId);
|
|
3310
|
+
if (rt === void 0) return void 0;
|
|
3311
|
+
if (req.signal?.aborted === true) return "cancelled";
|
|
3312
|
+
const channel = this.channelFor(rt.botId);
|
|
3313
|
+
if (channel === void 0) {
|
|
3314
|
+
this.warn(`[project-bot] bot "${rt.botId}" 的渠道无审批能力,回退其他审批通道`);
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
const key = this.newId();
|
|
3318
|
+
let presentation;
|
|
3319
|
+
try {
|
|
3320
|
+
presentation = await channel.presenter.present({
|
|
3321
|
+
key,
|
|
3322
|
+
chatId: rt.chatId,
|
|
3323
|
+
botName: channel.botName,
|
|
3324
|
+
toolName: req.toolName,
|
|
3325
|
+
...req.reason !== void 0 ? { reason: req.reason } : {}
|
|
3326
|
+
});
|
|
3327
|
+
} catch (error) {
|
|
3328
|
+
this.warn(`[project-bot] 审批卡片发送失败,回退其他审批通道:${error instanceof Error ? error.message : String(error)}`);
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
return new Promise((resolve) => {
|
|
3332
|
+
const entry = {
|
|
3333
|
+
sessionId,
|
|
3334
|
+
resolve,
|
|
3335
|
+
presentation,
|
|
3336
|
+
signal: req.signal,
|
|
3337
|
+
onAbort: () => this.settle(key, "cancelled")
|
|
3338
|
+
};
|
|
3339
|
+
this.pending.set(key, entry);
|
|
3340
|
+
req.signal?.addEventListener("abort", entry.onAbort, { once: true });
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
handleCardAction(action) {
|
|
3344
|
+
const value = action.value;
|
|
3345
|
+
if (value === null || typeof value !== "object" || typeof value.key !== "string" || value.decision !== "allow" && value.decision !== "reject") return void 0;
|
|
3346
|
+
const entry = this.pending.get(value.key);
|
|
3347
|
+
if (entry === void 0) return { toast: "该申请已处理或已失效" };
|
|
3348
|
+
const rt = this.sessions.get(entry.sessionId);
|
|
3349
|
+
if (rt === void 0 || rt.initiatorOpenId !== action.operatorOpenId) return { toast: "仅会话发起人可审批" };
|
|
3350
|
+
this.settle(value.key, value.decision === "allow" ? "allowed-once" : "rejected", action.operatorName);
|
|
3351
|
+
return { toast: value.decision === "allow" ? "已允许" : "已拒绝" };
|
|
3352
|
+
}
|
|
3353
|
+
dispose() {
|
|
3354
|
+
for (const key of [...this.pending.keys()]) this.settle(key, "cancelled");
|
|
3355
|
+
}
|
|
3356
|
+
settle(key, outcome, operatorName) {
|
|
3357
|
+
const entry = this.pending.get(key);
|
|
3358
|
+
if (entry === void 0) return;
|
|
3359
|
+
this.pending.delete(key);
|
|
3360
|
+
entry.signal?.removeEventListener("abort", entry.onAbort);
|
|
3361
|
+
entry.resolve(outcome);
|
|
3362
|
+
const status = outcome === "allowed-once" ? "allowed" : outcome === "rejected" ? "rejected" : "cancelled";
|
|
3363
|
+
entry.presentation.finalize(status, operatorName).catch((error) => {
|
|
3364
|
+
this.warn(`[project-bot] 审批卡片定格失败:${error instanceof Error ? error.message : String(error)}`);
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3368
|
+
//#endregion
|
|
2783
3369
|
//#region src/channels/runtime.ts
|
|
2784
3370
|
var BotRuntime = class {
|
|
2785
3371
|
deps;
|
|
@@ -2787,6 +3373,7 @@ var BotRuntime = class {
|
|
|
2787
3373
|
router;
|
|
2788
3374
|
inbound;
|
|
2789
3375
|
outbound;
|
|
3376
|
+
approval;
|
|
2790
3377
|
handles = /* @__PURE__ */ new Map();
|
|
2791
3378
|
constructor(deps) {
|
|
2792
3379
|
this.deps = deps;
|
|
@@ -2800,6 +3387,14 @@ var BotRuntime = class {
|
|
|
2800
3387
|
onError: (m) => deps.log.warn(m)
|
|
2801
3388
|
});
|
|
2802
3389
|
this.outbound = new Outbound(this.sessions, (m) => deps.log.warn(m), deps.maxErrorDetailChars);
|
|
3390
|
+
this.approval = new ApprovalCenter(this.sessions, (botId) => {
|
|
3391
|
+
const presenter = this.handles.get(botId)?.approval;
|
|
3392
|
+
if (presenter === void 0) return void 0;
|
|
3393
|
+
return {
|
|
3394
|
+
presenter,
|
|
3395
|
+
botName: this.deps.bots.get(botId)?.name ?? botId
|
|
3396
|
+
};
|
|
3397
|
+
}, (m) => deps.log.warn(m));
|
|
2803
3398
|
}
|
|
2804
3399
|
async startAll() {
|
|
2805
3400
|
for (const botId of [...this.deps.bots.keys()]) await this.reconcile(botId);
|
|
@@ -2828,7 +3423,10 @@ var BotRuntime = class {
|
|
|
2828
3423
|
const handle = await channel.start({
|
|
2829
3424
|
record,
|
|
2830
3425
|
secret
|
|
2831
|
-
}, {
|
|
3426
|
+
}, {
|
|
3427
|
+
onMessage: (msg) => this.inbound.onMessage(msg),
|
|
3428
|
+
onCardAction: (action) => this.approval.handleCardAction(action)
|
|
3429
|
+
}, this.deps.tunables, (m) => this.deps.log.warn(m));
|
|
2832
3430
|
this.handles.set(botId, handle);
|
|
2833
3431
|
} catch (error) {
|
|
2834
3432
|
this.deps.log.warn(`[project-bot] bot "${botId}" 渠道启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2837,19 +3435,23 @@ var BotRuntime = class {
|
|
|
2837
3435
|
/** 删除 bot:停渠道、取消会话、清绑定。 */
|
|
2838
3436
|
async stopBot(botId) {
|
|
2839
3437
|
await this.stopChannel(botId);
|
|
2840
|
-
for (const [sessionId, rt] of [...this.sessions]) if (rt.botId === botId)
|
|
2841
|
-
rt.agent.cancel();
|
|
2842
|
-
this.sessions.delete(sessionId);
|
|
2843
|
-
}
|
|
3438
|
+
for (const [sessionId, rt] of [...this.sessions]) if (rt.botId === botId) this.retire(sessionId, rt);
|
|
2844
3439
|
await this.bindingStore().deleteBot(botId);
|
|
2845
3440
|
}
|
|
2846
3441
|
/** 解绑渠道:停渠道、取消在飞会话;绑定表与持久会话保留(重绑后 resume 接续)。 */
|
|
2847
3442
|
async unbindBot(botId) {
|
|
2848
3443
|
await this.stopChannel(botId);
|
|
2849
|
-
for (const [sessionId, rt] of [...this.sessions]) if (rt.botId === botId)
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
3444
|
+
for (const [sessionId, rt] of [...this.sessions]) if (rt.botId === botId) this.retire(sessionId, rt);
|
|
3445
|
+
}
|
|
3446
|
+
/** 取消会话并等出站链落定后摘出 sessions(让在飞 turn 的 turn/end 正常 finalize 旧卡)。 */
|
|
3447
|
+
retire(sessionId, rt) {
|
|
3448
|
+
rt.retiring = true;
|
|
3449
|
+
rt.agent.cancel();
|
|
3450
|
+
(async () => {
|
|
3451
|
+
await rt.agent.whenIdle().catch(() => void 0);
|
|
3452
|
+
await rt.tail.catch(() => void 0);
|
|
3453
|
+
if (this.sessions.get(sessionId) === rt) this.sessions.delete(sessionId);
|
|
3454
|
+
})();
|
|
2853
3455
|
}
|
|
2854
3456
|
statusOf(botId) {
|
|
2855
3457
|
const record = this.deps.bots.get(botId);
|
|
@@ -2865,6 +3467,7 @@ var BotRuntime = class {
|
|
|
2865
3467
|
}));
|
|
2866
3468
|
await Promise.allSettled([...this.handles.values()].map((h) => h.close()));
|
|
2867
3469
|
this.handles.clear();
|
|
3470
|
+
this.approval.dispose();
|
|
2868
3471
|
}
|
|
2869
3472
|
async stopChannel(botId) {
|
|
2870
3473
|
const handle = this.handles.get(botId);
|
|
@@ -2891,6 +3494,20 @@ var BotRuntime = class {
|
|
|
2891
3494
|
}
|
|
2892
3495
|
};
|
|
2893
3496
|
//#endregion
|
|
3497
|
+
//#region src/channels/scope-joiner.ts
|
|
3498
|
+
function createScopeJoiner(ctx, presetId, fallback, warn) {
|
|
3499
|
+
return { async join(agentCtx) {
|
|
3500
|
+
const agentPresets = ctx.get("agentPresets", false);
|
|
3501
|
+
if (agentPresets !== void 0) try {
|
|
3502
|
+
await agentPresets.mount(agentCtx, presetId);
|
|
3503
|
+
return;
|
|
3504
|
+
} catch (error) {
|
|
3505
|
+
warn(`dsh-agent-toolkit: bot 会话挂载 preset "${presetId}" 失败,回退基础工具 standing scope(此后该会话委派子会话将看不到基础工具):${error instanceof Error ? error.message : String(error)}`);
|
|
3506
|
+
}
|
|
3507
|
+
await fallback.join(agentCtx);
|
|
3508
|
+
} };
|
|
3509
|
+
}
|
|
3510
|
+
//#endregion
|
|
2894
3511
|
//#region src/channels/tool-scope.ts
|
|
2895
3512
|
/** 默认加载器:复刻 preset mount 的模块解析——loader 的 unwrapExports 取 `default ?? 命名导出模块`。 */
|
|
2896
3513
|
async function loadToolModule(specifier) {
|
|
@@ -2940,10 +3557,17 @@ function createToolsScope(ctx, loadTool = loadToolModule) {
|
|
|
2940
3557
|
};
|
|
2941
3558
|
}
|
|
2942
3559
|
//#endregion
|
|
3560
|
+
//#region src/channels/approval/answerer.ts
|
|
3561
|
+
function createApprovalAnswerer(centerOf) {
|
|
3562
|
+
return async (req, next) => {
|
|
3563
|
+
return await centerOf()?.handleRequest(req) ?? next();
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
//#endregion
|
|
2943
3567
|
//#region src/bots/api.ts
|
|
2944
3568
|
/** 浏览器半 RPC:单前缀路由 /dsh-agent-toolkit/api/bots + 内部路径分发。 */
|
|
2945
3569
|
const MAX_BODY_BYTES = 65536;
|
|
2946
|
-
const CreateBodySchema = z$1.object({
|
|
3570
|
+
const CreateBodySchema$1 = z$1.object({
|
|
2947
3571
|
/** 缺省时后端自动生成(bot-<8 位随机小写字母数字>)。 */
|
|
2948
3572
|
id: z$1.string().regex(BOT_ID_RE).optional(),
|
|
2949
3573
|
name: z$1.string().min(1).max(64),
|
|
@@ -2964,7 +3588,7 @@ const CreateBodySchema = z$1.object({
|
|
|
2964
3588
|
appSecretRef: z$1.string().regex(CREDENTIAL_REF_RE).optional()
|
|
2965
3589
|
})
|
|
2966
3590
|
});
|
|
2967
|
-
const UpdateBodySchema = z$1.object({
|
|
3591
|
+
const UpdateBodySchema$1 = z$1.object({
|
|
2968
3592
|
name: z$1.string().min(1).max(64).optional(),
|
|
2969
3593
|
project: z$1.string().min(1).optional(),
|
|
2970
3594
|
persona: z$1.string().max(8e3).nullable().optional(),
|
|
@@ -3030,7 +3654,7 @@ function createApiHandler(deps) {
|
|
|
3030
3654
|
if (sub === "/bots" && method === "POST") {
|
|
3031
3655
|
const body = await readJsonBody(req, res);
|
|
3032
3656
|
if (body === void 0) return;
|
|
3033
|
-
const parsed = CreateBodySchema.safeParse(body);
|
|
3657
|
+
const parsed = CreateBodySchema$1.safeParse(body);
|
|
3034
3658
|
if (!parsed.success) {
|
|
3035
3659
|
json(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid body" });
|
|
3036
3660
|
return;
|
|
@@ -3090,7 +3714,7 @@ function createApiHandler(deps) {
|
|
|
3090
3714
|
}
|
|
3091
3715
|
const body = await readJsonBody(req, res);
|
|
3092
3716
|
if (body === void 0) return;
|
|
3093
|
-
const parsed = UpdateBodySchema.safeParse(body);
|
|
3717
|
+
const parsed = UpdateBodySchema$1.safeParse(body);
|
|
3094
3718
|
if (!parsed.success) {
|
|
3095
3719
|
json(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid body" });
|
|
3096
3720
|
return;
|
|
@@ -3209,7 +3833,7 @@ function createApiHandler(deps) {
|
|
|
3209
3833
|
//#endregion
|
|
3210
3834
|
//#region src/bots/register-app.ts
|
|
3211
3835
|
/** 扫码一键创建飞书应用:lark.registerApp(OAuth 2.0 Device Authorization Grant)的状态机封装。 */
|
|
3212
|
-
/**
|
|
3836
|
+
/** 扫码创建应用时申请的权限/事件/回调(流式卡片 + 收发消息 + 表情 + 通讯录基础信息 + 审批卡片回传)。
|
|
3213
3837
|
* 故意不加 as const:readonly 元组不可赋值给 SDK AppAddons 的 mutable string[],
|
|
3214
3838
|
* 否则 bots/index.ts 的 lark.registerApp(options) 透传会 typecheck 失败。 */
|
|
3215
3839
|
const FEISHU_REGISTER_APP_ADDONS = {
|
|
@@ -3219,7 +3843,8 @@ const FEISHU_REGISTER_APP_ADDONS = {
|
|
|
3219
3843
|
"cardkit:card:write",
|
|
3220
3844
|
"contact:user.base:readonly"
|
|
3221
3845
|
] },
|
|
3222
|
-
events: { items: { tenant: ["im.message.receive_v1"] } }
|
|
3846
|
+
events: { items: { tenant: ["im.message.receive_v1"] } },
|
|
3847
|
+
callbacks: { items: ["card.action.trigger"] }
|
|
3223
3848
|
};
|
|
3224
3849
|
var RegisterAppService = class {
|
|
3225
3850
|
deps;
|
|
@@ -3293,6 +3918,7 @@ function setupBots(ctx, config, deps) {
|
|
|
3293
3918
|
cardUpdateThrottleMs: config.cardUpdateThrottleMs,
|
|
3294
3919
|
cardMaxBytes: config.cardMaxBytes,
|
|
3295
3920
|
processMaxBytes: config.processMaxBytes,
|
|
3921
|
+
cardPrintStep: config.cardPrintStep,
|
|
3296
3922
|
processingReactionEmoji: config.processingReactionEmoji
|
|
3297
3923
|
};
|
|
3298
3924
|
const storeSecret = async (key, secret) => {
|
|
@@ -3300,34 +3926,10 @@ function setupBots(ctx, config, deps) {
|
|
|
3300
3926
|
await ctx.credentials.set(credentialRef(ref), secret);
|
|
3301
3927
|
return ref;
|
|
3302
3928
|
};
|
|
3303
|
-
/** 创作期注入已迁至 agent-setup.ts + tool-scope.ts(基础工具行 standing scope 挂载 + persona/tools
|
|
3929
|
+
/** 创作期注入已迁至 agent-setup.ts + tool-scope.ts(基础工具行 standing scope 挂载 + persona/tools)。
|
|
3930
|
+
* preset 优先 joiner(agent-bot 组合):mount 成功后委派子会话 composeFrom 认父(spec: docs/superpowers/specs/2026-09-07-bot-delegation-preset-mount-design.md)。 */
|
|
3304
3931
|
const toolsScope = createToolsScope(ctx);
|
|
3305
|
-
const agentsPort =
|
|
3306
|
-
async create(input) {
|
|
3307
|
-
return adaptAgent(await ctx.agents.create({
|
|
3308
|
-
sessionId: SessionId(input.sessionId),
|
|
3309
|
-
meta: { cwd: input.cwd },
|
|
3310
|
-
...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
|
|
3311
|
-
setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks, toolsScope)
|
|
3312
|
-
}));
|
|
3313
|
-
},
|
|
3314
|
-
async resume(input) {
|
|
3315
|
-
return adaptAgent(await ctx.agents.resume({
|
|
3316
|
-
resumeSessionId: SessionId(input.sessionId),
|
|
3317
|
-
...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
|
|
3318
|
-
setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks, toolsScope)
|
|
3319
|
-
}));
|
|
3320
|
-
}
|
|
3321
|
-
};
|
|
3322
|
-
function adaptAgent(handle) {
|
|
3323
|
-
const { agent } = handle;
|
|
3324
|
-
return {
|
|
3325
|
-
sessionId: String(agent.id),
|
|
3326
|
-
followup: (message) => agent.followup(message),
|
|
3327
|
-
cancel: () => agent.cancel({ kind: "user" }),
|
|
3328
|
-
whenIdle: () => agent.whenIdle()
|
|
3329
|
-
};
|
|
3330
|
-
}
|
|
3932
|
+
const agentsPort = createAgentsPort(ctx, deps.botPresetId !== void 0 ? createScopeJoiner(ctx, deps.botPresetId, toolsScope, log.warn) : toolsScope, deps.ownedSessions);
|
|
3331
3933
|
const workspaceRegistry = ctx.get("workspaceRegistry", false);
|
|
3332
3934
|
const workspacePort = { async attach(cwd, sessionId) {
|
|
3333
3935
|
if (workspaceRegistry === void 0) throw new Error("workspaceRegistry 服务不可用");
|
|
@@ -3393,6 +3995,7 @@ function setupBots(ctx, config, deps) {
|
|
|
3393
3995
|
ctx.on("session/event", (session, event) => {
|
|
3394
3996
|
runtime?.outbound.handleSessionEvent(String(session.header.id), event);
|
|
3395
3997
|
});
|
|
3998
|
+
if (config.approval) ctx.on("approval/request", createApprovalAnswerer(() => runtime?.approval), { prepend: true });
|
|
3396
3999
|
ctx.on("agent/error", ({ agent, error }) => {
|
|
3397
4000
|
const text = error instanceof Error ? error.message : String(error?.message ?? error);
|
|
3398
4001
|
runtime?.outbound.handleAgentError(String(agent.session.id), text);
|
|
@@ -3441,6 +4044,37 @@ function setupBots(ctx, config, deps) {
|
|
|
3441
4044
|
});
|
|
3442
4045
|
}
|
|
3443
4046
|
//#endregion
|
|
4047
|
+
//#region src/agents/bot-preset.ts
|
|
4048
|
+
/** agent-bot preset 组合序列化:BASIC_TOOLS → preset 行(bot 会话挂载的最小组合,
|
|
4049
|
+
* 委派子会话 composeFrom 认父的前提;spec: docs/superpowers/specs/2026-09-07-bot-delegation-preset-mount-design.md)。 */
|
|
4050
|
+
/** BASIC_TOOLS 插件包名 → preset 行 id(与 standard/agent-team 的行 id 对齐)。新增 BASIC_TOOLS 行必须在此登记。 */
|
|
4051
|
+
const ROW_IDS = {
|
|
4052
|
+
"@deepseek-ai/dsh-persona": "persona",
|
|
4053
|
+
"@deepseek-ai/dsh-agent-instructions": "agent-instructions",
|
|
4054
|
+
"@deepseek-ai/dsh-tool-pwsh": "tool-pwsh",
|
|
4055
|
+
"@deepseek-ai/dsh-tool-bash": "tool-bash",
|
|
4056
|
+
"@deepseek-ai/dsh-tool-fs": "tool-fs",
|
|
4057
|
+
"@deepseek-ai/dsh-tool-fs-search": "tool-fs-search"
|
|
4058
|
+
};
|
|
4059
|
+
const BOT_PRESET_NAME = "Bot 会话";
|
|
4060
|
+
const BOT_PRESET_DESCRIPTION = "飞书 bot 会话组合:基础工具行(persona/instructions/shell/fs/fs-search);委派子会话经 composeFrom 继承同一组合";
|
|
4061
|
+
/** BASIC_TOOLS → preset 行对象数组(未登记映射的包名抛错,防新增工具行静默丢 id)。 */
|
|
4062
|
+
function serializeBotRows(tools) {
|
|
4063
|
+
return tools.map((tool) => {
|
|
4064
|
+
const rowId = ROW_IDS[tool.id];
|
|
4065
|
+
if (rowId === void 0) throw new Error(`bot-preset: BASIC_TOOLS 行 ${tool.id} 未登记 preset 行 id 映射`);
|
|
4066
|
+
return {
|
|
4067
|
+
id: rowId,
|
|
4068
|
+
name: tool.id,
|
|
4069
|
+
...tool.config !== void 0 ? { config: tool.config } : {}
|
|
4070
|
+
};
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
/** agent-bot composition 文本(平台相关 shell 行已由 BASIC_TOOLS 选定,不用 !!js)。 */
|
|
4074
|
+
function botPresetComposition() {
|
|
4075
|
+
return yaml.dump(serializeBotRows(BASIC_TOOLS), { lineWidth: -1 });
|
|
4076
|
+
}
|
|
4077
|
+
//#endregion
|
|
3444
4078
|
//#region src/agents/team-preset.ts
|
|
3445
4079
|
/**
|
|
3446
4080
|
* Agent 团队 preset 自动生成:派生宿主当前 shipped standard composition,
|
|
@@ -3501,9 +4135,38 @@ const MARKER_CONTENT = "dsh-agent-toolkit";
|
|
|
3501
4135
|
const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
3502
4136
|
const GENERATED_HEADER = "# 本文件由 dsh-agent-toolkit 自动生成,勿手改(每次启动重写)。\n";
|
|
3503
4137
|
/**
|
|
3504
|
-
*
|
|
3505
|
-
*
|
|
3506
|
-
|
|
4138
|
+
* 写一个生成 preset 目录:无标记的同名用户目录不覆盖(warn 返回 false);marker-first 写入。
|
|
4139
|
+
* composition / metadata 写失败(含半途失败)留下的带标记目录,下次启动会被正常重写自愈。
|
|
4140
|
+
*/
|
|
4141
|
+
async function writeGeneratedPreset(dir, composition, metadata, warn) {
|
|
4142
|
+
const markerPath = join(dir, MARKER_FILE);
|
|
4143
|
+
let dirExists = true;
|
|
4144
|
+
try {
|
|
4145
|
+
await access(dir);
|
|
4146
|
+
} catch {
|
|
4147
|
+
dirExists = false;
|
|
4148
|
+
}
|
|
4149
|
+
if (dirExists) {
|
|
4150
|
+
let marked = false;
|
|
4151
|
+
try {
|
|
4152
|
+
marked = (await readFile(markerPath, "utf8")).trim() === MARKER_CONTENT;
|
|
4153
|
+
} catch {}
|
|
4154
|
+
if (!marked) {
|
|
4155
|
+
warn(`dsh-agent-toolkit: ${dir} 已存在且非本插件生成,不覆盖,跳过生成`);
|
|
4156
|
+
return false;
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
await mkdir(dir, { recursive: true });
|
|
4160
|
+
await writeFile(markerPath, `${MARKER_CONTENT}\n`, "utf8");
|
|
4161
|
+
await writeFile(join(dir, COMPOSITION_FILE), composition, "utf8");
|
|
4162
|
+
await writeFile(join(dir, METADATA_FILE), yaml.dump(metadata, { lineWidth: -1 }), "utf8");
|
|
4163
|
+
return true;
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* 启动时生成/刷新 agent-team 与 agent-bot 两个 preset。所有失败路径 warn 降级,不影响插件其余功能。
|
|
4167
|
+
* 不设为默认 preset、卸载不删目录(可能有会话在用;composition 不引用 toolkit 行,残留 preset 自身
|
|
4168
|
+
* 仍可用)。每次启动重写:standing mount 按文件代际,重写只影响新会话。agent-bot 内容来自 BASIC_TOOLS,
|
|
4169
|
+
* 不依赖源 preset 读取(read 失败只跳过 agent-team);两块独立 try/catch、独立 marker 保护。
|
|
3507
4170
|
*/
|
|
3508
4171
|
async function setupAgentTeamPreset(ctx, config) {
|
|
3509
4172
|
if (!config.enabled) return;
|
|
@@ -3512,53 +4175,980 @@ async function setupAgentTeamPreset(ctx, config) {
|
|
|
3512
4175
|
};
|
|
3513
4176
|
const agentPresets = ctx.get("agentPresets", false);
|
|
3514
4177
|
if (agentPresets === void 0) return;
|
|
3515
|
-
if (!PRESET_ID.test(config.id)) {
|
|
3516
|
-
warn(`dsh-agent-toolkit: agentTeamPreset.id "${config.id}" 不是合法 preset id,跳过 agent-team 生成`);
|
|
3517
|
-
return;
|
|
3518
|
-
}
|
|
3519
|
-
let source;
|
|
3520
|
-
try {
|
|
3521
|
-
source = await agentPresets.read(config.source);
|
|
3522
|
-
} catch (error) {
|
|
3523
|
-
warn(`dsh-agent-toolkit: 读取源 preset "${config.source}" 失败,跳过 agent-team 生成:${error instanceof Error ? error.message : String(error)}`);
|
|
3524
|
-
return;
|
|
3525
|
-
}
|
|
3526
4178
|
const root = agentPresets.roots.find((r) => r.trust === "user");
|
|
3527
4179
|
if (root === void 0) {
|
|
3528
|
-
warn("dsh-agent-toolkit: preset roots 中无 trust=user 的目录,跳过 agent-team 生成");
|
|
4180
|
+
warn("dsh-agent-toolkit: preset roots 中无 trust=user 的目录,跳过 agent-team / agent-bot 生成");
|
|
3529
4181
|
return;
|
|
3530
4182
|
}
|
|
3531
|
-
const
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
let dirExists = true;
|
|
4183
|
+
const presetDir = (id) => join(resolve(expandHomePath(root.path)), id);
|
|
4184
|
+
if (!PRESET_ID.test(config.id)) warn(`dsh-agent-toolkit: agentTeamPreset.id "${config.id}" 不是合法 preset id,跳过 agent-team 生成`);
|
|
4185
|
+
else {
|
|
4186
|
+
let source;
|
|
3536
4187
|
try {
|
|
3537
|
-
await
|
|
3538
|
-
} catch {
|
|
3539
|
-
|
|
4188
|
+
source = await agentPresets.read(config.source);
|
|
4189
|
+
} catch (error) {
|
|
4190
|
+
warn(`dsh-agent-toolkit: 读取源 preset "${config.source}" 失败,跳过 agent-team 生成:${error instanceof Error ? error.message : String(error)}`);
|
|
3540
4191
|
}
|
|
3541
|
-
if (
|
|
3542
|
-
|
|
4192
|
+
if (source !== void 0) {
|
|
4193
|
+
const dir = presetDir(config.id);
|
|
3543
4194
|
try {
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
4195
|
+
await writeGeneratedPreset(dir, GENERATED_HEADER + disableSubagentRows(source, warn), {
|
|
4196
|
+
name: config.name,
|
|
4197
|
+
description: config.description
|
|
4198
|
+
}, warn);
|
|
4199
|
+
} catch (error) {
|
|
4200
|
+
warn(`dsh-agent-toolkit: 写入 agent-team preset 失败(${dir}):${error instanceof Error ? error.message : String(error)}`);
|
|
3549
4201
|
}
|
|
3550
4202
|
}
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
4203
|
+
}
|
|
4204
|
+
if (config.id === config.botsId) warn(`dsh-agent-toolkit: agentTeamPreset.id 与 agentTeamPreset.botsId 相同(均为 "${config.id}"),跳过 agent-bot 生成`);
|
|
4205
|
+
else if (!PRESET_ID.test(config.botsId)) warn(`dsh-agent-toolkit: agentTeamPreset.botsId "${config.botsId}" 不是合法 preset id,跳过 agent-bot 生成`);
|
|
4206
|
+
else try {
|
|
4207
|
+
await writeGeneratedPreset(presetDir(config.botsId), GENERATED_HEADER + botPresetComposition(), {
|
|
4208
|
+
name: BOT_PRESET_NAME,
|
|
4209
|
+
description: BOT_PRESET_DESCRIPTION
|
|
4210
|
+
}, warn);
|
|
3558
4211
|
} catch (error) {
|
|
3559
|
-
warn(`dsh-agent-toolkit: 写入 agent-
|
|
4212
|
+
warn(`dsh-agent-toolkit: 写入 agent-bot preset 失败:${error instanceof Error ? error.message : String(error)}`);
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
//#endregion
|
|
4216
|
+
//#region src/schedule/store.ts
|
|
4217
|
+
/** schedule 模块存储域声明:CronTask/CronRun 记录 schema + domain 布局的单一来源。*/
|
|
4218
|
+
/** 调度规则三选一:cron(5 字段,可选 IANA 时区,省略 = 进程时区)/ at(RFC 3339 一次性)/ every(≥60s,创建时间为锚)。*/
|
|
4219
|
+
const CronScheduleSchema = z$1.discriminatedUnion("kind", [
|
|
4220
|
+
z$1.object({
|
|
4221
|
+
kind: z$1.literal("cron"),
|
|
4222
|
+
expr: z$1.string().min(1),
|
|
4223
|
+
timeZone: z$1.string().min(1).optional()
|
|
4224
|
+
}),
|
|
4225
|
+
z$1.object({
|
|
4226
|
+
kind: z$1.literal("at"),
|
|
4227
|
+
at: z$1.string().min(1)
|
|
4228
|
+
}),
|
|
4229
|
+
z$1.object({
|
|
4230
|
+
kind: z$1.literal("every"),
|
|
4231
|
+
seconds: z$1.number().int().min(60)
|
|
4232
|
+
})
|
|
4233
|
+
]);
|
|
4234
|
+
/** 执行目标:主 Agent(宿主默认模型,无 persona/restrict)或注册表角色(persona/model/工具白名单随角色)。*/
|
|
4235
|
+
const CronTargetSchema = z$1.discriminatedUnion("kind", [z$1.object({ kind: z$1.literal("main") }), z$1.object({
|
|
4236
|
+
kind: z$1.literal("role"),
|
|
4237
|
+
roleId: z$1.string().min(1)
|
|
4238
|
+
})]);
|
|
4239
|
+
const CronTaskSchema = z$1.object({
|
|
4240
|
+
id: z$1.string().min(1),
|
|
4241
|
+
name: z$1.string().min(1).max(64),
|
|
4242
|
+
prompt: z$1.string().min(1).max(8e3),
|
|
4243
|
+
cwd: z$1.string().min(1),
|
|
4244
|
+
schedule: CronScheduleSchema,
|
|
4245
|
+
target: CronTargetSchema,
|
|
4246
|
+
catchup: z$1.boolean(),
|
|
4247
|
+
enabled: z$1.boolean(),
|
|
4248
|
+
/** 调度器维护的持久缓存(重启 rearm 依据),UTC ISO 串。*/
|
|
4249
|
+
nextRunAt: z$1.string().nullable(),
|
|
4250
|
+
createdAt: z$1.string(),
|
|
4251
|
+
updatedAt: z$1.string()
|
|
4252
|
+
});
|
|
4253
|
+
const CronRunSchema = z$1.object({
|
|
4254
|
+
id: z$1.string().min(1),
|
|
4255
|
+
taskId: z$1.string().min(1),
|
|
4256
|
+
triggeredAt: z$1.string(),
|
|
4257
|
+
finishedAt: z$1.string().optional(),
|
|
4258
|
+
status: z$1.enum([
|
|
4259
|
+
"running",
|
|
4260
|
+
"ok",
|
|
4261
|
+
"error",
|
|
4262
|
+
"skipped-overlap"
|
|
4263
|
+
]),
|
|
4264
|
+
/** 本次执行新建的会话 id(skipped-overlap 无会话)。*/
|
|
4265
|
+
sessionId: z$1.string().optional(),
|
|
4266
|
+
/** 错误摘要(截断)。*/
|
|
4267
|
+
error: z$1.string().optional()
|
|
4268
|
+
});
|
|
4269
|
+
/** domain 名/表名均受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。*/
|
|
4270
|
+
const scheduleDomain = defineDomain({
|
|
4271
|
+
name: "dsh_agent_toolkit_schedule",
|
|
4272
|
+
version: 1,
|
|
4273
|
+
tables: {
|
|
4274
|
+
tasks: domainTable(CronTaskSchema),
|
|
4275
|
+
runs: domainTable(CronRunSchema),
|
|
4276
|
+
meta: domainTable(z$1.object({ value: z$1.string() }))
|
|
4277
|
+
}
|
|
4278
|
+
});
|
|
4279
|
+
/** 某任务的 run 列表(新 → 旧;同刻按 id 倒序稳定化)。*/
|
|
4280
|
+
function listRuns(runs, taskId) {
|
|
4281
|
+
return [...runs.entries()].map(([, run]) => run).filter((run) => run.taskId === taskId).sort((a, b) => b.triggeredAt.localeCompare(a.triggeredAt) || b.id.localeCompare(a.id));
|
|
4282
|
+
}
|
|
4283
|
+
/** 环形保留某任务最近 limit 条 run,超出删最旧。*/
|
|
4284
|
+
async function trimRuns(runs, taskId, limit) {
|
|
4285
|
+
const mine = listRuns(runs, taskId);
|
|
4286
|
+
for (const run of mine.slice(limit)) await runs.delete(run.id);
|
|
4287
|
+
}
|
|
4288
|
+
/** 删除任务连带清运行历史。*/
|
|
4289
|
+
async function deleteRunsOf(runs, taskId) {
|
|
4290
|
+
for (const run of listRuns(runs, taskId)) await runs.delete(run.id);
|
|
4291
|
+
}
|
|
4292
|
+
//#endregion
|
|
4293
|
+
//#region src/schedule/api.ts
|
|
4294
|
+
const CreateBodySchema = z$1.object({
|
|
4295
|
+
name: z$1.string().min(1).max(64),
|
|
4296
|
+
prompt: z$1.string().min(1).max(8e3),
|
|
4297
|
+
cwd: z$1.string().min(1),
|
|
4298
|
+
schedule: CronScheduleSchema,
|
|
4299
|
+
target: CronTargetSchema,
|
|
4300
|
+
catchup: z$1.boolean(),
|
|
4301
|
+
enabled: z$1.boolean()
|
|
4302
|
+
});
|
|
4303
|
+
const UpdateBodySchema = CreateBodySchema.partial();
|
|
4304
|
+
function createCronApiHandler(deps) {
|
|
4305
|
+
return async (req, res) => {
|
|
4306
|
+
const sub = new URL(req.url ?? "/", "http://127.0.0.1").pathname.replace(/^\/dsh-agent-toolkit\/api\/cron/, "") || "/";
|
|
4307
|
+
const method = req.method ?? "GET";
|
|
4308
|
+
const taskMatch = /^\/tasks\/([^/]+)$/.exec(sub);
|
|
4309
|
+
const triggerMatch = /^\/tasks\/([^/]+)\/trigger$/.exec(sub);
|
|
4310
|
+
const runsMatch = /^\/tasks\/([^/]+)\/runs$/.exec(sub);
|
|
4311
|
+
if (sub === "/tasks" && method === "GET") {
|
|
4312
|
+
json$1(res, 200, { tasks: deps.service.list() });
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
if (sub === "/tasks" && method === "POST") {
|
|
4316
|
+
const body = await readJsonBody$1(req, res);
|
|
4317
|
+
if (body === void 0) return;
|
|
4318
|
+
const parsed = CreateBodySchema.safeParse(body);
|
|
4319
|
+
if (!parsed.success) {
|
|
4320
|
+
json$1(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid body" });
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
const result = await deps.service.create(parsed.data);
|
|
4324
|
+
if (!result.ok) {
|
|
4325
|
+
json$1(res, 400, { error: result.error });
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
json$1(res, 200, { task: result.value });
|
|
4329
|
+
return;
|
|
4330
|
+
}
|
|
4331
|
+
if (taskMatch !== null && method === "PUT") {
|
|
4332
|
+
const id = decodeURIComponent(taskMatch[1]);
|
|
4333
|
+
if (deps.service.get(id) === void 0) {
|
|
4334
|
+
json$1(res, 404, { error: `定时任务 "${id}" 不存在` });
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
const body = await readJsonBody$1(req, res);
|
|
4338
|
+
if (body === void 0) return;
|
|
4339
|
+
const parsed = UpdateBodySchema.safeParse(body);
|
|
4340
|
+
if (!parsed.success) {
|
|
4341
|
+
json$1(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid body" });
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
const result = await deps.service.update(id, parsed.data);
|
|
4345
|
+
if (!result.ok) {
|
|
4346
|
+
json$1(res, 400, { error: result.error });
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
json$1(res, 200, { task: result.value });
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
if (taskMatch !== null && method === "DELETE") {
|
|
4353
|
+
const id = decodeURIComponent(taskMatch[1]);
|
|
4354
|
+
if (deps.service.get(id) === void 0) {
|
|
4355
|
+
json$1(res, 404, { error: `定时任务 "${id}" 不存在` });
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4358
|
+
const result = await deps.service.remove(id);
|
|
4359
|
+
if (!result.ok) {
|
|
4360
|
+
json$1(res, 400, { error: result.error });
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
json$1(res, 200, { ok: true });
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
if (triggerMatch !== null && method === "POST") {
|
|
4367
|
+
const id = decodeURIComponent(triggerMatch[1]);
|
|
4368
|
+
if (deps.service.get(id) === void 0) {
|
|
4369
|
+
json$1(res, 404, { error: `定时任务 "${id}" 不存在` });
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
const result = await deps.service.trigger(id);
|
|
4373
|
+
if (!result.ok) {
|
|
4374
|
+
json$1(res, /正在运行/.test(result.error) ? 409 : 400, { error: result.error });
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
json$1(res, 200, { run: result.value });
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
if (runsMatch !== null && method === "GET") {
|
|
4381
|
+
const id = decodeURIComponent(runsMatch[1]);
|
|
4382
|
+
if (deps.service.get(id) === void 0) {
|
|
4383
|
+
json$1(res, 404, { error: `定时任务 "${id}" 不存在` });
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
json$1(res, 200, { runs: deps.service.runsOf(id, deps.runHistoryLimit) });
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
if (sub === "/projects" && method === "GET") {
|
|
4390
|
+
json$1(res, 200, { projects: deps.listProjects() });
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
if (sub === "/tasks" || taskMatch !== null || triggerMatch !== null || runsMatch !== null || sub === "/projects") {
|
|
4394
|
+
json$1(res, 405, { error: "method not allowed" });
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
json$1(res, 404, { error: "not found" });
|
|
4398
|
+
};
|
|
4399
|
+
}
|
|
4400
|
+
//#endregion
|
|
4401
|
+
//#region src/schedule/executor.ts
|
|
4402
|
+
/** 定时任务执行器:输入一条 task,输出一条 run 记录(建会话 → followup → whenIdle/超时 → 落库)。 */
|
|
4403
|
+
/** 来源段名:声明本会话由定时任务触发(order 20,与渠道 sender 段同位)。 */
|
|
4404
|
+
const TASK_SECTION_NAME = "dsh-agent-toolkit:schedule:task";
|
|
4405
|
+
function taskSectionText(task, triggeredAt) {
|
|
4406
|
+
return `本会话由定时任务「${task.name}」(id: ${task.id})于 ${triggeredAt} 触发。`;
|
|
4407
|
+
}
|
|
4408
|
+
/** 错误摘要最大字符数(对齐 feishu.errorDetailMaxChars 默认 500)。 */
|
|
4409
|
+
const ERROR_MAX_CHARS = 500;
|
|
4410
|
+
var RunTimeoutError = class extends Error {
|
|
4411
|
+
constructor() {
|
|
4412
|
+
super("timeout");
|
|
4413
|
+
}
|
|
4414
|
+
};
|
|
4415
|
+
function createExecutor(deps) {
|
|
4416
|
+
async function persist(run) {
|
|
4417
|
+
await deps.runs.put(run.id, run);
|
|
4418
|
+
await trimRuns(deps.runs, run.taskId, deps.runHistoryLimit);
|
|
4419
|
+
}
|
|
4420
|
+
return { async trigger(task) {
|
|
4421
|
+
const triggeredAt = new Date(deps.now()).toISOString();
|
|
4422
|
+
const sessionId = deps.newSessionId();
|
|
4423
|
+
const run = {
|
|
4424
|
+
id: deps.newRunId(),
|
|
4425
|
+
taskId: task.id,
|
|
4426
|
+
triggeredAt,
|
|
4427
|
+
status: "running",
|
|
4428
|
+
sessionId
|
|
4429
|
+
};
|
|
4430
|
+
await persist(run);
|
|
4431
|
+
const finish = async (status, error) => {
|
|
4432
|
+
const done = {
|
|
4433
|
+
...run,
|
|
4434
|
+
status,
|
|
4435
|
+
finishedAt: new Date(deps.now()).toISOString(),
|
|
4436
|
+
...error !== void 0 ? { error: error.slice(0, ERROR_MAX_CHARS) } : {}
|
|
4437
|
+
};
|
|
4438
|
+
await persist(done);
|
|
4439
|
+
return done;
|
|
4440
|
+
};
|
|
4441
|
+
const source = {
|
|
4442
|
+
name: TASK_SECTION_NAME,
|
|
4443
|
+
order: 20,
|
|
4444
|
+
text: taskSectionText(task, triggeredAt)
|
|
4445
|
+
};
|
|
4446
|
+
let agentOptions;
|
|
4447
|
+
let hooks;
|
|
4448
|
+
const roleId = task.target.kind === "role" ? task.target.roleId : "main";
|
|
4449
|
+
const role = roleId === "main" ? void 0 : deps.registry.get(roleId);
|
|
4450
|
+
if (roleId === "main" || role === void 0) {
|
|
4451
|
+
if (roleId !== "main" && role === void 0) deps.warn(`[schedule] 定时任务 "${task.id}" 的角色 "${roleId}" 不存在,降级主 Agent 形态`);
|
|
4452
|
+
agentOptions = deps.defaultModel();
|
|
4453
|
+
hooks = { sections: [source] };
|
|
4454
|
+
} else {
|
|
4455
|
+
const base = roleHooks(role);
|
|
4456
|
+
agentOptions = roleAgentOptions(role, deps.defaultModel);
|
|
4457
|
+
hooks = {
|
|
4458
|
+
...base,
|
|
4459
|
+
sections: [...base.sections ?? [], source]
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
let agent;
|
|
4463
|
+
try {
|
|
4464
|
+
agent = await deps.agents.create({
|
|
4465
|
+
sessionId,
|
|
4466
|
+
cwd: task.cwd,
|
|
4467
|
+
agentOptions,
|
|
4468
|
+
hooks
|
|
4469
|
+
});
|
|
4470
|
+
} catch (error) {
|
|
4471
|
+
return finish("error", error instanceof Error ? error.message : String(error));
|
|
4472
|
+
}
|
|
4473
|
+
try {
|
|
4474
|
+
await deps.workspace.attach(task.cwd, sessionId);
|
|
4475
|
+
} catch (error) {
|
|
4476
|
+
deps.warn(`[schedule] 会话 ${sessionId} 挂载 workspace 失败:${error instanceof Error ? error.message : String(error)}`);
|
|
4477
|
+
}
|
|
4478
|
+
agent.followup(createUserMessage({
|
|
4479
|
+
content: [{
|
|
4480
|
+
type: "text",
|
|
4481
|
+
text: task.prompt
|
|
4482
|
+
}],
|
|
4483
|
+
source: { kind: "user" }
|
|
4484
|
+
}));
|
|
4485
|
+
const idle = agent.whenIdle();
|
|
4486
|
+
idle.catch(() => void 0);
|
|
4487
|
+
const timeout = deps.delay(deps.runTimeoutMs).then(() => {
|
|
4488
|
+
throw new RunTimeoutError();
|
|
4489
|
+
});
|
|
4490
|
+
timeout.catch(() => void 0);
|
|
4491
|
+
try {
|
|
4492
|
+
await Promise.race([idle, timeout]);
|
|
4493
|
+
return await finish("ok");
|
|
4494
|
+
} catch (error) {
|
|
4495
|
+
if (error instanceof RunTimeoutError) {
|
|
4496
|
+
agent.cancel();
|
|
4497
|
+
return await finish("error", `timeout(超过 ${Math.round(deps.runTimeoutMs / 6e4)} 分钟未空闲,已取消)`);
|
|
4498
|
+
}
|
|
4499
|
+
return await finish("error", error instanceof Error ? error.message : String(error));
|
|
4500
|
+
}
|
|
4501
|
+
} };
|
|
4502
|
+
}
|
|
4503
|
+
//#endregion
|
|
4504
|
+
//#region src/schedule/timing.ts
|
|
4505
|
+
/** cron/at/every 纯时间运算:校验、下一触发点、预览、启动 rearm。全部注入 now,无隐藏时钟。*/
|
|
4506
|
+
/** croner 选项:paused 仅做解析与预测,不注册真实定时器(计时由调度器 tick 驱动)。*/
|
|
4507
|
+
function cronerOptions(schedule) {
|
|
4508
|
+
return {
|
|
4509
|
+
paused: true,
|
|
4510
|
+
...schedule.timeZone !== void 0 ? { timezone: schedule.timeZone } : {}
|
|
4511
|
+
};
|
|
4512
|
+
}
|
|
4513
|
+
/** 创建/更新即校验;合法返回 undefined,非法返回错误消息(不入库由调用方保证)。*/
|
|
4514
|
+
function validateSchedule(schedule, nowMs) {
|
|
4515
|
+
switch (schedule.kind) {
|
|
4516
|
+
case "cron": {
|
|
4517
|
+
const fields = schedule.expr.trim().split(/\s+/);
|
|
4518
|
+
if (fields.length !== 5) return `cron 表达式须为 5 字段(分 时 日 月 周),收到 ${fields.length} 字段;秒级/年字段暂不开放`;
|
|
4519
|
+
if (schedule.timeZone !== void 0) try {
|
|
4520
|
+
new Intl.DateTimeFormat("en-US", { timeZone: schedule.timeZone });
|
|
4521
|
+
} catch {
|
|
4522
|
+
return `未知时区:${schedule.timeZone}`;
|
|
4523
|
+
}
|
|
4524
|
+
try {
|
|
4525
|
+
new Cron(schedule.expr, cronerOptions(schedule));
|
|
4526
|
+
} catch (error) {
|
|
4527
|
+
return `非法 cron 表达式"${schedule.expr}":${error instanceof Error ? error.message : String(error)}`;
|
|
4528
|
+
}
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
case "at": {
|
|
4532
|
+
const t = Date.parse(schedule.at);
|
|
4533
|
+
if (Number.isNaN(t)) return `非法 at 时间(须 RFC 3339):${schedule.at}`;
|
|
4534
|
+
if (t <= nowMs) return `at 时间须在未来:${schedule.at}`;
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
case "every": return;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
/** 下一触发点(epoch ms);无未来触发点(at 过期等)返回 null。*/
|
|
4541
|
+
function nextOccurrence(schedule, createdAtMs, nowMs) {
|
|
4542
|
+
switch (schedule.kind) {
|
|
4543
|
+
case "cron": {
|
|
4544
|
+
const next = new Cron(schedule.expr, cronerOptions(schedule)).nextRun(new Date(nowMs));
|
|
4545
|
+
return next === null ? null : next.getTime();
|
|
4546
|
+
}
|
|
4547
|
+
case "at": {
|
|
4548
|
+
const t = Date.parse(schedule.at);
|
|
4549
|
+
return t > nowMs ? t : null;
|
|
4550
|
+
}
|
|
4551
|
+
case "every": {
|
|
4552
|
+
const intervalMs = schedule.seconds * 1e3;
|
|
4553
|
+
if (nowMs < createdAtMs) return createdAtMs;
|
|
4554
|
+
return createdAtMs + (Math.floor((nowMs - createdAtMs) / intervalMs) + 1) * intervalMs;
|
|
4555
|
+
}
|
|
3560
4556
|
}
|
|
3561
4557
|
}
|
|
4558
|
+
/**
|
|
4559
|
+
* 启动 rearm:对一条持久化任务重算 enabled/nextRunAt(无变化返回原引用,调用方按引用决定是否写回)。
|
|
4560
|
+
* - nextRunAt 仍在未来 → 保持;
|
|
4561
|
+
* - 已过期且 catchup → 设为「立即」(首个 tick 补跑一次);
|
|
4562
|
+
* - 已过期且非 catchup → 下一未来触发点;没有(at 一次性)→ enabled=false、nextRunAt=null(过期即作废)。
|
|
4563
|
+
*/
|
|
4564
|
+
function rearmTask(task, nowMs) {
|
|
4565
|
+
if (!task.enabled) return task.nextRunAt === null ? task : {
|
|
4566
|
+
...task,
|
|
4567
|
+
nextRunAt: null
|
|
4568
|
+
};
|
|
4569
|
+
const persisted = task.nextRunAt === null ? null : Date.parse(task.nextRunAt);
|
|
4570
|
+
if (persisted !== null && persisted > nowMs) return task;
|
|
4571
|
+
if (persisted !== null && task.catchup) return {
|
|
4572
|
+
...task,
|
|
4573
|
+
nextRunAt: new Date(nowMs).toISOString()
|
|
4574
|
+
};
|
|
4575
|
+
const next = nextOccurrence(task.schedule, Date.parse(task.createdAt), nowMs);
|
|
4576
|
+
return next === null ? {
|
|
4577
|
+
...task,
|
|
4578
|
+
enabled: false,
|
|
4579
|
+
nextRunAt: null
|
|
4580
|
+
} : {
|
|
4581
|
+
...task,
|
|
4582
|
+
nextRunAt: new Date(next).toISOString()
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
/** 创建/更新后的 fresh 重算(catchup 是停机补跑语义,不适用于编辑路径)。*/
|
|
4586
|
+
function recomputeTask(task, nowMs) {
|
|
4587
|
+
if (!task.enabled) return {
|
|
4588
|
+
...task,
|
|
4589
|
+
nextRunAt: null
|
|
4590
|
+
};
|
|
4591
|
+
const next = nextOccurrence(task.schedule, Date.parse(task.createdAt), nowMs);
|
|
4592
|
+
return next === null ? {
|
|
4593
|
+
...task,
|
|
4594
|
+
enabled: false,
|
|
4595
|
+
nextRunAt: null
|
|
4596
|
+
} : {
|
|
4597
|
+
...task,
|
|
4598
|
+
nextRunAt: new Date(next).toISOString()
|
|
4599
|
+
};
|
|
4600
|
+
}
|
|
4601
|
+
/** 触发/跳过后推进:cron/every 取 now 之后下一触发点;at 一次性作废。*/
|
|
4602
|
+
function advanceAfterTrigger(task, nowMs) {
|
|
4603
|
+
if (task.schedule.kind === "at") return {
|
|
4604
|
+
...task,
|
|
4605
|
+
enabled: false,
|
|
4606
|
+
nextRunAt: null
|
|
4607
|
+
};
|
|
4608
|
+
const next = nextOccurrence(task.schedule, Date.parse(task.createdAt), nowMs);
|
|
4609
|
+
return next === null ? {
|
|
4610
|
+
...task,
|
|
4611
|
+
enabled: false,
|
|
4612
|
+
nextRunAt: null
|
|
4613
|
+
} : {
|
|
4614
|
+
...task,
|
|
4615
|
+
nextRunAt: new Date(next).toISOString()
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
//#endregion
|
|
4619
|
+
//#region src/schedule/scheduler.ts
|
|
4620
|
+
function createScheduler(deps) {
|
|
4621
|
+
/** 内存重叠锁:同任务上次未结束则本次记 skipped-overlap,不并发执行。 */
|
|
4622
|
+
const running = /* @__PURE__ */ new Set();
|
|
4623
|
+
async function recordSkipped(task) {
|
|
4624
|
+
const nowIso = new Date(deps.now()).toISOString();
|
|
4625
|
+
const run = {
|
|
4626
|
+
id: deps.newRunId(),
|
|
4627
|
+
taskId: task.id,
|
|
4628
|
+
triggeredAt: nowIso,
|
|
4629
|
+
finishedAt: nowIso,
|
|
4630
|
+
status: "skipped-overlap"
|
|
4631
|
+
};
|
|
4632
|
+
await deps.runs.put(run.id, run);
|
|
4633
|
+
await trimRuns(deps.runs, task.id, deps.runHistoryLimit);
|
|
4634
|
+
}
|
|
4635
|
+
function launch(task) {
|
|
4636
|
+
running.add(task.id);
|
|
4637
|
+
deps.execute(task).catch((error) => deps.warn(`[schedule] 任务 "${task.id}" 执行器异常:${error instanceof Error ? error.message : String(error)}`)).finally(() => {
|
|
4638
|
+
running.delete(task.id);
|
|
4639
|
+
const current = deps.tasks.get(task.id);
|
|
4640
|
+
if (current === void 0 || !current.enabled) return;
|
|
4641
|
+
deps.tasks.put(task.id, advanceAfterTrigger(current, deps.now()));
|
|
4642
|
+
});
|
|
4643
|
+
}
|
|
4644
|
+
return {
|
|
4645
|
+
async rearmAll() {
|
|
4646
|
+
for (const [id, task] of deps.tasks.entries()) {
|
|
4647
|
+
const rearmed = rearmTask(task, deps.now());
|
|
4648
|
+
if (rearmed !== task) await deps.tasks.put(id, rearmed);
|
|
4649
|
+
}
|
|
4650
|
+
},
|
|
4651
|
+
async tick() {
|
|
4652
|
+
for (const [, task] of deps.tasks.entries()) {
|
|
4653
|
+
if (!task.enabled || task.nextRunAt === null) continue;
|
|
4654
|
+
if (Date.parse(task.nextRunAt) > deps.now()) continue;
|
|
4655
|
+
if (running.has(task.id)) {
|
|
4656
|
+
await recordSkipped(task);
|
|
4657
|
+
await deps.tasks.put(task.id, advanceAfterTrigger(task, deps.now()));
|
|
4658
|
+
continue;
|
|
4659
|
+
}
|
|
4660
|
+
launch(task);
|
|
4661
|
+
}
|
|
4662
|
+
},
|
|
4663
|
+
recompute: (task) => recomputeTask(task, deps.now()),
|
|
4664
|
+
async triggerManual(taskId) {
|
|
4665
|
+
const task = deps.tasks.get(taskId);
|
|
4666
|
+
if (task === void 0) throw new Error(`定时任务 "${taskId}" 不存在`);
|
|
4667
|
+
if (running.has(taskId)) throw new Error(`定时任务 "${task.name}" 正在运行中,本次手动触发已跳过`);
|
|
4668
|
+
running.add(taskId);
|
|
4669
|
+
try {
|
|
4670
|
+
return await deps.execute(task);
|
|
4671
|
+
} finally {
|
|
4672
|
+
running.delete(taskId);
|
|
4673
|
+
}
|
|
4674
|
+
},
|
|
4675
|
+
isRunning: (taskId) => running.has(taskId)
|
|
4676
|
+
};
|
|
4677
|
+
}
|
|
4678
|
+
//#endregion
|
|
4679
|
+
//#region src/schedule/service.ts
|
|
4680
|
+
function createCronService(deps) {
|
|
4681
|
+
/** 创建/更新共用校验:schedule 语法与时值 → cwd → roleId 存在性。 */
|
|
4682
|
+
const validateIO = (input) => {
|
|
4683
|
+
const scheduleError = validateSchedule(input.schedule, deps.now());
|
|
4684
|
+
if (scheduleError !== void 0) return scheduleError;
|
|
4685
|
+
if (!deps.validateProject(input.cwd)) return `项目路径不可用:${input.cwd}`;
|
|
4686
|
+
if (input.target.kind === "role" && deps.registry.get(input.target.roleId) === void 0) return `角色 "${input.target.roleId}" 不存在`;
|
|
4687
|
+
};
|
|
4688
|
+
return {
|
|
4689
|
+
list() {
|
|
4690
|
+
return [...deps.tasks.entries()].map(([, task]) => {
|
|
4691
|
+
const last = listRuns(deps.runs, task.id)[0];
|
|
4692
|
+
return last === void 0 ? task : {
|
|
4693
|
+
...task,
|
|
4694
|
+
lastRun: {
|
|
4695
|
+
status: last.status,
|
|
4696
|
+
triggeredAt: last.triggeredAt
|
|
4697
|
+
}
|
|
4698
|
+
};
|
|
4699
|
+
});
|
|
4700
|
+
},
|
|
4701
|
+
get: (id) => deps.tasks.get(id),
|
|
4702
|
+
async create(input) {
|
|
4703
|
+
const invalid = validateIO(input);
|
|
4704
|
+
if (invalid !== void 0) return {
|
|
4705
|
+
ok: false,
|
|
4706
|
+
error: invalid
|
|
4707
|
+
};
|
|
4708
|
+
const nowIso = new Date(deps.now()).toISOString();
|
|
4709
|
+
const parsed = CronTaskSchema.safeParse({
|
|
4710
|
+
id: deps.newTaskId(),
|
|
4711
|
+
...input,
|
|
4712
|
+
nextRunAt: null,
|
|
4713
|
+
createdAt: nowIso,
|
|
4714
|
+
updatedAt: nowIso
|
|
4715
|
+
});
|
|
4716
|
+
if (!parsed.success) return {
|
|
4717
|
+
ok: false,
|
|
4718
|
+
error: parsed.error.issues[0]?.message ?? "invalid task"
|
|
4719
|
+
};
|
|
4720
|
+
const task = deps.scheduler.recompute(parsed.data);
|
|
4721
|
+
await deps.tasks.put(task.id, task);
|
|
4722
|
+
return {
|
|
4723
|
+
ok: true,
|
|
4724
|
+
value: task
|
|
4725
|
+
};
|
|
4726
|
+
},
|
|
4727
|
+
async update(id, patch) {
|
|
4728
|
+
const existing = deps.tasks.get(id);
|
|
4729
|
+
if (existing === void 0) return {
|
|
4730
|
+
ok: false,
|
|
4731
|
+
error: `定时任务 "${id}" 不存在`
|
|
4732
|
+
};
|
|
4733
|
+
const merged = {
|
|
4734
|
+
...existing,
|
|
4735
|
+
...Object.fromEntries(Object.entries(patch).filter(([, v]) => v !== void 0)),
|
|
4736
|
+
id: existing.id,
|
|
4737
|
+
updatedAt: new Date(deps.now()).toISOString()
|
|
4738
|
+
};
|
|
4739
|
+
const invalid = validateIO(merged);
|
|
4740
|
+
if (invalid !== void 0) return {
|
|
4741
|
+
ok: false,
|
|
4742
|
+
error: invalid
|
|
4743
|
+
};
|
|
4744
|
+
const parsed = CronTaskSchema.safeParse(merged);
|
|
4745
|
+
if (!parsed.success) return {
|
|
4746
|
+
ok: false,
|
|
4747
|
+
error: parsed.error.issues[0]?.message ?? "invalid task"
|
|
4748
|
+
};
|
|
4749
|
+
const task = deps.scheduler.recompute(parsed.data);
|
|
4750
|
+
await deps.tasks.put(id, task);
|
|
4751
|
+
return {
|
|
4752
|
+
ok: true,
|
|
4753
|
+
value: task
|
|
4754
|
+
};
|
|
4755
|
+
},
|
|
4756
|
+
async remove(id) {
|
|
4757
|
+
if (deps.tasks.get(id) === void 0) return {
|
|
4758
|
+
ok: false,
|
|
4759
|
+
error: `定时任务 "${id}" 不存在`
|
|
4760
|
+
};
|
|
4761
|
+
await deps.tasks.delete(id);
|
|
4762
|
+
await deleteRunsOf(deps.runs, id);
|
|
4763
|
+
return {
|
|
4764
|
+
ok: true,
|
|
4765
|
+
value: null
|
|
4766
|
+
};
|
|
4767
|
+
},
|
|
4768
|
+
async trigger(id) {
|
|
4769
|
+
try {
|
|
4770
|
+
return {
|
|
4771
|
+
ok: true,
|
|
4772
|
+
value: await deps.scheduler.triggerManual(id)
|
|
4773
|
+
};
|
|
4774
|
+
} catch (error) {
|
|
4775
|
+
return {
|
|
4776
|
+
ok: false,
|
|
4777
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4778
|
+
};
|
|
4779
|
+
}
|
|
4780
|
+
},
|
|
4781
|
+
runsOf: (id, limit) => listRuns(deps.runs, id).slice(0, limit)
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
//#endregion
|
|
4785
|
+
//#region src/schedule/tools.ts
|
|
4786
|
+
/** 互斥探测目标:宿主 @deepseek-ai/dsh-schedule 的会话内提醒工具。 */
|
|
4787
|
+
const HOST_SCHEDULE_TOOL = "schedule_create";
|
|
4788
|
+
/** 扁平参数 → CronSchedule(缺 kind 对应字段抛错——模型可读的自描述消息)。 */
|
|
4789
|
+
function scheduleOf(args) {
|
|
4790
|
+
const kind = args.scheduleKind;
|
|
4791
|
+
if (kind === "cron") {
|
|
4792
|
+
if (typeof args.cronExpr !== "string" || args.cronExpr.trim() === "") throw new Error("scheduleKind=cron 需要 cronExpr(5 字段 cron 表达式)");
|
|
4793
|
+
return {
|
|
4794
|
+
kind: "cron",
|
|
4795
|
+
expr: args.cronExpr,
|
|
4796
|
+
...typeof args.timeZone === "string" && args.timeZone !== "" ? { timeZone: args.timeZone } : {}
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
if (kind === "at") {
|
|
4800
|
+
if (typeof args.atTime !== "string" || args.atTime === "") throw new Error("scheduleKind=at 需要 atTime(RFC 3339 时间)");
|
|
4801
|
+
return {
|
|
4802
|
+
kind: "at",
|
|
4803
|
+
at: args.atTime
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
if (kind === "every") {
|
|
4807
|
+
if (typeof args.everySeconds !== "number") throw new Error("scheduleKind=every 需要 everySeconds(≥60 的秒数)");
|
|
4808
|
+
return {
|
|
4809
|
+
kind: "every",
|
|
4810
|
+
seconds: args.everySeconds
|
|
4811
|
+
};
|
|
4812
|
+
}
|
|
4813
|
+
throw new Error(`未知 scheduleKind:${String(kind)}(可选 cron / at / every)`);
|
|
4814
|
+
}
|
|
4815
|
+
function targetOf(args) {
|
|
4816
|
+
if (args.targetKind === "role") {
|
|
4817
|
+
if (typeof args.roleId !== "string" || args.roleId === "") throw new Error("targetKind=role 需要 roleId");
|
|
4818
|
+
return {
|
|
4819
|
+
kind: "role",
|
|
4820
|
+
roleId: args.roleId
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4823
|
+
return { kind: "main" };
|
|
4824
|
+
}
|
|
4825
|
+
/** 服务结果展开:ok:false → 抛错(defineTool 约定:抛错即工具错误结果)。 */
|
|
4826
|
+
function unwrap(result) {
|
|
4827
|
+
if (!result.ok) throw new Error(result.error);
|
|
4828
|
+
return result.value;
|
|
4829
|
+
}
|
|
4830
|
+
const JSON_OUTPUT = {
|
|
4831
|
+
schema: { type: "json" },
|
|
4832
|
+
render: (_args, value) => [{
|
|
4833
|
+
type: "text",
|
|
4834
|
+
text: JSON.stringify(value, null, 2)
|
|
4835
|
+
}]
|
|
4836
|
+
};
|
|
4837
|
+
const SCHEDULE_PARAMS_REQUIRED = { scheduleKind: {
|
|
4838
|
+
type: "string",
|
|
4839
|
+
required: true,
|
|
4840
|
+
description: "One of: cron / at / every"
|
|
4841
|
+
} };
|
|
4842
|
+
const SCHEDULE_PARAMS_OPTIONAL = { scheduleKind: {
|
|
4843
|
+
type: "string",
|
|
4844
|
+
description: "Replace schedule kind: cron / at / every (provide the matching fields too)"
|
|
4845
|
+
} };
|
|
4846
|
+
const SCHEDULE_FIELD_PARAMS = {
|
|
4847
|
+
cronExpr: {
|
|
4848
|
+
type: "string",
|
|
4849
|
+
description: "Required when scheduleKind=cron: 5-field cron expression \"minute hour day-of-month month day-of-week\" (seconds/years not supported)"
|
|
4850
|
+
},
|
|
4851
|
+
timeZone: {
|
|
4852
|
+
type: "string",
|
|
4853
|
+
description: "Optional IANA time zone for cron (e.g. \"Asia/Shanghai\"); default: server process time zone"
|
|
4854
|
+
},
|
|
4855
|
+
atTime: {
|
|
4856
|
+
type: "string",
|
|
4857
|
+
description: "Required when scheduleKind=at: one-shot RFC 3339 time (e.g. \"2026-09-08T09:00:00+08:00\")"
|
|
4858
|
+
},
|
|
4859
|
+
everySeconds: {
|
|
4860
|
+
type: "number",
|
|
4861
|
+
description: "Required when scheduleKind=every: interval seconds (>= 60), anchored at creation time"
|
|
4862
|
+
}
|
|
4863
|
+
};
|
|
4864
|
+
const TARGET_PARAMS_REQUIRED = { targetKind: {
|
|
4865
|
+
type: "string",
|
|
4866
|
+
required: true,
|
|
4867
|
+
description: "One of: main (main agent, default model) / role (registry role with its persona/model/tool allowlist)"
|
|
4868
|
+
} };
|
|
4869
|
+
const TARGET_PARAMS_OPTIONAL = { targetKind: {
|
|
4870
|
+
type: "string",
|
|
4871
|
+
description: "Replace target: main / role"
|
|
4872
|
+
} };
|
|
4873
|
+
const ROLE_ID_PARAM = { roleId: {
|
|
4874
|
+
type: "string",
|
|
4875
|
+
description: "Required when targetKind=role: registry role id"
|
|
4876
|
+
} };
|
|
4877
|
+
function createCronTools(service) {
|
|
4878
|
+
return [
|
|
4879
|
+
defineTool({
|
|
4880
|
+
name: "cron_task_create",
|
|
4881
|
+
description: "Create a scheduled task that runs a prompt in a NEW standalone session (main agent or a registry role) on a cron/at/every schedule. The task survives restarts; a run missed while stopped is caught up once when catchup=true. Use cron_task_list to see existing tasks.",
|
|
4882
|
+
parameters: {
|
|
4883
|
+
name: {
|
|
4884
|
+
type: "string",
|
|
4885
|
+
required: true,
|
|
4886
|
+
description: "Task name (display)"
|
|
4887
|
+
},
|
|
4888
|
+
prompt: {
|
|
4889
|
+
type: "string",
|
|
4890
|
+
required: true,
|
|
4891
|
+
description: "The prompt sent as the user message when the task fires. Self-contained: the session has no other context."
|
|
4892
|
+
},
|
|
4893
|
+
cwd: {
|
|
4894
|
+
type: "string",
|
|
4895
|
+
required: true,
|
|
4896
|
+
description: "Working directory of the new session (must be a valid project path)"
|
|
4897
|
+
},
|
|
4898
|
+
...SCHEDULE_PARAMS_REQUIRED,
|
|
4899
|
+
...SCHEDULE_FIELD_PARAMS,
|
|
4900
|
+
...TARGET_PARAMS_REQUIRED,
|
|
4901
|
+
...ROLE_ID_PARAM,
|
|
4902
|
+
catchup: {
|
|
4903
|
+
type: "boolean",
|
|
4904
|
+
required: true,
|
|
4905
|
+
description: "true: run once immediately after restart if a run was missed while stopped; false: skip to the next future occurrence"
|
|
4906
|
+
},
|
|
4907
|
+
enabled: {
|
|
4908
|
+
type: "boolean",
|
|
4909
|
+
required: true,
|
|
4910
|
+
description: "false: create paused (nextRunAt stays null until enabled via cron_task_update)"
|
|
4911
|
+
}
|
|
4912
|
+
},
|
|
4913
|
+
output: JSON_OUTPUT,
|
|
4914
|
+
isConcurrencySafe: () => true,
|
|
4915
|
+
async execute(rawArgs) {
|
|
4916
|
+
const args = rawArgs;
|
|
4917
|
+
const input = {
|
|
4918
|
+
name: args.name,
|
|
4919
|
+
prompt: args.prompt,
|
|
4920
|
+
cwd: args.cwd,
|
|
4921
|
+
schedule: scheduleOf(args),
|
|
4922
|
+
target: targetOf(args),
|
|
4923
|
+
catchup: args.catchup,
|
|
4924
|
+
enabled: args.enabled
|
|
4925
|
+
};
|
|
4926
|
+
return unwrap(await service.create(input));
|
|
4927
|
+
}
|
|
4928
|
+
}),
|
|
4929
|
+
defineTool({
|
|
4930
|
+
name: "cron_task_list",
|
|
4931
|
+
description: "List all scheduled tasks with enabled flag, next run time and last run status.",
|
|
4932
|
+
parameters: {},
|
|
4933
|
+
output: JSON_OUTPUT,
|
|
4934
|
+
isConcurrencySafe: () => true,
|
|
4935
|
+
async execute() {
|
|
4936
|
+
return service.list();
|
|
4937
|
+
}
|
|
4938
|
+
}),
|
|
4939
|
+
defineTool({
|
|
4940
|
+
name: "cron_task_update",
|
|
4941
|
+
description: "Update any fields of a scheduled task by id. nextRunAt is recomputed.",
|
|
4942
|
+
parameters: {
|
|
4943
|
+
id: {
|
|
4944
|
+
type: "string",
|
|
4945
|
+
required: true,
|
|
4946
|
+
description: "Task id"
|
|
4947
|
+
},
|
|
4948
|
+
name: {
|
|
4949
|
+
type: "string",
|
|
4950
|
+
description: "New name"
|
|
4951
|
+
},
|
|
4952
|
+
prompt: {
|
|
4953
|
+
type: "string",
|
|
4954
|
+
description: "New prompt"
|
|
4955
|
+
},
|
|
4956
|
+
cwd: {
|
|
4957
|
+
type: "string",
|
|
4958
|
+
description: "New working directory"
|
|
4959
|
+
},
|
|
4960
|
+
...SCHEDULE_PARAMS_OPTIONAL,
|
|
4961
|
+
...SCHEDULE_FIELD_PARAMS,
|
|
4962
|
+
...TARGET_PARAMS_OPTIONAL,
|
|
4963
|
+
...ROLE_ID_PARAM,
|
|
4964
|
+
catchup: {
|
|
4965
|
+
type: "boolean",
|
|
4966
|
+
description: "New catchup flag"
|
|
4967
|
+
},
|
|
4968
|
+
enabled: {
|
|
4969
|
+
type: "boolean",
|
|
4970
|
+
description: "Enable/disable the task"
|
|
4971
|
+
}
|
|
4972
|
+
},
|
|
4973
|
+
output: JSON_OUTPUT,
|
|
4974
|
+
isConcurrencySafe: () => true,
|
|
4975
|
+
async execute(rawArgs) {
|
|
4976
|
+
const args = rawArgs;
|
|
4977
|
+
const patch = {};
|
|
4978
|
+
if (args.name !== void 0) patch.name = args.name;
|
|
4979
|
+
if (args.prompt !== void 0) patch.prompt = args.prompt;
|
|
4980
|
+
if (args.cwd !== void 0) patch.cwd = args.cwd;
|
|
4981
|
+
if (args.scheduleKind !== void 0) patch.schedule = scheduleOf(args);
|
|
4982
|
+
if (args.targetKind !== void 0) patch.target = targetOf(args);
|
|
4983
|
+
if (args.catchup !== void 0) patch.catchup = args.catchup;
|
|
4984
|
+
if (args.enabled !== void 0) patch.enabled = args.enabled;
|
|
4985
|
+
return unwrap(await service.update(args.id, patch));
|
|
4986
|
+
}
|
|
4987
|
+
}),
|
|
4988
|
+
defineTool({
|
|
4989
|
+
name: "cron_task_delete",
|
|
4990
|
+
description: "Delete a scheduled task by id, including its run history.",
|
|
4991
|
+
parameters: { id: {
|
|
4992
|
+
type: "string",
|
|
4993
|
+
required: true,
|
|
4994
|
+
description: "Task id"
|
|
4995
|
+
} },
|
|
4996
|
+
output: JSON_OUTPUT,
|
|
4997
|
+
isConcurrencySafe: () => true,
|
|
4998
|
+
async execute(rawArgs) {
|
|
4999
|
+
return unwrap(await service.remove(rawArgs.id));
|
|
5000
|
+
}
|
|
5001
|
+
}),
|
|
5002
|
+
defineTool({
|
|
5003
|
+
name: "cron_task_trigger",
|
|
5004
|
+
description: "Trigger a scheduled task once right now (works even when disabled; fails with an error if the task is already running).",
|
|
5005
|
+
parameters: { id: {
|
|
5006
|
+
type: "string",
|
|
5007
|
+
required: true,
|
|
5008
|
+
description: "Task id"
|
|
5009
|
+
} },
|
|
5010
|
+
output: JSON_OUTPUT,
|
|
5011
|
+
isConcurrencySafe: () => true,
|
|
5012
|
+
async execute(rawArgs) {
|
|
5013
|
+
return unwrap(await service.trigger(rawArgs.id));
|
|
5014
|
+
}
|
|
5015
|
+
})
|
|
5016
|
+
];
|
|
5017
|
+
}
|
|
5018
|
+
/**
|
|
5019
|
+
* 注册 cron_* 工具到主 Agent scope 并探测宿主 schedule 互斥(spec §1/§5)。
|
|
5020
|
+
* 门控:跳过 subagent(session.header.origin === 'subagent')与插件自有会话(ownedSessions:
|
|
5021
|
+
* bot/schedule 会话)——dsh-schedule src/index.ts:45-49 同款 agent/created 模式,
|
|
5022
|
+
* 外加存量 roots 立即注册(HMR 重挂后当前主会话不丢工具)。
|
|
5023
|
+
* HMR 安全:镜像 dsh-schedule src/index.ts:44-76——attached 身份表防同一 agent 二次挂载,
|
|
5024
|
+
* toolkit 级 ctx.effect 卸载时 Promise.allSettled 摘下所有 agent 上挂的工具
|
|
5025
|
+
* (agent.ctx 属宿主、长于 toolkit fiber,仅靠 agent.ctx.effect 会在重挂时残留)。
|
|
5026
|
+
*/
|
|
5027
|
+
function setupCronTools(ctx, tools, ownedSessions) {
|
|
5028
|
+
const attached = /* @__PURE__ */ new Map();
|
|
5029
|
+
let stopping = false;
|
|
5030
|
+
ctx.effect(() => {
|
|
5031
|
+
const stopCreated = ctx.on("agent/created", ({ agent }) => {
|
|
5032
|
+
if (stopping || attached.has(agent)) return;
|
|
5033
|
+
attach(agent);
|
|
5034
|
+
});
|
|
5035
|
+
for (const agent of ctx.agents.roots()) attach(agent);
|
|
5036
|
+
return async () => {
|
|
5037
|
+
stopping = true;
|
|
5038
|
+
stopCreated();
|
|
5039
|
+
const cleanups = [...attached.values()];
|
|
5040
|
+
attached.clear();
|
|
5041
|
+
await Promise.allSettled(cleanups.map((cleanup) => Promise.resolve(cleanup())));
|
|
5042
|
+
};
|
|
5043
|
+
}, "dsh-agent-toolkit.cron-tools()");
|
|
5044
|
+
function attach(agent) {
|
|
5045
|
+
if (attached.has(agent)) return;
|
|
5046
|
+
if (agent.session.header.origin === "subagent") return;
|
|
5047
|
+
if (ownedSessions.has(String(agent.session.id))) return;
|
|
5048
|
+
const cleanup = agent.ctx.effect(() => {
|
|
5049
|
+
const scope = scopeOf(agent.ctx);
|
|
5050
|
+
if (scope !== void 0 ? ctx.tools.get(HOST_SCHEDULE_TOOL, scope) !== void 0 : ctx.tools.get(HOST_SCHEDULE_TOOL) !== void 0) ctx.logger.warn("dsh-agent-toolkit: 检测到宿主 @deepseek-ai/dsh-schedule 的 schedule_create 与 cron_* 并存——两套调度工具并存会显著提高模型误选率,且会话内提醒在会话冷时静默失败;请从组合中移除 @deepseek-ai/dsh-schedule(二选一)");
|
|
5051
|
+
const disposers = tools.map((tool) => agent.ctx.tools.register(tool));
|
|
5052
|
+
return () => {
|
|
5053
|
+
for (const dispose of disposers) dispose();
|
|
5054
|
+
if (attached.get(agent) === cleanup) attached.delete(agent);
|
|
5055
|
+
};
|
|
5056
|
+
}, "dsh-agent-toolkit.cron-tools.attach()");
|
|
5057
|
+
attached.set(agent, cleanup);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
//#endregion
|
|
5061
|
+
//#region src/schedule/index.ts
|
|
5062
|
+
/** schedule 模块接线:存储域 → executor/scheduler/service → cron_* 工具 + HTTP API + 30s tick。 */
|
|
5063
|
+
/** tick 间隔固定 30s(spec §8:不进 Config——YAGNI)。 */
|
|
5064
|
+
const TICK_MS = 3e4;
|
|
5065
|
+
function setupSchedule(ctx, config, deps) {
|
|
5066
|
+
const warn = (m) => ctx.logger.warn(m);
|
|
5067
|
+
const toolsScope = createToolsScope(ctx);
|
|
5068
|
+
const agentsPort = createAgentsPort(ctx, deps.botPresetId !== void 0 ? createScopeJoiner(ctx, deps.botPresetId, toolsScope, warn) : toolsScope, deps.ownedSessions);
|
|
5069
|
+
const workspaceRegistryOf = () => ctx.get("workspaceRegistry", false);
|
|
5070
|
+
const workspacePort = { async attach(cwd, sessionId) {
|
|
5071
|
+
const registry = workspaceRegistryOf();
|
|
5072
|
+
if (registry === void 0) throw new Error("workspaceRegistry 服务不可用");
|
|
5073
|
+
await (await registry.create(cwd)).attachSession(SessionId(sessionId));
|
|
5074
|
+
} };
|
|
5075
|
+
let scheduler;
|
|
5076
|
+
const started = openDomainSafely(ctx, scheduleDomain, warn).then(async (domain) => {
|
|
5077
|
+
const tasks = domain.table("tasks");
|
|
5078
|
+
const runs = domain.table("runs");
|
|
5079
|
+
const executor = createExecutor({
|
|
5080
|
+
agents: agentsPort,
|
|
5081
|
+
registry: deps.registry,
|
|
5082
|
+
defaultModel: () => {
|
|
5083
|
+
const selection = ctx.agentDefaultModel.currentSelection();
|
|
5084
|
+
return {
|
|
5085
|
+
provider: selection.provider,
|
|
5086
|
+
model: selection.model
|
|
5087
|
+
};
|
|
5088
|
+
},
|
|
5089
|
+
workspace: workspacePort,
|
|
5090
|
+
runs,
|
|
5091
|
+
runHistoryLimit: config.runHistoryLimit,
|
|
5092
|
+
runTimeoutMs: config.runTimeoutMinutes * 6e4,
|
|
5093
|
+
warn,
|
|
5094
|
+
now: () => Date.now(),
|
|
5095
|
+
newSessionId: () => randomUUID(),
|
|
5096
|
+
newRunId: () => randomUUID(),
|
|
5097
|
+
delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
5098
|
+
});
|
|
5099
|
+
scheduler = createScheduler({
|
|
5100
|
+
tasks,
|
|
5101
|
+
runs,
|
|
5102
|
+
runHistoryLimit: config.runHistoryLimit,
|
|
5103
|
+
execute: (task) => executor.trigger(task),
|
|
5104
|
+
now: () => Date.now(),
|
|
5105
|
+
newRunId: () => randomUUID(),
|
|
5106
|
+
warn
|
|
5107
|
+
});
|
|
5108
|
+
const service = createCronService({
|
|
5109
|
+
tasks,
|
|
5110
|
+
runs,
|
|
5111
|
+
scheduler,
|
|
5112
|
+
registry: deps.registry,
|
|
5113
|
+
validateProject: (path) => existsSync(path),
|
|
5114
|
+
now: () => Date.now(),
|
|
5115
|
+
newTaskId: () => randomUUID()
|
|
5116
|
+
});
|
|
5117
|
+
setupCronTools(ctx, createCronTools(service), deps.ownedSessions);
|
|
5118
|
+
registerOptionalRoutes(ctx, (webCtx) => {
|
|
5119
|
+
const dispose = webCtx.webServer.register({
|
|
5120
|
+
kind: "prefix",
|
|
5121
|
+
path: "/dsh-agent-toolkit/api/cron",
|
|
5122
|
+
handler: async (req, res) => {
|
|
5123
|
+
try {
|
|
5124
|
+
await started;
|
|
5125
|
+
await createCronApiHandler({
|
|
5126
|
+
service,
|
|
5127
|
+
listProjects: () => workspaceRegistryOf()?.list().map((w) => w.path) ?? [],
|
|
5128
|
+
runHistoryLimit: config.runHistoryLimit
|
|
5129
|
+
})(req, res);
|
|
5130
|
+
} catch (error) {
|
|
5131
|
+
res.writeHead(500, { "content-type": "application/json" }).end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
});
|
|
5135
|
+
return () => dispose();
|
|
5136
|
+
});
|
|
5137
|
+
await scheduler.rearmAll();
|
|
5138
|
+
});
|
|
5139
|
+
started.catch((error) => {
|
|
5140
|
+
warn(`[schedule] 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
5141
|
+
});
|
|
5142
|
+
ctx.effect(() => {
|
|
5143
|
+
const timer = setInterval(() => {
|
|
5144
|
+
started.then(() => scheduler?.tick());
|
|
5145
|
+
}, TICK_MS);
|
|
5146
|
+
return () => clearInterval(timer);
|
|
5147
|
+
});
|
|
5148
|
+
ctx.effect(() => async () => {
|
|
5149
|
+
await toolsScope.dispose();
|
|
5150
|
+
});
|
|
5151
|
+
}
|
|
3562
5152
|
//#endregion
|
|
3563
5153
|
//#region src/index.ts
|
|
3564
5154
|
const name = "dsh-agent-toolkit";
|
|
@@ -3602,33 +5192,46 @@ const Config = z.object({
|
|
|
3602
5192
|
toolName: z.string().default("team_delegate"),
|
|
3603
5193
|
feishu: z.object({
|
|
3604
5194
|
cardUpdateThrottleMs: z.number().default(500),
|
|
3605
|
-
cardMaxBytes: z.number().default(
|
|
5195
|
+
cardMaxBytes: z.number().default(26e3),
|
|
5196
|
+
cardPrintStep: z.number().default(5),
|
|
3606
5197
|
processMaxBytes: z.number().default(8e3),
|
|
3607
5198
|
registerAppTimeoutMs: z.number().default(6e5),
|
|
3608
5199
|
processingReactionEmoji: z.string().default("OneSecond"),
|
|
3609
5200
|
errorDetailMaxChars: z.number().default(500),
|
|
3610
|
-
injectSender: z.boolean().default(true)
|
|
5201
|
+
injectSender: z.boolean().default(true),
|
|
5202
|
+
approval: z.boolean().default(true)
|
|
3611
5203
|
}).default({
|
|
3612
5204
|
cardUpdateThrottleMs: 500,
|
|
3613
|
-
cardMaxBytes:
|
|
5205
|
+
cardMaxBytes: 26e3,
|
|
5206
|
+
cardPrintStep: 5,
|
|
3614
5207
|
processMaxBytes: 8e3,
|
|
3615
5208
|
registerAppTimeoutMs: 6e5,
|
|
3616
5209
|
processingReactionEmoji: "OneSecond",
|
|
3617
5210
|
errorDetailMaxChars: 500,
|
|
3618
|
-
injectSender: true
|
|
5211
|
+
injectSender: true,
|
|
5212
|
+
approval: true
|
|
3619
5213
|
}),
|
|
3620
5214
|
agentTeamPreset: z.object({
|
|
3621
5215
|
enabled: z.boolean().default(true),
|
|
3622
5216
|
id: z.string().default("agent-team"),
|
|
3623
5217
|
source: z.string().default("standard"),
|
|
3624
5218
|
name: z.string().default("Agent 团队"),
|
|
3625
|
-
description: z.string().default("Agent 团队模式:禁用原生 subagent 工具族,委派统一走 team_delegate 团队角色")
|
|
5219
|
+
description: z.string().default("Agent 团队模式:禁用原生 subagent 工具族,委派统一走 team_delegate 团队角色"),
|
|
5220
|
+
botsId: z.string().default("agent-bot")
|
|
3626
5221
|
}).default({
|
|
3627
5222
|
enabled: true,
|
|
3628
5223
|
id: "agent-team",
|
|
3629
5224
|
source: "standard",
|
|
3630
5225
|
name: "Agent 团队",
|
|
3631
|
-
description: "Agent 团队模式:禁用原生 subagent 工具族,委派统一走 team_delegate 团队角色"
|
|
5226
|
+
description: "Agent 团队模式:禁用原生 subagent 工具族,委派统一走 team_delegate 团队角色",
|
|
5227
|
+
botsId: "agent-bot"
|
|
5228
|
+
}),
|
|
5229
|
+
schedule: z.object({
|
|
5230
|
+
runTimeoutMinutes: z.number().min(1).default(60),
|
|
5231
|
+
runHistoryLimit: z.natural().min(1).default(20)
|
|
5232
|
+
}).default({
|
|
5233
|
+
runTimeoutMinutes: 60,
|
|
5234
|
+
runHistoryLimit: 20
|
|
3632
5235
|
})
|
|
3633
5236
|
});
|
|
3634
5237
|
async function apply(ctx, config) {
|
|
@@ -3643,10 +5246,12 @@ async function apply(ctx, config) {
|
|
|
3643
5246
|
meta: domain.table("meta"),
|
|
3644
5247
|
promptLayers: domain.table("prompt_layers")
|
|
3645
5248
|
};
|
|
5249
|
+
await setupAgentTeamPreset(ctx, config.agentTeamPreset);
|
|
5250
|
+
const toolCatalog = createToolCatalog(ctx, config.agentTeamPreset.id);
|
|
3646
5251
|
const registry = await createRegistry(warn, {
|
|
3647
5252
|
agents: tables.agents,
|
|
3648
5253
|
meta: tables.meta
|
|
3649
|
-
});
|
|
5254
|
+
}, toolCatalog.listPresetTools);
|
|
3650
5255
|
const layerSource = await openLayerSource({
|
|
3651
5256
|
promptLayers: tables.promptLayers,
|
|
3652
5257
|
meta: tables.meta
|
|
@@ -3675,10 +5280,10 @@ async function apply(ctx, config) {
|
|
|
3675
5280
|
active: activeRoutes,
|
|
3676
5281
|
routes: routesTable
|
|
3677
5282
|
});
|
|
3678
|
-
const listTools = () => ctx.tools.schemas().map((s) => s.name);
|
|
3679
5283
|
setupAgentsApi(ctx, {
|
|
3680
5284
|
registry,
|
|
3681
|
-
listTools,
|
|
5285
|
+
listTools: toolCatalog.listGlobalTools,
|
|
5286
|
+
listPresetTools: toolCatalog.listPresetTools,
|
|
3682
5287
|
listProviders: () => ctx.llm.listProviders().map(({ id, name }) => ({
|
|
3683
5288
|
id,
|
|
3684
5289
|
name
|
|
@@ -3690,7 +5295,8 @@ async function apply(ctx, config) {
|
|
|
3690
5295
|
});
|
|
3691
5296
|
setupCreateAgentCommand(ctx, {
|
|
3692
5297
|
registry,
|
|
3693
|
-
listTools
|
|
5298
|
+
listTools: toolCatalog.listGlobalTools,
|
|
5299
|
+
listPresetTools: toolCatalog.listPresetTools
|
|
3694
5300
|
});
|
|
3695
5301
|
setupPromptLayersApi(ctx, {
|
|
3696
5302
|
source: layerSource,
|
|
@@ -3710,9 +5316,18 @@ async function apply(ctx, config) {
|
|
|
3710
5316
|
};
|
|
3711
5317
|
}
|
|
3712
5318
|
});
|
|
3713
|
-
|
|
3714
|
-
if (config.modules.feishu) setupBots(ctx, config.feishu, {
|
|
5319
|
+
const ownedSessions = /* @__PURE__ */ new Set();
|
|
5320
|
+
if (config.modules.feishu) setupBots(ctx, config.feishu, {
|
|
5321
|
+
registry,
|
|
5322
|
+
botPresetId: config.agentTeamPreset.enabled ? config.agentTeamPreset.botsId : void 0,
|
|
5323
|
+
ownedSessions
|
|
5324
|
+
});
|
|
3715
5325
|
if (config.modules.usage) setupUsage(ctx, { timezone: config.timezone }, name);
|
|
5326
|
+
setupSchedule(ctx, config.schedule, {
|
|
5327
|
+
registry,
|
|
5328
|
+
botPresetId: config.agentTeamPreset.enabled ? config.agentTeamPreset.botsId : void 0,
|
|
5329
|
+
ownedSessions
|
|
5330
|
+
});
|
|
3716
5331
|
}
|
|
3717
5332
|
//#endregion
|
|
3718
5333
|
export { Config, apply, inject, name };
|