dsh-agent-toolkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +55625 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +71 -0
- package/lib/index.js +3151 -0
- package/lib/index.js.map +1 -0
- package/package.json +112 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,3151 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { z as z$1 } from "zod";
|
|
3
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
4
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import yaml from "js-yaml";
|
|
7
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
8
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
11
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
12
|
+
import * as lark from "@larksuiteoapi/node-sdk";
|
|
13
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
14
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
15
|
+
import { setupUsage } from "@dsh-agent-toolkit/token-usage";
|
|
16
|
+
//#region src/agents/store.ts
|
|
17
|
+
/** agents 注册表存储域声明:记录 schema + domain 布局的单一来源。 */
|
|
18
|
+
/** Agent id 约束:'main' 或小写字母开头、仅 [a-z0-9-]、总长 ≤ 32。 */
|
|
19
|
+
const AGENT_ID_RE = /^(?:main|[a-z][a-z0-9-]{0,31})$/;
|
|
20
|
+
const LayerConfigSchema = z$1.object({
|
|
21
|
+
name: z$1.string(),
|
|
22
|
+
order: z$1.number(),
|
|
23
|
+
text: z$1.string()
|
|
24
|
+
});
|
|
25
|
+
const AgentRecordSchema = z$1.object({
|
|
26
|
+
id: z$1.string().regex(AGENT_ID_RE),
|
|
27
|
+
name: z$1.string().min(1),
|
|
28
|
+
description: z$1.string().optional(),
|
|
29
|
+
persona: z$1.string().optional(),
|
|
30
|
+
promptLayers: z$1.array(LayerConfigSchema).optional(),
|
|
31
|
+
model: z$1.object({
|
|
32
|
+
provider: z$1.string(),
|
|
33
|
+
model: z$1.string()
|
|
34
|
+
}).optional(),
|
|
35
|
+
tools: z$1.object({ allow: z$1.array(z$1.string()).min(1) }).optional(),
|
|
36
|
+
builtin: z$1.boolean().optional()
|
|
37
|
+
});
|
|
38
|
+
/** domain 名/表名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
|
|
39
|
+
const agentToolkitDomain = defineDomain({
|
|
40
|
+
name: "dsh_agent_toolkit",
|
|
41
|
+
version: 1,
|
|
42
|
+
tables: {
|
|
43
|
+
agents: domainTable(AgentRecordSchema),
|
|
44
|
+
meta: domainTable(z$1.object({ value: z$1.string() })),
|
|
45
|
+
prompt_layers: domainTable(z$1.object({ layers: z$1.array(LayerConfigSchema) }))
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* 旧记录迁移:promptLayers 按 order 升序拼接进 persona(忽略纯空白层;persona 已存在不覆盖),
|
|
50
|
+
* 剥离 promptLayers 返回新对象;无需迁移返回原引用(调用方按引用比较决定是否写回)。
|
|
51
|
+
*/
|
|
52
|
+
function migrateAgentRecord(record) {
|
|
53
|
+
if (record.promptLayers === void 0) return record;
|
|
54
|
+
const { promptLayers, ...rest } = record;
|
|
55
|
+
const joined = [...promptLayers].sort((a, b) => a.order - b.order).map((layer) => layer.text).filter((text) => text.trim().length > 0).join("\n\n");
|
|
56
|
+
const persona = rest.persona ?? joined;
|
|
57
|
+
return persona.length > 0 ? {
|
|
58
|
+
...rest,
|
|
59
|
+
persona
|
|
60
|
+
} : rest;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/agents/builtin.ts
|
|
64
|
+
const BUILTIN_AGENTS = [
|
|
65
|
+
{
|
|
66
|
+
id: "main",
|
|
67
|
+
name: "主 Agent",
|
|
68
|
+
builtin: true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "explorer",
|
|
72
|
+
name: "Explorer",
|
|
73
|
+
description: "快速只读代码库探索:定位文件/符号、回答结构与调用关系问题,不做任何修改",
|
|
74
|
+
persona: `你是代码库探索员。快速定位与任务相关的文件与符号,回答关于代码结构、
|
|
75
|
+
调用关系、实现位置的问题。你只读不写:不修改任何文件、不运行有副作用的命令。
|
|
76
|
+
输出结论清单,每条附文件路径与行号;信息不足时说明缺口,不要猜测。`,
|
|
77
|
+
builtin: true
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "general",
|
|
81
|
+
name: "General",
|
|
82
|
+
description: "通用多步骤任务执行:可读可写、可运行命令,完成实现/修复类任务",
|
|
83
|
+
persona: `你是通用执行员。按任务书独立完成多步骤工作,可以读写文件、运行命令。
|
|
84
|
+
动手前先阅读相关 AGENTS.md 并遵循项目约定;完成后运行与改动相关的检查
|
|
85
|
+
(测试/类型检查)验证改动,并在最终输出中报告验证结果。`,
|
|
86
|
+
builtin: true
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/agents/import-yaml.ts
|
|
91
|
+
/** 首启导入:把旧 agent-team 角色名册 $DSH_HOME/agent-team/roles/*.yml 一次性并入注册表。 */
|
|
92
|
+
/** 导入一次性标记在 meta 表中的键。 */
|
|
93
|
+
const ROLES_YAML_IMPORTED_KEY = "roles_yaml_imported";
|
|
94
|
+
const RoleYamlSchema = z$1.object({
|
|
95
|
+
name: z$1.string().optional(),
|
|
96
|
+
description: z$1.string().min(1),
|
|
97
|
+
persona: z$1.string().min(1),
|
|
98
|
+
provider: z$1.string().optional(),
|
|
99
|
+
model: z$1.string().optional(),
|
|
100
|
+
tools: z$1.object({
|
|
101
|
+
allow: z$1.array(z$1.string()).optional(),
|
|
102
|
+
deny: z$1.array(z$1.string()).optional()
|
|
103
|
+
}).optional()
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* 解析校验单个角色 YAML 文件并转成 AgentRecord。
|
|
107
|
+
* @param text - 文件内容。
|
|
108
|
+
* @param source - 用于错误信息的来源名(通常是文件路径)。
|
|
109
|
+
* @param fileName - 文件名(去 .yml),name 省略时的取值;显式 name 须与它一致。
|
|
110
|
+
* @param warn - 非致命丢弃(如 tools.deny)的通知通道。
|
|
111
|
+
* @throws YAML 语法错误、结构非法、name 与文件名不一致、id 非法、tools 空。
|
|
112
|
+
*/
|
|
113
|
+
function parseRoleYaml(text, source, fileName, warn) {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = yaml.load(text);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 不是合法 YAML:${error instanceof Error ? error.message : String(error)}`);
|
|
119
|
+
}
|
|
120
|
+
const hasTools = parsed !== null && typeof parsed === "object" && "tools" in parsed;
|
|
121
|
+
let raw;
|
|
122
|
+
try {
|
|
123
|
+
raw = RoleYamlSchema.parse(parsed);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 校验失败:${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
+
}
|
|
127
|
+
const id = raw.name ?? fileName;
|
|
128
|
+
if (raw.name !== void 0 && raw.name !== fileName) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 name "${raw.name}" 与文件名 "${fileName}" 不一致(省略 name 即取文件名)`);
|
|
129
|
+
if (!AGENT_ID_RE.test(id)) throw new Error(`dsh-agent-toolkit: 角色 id "${id}" 非法(${source}):只允许小写字母、数字、-,且以小写字母开头`);
|
|
130
|
+
if (hasTools && raw.tools !== void 0 && (raw.tools.allow?.length ?? 0) === 0 && (raw.tools.deny?.length ?? 0) === 0) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 tools 为空:allow/deny 至少配一个`);
|
|
131
|
+
if (raw.tools?.deny !== void 0 && raw.tools.deny.length > 0) warn?.(`dsh-agent-toolkit: 角色文件 ${source} 的 tools.deny 已忽略(注册表仅支持 allow 白名单)`);
|
|
132
|
+
const model = raw.provider !== void 0 && raw.model !== void 0 ? {
|
|
133
|
+
provider: raw.provider,
|
|
134
|
+
model: raw.model
|
|
135
|
+
} : void 0;
|
|
136
|
+
const tools = hasTools && raw.tools !== void 0 && raw.tools.allow !== void 0 && raw.tools.allow.length > 0 ? { allow: raw.tools.allow } : void 0;
|
|
137
|
+
return {
|
|
138
|
+
id,
|
|
139
|
+
name: id,
|
|
140
|
+
description: raw.description,
|
|
141
|
+
persona: raw.persona,
|
|
142
|
+
...model !== void 0 ? { model } : {},
|
|
143
|
+
...tools !== void 0 ? { tools } : {}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 枚举 roles 目录下全部 .yml 角色文件,按文件名字典序返回。
|
|
148
|
+
* 共享骨架(readdir/ENOENT/过滤/排序/文件名推导)——YAML 文件命名约定只在这里维护。
|
|
149
|
+
* @param dir - roles 目录绝对路径。
|
|
150
|
+
* @returns 文件引用列表;目录不存在或无 .yml 文件时返回空列表(静默跳过,属正常态)。
|
|
151
|
+
* @throws 目录存在但不可读(非 ENOENT)。读取/解析的严格与宽容由调用方自行处理。
|
|
152
|
+
*/
|
|
153
|
+
async function enumerateRoleFiles(dir) {
|
|
154
|
+
let entries;
|
|
155
|
+
try {
|
|
156
|
+
entries = await readdir(dir);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code === "ENOENT") return [];
|
|
159
|
+
throw new Error(`dsh-agent-toolkit: 角色目录不可读:${dir}(${error instanceof Error ? error.message : String(error)})`);
|
|
160
|
+
}
|
|
161
|
+
return entries.filter((f) => f.endsWith(".yml")).sort().map((file) => ({
|
|
162
|
+
name: file,
|
|
163
|
+
path: join(dir, file),
|
|
164
|
+
fileName: file.slice(0, -4)
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 首启一次性导入:meta 表 roles_yaml_imported 标记短路;逐文件解析失败 warn 跳过(不阻塞激活);
|
|
169
|
+
* 同名 YAML 覆盖内置保底记录。
|
|
170
|
+
* @param ctx - agents/meta 表句柄与 warn 通道。
|
|
171
|
+
* @param rolesDir - roles 目录;缺省为 $DSH_HOME/agent-team/roles。
|
|
172
|
+
*/
|
|
173
|
+
async function importRolesYaml(ctx, rolesDir) {
|
|
174
|
+
const result = {
|
|
175
|
+
imported: 0,
|
|
176
|
+
skipped: []
|
|
177
|
+
};
|
|
178
|
+
if (ctx.meta.get("roles_yaml_imported") !== void 0) return result;
|
|
179
|
+
const dir = rolesDir ?? join(resolveDshHome(), "agent-team", "roles");
|
|
180
|
+
let refs;
|
|
181
|
+
try {
|
|
182
|
+
refs = await enumerateRoleFiles(dir);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
ctx.warn(error instanceof Error ? error.message : String(error));
|
|
185
|
+
await markImported(ctx);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
for (const ref of refs) try {
|
|
189
|
+
const record = parseRoleYaml(await readFile(ref.path, "utf8"), ref.path, ref.fileName, ctx.warn);
|
|
190
|
+
await ctx.agents.put(record.id, record);
|
|
191
|
+
result.imported++;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
result.skipped.push(ref.name);
|
|
194
|
+
ctx.warn(`dsh-agent-toolkit: 角色文件 ${ref.path} 导入失败,已跳过:${error instanceof Error ? error.message : String(error)}`);
|
|
195
|
+
}
|
|
196
|
+
await markImported(ctx);
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
async function markImported(ctx) {
|
|
200
|
+
await ctx.meta.put(ROLES_YAML_IMPORTED_KEY, { value: "1" });
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/channels/basic-tools.ts
|
|
204
|
+
const BASIC_TOOLS = [
|
|
205
|
+
{
|
|
206
|
+
id: "@deepseek-ai/dsh-persona",
|
|
207
|
+
config: { text: "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}." }
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "@deepseek-ai/dsh-agent-instructions",
|
|
211
|
+
config: { maxBytes: 65536 }
|
|
212
|
+
},
|
|
213
|
+
...process.platform === "win32" ? [{ id: "@deepseek-ai/dsh-tool-pwsh" }] : [{ id: "@deepseek-ai/dsh-tool-bash" }],
|
|
214
|
+
{ id: "@deepseek-ai/dsh-tool-fs" },
|
|
215
|
+
{
|
|
216
|
+
id: "@deepseek-ai/dsh-tool-fs-search",
|
|
217
|
+
config: { sampleOverCapGlobResults: false }
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
/** 原生工具名(白名单 UI 与存量迁移用):与 BASIC_TOOLS 挂载插件注册的工具名一一对应。
|
|
221
|
+
* 名字来源(摘自 deepseek-harness 源码):dsh-tool-pwsh/dsh-tool-bash → 'pwsh'/'bash'(平台互斥);
|
|
222
|
+
* dsh-tool-fs → 'read'/'write'/'edit'/'read_image';dsh-tool-fs-search → 'glob'/'grep'。
|
|
223
|
+
* 这些工具 scoped 挂载在 agentCtx,不出现在顶层 ctx.tools.schemas(),故需显式常量。 */
|
|
224
|
+
const NATIVE_TOOL_NAMES = [
|
|
225
|
+
process.platform === "win32" ? "pwsh" : "bash",
|
|
226
|
+
"read",
|
|
227
|
+
"write",
|
|
228
|
+
"edit",
|
|
229
|
+
"read_image",
|
|
230
|
+
"glob",
|
|
231
|
+
"grep"
|
|
232
|
+
];
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/agents/registry.ts
|
|
235
|
+
/** tools.allow 一次性并入原生工具名的 meta 表标记键。 */
|
|
236
|
+
const TOOLS_NATIVE_MIGRATED_KEY = "tools_native_migrated";
|
|
237
|
+
/**
|
|
238
|
+
* 打开 dsh_agent_toolkit 域 → 缺 main/explorer/general 时种入内置 → 首启 YAML 导入 →
|
|
239
|
+
* 构建内存缓存。域由 apply 统一 open(storage-domain 同名单开),此处只消费表句柄。
|
|
240
|
+
*/
|
|
241
|
+
async function createRegistry(warn, tables) {
|
|
242
|
+
const { agents, meta } = tables;
|
|
243
|
+
await seedBuiltins(agents);
|
|
244
|
+
await importRolesYaml({
|
|
245
|
+
agents,
|
|
246
|
+
meta,
|
|
247
|
+
warn
|
|
248
|
+
});
|
|
249
|
+
const nativeMigrated = meta.get(TOOLS_NATIVE_MIGRATED_KEY) !== void 0;
|
|
250
|
+
for (const [id, record] of agents.entries()) {
|
|
251
|
+
let next = migrateAgentRecord(record);
|
|
252
|
+
if (!nativeMigrated && next.tools !== void 0) {
|
|
253
|
+
const allow = next.tools.allow;
|
|
254
|
+
const missing = NATIVE_TOOL_NAMES.filter((name) => !allow.includes(name));
|
|
255
|
+
if (missing.length > 0) next = {
|
|
256
|
+
...next,
|
|
257
|
+
tools: { allow: [...allow, ...missing] }
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (next !== record) await agents.put(id, next);
|
|
261
|
+
}
|
|
262
|
+
if (!nativeMigrated) await meta.put(TOOLS_NATIVE_MIGRATED_KEY, { value: "1" });
|
|
263
|
+
const cache = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const [id, record] of agents.entries()) cache.set(id, record);
|
|
265
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
266
|
+
const notify = () => {
|
|
267
|
+
for (const listener of [...listeners]) listener();
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
list() {
|
|
271
|
+
const main = cache.get("main");
|
|
272
|
+
const rest = [...cache.entries()].filter(([id]) => id !== "main").map(([, record]) => record).sort((a, b) => a.id.localeCompare(b.id));
|
|
273
|
+
return main === void 0 ? rest : [main, ...rest];
|
|
274
|
+
},
|
|
275
|
+
get(id) {
|
|
276
|
+
return cache.get(id);
|
|
277
|
+
},
|
|
278
|
+
async upsert(record) {
|
|
279
|
+
const parsed = AgentRecordSchema.safeParse(record);
|
|
280
|
+
if (!parsed.success) throw new Error(`dsh-agent-toolkit: Agent 记录校验失败:${parsed.error.message}`);
|
|
281
|
+
const normalized = migrateAgentRecord(parsed.data);
|
|
282
|
+
const existing = cache.get(record.id);
|
|
283
|
+
if (record.id === "main" && existing !== void 0) {
|
|
284
|
+
if (existing.name !== record.name || existing.builtin !== record.builtin) throw new Error("dsh-agent-toolkit: 主 Agent(main)的 name/builtin 字段不可修改");
|
|
285
|
+
}
|
|
286
|
+
if (existing?.builtin === true && record.builtin !== true) throw new Error(`dsh-agent-toolkit: 内置角色 ${record.id} 的 builtin 标记不可修改`);
|
|
287
|
+
await agents.put(record.id, normalized);
|
|
288
|
+
cache.set(record.id, normalized);
|
|
289
|
+
notify();
|
|
290
|
+
},
|
|
291
|
+
async remove(id) {
|
|
292
|
+
if (id === "main") throw new Error("dsh-agent-toolkit: 主 Agent(main)不可删除");
|
|
293
|
+
if (cache.get(id)?.builtin === true) throw new Error(`dsh-agent-toolkit: 内置角色 ${id} 不可删除`);
|
|
294
|
+
if (await agents.delete(id)) {
|
|
295
|
+
cache.delete(id);
|
|
296
|
+
notify();
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
subscribe(listener) {
|
|
300
|
+
listeners.add(listener);
|
|
301
|
+
return () => {
|
|
302
|
+
listeners.delete(listener);
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function seedBuiltins(agents) {
|
|
308
|
+
for (const builtin of BUILTIN_AGENTS) if (agents.get(builtin.id) === void 0) await agents.put(builtin.id, builtin);
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/shared/storage.ts
|
|
312
|
+
function openDomainSafely(ctx, domain, warn, beforeClose) {
|
|
313
|
+
const ready = ctx.storageDomain.open(domain);
|
|
314
|
+
ready.catch((error) => {
|
|
315
|
+
warn(error instanceof Error ? error.message : String(error));
|
|
316
|
+
});
|
|
317
|
+
ctx.effect(() => async () => {
|
|
318
|
+
await Promise.resolve(beforeClose?.()).catch(() => void 0);
|
|
319
|
+
await ready.then((domain) => domain.close()).catch(() => void 0);
|
|
320
|
+
});
|
|
321
|
+
return ready;
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/prompt/defaults.ts
|
|
325
|
+
/** 通用基座层文本(default.txt 改写版)。 */
|
|
326
|
+
const BASE_TEXT = `IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
327
|
+
|
|
328
|
+
# Tone and style
|
|
329
|
+
You should be concise, direct, and to the point. When you run a non-trivial shell command, explain what the command does and why you are running it, so the user understands what you are doing (this is especially important when the command changes the user's system).
|
|
330
|
+
Your output will be displayed on a command line interface. You can use GitHub-flavored markdown for formatting; it will be rendered in a monospace font using the CommonMark specification.
|
|
331
|
+
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools or code comments as a means to communicate with the user during the session.
|
|
332
|
+
If you cannot or will not help the user with something, do not preach about why or what it could lead to. Offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
|
|
333
|
+
Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
|
|
334
|
+
IMPORTANT: Minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical. If you can answer in 1-3 sentences or a short paragraph, please do.
|
|
335
|
+
IMPORTANT: Do not answer with unnecessary preamble or postamble (such as explaining your code or summarizing your actions) unless the user asks you to.
|
|
336
|
+
Keep responses short: answer the user's question directly, without elaboration. Avoid introductions, conclusions, and restatements such as "The answer is ..." or "Here is what I will do next ...".
|
|
337
|
+
<example>
|
|
338
|
+
user: what is 2+2?
|
|
339
|
+
assistant: 4
|
|
340
|
+
</example>
|
|
341
|
+
|
|
342
|
+
<example>
|
|
343
|
+
user: what files are in the directory src/?
|
|
344
|
+
assistant: [lists files and sees foo.c, bar.c, baz.c]
|
|
345
|
+
user: which file contains the implementation of foo?
|
|
346
|
+
assistant: src/foo.c
|
|
347
|
+
</example>
|
|
348
|
+
|
|
349
|
+
# Proactiveness
|
|
350
|
+
Be proactive only when the user asks you to do something. Strike a balance between:
|
|
351
|
+
1. Doing the right thing when asked, including follow-up actions the request implies.
|
|
352
|
+
2. Not surprising the user with actions taken without asking.
|
|
353
|
+
If the user asks how to approach something, answer the question first instead of immediately jumping into action.
|
|
354
|
+
Do not add a code-explanation summary unless requested. After working on a file, just stop.
|
|
355
|
+
|
|
356
|
+
# Following conventions
|
|
357
|
+
When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
|
|
358
|
+
- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses it (neighboring files, package manifests such as package.json or cargo.toml).
|
|
359
|
+
- When you create a new component, first look at existing components: framework choice, naming conventions, typing, and other conventions.
|
|
360
|
+
- When you edit a piece of code, read its surrounding context (especially its imports) and make the change idiomatic to it.
|
|
361
|
+
- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
|
|
362
|
+
|
|
363
|
+
# Code style
|
|
364
|
+
- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked.
|
|
365
|
+
|
|
366
|
+
# Doing tasks
|
|
367
|
+
For software engineering tasks (fixing bugs, adding features, refactoring, explaining code, and more):
|
|
368
|
+
- Use the available search tools extensively, in parallel and sequentially, to understand the codebase and the user's query.
|
|
369
|
+
- Implement the solution using all tools available to you.
|
|
370
|
+
- Verify the solution with tests where possible. NEVER assume a specific test framework or test script; check the README or search the codebase to determine the testing approach.
|
|
371
|
+
- When you have completed a task, run the project's lint and typecheck commands if they exist. If you cannot find the correct command, ask the user for it.
|
|
372
|
+
- NEVER commit changes unless the user explicitly asks you to.
|
|
373
|
+
|
|
374
|
+
Tool results and user messages may include <system-reminder> tags. They contain useful information and reminders. They are NOT part of the user's provided input or the tool result.
|
|
375
|
+
|
|
376
|
+
# Tool usage policy
|
|
377
|
+
- For file or content search, prefer dedicated search tools over shell commands to reduce context usage.
|
|
378
|
+
- You can call multiple tools in a single response. Batch independent calls together for optimal performance; run dependent calls sequentially.
|
|
379
|
+
|
|
380
|
+
Before you begin work, think about what the code you are editing is supposed to do, based on filenames and directory structure.
|
|
381
|
+
|
|
382
|
+
# Code references
|
|
383
|
+
When referencing specific functions or pieces of code, use the pattern \`file_path:line_number\` so the user can easily navigate to the source location.`;
|
|
384
|
+
/** 模型族行为指导文本(anthropic/gemini/beast/codex/gpt/kimi 改写版,
|
|
385
|
+
* 内容按下文"6 个模型族 TEXT 的改写契约"逐条产出,英文,不写身份首句)。 */
|
|
386
|
+
const ANTHROPIC_TEXT = `IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
387
|
+
|
|
388
|
+
# Tone and style
|
|
389
|
+
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
|
|
390
|
+
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use GitHub-flavored markdown for formatting, and it will be rendered in a monospace font using the CommonMark specification.
|
|
391
|
+
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use shell commands or code comments as a means to communicate with the user during the session.
|
|
392
|
+
- NEVER create files unless they are absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one.
|
|
393
|
+
|
|
394
|
+
# Professional objectivity
|
|
395
|
+
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical information without unnecessary superlatives, praise, or emotional validation. Apply the same rigorous standards to all ideas and disagree when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, investigate to find the truth first rather than instinctively confirming the user's beliefs.
|
|
396
|
+
|
|
397
|
+
# Planning and tracking
|
|
398
|
+
Use a task list frequently to plan and track your work, giving the user visibility into your progress. These lists are also helpful for breaking down larger, complex tasks into smaller steps. Mark each task as completed as soon as you are done; do not batch up multiple tasks before marking them completed.
|
|
399
|
+
|
|
400
|
+
# Doing tasks
|
|
401
|
+
The user will primarily request software engineering tasks: fixing bugs, adding new functionality, refactoring code, explaining code, and more. Search the codebase to understand the user's query, implement the solution using all tools available to you, then verify it. Tool results and user messages may include <system-reminder> tags. They contain useful information and reminders; they are authoritative and are NOT part of the user's provided input or the tool result.
|
|
402
|
+
|
|
403
|
+
# Tool usage policy
|
|
404
|
+
- For file or content search, prefer dedicated search tools over shell commands to reduce context usage.
|
|
405
|
+
- Prefer dedicated editing tools over shell for modifying files.
|
|
406
|
+
- You can call multiple tools in a single response. Make all independent tool calls in parallel; run dependent calls sequentially. Never use placeholders or guess missing parameters in tool calls.
|
|
407
|
+
|
|
408
|
+
# Code references
|
|
409
|
+
When referencing specific functions or pieces of code, use the pattern \`file_path:line_number\` so the user can easily navigate to the source location.`;
|
|
410
|
+
const GEMINI_TEXT = `# Core Mandates
|
|
411
|
+
|
|
412
|
+
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
|
|
413
|
+
- **Libraries/Frameworks:** NEVER assume a library or framework is available or appropriate. Verify its established usage within the project (check imports and configuration files, or observe neighboring files) before employing it.
|
|
414
|
+
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
|
|
415
|
+
- **Idiomatic Changes:** When editing, understand the local context (imports, functions, classes) to ensure your changes integrate naturally and idiomatically.
|
|
416
|
+
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
|
417
|
+
- **Proactiveness:** Fulfill the user's request thoroughly, including reasonable, directly implied follow-up actions.
|
|
418
|
+
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first; do not just do it.
|
|
419
|
+
- **Explaining Changes:** After completing a code modification or file operation, do not provide summaries unless asked.
|
|
420
|
+
- **Path Construction:** Before using any file system tool, construct the full absolute path for the file argument. Always combine the absolute path of the project root with the file's path relative to the root. If the user provides a relative path, resolve it against the root to create an absolute path.
|
|
421
|
+
- **Do Not Revert Changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they resulted in an error or if the user has explicitly asked you to revert them.
|
|
422
|
+
|
|
423
|
+
# Primary Workflows
|
|
424
|
+
|
|
425
|
+
## Software Engineering Tasks
|
|
426
|
+
When requested to fix bugs, add features, refactor, or explain code, follow this sequence:
|
|
427
|
+
1. **Understand:** Think about the request and the relevant codebase context. Search extensively (in parallel if independent) to understand file structures, existing patterns, and conventions. Read to validate any assumptions.
|
|
428
|
+
2. **Plan:** Build a coherent, grounded plan for how you intend to resolve the task. Share an extremely concise yet clear plan with the user if it would help. Use a self-verification loop by writing unit tests where relevant; use output logs or debug statements as part of this loop to arrive at a solution.
|
|
429
|
+
3. **Implement:** Act on the plan using available tools, strictly adhering to the project's established conventions.
|
|
430
|
+
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining README files and build or package configuration; NEVER assume standard test commands.
|
|
431
|
+
5. **Verify (Standards):** After making code changes, execute the project-specific build, lint, and type-checking commands you have identified. If unsure about these commands, ask the user.
|
|
432
|
+
|
|
433
|
+
## New Applications
|
|
434
|
+
Autonomously implement and deliver a visually appealing, substantially complete, functional prototype:
|
|
435
|
+
1. **Understand Requirements:** Analyze the request to identify core features, desired user experience, visual aesthetic, application type and platform, and explicit constraints. If critical information for planning is missing or ambiguous, ask concise, targeted clarification questions.
|
|
436
|
+
2. **Propose Plan:** Formulate an internal development plan and present a clear, concise summary covering the application's type and core purpose, key technologies, main features, interaction model, and the approach to a polished visual design and user experience.
|
|
437
|
+
3. **User Approval:** Obtain user approval for the proposed plan before implementing.
|
|
438
|
+
4. **Implementation:** Autonomously implement each feature and design element per the approved plan. Use placeholder assets only when essential for progress, intending to replace them with more refined versions or instructing the user on replacement during polishing.
|
|
439
|
+
5. **Verify:** Review work against the original request and approved plan. Fix bugs, deviations, and placeholders where feasible. Build the application and ensure there are no compile errors.
|
|
440
|
+
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback.
|
|
441
|
+
|
|
442
|
+
# Operational Guidelines
|
|
443
|
+
|
|
444
|
+
## Tone and Style (CLI Interaction)
|
|
445
|
+
- **Concise & Direct:** Adopt a professional, direct, and concise tone suitable for a CLI environment.
|
|
446
|
+
- **Minimal Output:** Aim for fewer than 3 lines of text output (excluding tool use and code generation) per response whenever practical. Focus strictly on the user's query.
|
|
447
|
+
- **Clarity over Brevity (When Needed):** While conciseness is key, prioritize clarity for essential explanations or when seeking necessary clarification.
|
|
448
|
+
- **No Chitchat:** Avoid conversational filler, preambles, or postambles. Get straight to the action or answer.
|
|
449
|
+
- **Formatting:** Use GitHub-flavored Markdown. Responses will be rendered in monospace.
|
|
450
|
+
- **Tools vs. Text:** Use tools for actions; text output only for communication. Do not add explanatory comments within tool calls or code blocks unless specifically part of the required code.
|
|
451
|
+
- **Handling Inability:** If unable or unwilling to fulfill a request, state so briefly (1-2 sentences) without excessive justification. Offer alternatives if appropriate.
|
|
452
|
+
|
|
453
|
+
## Security and Safety Rules
|
|
454
|
+
- **Explain Critical Commands:** Before executing commands that modify the file system, codebase, or system state, provide a brief explanation of the command's purpose and potential impact. Prioritize user understanding and safety.
|
|
455
|
+
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
|
|
456
|
+
|
|
457
|
+
## Tool Usage
|
|
458
|
+
- **File Paths:** Always use absolute paths when referring to files with file tools. Relative paths are not supported; you must provide an absolute path.
|
|
459
|
+
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible.
|
|
460
|
+
- **Command Execution:** Run shell commands for actual system commands and terminal operations, remembering to explain modifying commands first.
|
|
461
|
+
- **Respect User Confirmations:** Tool calls will first require confirmation from the user, where they will either approve or cancel. If a user cancels a call, respect their choice and do not try to make the call again; it is okay to request it again only if the user asks on a subsequent prompt.
|
|
462
|
+
|
|
463
|
+
# Final Reminder
|
|
464
|
+
Your core function is efficient and safe assistance. Balance extreme conciseness with the crucial need for clarity, especially regarding safety and potential system modifications. Always prioritize user control and project conventions. Never make assumptions about the contents of files; instead read to ensure you are not making broad assumptions. Keep going until the user's query is completely resolved.`;
|
|
465
|
+
const BEAST_TEXT = `Keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
|
|
466
|
+
|
|
467
|
+
Your thinking should be thorough, and it is fine if it is very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
|
|
468
|
+
|
|
469
|
+
You MUST iterate and keep going until the problem is solved. You have everything you need to resolve this problem. Fully solve it autonomously before coming back. Only terminate your turn when you are sure the problem is solved and all items have been checked off. Go through the problem step by step, verify that your changes are correct, and never end your turn without having truly and completely solved the problem. When you say you are going to make a tool call, make sure you ACTUALLY make the tool call instead of ending your turn.
|
|
470
|
+
|
|
471
|
+
Your knowledge may be out of date because your training date is in the past. Use available search means to verify your understanding of third-party packages and dependencies is up to date every time you install or implement one. It is not enough to just search; you must also read the content of the pages you find and gather all relevant information until you have everything you need.
|
|
472
|
+
|
|
473
|
+
Always tell the user what you are going to do before making a tool call with a single concise sentence.
|
|
474
|
+
|
|
475
|
+
Take your time and think through every step. Check your solution rigorously and watch out for boundary cases, especially with the changes you made. Your solution must be perfect; if not, continue working on it. Test your code rigorously using the tools provided, many times, to catch all edge cases. If it is not robust, iterate more. Failing to test your code rigorously is the number one failure mode; make sure you handle all edge cases and run existing tests if they are provided.
|
|
476
|
+
|
|
477
|
+
Plan extensively before each function call, and reflect extensively on the outcomes of the previous calls. Do not do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully. Keep working until the problem is completely solved and all items in the todo list are checked off; do not end your turn until you have completed all steps and verified that everything is working correctly.
|
|
478
|
+
|
|
479
|
+
# Workflow
|
|
480
|
+
1. Fetch any URLs provided by the user using the search tool.
|
|
481
|
+
2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Break down the problem into manageable parts, considering the expected behavior, edge cases, potential pitfalls, the larger context of the codebase, and the dependencies and interactions with other parts of the code.
|
|
482
|
+
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
|
|
483
|
+
4. Research the problem using available search means by reading relevant articles, documentation, and forums.
|
|
484
|
+
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps and display the plan as a todo list, checking off each item as you go.
|
|
485
|
+
6. Implement the fix incrementally. Make small, testable code changes.
|
|
486
|
+
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
|
|
487
|
+
8. Test frequently. Run tests after each change to verify correctness.
|
|
488
|
+
9. Iterate until the root cause is fixed and all tests pass.
|
|
489
|
+
10. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
|
|
490
|
+
|
|
491
|
+
## Debugging
|
|
492
|
+
- Make code changes only if you have high confidence they can solve the problem.
|
|
493
|
+
- When debugging, determine the root cause rather than addressing symptoms. Use print statements, logs, or temporary code to inspect program state and test your hypotheses. Revisit your assumptions if unexpected behavior occurs.
|
|
494
|
+
- Always read enough context to understand the code before editing; avoid re-reading files whose content has not changed.
|
|
495
|
+
|
|
496
|
+
# Communication
|
|
497
|
+
Communicate clearly and concisely in a casual, friendly yet professional tone. Avoid unnecessary explanations, repetition, and filler. Always write code directly to the correct files, and do not display code to the user unless they specifically ask for it.
|
|
498
|
+
|
|
499
|
+
# Memory
|
|
500
|
+
Remember user preferences expressed across the session and apply them consistently.
|
|
501
|
+
|
|
502
|
+
# Git
|
|
503
|
+
Never stage and commit files automatically unless the user explicitly tells you to.`;
|
|
504
|
+
const CODEX_TEXT = `## Editing constraints
|
|
505
|
+
- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
|
|
506
|
+
- Only add comments if they are necessary to make a non-obvious block easier to understand.
|
|
507
|
+
|
|
508
|
+
## Tool usage
|
|
509
|
+
- Prefer dedicated tools over shell for file operations: use an editing tool to modify files, a reading tool to view files, and search tools to find files by name or search file contents.
|
|
510
|
+
- Use shell for terminal operations: git, builds, tests, and running scripts.
|
|
511
|
+
- Run tool calls in parallel when neither call needs the other's output; otherwise run them sequentially.
|
|
512
|
+
|
|
513
|
+
## Git and workspace hygiene
|
|
514
|
+
- You may be in a dirty git worktree.
|
|
515
|
+
* NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.
|
|
516
|
+
* If asked to make a commit or code edits and there are unrelated changes to your work or changes you did not make in those files, do not revert those changes.
|
|
517
|
+
* If the changes are in files you have touched recently, read them carefully and understand how you can work with the changes rather than reverting them.
|
|
518
|
+
* If the changes are in unrelated files, just ignore them and do not revert them.
|
|
519
|
+
- Do not amend commits unless explicitly requested.
|
|
520
|
+
- **NEVER** use destructive commands like \`git reset --hard\` or \`git checkout --\` unless specifically requested or approved by the user.
|
|
521
|
+
|
|
522
|
+
## Frontend tasks
|
|
523
|
+
When doing frontend design tasks, avoid collapsing into bland, generic layouts. Aim for interfaces that feel intentional and deliberate.
|
|
524
|
+
- Typography: Use expressive, purposeful fonts and avoid default stacks.
|
|
525
|
+
- Color & Look: Choose a clear visual direction; define CSS variables; avoid generic defaults.
|
|
526
|
+
- Motion: Use a few meaningful animations (page-load, staggered reveals) instead of generic micro-motions.
|
|
527
|
+
- Background: Don't rely on flat, single-color backgrounds; use gradients, shapes, or subtle patterns to build atmosphere.
|
|
528
|
+
- Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs.
|
|
529
|
+
- Ensure the page loads properly on both desktop and mobile.
|
|
530
|
+
Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language.
|
|
531
|
+
|
|
532
|
+
## Presenting your work and final message
|
|
533
|
+
- Default: be very concise; friendly coding teammate tone.
|
|
534
|
+
- Default: do the work without asking questions. Treat short tasks as sufficient direction; infer missing details by reading the codebase and following existing conventions.
|
|
535
|
+
- Questions: only ask when you are truly blocked after checking relevant context AND you cannot safely pick a reasonable default. This usually means one of: the request is ambiguous in a way that materially changes the result and you cannot disambiguate by reading the repo; the action is destructive or irreversible, touches production, or changes billing or security posture; you need a secret or credential or value that cannot be inferred.
|
|
536
|
+
- If you must ask: do all non-blocked work first, then ask exactly one targeted question, include your recommended default, and state what would change based on the answer.
|
|
537
|
+
- Never ask permission questions like "Should I proceed?"; proceed with the most reasonable option and mention what you did.
|
|
538
|
+
- For substantial work, summarize clearly and follow the final-answer formatting rules.
|
|
539
|
+
- Skip heavy formatting for simple confirmations.
|
|
540
|
+
- Don't dump large files you have written; reference paths only.
|
|
541
|
+
- Offer logical next steps (tests, commits, build) briefly; add verify steps if you could not do something.
|
|
542
|
+
- For code changes: lead with a quick explanation of the change, then give more details on the context covering where and why the change was made. If there are natural next steps, suggest them at the end of your response.
|
|
543
|
+
|
|
544
|
+
## Final answer structure and style guidelines
|
|
545
|
+
- Plain text; the CLI handles styling. Use structure only when it helps scannability.
|
|
546
|
+
- Headers: optional; short Title Case (1-3 words); add only if they truly help.
|
|
547
|
+
- Bullets: use -; merge related points; keep to one line when possible; keep phrasing consistent.
|
|
548
|
+
- Monospace: use inline code for commands, paths, env vars, code ids, and inline examples; never combine with bold.
|
|
549
|
+
- Code samples or multi-line snippets should be wrapped in fenced code blocks; include a language tag as often as possible.
|
|
550
|
+
- Structure: group related bullets; order sections general to specific.
|
|
551
|
+
- Tone: collaborative, concise, factual; present tense, active voice; self-contained.
|
|
552
|
+
- Don'ts: no nested bullets or hierarchies; no ANSI codes; don't cram unrelated keywords; avoid naming formatting styles in answers.
|
|
553
|
+
- File references: reference files with inline code so paths are clickable; each reference should have a standalone path; optionally include line/column (1-based) using the pattern \`file_path:line_number\`.`;
|
|
554
|
+
const GPT_TEXT = `Be a deeply pragmatic, effective software engineer. Take engineering quality seriously; collaboration comes through as direct, factual statements. Communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail. Build context by examining the codebase first without making assumptions or jumping to conclusions. Think through the nuances of the code you encounter and embody the mentality of a skilled senior software engineer.
|
|
555
|
+
|
|
556
|
+
## Editing Approach
|
|
557
|
+
|
|
558
|
+
- The best changes are often the smallest correct changes.
|
|
559
|
+
- When you are weighing two correct approaches, prefer the more minimal one (less new names, helpers, tests, and so on).
|
|
560
|
+
- Keep things in one function unless composable or reusable.
|
|
561
|
+
- Do not add backward-compatibility code unless there is a concrete need, such as persisted data, shipped behavior, external consumers, or an explicit user requirement; if unclear, ask one short question instead of guessing.
|
|
562
|
+
|
|
563
|
+
## Autonomy and persistence
|
|
564
|
+
|
|
565
|
+
Unless the user explicitly asks for a plan, asks a question about the code, is brainstorming potential solutions, or some other intent makes it clear that code should not be written, assume the user wants you to make code changes or run tools to solve the user's problem. In these cases it is bad to output your proposed solution in a message; you should go ahead and actually implement the change. If you encounter challenges or blockers, attempt to resolve them yourself.
|
|
566
|
+
|
|
567
|
+
Persist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you.
|
|
568
|
+
|
|
569
|
+
If you notice unexpected changes in the worktree or staging area that you did not make, continue with your task. NEVER revert, undo, or modify changes you did not make unless the user explicitly asks you to. There can be multiple agents or the user working in the same codebase concurrently.
|
|
570
|
+
|
|
571
|
+
## Editing constraints
|
|
572
|
+
|
|
573
|
+
- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
|
|
574
|
+
- Add succinct code comments that explain what is going on if code is not self-explanatory. Do not add comments like "assigns the value to the variable"; a brief comment might be useful ahead of a complex code block. Usage of these comments should be rare.
|
|
575
|
+
- Use dedicated tools rather than scripts for reading and writing files when a simple dedicated tool call would suffice.
|
|
576
|
+
- You may be in a dirty git worktree.
|
|
577
|
+
* NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.
|
|
578
|
+
* If asked to make a commit or code edits and there are unrelated changes to your work or changes you did not make in those files, do not revert those changes.
|
|
579
|
+
* If the changes are in files you have touched recently, read them carefully and understand how you can work with them rather than reverting them.
|
|
580
|
+
* If the changes are in unrelated files, just ignore them and do not revert them.
|
|
581
|
+
- Do not amend a commit unless explicitly requested.
|
|
582
|
+
- **NEVER** use destructive commands like \`git reset --hard\` or \`git checkout --\` unless specifically requested or approved by the user.
|
|
583
|
+
|
|
584
|
+
## Special user requests
|
|
585
|
+
|
|
586
|
+
If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command, do so.
|
|
587
|
+
|
|
588
|
+
If the user pastes an error description or a bug report, help them diagnose the root cause. You can try to reproduce it if it seems feasible with the available tools.
|
|
589
|
+
|
|
590
|
+
If the user asks for a "review", default to a code review mindset: prioritize identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response; keep summaries or overviews brief and only after enumerating the issues. Present findings first, ordered by severity with file/line references, followed by open questions or assumptions, and offer a change summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps.
|
|
591
|
+
|
|
592
|
+
## Frontend tasks
|
|
593
|
+
|
|
594
|
+
When doing frontend design tasks, avoid collapsing into "AI slop" or safe, average-looking layouts.
|
|
595
|
+
- Ensure the page loads properly on both desktop and mobile.
|
|
596
|
+
- Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs.
|
|
597
|
+
Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language.
|
|
598
|
+
|
|
599
|
+
# Working with the user
|
|
600
|
+
|
|
601
|
+
## General
|
|
602
|
+
|
|
603
|
+
Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements or framing phrases.
|
|
604
|
+
|
|
605
|
+
Balance conciseness to not overwhelm the user with appropriate detail for the request. Do not narrate abstractly; explain what you are doing and why.
|
|
606
|
+
|
|
607
|
+
## Formatting rules
|
|
608
|
+
|
|
609
|
+
Your responses are rendered as GitHub-flavored Markdown.
|
|
610
|
+
|
|
611
|
+
Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections. For numbered lists, only use the \`1. 2. 3.\` style markers (with a period), never \`1)\`.
|
|
612
|
+
|
|
613
|
+
Headers are optional, only use them when you think they are necessary. If you do use them, use short Title Case (1-3 words). Don't add a blank line.
|
|
614
|
+
|
|
615
|
+
Use inline code blocks for commands, paths, environment variables, function names, inline examples, and keywords.
|
|
616
|
+
|
|
617
|
+
Code samples or multi-line snippets should be wrapped in fenced code blocks. Include a language tag when possible.
|
|
618
|
+
|
|
619
|
+
Don't use emojis or em dashes unless explicitly instructed.
|
|
620
|
+
|
|
621
|
+
## Progress and final responses
|
|
622
|
+
|
|
623
|
+
Keep intermediate progress updates short and send them only when they add meaningful new information: a discovery, a tradeoff, a blocker, a substantial plan, or the start of a non-trivial edit or verification step. Do not narrate routine reads, searches, obvious next steps, or minor confirmations. Do not begin responses with conversational interjections or meta commentary. Before substantial work, send a short update describing your first step. Before editing files, send an update describing the edit.
|
|
624
|
+
|
|
625
|
+
Match the final answer to the complexity of the task. Structure it if necessary, ordering sections from general to specific to supporting. If the task is simple, answer with a one-liner. If the user asks for a code explanation, include code references. For simple tasks, just state the outcome without heavy formatting. For large or complex changes, lead with the solution, then explain what you did and why. For casual chat, just chat. If something could not be done (tests, builds, and so on), say so. Suggest next steps only when they are natural and useful; if you list options, use numbered items.`;
|
|
626
|
+
const KIMI_TEXT = `Your primary goal is to help users with software engineering tasks by taking action: use the tools available to you to make real changes on the user's system. You should also answer questions when asked.
|
|
627
|
+
|
|
628
|
+
# Prompt and Tool Use
|
|
629
|
+
|
|
630
|
+
The user's messages may contain questions and/or task descriptions. Read them, understand them, and do what the user requested. For simple questions or greetings that do not involve any information in the working directory or on the internet, reply directly. For anything else, default to taking action with tools. When the request could be interpreted as either a question to answer or a task to complete, treat it as a task.
|
|
631
|
+
|
|
632
|
+
When handling the user's request, if it involves creating, modifying, or running code or files, you MUST use the appropriate tools to make actual changes; do not just describe the solution in text. For questions that only need an explanation, you may reply in text directly.
|
|
633
|
+
|
|
634
|
+
If a subagent tool is available, you can use it to delegate a focused subtask to a subagent instance. When delegating, provide a complete prompt with all necessary context because a newly created subagent does not automatically see your context.
|
|
635
|
+
|
|
636
|
+
You can output any number of tool calls in a single response. If you anticipate making multiple non-interfering tool calls, make them in parallel to significantly improve efficiency. The results of the tool calls will be returned to you; determine your next action based on the results.
|
|
637
|
+
|
|
638
|
+
Tool results and user messages may include <system-reminder> tags. These are authoritative system directives that you MUST follow. They may override or constrain your normal behavior, and they bear no direct relation to the specific tool results or user messages in which they appear.
|
|
639
|
+
|
|
640
|
+
When responding to the user, you MUST use the same language as the user, unless explicitly instructed to do otherwise.
|
|
641
|
+
|
|
642
|
+
# General Guidelines for Coding
|
|
643
|
+
|
|
644
|
+
When building something from scratch, you should:
|
|
645
|
+
- Understand the user's requirements.
|
|
646
|
+
- Ask the user for clarification if there is anything unclear.
|
|
647
|
+
- Design the architecture and make a plan for the implementation.
|
|
648
|
+
- Write the code in a modular and maintainable way.
|
|
649
|
+
|
|
650
|
+
Always use tools to implement your code changes: use editing tools to create or modify source files. Code that only appears in your text response is NOT saved to the file system and will not take effect. Use shell to run and test your code after writing it. Iterate: if tests fail, read the error, fix the code, and re-test.
|
|
651
|
+
|
|
652
|
+
When working on an existing codebase, you should:
|
|
653
|
+
- Understand the codebase by reading it with tools before making changes. Identify the ultimate goal and the most important criteria to achieve it.
|
|
654
|
+
- For a bug fix, check error logs or failed tests, scan the codebase to find the root cause, and figure out a fix. If the user mentioned failed tests, make sure they pass after the changes.
|
|
655
|
+
- For a feature, design the architecture and write the code with minimal intrusion into existing code. Add new tests if the project already has tests.
|
|
656
|
+
- For a refactor, update all call sites if the interface changes. DO NOT change existing logic, especially in tests; focus only on fixing errors caused by the interface changes.
|
|
657
|
+
- Make MINIMAL changes to achieve the goal. This is very important to your performance.
|
|
658
|
+
- Follow the coding style of existing code in the project.
|
|
659
|
+
|
|
660
|
+
DO NOT run git commit, git push, git reset, git rebase, or any other git mutations unless explicitly asked to do so. Ask for confirmation each time you need to do git mutations, even if the user has confirmed in earlier conversations.
|
|
661
|
+
|
|
662
|
+
# General Guidelines for Research and Data Processing
|
|
663
|
+
|
|
664
|
+
Search on the Internet if possible, with carefully-designed search queries to improve efficiency and accuracy. Use proper tools or shell commands to process or generate files, ensuring that third-party packages are installed in a virtual or isolated environment if needed. Once you generate or edit any files, read them again before proceeding to ensure the content is as expected. Avoid installing or deleting anything outside of the current working directory; if you have to, ask the user for confirmation.
|
|
665
|
+
|
|
666
|
+
# Working Environment
|
|
667
|
+
|
|
668
|
+
The operating environment is not in a sandbox. Any actions you do will immediately affect the user's system, so you MUST be extremely cautious. Unless explicitly instructed to do so, never access (read, write, or execute) files outside of the working directory. Use absolute paths for file operations.
|
|
669
|
+
|
|
670
|
+
# Project Information
|
|
671
|
+
|
|
672
|
+
Markdown files named AGENTS.md usually contain the background, structure, coding styles, user preferences, and other relevant information about the project. You should read the project's AGENTS.md and README files to understand its conventions and preferences. If they are empty or insufficient, check README files or AGENTS.md files in subdirectories for more information.
|
|
673
|
+
|
|
674
|
+
# Ultimate Reminders
|
|
675
|
+
|
|
676
|
+
At any time, you should be HELPFUL, CONCISE, and ACCURATE. Be thorough in your actions: test what you build and verify what you change, not in your explanations.
|
|
677
|
+
- Never diverge from the requirements and goals of the task you work on. Stay on track.
|
|
678
|
+
- Never give the user more than what they want.
|
|
679
|
+
- Try your best to avoid hallucination. Do fact checking before providing any factual information.
|
|
680
|
+
- Think about the best approach, then take action decisively.
|
|
681
|
+
- Do not give up too early.
|
|
682
|
+
- ALWAYS keep it stupidly simple. Do not overcomplicate things.
|
|
683
|
+
- When the task requires creating or modifying files, always use tools to do so. Never treat displaying code in your response as a substitute for actually writing it to the file system.`;
|
|
684
|
+
/** DeepSeek 官方建议蒸馏(仅追加,不覆盖 base)。 */
|
|
685
|
+
const DEEPSEEK_APPEND = `Notes for DeepSeek models:
|
|
686
|
+
- Tool calls follow the OpenAI function-calling style; pass arguments strictly according to each tool's JSON schema.
|
|
687
|
+
- Reasoning models (e.g. deepseek-reasoner) emit their reasoning before the final answer; do not ask them to skip the thinking process.`;
|
|
688
|
+
/** 智谱官方建议蒸馏(仅追加,不覆盖 base)。 */
|
|
689
|
+
const GLM_APPEND = `Notes for GLM models:
|
|
690
|
+
- Tool calls follow the OpenAI function-calling style; pass arguments strictly according to each tool's JSON schema.
|
|
691
|
+
- For thinking-enabled GLM models, the reasoning_content of each message must be passed back verbatim on the next request; never drop or rewrite it.
|
|
692
|
+
- With interleaved thinking, keep the reasoning context across tool-call rounds.`;
|
|
693
|
+
/** 默认语义层(固定层栈的唯一可编辑层):persona。
|
|
694
|
+
* 结构固定——UI 与服务端均不允许增删层、改名、改序,仅文本可编辑。
|
|
695
|
+
* persona 注册为普通 prompt-stack:persona(order 10),排在内置模型层
|
|
696
|
+
* prompt-stack:base(order 0)之后;默认空串:dsh「空段不渲染」,未填写时行为零变化。
|
|
697
|
+
* persona 是唯一可编辑存储层;base 为内置模型层(保留层名,不进存储),见 index.ts。 */
|
|
698
|
+
const DEFAULT_LAYERS = [{
|
|
699
|
+
name: "persona",
|
|
700
|
+
order: 10,
|
|
701
|
+
text: ""
|
|
702
|
+
}];
|
|
703
|
+
/** 默认模型规则(顺序即同分仲裁序,勿调整)。 */
|
|
704
|
+
const DEFAULT_RULES = [
|
|
705
|
+
{
|
|
706
|
+
match: { modelPattern: "claude*" },
|
|
707
|
+
overrides: { base: ANTHROPIC_TEXT }
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
match: { modelPattern: "gemini-*" },
|
|
711
|
+
overrides: { base: GEMINI_TEXT }
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
match: { modelPattern: "gpt-4*" },
|
|
715
|
+
overrides: { base: BEAST_TEXT }
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
match: { modelPattern: "o1*" },
|
|
719
|
+
overrides: { base: BEAST_TEXT }
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
match: { modelPattern: "o3*" },
|
|
723
|
+
overrides: { base: BEAST_TEXT }
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
match: { modelPattern: "gpt*codex*" },
|
|
727
|
+
overrides: { base: CODEX_TEXT }
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
match: { modelPattern: "gpt*" },
|
|
731
|
+
overrides: { base: GPT_TEXT }
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
match: { modelPattern: "kimi*" },
|
|
735
|
+
overrides: { base: KIMI_TEXT }
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
match: { modelPattern: "k2*" },
|
|
739
|
+
overrides: { base: KIMI_TEXT }
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
match: { modelPattern: "k3*" },
|
|
743
|
+
overrides: { base: KIMI_TEXT }
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
match: { provider: "moonshotai" },
|
|
747
|
+
overrides: { base: KIMI_TEXT }
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
match: { provider: "moonshotai-cn" },
|
|
751
|
+
overrides: { base: KIMI_TEXT }
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
match: { provider: "kimi-for-coding" },
|
|
755
|
+
overrides: { base: KIMI_TEXT }
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
match: { modelPattern: "deepseek*" },
|
|
759
|
+
append: DEEPSEEK_APPEND
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
match: { modelPattern: "glm-*" },
|
|
763
|
+
append: GLM_APPEND
|
|
764
|
+
}
|
|
765
|
+
];
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/prompt/match.ts
|
|
768
|
+
/** 正则元字符(`*` 与 `?` 除外——它们是 glob 通配符,之后单独展开)。 */
|
|
769
|
+
const REGEXP_META = /[.+^${}()|[\]\\]/g;
|
|
770
|
+
/**
|
|
771
|
+
* 把 glob(`*` = 任意串,`?` = 单字符)编译为锚定全串的正则。
|
|
772
|
+
* @param pattern - 非空 glob;空或全空白抛错(激活期响亮报错的调用点在 apply)。
|
|
773
|
+
* @returns 锚定全串的正则。
|
|
774
|
+
*/
|
|
775
|
+
function globToRegExp(pattern) {
|
|
776
|
+
if (pattern.trim() === "") throw new Error("prompt-stack: modelPattern must be a non-empty glob");
|
|
777
|
+
const source = pattern.replace(REGEXP_META, "\\$&").replaceAll("*", ".*").replaceAll("?", ".");
|
|
778
|
+
return new RegExp(`^${source}$`);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* 给一条规则的 match 打分:任一指定字段不匹配则整条不命中(返回 0);
|
|
782
|
+
* 命中分值为 model=4 / modelPattern=2 / provider=1 的累加。
|
|
783
|
+
* @param match - 规则的匹配条件。
|
|
784
|
+
* @param provider - 当前 agent 的 provider(创建期配置),可缺失。
|
|
785
|
+
* @param model - 当前 agent 的模型 id,可缺失。
|
|
786
|
+
* @returns 命中分值,0 表示不命中。
|
|
787
|
+
*/
|
|
788
|
+
function scoreRule(match, provider, model) {
|
|
789
|
+
let score = 0;
|
|
790
|
+
if (match.provider !== void 0) {
|
|
791
|
+
if (match.provider !== provider) return 0;
|
|
792
|
+
score += 1;
|
|
793
|
+
}
|
|
794
|
+
if (match.model !== void 0) {
|
|
795
|
+
if (match.model !== model) return 0;
|
|
796
|
+
score += 4;
|
|
797
|
+
}
|
|
798
|
+
if (match.modelPattern !== void 0) {
|
|
799
|
+
if (model === void 0 || !globToRegExp(match.modelPattern).test(model)) return 0;
|
|
800
|
+
score += 2;
|
|
801
|
+
}
|
|
802
|
+
return score;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* 选出唯一命中规则:最高分者,同分取配置序靠前者。
|
|
806
|
+
* @param rules - 配置中的规则数组(顺序即优先级仲裁序)。
|
|
807
|
+
* @param provider - 当前 agent 的 provider,可缺失。
|
|
808
|
+
* @param model - 当前 agent 的模型 id,可缺失。
|
|
809
|
+
* @returns 命中规则;无命中返回 undefined。
|
|
810
|
+
*/
|
|
811
|
+
function selectRule(rules, provider, model) {
|
|
812
|
+
let best;
|
|
813
|
+
let bestScore = 0;
|
|
814
|
+
for (const rule of rules) {
|
|
815
|
+
const score = scoreRule(rule.match, provider, model);
|
|
816
|
+
if (score > bestScore) {
|
|
817
|
+
best = rule;
|
|
818
|
+
bestScore = score;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return best;
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/prompt/index.ts
|
|
825
|
+
/** 固定追加层的层名(保留,用户层不得使用)。 */
|
|
826
|
+
const MODEL_NOTES_LAYER = "model-notes";
|
|
827
|
+
/** 内置模型层名(保留):固定注册 prompt-stack:base 段、不进存储,仍是 rules overrides 的合法目标。 */
|
|
828
|
+
const BASE_LAYER = "base";
|
|
829
|
+
const BASE_SECTION = `prompt-stack:${BASE_LAYER}`;
|
|
830
|
+
/** 原生身份段名与原生文本(与 dsh system-prompt 宿主一致;覆盖判定的基准值)。 */
|
|
831
|
+
const IDENTITY_SECTION = "harness:identity";
|
|
832
|
+
const NATIVE_IDENTITY_TEXT = "You are an AI agent powered by DeepSeek Harness.";
|
|
833
|
+
/** 层列表语义校验:空、层名重复、保留层名(base/model-notes)。 */
|
|
834
|
+
function validateLayers(layers) {
|
|
835
|
+
if (layers.length === 0) throw new Error("prompt-stack: config.layers must define at least one layer");
|
|
836
|
+
const names = /* @__PURE__ */ new Set();
|
|
837
|
+
for (const layer of layers) {
|
|
838
|
+
if (layer.name === "model-notes" || layer.name === "base") throw new Error(`prompt-stack: layer name "${layer.name}" is reserved`);
|
|
839
|
+
if (names.has(layer.name)) throw new Error(`prompt-stack: duplicate layer name "${layer.name}"`);
|
|
840
|
+
names.add(layer.name);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* 激活期校验(dsh「误配置响亮失败」惯例):空 layers、层名重复、保留层名、
|
|
845
|
+
* overrides 引用未知层(内置 base 与 model-notes 之外的存储层名)、空 match、非法 glob 全部抛错。
|
|
846
|
+
*/
|
|
847
|
+
function validateConfig(config) {
|
|
848
|
+
validateLayers(config.layers);
|
|
849
|
+
const names = /* @__PURE__ */ new Set([BASE_LAYER, ...config.layers.map((layer) => layer.name)]);
|
|
850
|
+
for (const [index, rule] of config.rules.entries()) {
|
|
851
|
+
const { provider, model, modelPattern } = rule.match;
|
|
852
|
+
if (provider === void 0 && model === void 0 && modelPattern === void 0) throw new Error(`prompt-stack: rules[${index}].match must set at least one of provider, model, modelPattern`);
|
|
853
|
+
if (modelPattern !== void 0) globToRegExp(modelPattern);
|
|
854
|
+
for (const key of Object.keys(rule.overrides ?? {})) if (!names.has(key)) throw new Error(`prompt-stack: rules[${index}].overrides references unknown layer "${key}"`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* 固定注册 prompt-stack:base(模型层,order 0)+ 每个存储层一个段 + model-notes。
|
|
859
|
+
* 文本在每次组装时按当前 agent 的 provider/model 选唯一命中规则(最高分、同分取配置序靠前),
|
|
860
|
+
* 模型层用 overrides.base 整体替换内置 BASE_TEXT,存储层用 overrides 替换该层文本。
|
|
861
|
+
* 裸组装(无 agent)静默用默认文本。deployment:persona 槽位还原生,本模块不触碰。
|
|
862
|
+
*
|
|
863
|
+
* 运行时选模型与首条消息钉住语义不变(见 waterfall 注释)。
|
|
864
|
+
*/
|
|
865
|
+
function setupPrompt(ctx, config) {
|
|
866
|
+
const { source, rules } = config;
|
|
867
|
+
const hitRule = (context) => selectRule(rules, context.agent?.options?.provider, context.agent?.options?.model);
|
|
868
|
+
const isSubagent = (context) => context.agent?.session?.header?.origin === "subagent";
|
|
869
|
+
let disposers = [];
|
|
870
|
+
const registerSections = () => {
|
|
871
|
+
for (const dispose of disposers) dispose();
|
|
872
|
+
disposers = [];
|
|
873
|
+
const layers = source.get();
|
|
874
|
+
validateLayers(layers);
|
|
875
|
+
const notesOrder = Math.max(0, ...layers.map((layer) => layer.order)) + 1;
|
|
876
|
+
disposers.push(ctx.systemPrompt.section({
|
|
877
|
+
name: BASE_SECTION,
|
|
878
|
+
order: 0,
|
|
879
|
+
text: (context) => isSubagent(context) ? "" : hitRule(context)?.overrides?.["base"] ?? BASE_TEXT
|
|
880
|
+
}));
|
|
881
|
+
for (const layer of layers) disposers.push(ctx.systemPrompt.section({
|
|
882
|
+
name: `prompt-stack:${layer.name}`,
|
|
883
|
+
order: layer.order,
|
|
884
|
+
text: (context) => isSubagent(context) ? "" : hitRule(context)?.overrides?.[layer.name] ?? layer.text
|
|
885
|
+
}));
|
|
886
|
+
disposers.push(ctx.systemPrompt.section({
|
|
887
|
+
name: `prompt-stack:${MODEL_NOTES_LAYER}`,
|
|
888
|
+
order: notesOrder,
|
|
889
|
+
text: (context) => hitRule(context)?.append ?? ""
|
|
890
|
+
}));
|
|
891
|
+
};
|
|
892
|
+
registerSections();
|
|
893
|
+
ctx.effect(() => source.subscribe(registerSections));
|
|
894
|
+
const notesSection = `prompt-stack:${MODEL_NOTES_LAYER}`;
|
|
895
|
+
const pinned = /* @__PURE__ */ new WeakMap();
|
|
896
|
+
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
897
|
+
const assembled = await next();
|
|
898
|
+
let provider = assembled.variables.provider ?? context.agent?.options?.provider;
|
|
899
|
+
let model = assembled.variables.model ?? context.agent?.options?.model;
|
|
900
|
+
const agent = context.agent;
|
|
901
|
+
if (agent !== void 0) {
|
|
902
|
+
const cached = pinned.get(agent);
|
|
903
|
+
if (cached !== void 0 && cached.sessionId === agent.session.id) {
|
|
904
|
+
provider = cached.provider;
|
|
905
|
+
model = cached.model;
|
|
906
|
+
} else if (provider !== void 0 || model !== void 0) pinned.set(agent, {
|
|
907
|
+
sessionId: agent.session.id,
|
|
908
|
+
provider,
|
|
909
|
+
model
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
const rule = selectRule(rules, provider, model);
|
|
913
|
+
const layers = source.get();
|
|
914
|
+
const ownSection = (section) => {
|
|
915
|
+
if (section.name === BASE_SECTION) return isSubagent(context) ? "" : hitRule(context)?.overrides?.["base"] ?? BASE_TEXT;
|
|
916
|
+
const layer = layers.find((l) => section.name === `prompt-stack:${l.name}`);
|
|
917
|
+
if (layer === void 0) return void 0;
|
|
918
|
+
return isSubagent(context) ? "" : hitRule(context)?.overrides?.[layer.name] ?? layer.text;
|
|
919
|
+
};
|
|
920
|
+
const sections = assembled.sections.map((section) => {
|
|
921
|
+
if (section.name === IDENTITY_SECTION) {
|
|
922
|
+
const override = source.getIdentity();
|
|
923
|
+
if (override === "" || isSubagent(context) || section.text !== NATIVE_IDENTITY_TEXT) return section;
|
|
924
|
+
return {
|
|
925
|
+
...section,
|
|
926
|
+
text: override
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (section.name === notesSection) return {
|
|
930
|
+
...section,
|
|
931
|
+
text: rule?.append ?? ""
|
|
932
|
+
};
|
|
933
|
+
const own = ownSection(section);
|
|
934
|
+
if (own === void 0 || section.text !== own) return section;
|
|
935
|
+
if (section.name === BASE_SECTION) return {
|
|
936
|
+
...section,
|
|
937
|
+
text: isSubagent(context) ? "" : rule?.overrides?.["base"] ?? BASE_TEXT
|
|
938
|
+
};
|
|
939
|
+
const layer = layers.find((l) => section.name === `prompt-stack:${l.name}`);
|
|
940
|
+
return {
|
|
941
|
+
...section,
|
|
942
|
+
text: isSubagent(context) ? "" : rule?.overrides?.[layer.name] ?? layer.text
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
return {
|
|
946
|
+
...assembled,
|
|
947
|
+
sections
|
|
948
|
+
};
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region src/prompt/layer-source.ts
|
|
953
|
+
/** prompt_layers 表单行的 key 常量。 */
|
|
954
|
+
const PROMPT_LAYERS_KEY = "layers";
|
|
955
|
+
/** meta 表首启种子一次性标记键。 */
|
|
956
|
+
const PROMPT_LAYERS_SEEDED_KEY = "prompt_layers_seeded";
|
|
957
|
+
/** 组装存储行:identity 仅非空时落字段(空串 = 还原原生,不留残字段)。 */
|
|
958
|
+
function row(layers, identity) {
|
|
959
|
+
return identity === "" ? { layers } : {
|
|
960
|
+
layers,
|
|
961
|
+
identity
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/** 固定结构校验:name+order 多重集合必须等于种子(仅 text 可变)。 */
|
|
965
|
+
function validateFixedLayers(layers, seedLayers) {
|
|
966
|
+
const key = (layer) => `${layer.name}@${String(layer.order)}`;
|
|
967
|
+
const seed = seedLayers.map(key).sort();
|
|
968
|
+
const actual = layers.map(key).sort();
|
|
969
|
+
if (actual.length !== seed.length || actual.some((k, i) => k !== seed[i])) throw new Error(`prompt-stack: layer structure is fixed (${seedLayers.map((l) => l.name).join(", ")}); only layer text is editable`);
|
|
970
|
+
}
|
|
971
|
+
/** 按种子结构 reconcile 已存储层:同名层保留已存文本(order 以种子为准)、补缺失、丢多余。 */
|
|
972
|
+
function reconcileLayers(stored, seedLayers) {
|
|
973
|
+
return seedLayers.map((seed) => ({
|
|
974
|
+
...seed,
|
|
975
|
+
text: stored.find((layer) => layer.name === seed.name)?.text ?? seed.text
|
|
976
|
+
}));
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* 打开层源。域由 apply 统一 open(storage-domain 同名单开),本函数只消费表句柄。
|
|
980
|
+
* 首启(表无数据)种入 Config 种子并置标记;此后读存储并按种子结构 reconcile
|
|
981
|
+
* (层栈固定化迁移:保留已编辑文本、补新层、丢多余层)。
|
|
982
|
+
*/
|
|
983
|
+
async function openLayerSource(tables, seedLayers) {
|
|
984
|
+
const { promptLayers, meta } = tables;
|
|
985
|
+
validateLayers(seedLayers);
|
|
986
|
+
let cache;
|
|
987
|
+
let identity;
|
|
988
|
+
const existing = promptLayers.get(PROMPT_LAYERS_KEY);
|
|
989
|
+
if (existing !== void 0) {
|
|
990
|
+
cache = reconcileLayers(existing.layers, seedLayers);
|
|
991
|
+
validateLayers(cache);
|
|
992
|
+
identity = existing.identity ?? "";
|
|
993
|
+
await promptLayers.put(PROMPT_LAYERS_KEY, row(cache, identity));
|
|
994
|
+
} else {
|
|
995
|
+
cache = seedLayers;
|
|
996
|
+
identity = "";
|
|
997
|
+
await promptLayers.put(PROMPT_LAYERS_KEY, { layers: cache });
|
|
998
|
+
}
|
|
999
|
+
if (meta.get("prompt_layers_seeded") === void 0) await meta.put(PROMPT_LAYERS_SEEDED_KEY, { value: "1" });
|
|
1000
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1001
|
+
const notify = () => {
|
|
1002
|
+
for (const listener of [...listeners]) listener();
|
|
1003
|
+
};
|
|
1004
|
+
return {
|
|
1005
|
+
get: () => cache,
|
|
1006
|
+
getIdentity: () => identity,
|
|
1007
|
+
async set(layers) {
|
|
1008
|
+
validateLayers(layers);
|
|
1009
|
+
validateFixedLayers(layers, seedLayers);
|
|
1010
|
+
await promptLayers.put(PROMPT_LAYERS_KEY, row(layers, identity));
|
|
1011
|
+
cache = layers;
|
|
1012
|
+
notify();
|
|
1013
|
+
},
|
|
1014
|
+
async setIdentity(text) {
|
|
1015
|
+
await promptLayers.put(PROMPT_LAYERS_KEY, row(cache, text));
|
|
1016
|
+
identity = text;
|
|
1017
|
+
notify();
|
|
1018
|
+
},
|
|
1019
|
+
async reset() {
|
|
1020
|
+
await promptLayers.delete(PROMPT_LAYERS_KEY);
|
|
1021
|
+
await meta.delete(PROMPT_LAYERS_SEEDED_KEY);
|
|
1022
|
+
await promptLayers.put(PROMPT_LAYERS_KEY, { layers: seedLayers });
|
|
1023
|
+
await meta.put(PROMPT_LAYERS_SEEDED_KEY, { value: "1" });
|
|
1024
|
+
cache = seedLayers;
|
|
1025
|
+
identity = "";
|
|
1026
|
+
notify();
|
|
1027
|
+
},
|
|
1028
|
+
subscribe(listener) {
|
|
1029
|
+
listeners.add(listener);
|
|
1030
|
+
return () => {
|
|
1031
|
+
listeners.delete(listener);
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
//#endregion
|
|
1037
|
+
//#region src/shared/webserver.ts
|
|
1038
|
+
function registerOptionalRoutes(ctx, register) {
|
|
1039
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
1040
|
+
webCtx.effect(() => register(webCtx));
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
function json$1(res, code, body) {
|
|
1044
|
+
res.writeHead(code, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
1045
|
+
}
|
|
1046
|
+
/** 读 JSON body;超限 413 / 非法 JSON 400(已写响应时返回 undefined)。 */
|
|
1047
|
+
async function readJsonBody$1(req, res) {
|
|
1048
|
+
const chunks = [];
|
|
1049
|
+
let received = 0;
|
|
1050
|
+
for await (const chunk of req) {
|
|
1051
|
+
const buffer = chunk;
|
|
1052
|
+
received += buffer.byteLength;
|
|
1053
|
+
if (received > 65536) {
|
|
1054
|
+
json$1(res, 413, { error: "body too large" });
|
|
1055
|
+
req.destroy();
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
chunks.push(buffer);
|
|
1059
|
+
}
|
|
1060
|
+
try {
|
|
1061
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1062
|
+
} catch {
|
|
1063
|
+
json$1(res, 400, { error: "invalid JSON body" });
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region src/prompt/api.ts
|
|
1069
|
+
const LayersBodySchema = z$1.object({
|
|
1070
|
+
layers: z$1.array(LayerConfigSchema),
|
|
1071
|
+
identityOverride: z$1.string().optional()
|
|
1072
|
+
});
|
|
1073
|
+
function createPromptLayersApiHandler(deps) {
|
|
1074
|
+
return async (req, res) => {
|
|
1075
|
+
const sub = new URL(req.url ?? "/", "http://127.0.0.1").pathname.replace(/^\/dsh-agent-toolkit\/api\/prompt-layers/, "") || "/";
|
|
1076
|
+
const method = req.method ?? "GET";
|
|
1077
|
+
if (sub === "/" && method === "GET") {
|
|
1078
|
+
let native = {
|
|
1079
|
+
sections: [],
|
|
1080
|
+
contexts: []
|
|
1081
|
+
};
|
|
1082
|
+
try {
|
|
1083
|
+
native = await deps.probe();
|
|
1084
|
+
} catch {}
|
|
1085
|
+
json$1(res, 200, {
|
|
1086
|
+
layers: deps.source.get(),
|
|
1087
|
+
rules: deps.rules,
|
|
1088
|
+
seedLayers: deps.seedLayers,
|
|
1089
|
+
native,
|
|
1090
|
+
modelFallbackText: BASE_TEXT,
|
|
1091
|
+
identityOverride: deps.source.getIdentity()
|
|
1092
|
+
});
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (sub === "/" && method === "PUT") {
|
|
1096
|
+
const body = await readJsonBody$1(req, res);
|
|
1097
|
+
if (body === void 0) return;
|
|
1098
|
+
const parsed = LayersBodySchema.safeParse(body);
|
|
1099
|
+
if (!parsed.success) {
|
|
1100
|
+
json$1(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid layers body" });
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
validateLayers(parsed.data.layers);
|
|
1105
|
+
validateFixedLayers(parsed.data.layers, deps.seedLayers);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
json$1(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
await deps.source.set(parsed.data.layers);
|
|
1112
|
+
if (parsed.data.identityOverride !== void 0) await deps.source.setIdentity(parsed.data.identityOverride);
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
json$1(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
json$1(res, 200, { ok: true });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (sub === "/reset" && method === "POST") {
|
|
1121
|
+
try {
|
|
1122
|
+
await deps.source.reset();
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
json$1(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
json$1(res, 200, { ok: true });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (sub === "/reset" || sub === "/") {
|
|
1131
|
+
json$1(res, 405, { error: "method not allowed" });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
json$1(res, 404, { error: "not found" });
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/** 注册 /dsh-agent-toolkit/api/prompt-layers 前缀路由(webServer 可选服务,缺席惰性不注册)。 */
|
|
1138
|
+
function setupPromptLayersApi(ctx, deps) {
|
|
1139
|
+
const handler = createPromptLayersApiHandler(deps);
|
|
1140
|
+
registerOptionalRoutes(ctx, (webCtx) => {
|
|
1141
|
+
const dispose = webCtx.webServer.register({
|
|
1142
|
+
kind: "prefix",
|
|
1143
|
+
path: "/dsh-agent-toolkit/api/prompt-layers",
|
|
1144
|
+
handler
|
|
1145
|
+
});
|
|
1146
|
+
return () => dispose();
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region src/prompt/persona.ts
|
|
1151
|
+
/** 委派子 Agent 的 persona 装配:契约段 + 内置模型层(按子的模型改写)+ 角色 persona。 */
|
|
1152
|
+
const SECTION_A = (roleName) => `你是团队中的一名成员(角色:${roleName}),由主 Agent 委派任务。
|
|
1153
|
+
- 你看不到主对话;任务书包含你完成工作所需的全部上下文。
|
|
1154
|
+
- 你的最终输出会作为结果完整返回给主 Agent:直接给出结论与必要细节。
|
|
1155
|
+
- 你不能再次委派他人;任务需要拆分时,自己按顺序完成。`;
|
|
1156
|
+
const SECTION_B = `能力使用守则:
|
|
1157
|
+
- 你可以使用与主 Agent 相同的工具与 MCP 资源,但只在任务必需时调用。
|
|
1158
|
+
- 动手修改代码前,先阅读项目根目录及涉及目录的 AGENTS.md,并遵循其中约定。
|
|
1159
|
+
- 产生或修改文件后,运行相关检查(测试、类型检查)验证你的改动。`;
|
|
1160
|
+
/**
|
|
1161
|
+
* 子 Agent 提示词 = 契约段 A/B + 模型层文本(命中规则 overrides.base 整份替换内置
|
|
1162
|
+
* BASE_TEXT)+ 角色 persona(非空时,排在模型层之后)+ 命中规则的 append(model-notes)。
|
|
1163
|
+
* 全局 persona 层是主 Agent 的人设,不进入子 Agent(子的角色由 role.persona 顶替)。
|
|
1164
|
+
*/
|
|
1165
|
+
function buildAgentPersona(config, role, model) {
|
|
1166
|
+
const rule = selectRule(config.rules, model?.provider, model?.model);
|
|
1167
|
+
const texts = [rule?.overrides?.base ?? BASE_TEXT];
|
|
1168
|
+
if (role.persona !== void 0 && role.persona.trim().length > 0) texts.push(role.persona);
|
|
1169
|
+
if (rule?.append !== void 0) texts.push(rule.append);
|
|
1170
|
+
return [
|
|
1171
|
+
SECTION_A(role.name),
|
|
1172
|
+
SECTION_B,
|
|
1173
|
+
...texts
|
|
1174
|
+
].join("\n\n");
|
|
1175
|
+
}
|
|
1176
|
+
//#endregion
|
|
1177
|
+
//#region src/delegate/tool.ts
|
|
1178
|
+
/** 非 completed 的 stopReason 意味着成员未干净完成。 */
|
|
1179
|
+
function stopReasonError(result) {
|
|
1180
|
+
switch (result.stopReason) {
|
|
1181
|
+
case "completed": return;
|
|
1182
|
+
case "aborted": return "成员运行被取消";
|
|
1183
|
+
case "error": return "成员运行失败";
|
|
1184
|
+
case "max-tokens": return "成员在结束前触及 token 上限";
|
|
1185
|
+
case "refusal": return "成员拒绝了该任务";
|
|
1186
|
+
default: return `成员运行异常结束(${String(result.stopReason)})`;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/** 报错时附上成员已产出的部分文本,让截断/取消的真实产出仍回到主 Agent。 */
|
|
1190
|
+
function withPartialText(error, output) {
|
|
1191
|
+
const text = output.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1192
|
+
return text.length === 0 ? error : `${error}\n成员中断前的部分产出:\n${text}`;
|
|
1193
|
+
}
|
|
1194
|
+
/** 收集并释放一次前台运行;dispose 失败不掩盖独立的结果失败。 */
|
|
1195
|
+
async function settleForegroundRun(run, roleId) {
|
|
1196
|
+
const childSessionId = String(run.id);
|
|
1197
|
+
const [execution] = await Promise.allSettled([run.result.then((result) => {
|
|
1198
|
+
const error = stopReasonError(result);
|
|
1199
|
+
if (error !== void 0) throw new Error(withPartialText(error, result.output));
|
|
1200
|
+
return {
|
|
1201
|
+
kind: "foreground",
|
|
1202
|
+
role: roleId,
|
|
1203
|
+
runId: String(run.id),
|
|
1204
|
+
childSessionId,
|
|
1205
|
+
output: result.output
|
|
1206
|
+
};
|
|
1207
|
+
})]);
|
|
1208
|
+
const [disposal] = await Promise.allSettled([Promise.resolve().then(() => run.dispose())]);
|
|
1209
|
+
if (execution.status === "rejected") {
|
|
1210
|
+
if (disposal.status === "rejected") throw new AggregateError([execution.reason, disposal.reason], `成员运行失败:${String(execution.reason)};dispose 失败:${String(disposal.reason)}`);
|
|
1211
|
+
throw execution.reason;
|
|
1212
|
+
}
|
|
1213
|
+
if (disposal.status === "rejected") throw disposal.reason;
|
|
1214
|
+
return execution.value;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* 创建 team_delegate 工具。
|
|
1218
|
+
* @param toolName - 模型可见工具名(Config.toolName,默认 team_delegate)。
|
|
1219
|
+
* @param deps - 名册、provider、persona 装配与委派入口。
|
|
1220
|
+
* @returns defineTool 产物,交给 ctx.tools.register。
|
|
1221
|
+
*
|
|
1222
|
+
* 工具注册是 standing scope 的单次注册,description 无法内嵌名册;名册对模型的
|
|
1223
|
+
* 可见性走系统提示团队段(index.ts)。
|
|
1224
|
+
*/
|
|
1225
|
+
function createDelegateTool(toolName, deps) {
|
|
1226
|
+
return defineTool({
|
|
1227
|
+
name: toolName,
|
|
1228
|
+
description: "Delegate a self-contained task to a team member (a separate agent with its own persona and optional model override). The member does NOT see this conversation — give it a complete, standalone prompt. Available members and their descriptions are listed in the team section of the system prompt. This call waits for the member and returns its result.",
|
|
1229
|
+
parameters: {
|
|
1230
|
+
role: {
|
|
1231
|
+
type: "string",
|
|
1232
|
+
required: true,
|
|
1233
|
+
description: "The member to delegate to. Must be one of the listed names."
|
|
1234
|
+
},
|
|
1235
|
+
description: {
|
|
1236
|
+
type: "string",
|
|
1237
|
+
required: true,
|
|
1238
|
+
description: "A short (3-5 word) description of the delegated task, for display."
|
|
1239
|
+
},
|
|
1240
|
+
prompt: {
|
|
1241
|
+
type: "string",
|
|
1242
|
+
required: true,
|
|
1243
|
+
description: "The complete, self-contained task for the member. It does not share this conversation's context, so include everything it needs."
|
|
1244
|
+
}
|
|
1245
|
+
},
|
|
1246
|
+
output: {
|
|
1247
|
+
schema: {
|
|
1248
|
+
type: "object",
|
|
1249
|
+
additionalProperties: false,
|
|
1250
|
+
properties: {
|
|
1251
|
+
kind: {
|
|
1252
|
+
type: "string",
|
|
1253
|
+
required: true,
|
|
1254
|
+
const: "foreground"
|
|
1255
|
+
},
|
|
1256
|
+
role: {
|
|
1257
|
+
type: "string",
|
|
1258
|
+
required: true
|
|
1259
|
+
},
|
|
1260
|
+
runId: {
|
|
1261
|
+
type: "string",
|
|
1262
|
+
required: true
|
|
1263
|
+
},
|
|
1264
|
+
childSessionId: {
|
|
1265
|
+
type: "string",
|
|
1266
|
+
required: true
|
|
1267
|
+
},
|
|
1268
|
+
output: {
|
|
1269
|
+
type: "array",
|
|
1270
|
+
required: true,
|
|
1271
|
+
items: { type: "json" }
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
render: (_args, value) => [{
|
|
1276
|
+
type: "text",
|
|
1277
|
+
text: value.output.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("")
|
|
1278
|
+
}],
|
|
1279
|
+
presentationMeta: (_args, value) => ({
|
|
1280
|
+
role: value.role,
|
|
1281
|
+
runId: value.runId,
|
|
1282
|
+
childSessionId: value.childSessionId
|
|
1283
|
+
})
|
|
1284
|
+
},
|
|
1285
|
+
isConcurrencySafe: () => true,
|
|
1286
|
+
presentCall: (args) => ({
|
|
1287
|
+
card: "generic",
|
|
1288
|
+
title: `委派 · ${args.role}: ${args.description}`,
|
|
1289
|
+
rawInput: args.prompt
|
|
1290
|
+
}),
|
|
1291
|
+
presentResult: (_args, result) => result.isError ? void 0 : { card: "generic" },
|
|
1292
|
+
async execute(args, exec) {
|
|
1293
|
+
const parent = exec.agent;
|
|
1294
|
+
if (!parent) throw new Error("team_delegate 需要调用方 agent(exec.agent 为空)");
|
|
1295
|
+
const roster = deps.roster().filter((r) => r.id !== "main");
|
|
1296
|
+
const role = roster.find((r) => r.id === args.role);
|
|
1297
|
+
if (!role) throw new Error(`未知角色 "${args.role}"。可用角色:${roster.map((r) => r.id).join(", ")}`);
|
|
1298
|
+
const persona = deps.buildPersona(role);
|
|
1299
|
+
const request = {
|
|
1300
|
+
label: `role:${role.id}: ${args.description}`,
|
|
1301
|
+
prompt: [{
|
|
1302
|
+
type: "text",
|
|
1303
|
+
text: args.prompt
|
|
1304
|
+
}],
|
|
1305
|
+
parent,
|
|
1306
|
+
persona,
|
|
1307
|
+
maxDepth: 1,
|
|
1308
|
+
signal: exec.signal,
|
|
1309
|
+
...role.model !== void 0 ? { agentOptions: {
|
|
1310
|
+
provider: role.model.provider,
|
|
1311
|
+
model: role.model.model
|
|
1312
|
+
} } : {},
|
|
1313
|
+
...role.tools !== void 0 ? { toolFilter: { allow: [...role.tools.allow] } } : {}
|
|
1314
|
+
};
|
|
1315
|
+
return settleForegroundRun(await deps.startRun(deps.provider, request), role.id);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
//#endregion
|
|
1320
|
+
//#region src/delegate/index.ts
|
|
1321
|
+
/** 团队名册段在 prompt 中的位置:紧随内置 subagent 段(116.5)之后。 */
|
|
1322
|
+
const TEAM_SECTION_ORDER = 116.6;
|
|
1323
|
+
/**
|
|
1324
|
+
* 挂载 team_delegate 工具与函数式团队提示段。
|
|
1325
|
+
* provider 能力守卫与生命周期镜像同归档 tool-subagent:persona/depthLimit 缺失响亮报错,
|
|
1326
|
+
* 工具随 provider 在场与否挂载/摘除。名册来自注册表(main 排除),工具与提示段都经
|
|
1327
|
+
* 闭包读取 registry,UI 改角色后新会话即生效。
|
|
1328
|
+
*/
|
|
1329
|
+
function setupDelegate(ctx, config, registry) {
|
|
1330
|
+
const { provider, toolName } = config;
|
|
1331
|
+
let disposeTool;
|
|
1332
|
+
let providerFailed = false;
|
|
1333
|
+
const mountTool = (p) => {
|
|
1334
|
+
const missing = [];
|
|
1335
|
+
if (!p.capabilities.persona) missing.push("persona");
|
|
1336
|
+
if (!p.capabilities.depthLimit) missing.push("depthLimit");
|
|
1337
|
+
if (missing.length > 0) throw new Error(`dsh-agent-toolkit: provider "${p.name}" 缺少 team_delegate 委派必需能力 ${missing.join("/")}(固定发送 persona 与 maxDepth:1)——请配置具备 persona 与 depthLimit 能力的 provider(如 spawn/fork)`);
|
|
1338
|
+
if (disposeTool === void 0) disposeTool = ctx.tools.register(createDelegateTool(toolName, {
|
|
1339
|
+
roster: () => registry.list(),
|
|
1340
|
+
provider,
|
|
1341
|
+
buildPersona: (role) => buildAgentPersona({ rules: config.rules }, role, role.model),
|
|
1342
|
+
startRun: (pr, request) => ctx.subagents.start(pr, request)
|
|
1343
|
+
}));
|
|
1344
|
+
};
|
|
1345
|
+
ctx.on("subagent/provider-added", (p) => {
|
|
1346
|
+
if (p.name !== provider || providerFailed) return;
|
|
1347
|
+
try {
|
|
1348
|
+
mountTool(p);
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
providerFailed = true;
|
|
1351
|
+
ctx.logger.error(error instanceof Error ? error.message : String(error));
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
ctx.on("subagent/provider-removed", (removed) => {
|
|
1355
|
+
if (removed !== provider) return;
|
|
1356
|
+
providerFailed = false;
|
|
1357
|
+
if (disposeTool !== void 0) {
|
|
1358
|
+
disposeTool();
|
|
1359
|
+
disposeTool = void 0;
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
const present = ctx.subagents.getProvider(provider);
|
|
1363
|
+
if (present !== void 0) mountTool(present);
|
|
1364
|
+
else ctx.logger.info(`dsh-agent-toolkit: subagent provider "${provider}" 尚未注册;team_delegate 将等它出现时挂载`);
|
|
1365
|
+
ctx.systemPrompt.section({
|
|
1366
|
+
name: "plugin:dsh-agent-toolkit:team",
|
|
1367
|
+
order: TEAM_SECTION_ORDER,
|
|
1368
|
+
text: () => {
|
|
1369
|
+
const rosterText = registry.list().filter((r) => r.id !== "main").map((r) => `${r.id}: ${r.description ?? r.name}`).join("\n");
|
|
1370
|
+
return `你有一组可委派的成员:用 ${config.toolName} 把自包含的子任务委派给合适的成员,成员结果会作为工具返回值回到本对话。\n可用成员:\n${rosterText}`;
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/agents/api.ts
|
|
1376
|
+
function createAgentsApiHandler(deps) {
|
|
1377
|
+
return async (req, res) => {
|
|
1378
|
+
const sub = new URL(req.url ?? "/", "http://127.0.0.1").pathname.replace(/^\/dsh-agent-toolkit\/api/, "") || "/";
|
|
1379
|
+
const method = req.method ?? "GET";
|
|
1380
|
+
if (sub === "/agents" && method === "GET") {
|
|
1381
|
+
json$1(res, 200, deps.registry.list());
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (sub === "/tools" && method === "GET") {
|
|
1385
|
+
json$1(res, 200, {
|
|
1386
|
+
native: [...NATIVE_TOOL_NAMES],
|
|
1387
|
+
global: deps.listTools()
|
|
1388
|
+
});
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (sub === "/providers" && method === "GET") {
|
|
1392
|
+
json$1(res, 200, deps.listProviders());
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const modelsMatch = /^\/providers\/([^/]+)\/models$/.exec(sub);
|
|
1396
|
+
if (modelsMatch !== null && method === "GET") {
|
|
1397
|
+
let models = [];
|
|
1398
|
+
try {
|
|
1399
|
+
models = await deps.listModels(decodeURIComponent(modelsMatch[1]));
|
|
1400
|
+
} catch {
|
|
1401
|
+
models = [];
|
|
1402
|
+
}
|
|
1403
|
+
json$1(res, 200, models);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const agentMatch = /^\/agents\/([^/]+)$/.exec(sub);
|
|
1407
|
+
if (agentMatch !== null) {
|
|
1408
|
+
const id = decodeURIComponent(agentMatch[1]);
|
|
1409
|
+
if (method === "PUT") {
|
|
1410
|
+
const body = await readJsonBody$1(req, res);
|
|
1411
|
+
if (body === void 0) return;
|
|
1412
|
+
const bodyRecord = { ...body };
|
|
1413
|
+
delete bodyRecord.builtin;
|
|
1414
|
+
const candidate = {
|
|
1415
|
+
...bodyRecord,
|
|
1416
|
+
id
|
|
1417
|
+
};
|
|
1418
|
+
if (deps.registry.get(id)?.builtin === true) candidate.builtin = true;
|
|
1419
|
+
const parsed = AgentRecordSchema.safeParse(candidate);
|
|
1420
|
+
if (!parsed.success) {
|
|
1421
|
+
json$1(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid agent record" });
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
try {
|
|
1425
|
+
await deps.registry.upsert(parsed.data);
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
json$1(res, 409, { error: error instanceof Error ? error.message : String(error) });
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
json$1(res, 200, { ok: true });
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (method === "DELETE") {
|
|
1434
|
+
if (deps.registry.get(id) === void 0) {
|
|
1435
|
+
json$1(res, 404, { error: `agent "${id}" 不存在` });
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
try {
|
|
1439
|
+
await deps.registry.remove(id);
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
json$1(res, 409, { error: error instanceof Error ? error.message : String(error) });
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
json$1(res, 200, { ok: true });
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
json$1(res, 405, { error: "method not allowed" });
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if ([
|
|
1451
|
+
"/agents",
|
|
1452
|
+
"/providers",
|
|
1453
|
+
"/tools"
|
|
1454
|
+
].some((p) => sub === p || sub.startsWith(`${p}/`))) {
|
|
1455
|
+
json$1(res, 405, { error: "method not allowed" });
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
json$1(res, 404, { error: "not found" });
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* 注册 agents 核心 RPC 路由(恒启用,不随 modules.feishu 门控——Agents 面板总是挂载,
|
|
1463
|
+
* 这些端点缺失即「加载失败」)。webServer 为可选服务:缺席时经 registerOptionalRoutes
|
|
1464
|
+
* 惰性不注册。挂载点与 bots 同处 /dsh-agent-toolkit/api 前缀,但按路径空间拆成独立
|
|
1465
|
+
* prefix/exact 条目(webServer 精确先于最长前缀),互不重叠:
|
|
1466
|
+
* - prefix /dsh-agent-toolkit/api/agents → /agents 与 /agents/:id
|
|
1467
|
+
* - prefix /dsh-agent-toolkit/api/providers → /providers 与 /providers/:p/models
|
|
1468
|
+
* - exact /dsh-agent-toolkit/api/tools
|
|
1469
|
+
* - prefix /dsh-agent-toolkit/api → 兜底:api 前缀下未被更具体路由(bots/usage)
|
|
1470
|
+
* 认领的未知路径仍回 404 JSON(与旧统一分发行为字节一致)。
|
|
1471
|
+
*/
|
|
1472
|
+
function setupAgentsApi(ctx, deps) {
|
|
1473
|
+
const handler = createAgentsApiHandler(deps);
|
|
1474
|
+
registerOptionalRoutes(ctx, (webCtx) => {
|
|
1475
|
+
const dispose = [
|
|
1476
|
+
webCtx.webServer.register({
|
|
1477
|
+
kind: "prefix",
|
|
1478
|
+
path: "/dsh-agent-toolkit/api/agents",
|
|
1479
|
+
handler
|
|
1480
|
+
}),
|
|
1481
|
+
webCtx.webServer.register({
|
|
1482
|
+
kind: "prefix",
|
|
1483
|
+
path: "/dsh-agent-toolkit/api/providers",
|
|
1484
|
+
handler
|
|
1485
|
+
}),
|
|
1486
|
+
webCtx.webServer.register({
|
|
1487
|
+
kind: "exact",
|
|
1488
|
+
path: "/dsh-agent-toolkit/api/tools",
|
|
1489
|
+
handler
|
|
1490
|
+
}),
|
|
1491
|
+
webCtx.webServer.register({
|
|
1492
|
+
kind: "prefix",
|
|
1493
|
+
path: "/dsh-agent-toolkit/api",
|
|
1494
|
+
handler
|
|
1495
|
+
})
|
|
1496
|
+
];
|
|
1497
|
+
return () => dispose.forEach((unregister) => unregister());
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
//#endregion
|
|
1501
|
+
//#region src/channels/agent-setup.ts
|
|
1502
|
+
/** bot 会话角色 persona 的 scoped 段名:与全局 persona 层同名,scoped 注册即 shadow 覆盖主 Agent persona。 */
|
|
1503
|
+
const TOOLKIT_PERSONA_SECTION = "prompt-stack:persona";
|
|
1504
|
+
/** 默认加载器:复刻 preset mount 的模块解析——loader 的 unwrapExports 取 `default ?? 命名导出模块`。 */
|
|
1505
|
+
async function loadToolModule(specifier) {
|
|
1506
|
+
const mod = await import(specifier);
|
|
1507
|
+
return mod.default ?? mod;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* 组合 agent 作用域:先 scoped 挂载基础工具行(persona/instructions/shell/fs/fs-search,
|
|
1511
|
+
* 与原生 UI 会话的 standard preset 同源),再叠 bot 的 persona 与 tools 白名单。
|
|
1512
|
+
* 顺序敏感:restrict 必须在工具行挂载之后,否则白名单命中不到挂载带入的工具名。
|
|
1513
|
+
*/
|
|
1514
|
+
async function setupAgentScope(agentCtx, hooks, loadTool = loadToolModule) {
|
|
1515
|
+
for (const tool of BASIC_TOOLS) await agentCtx.plugin(await loadTool(tool.id), tool.config);
|
|
1516
|
+
if (hooks.persona !== void 0) agentCtx.systemPrompt.section({
|
|
1517
|
+
name: TOOLKIT_PERSONA_SECTION,
|
|
1518
|
+
order: 10,
|
|
1519
|
+
text: hooks.persona
|
|
1520
|
+
});
|
|
1521
|
+
if (hooks.sections !== void 0) for (const section of hooks.sections) agentCtx.systemPrompt.section({
|
|
1522
|
+
name: section.name,
|
|
1523
|
+
order: section.order,
|
|
1524
|
+
text: section.text
|
|
1525
|
+
});
|
|
1526
|
+
if (hooks.tools !== void 0) agentCtx.tools.restrict({ allow: hooks.tools });
|
|
1527
|
+
}
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region src/channels/feishu/api.ts
|
|
1530
|
+
/** SDK 薄封装:tenant_access_token 由 SDK 自动管理;错误带 code/msg 上下文。 */
|
|
1531
|
+
function createFeishuApi(client) {
|
|
1532
|
+
return {
|
|
1533
|
+
async createCard(cardJson) {
|
|
1534
|
+
const res = await client.cardkit.v1.card.create({ data: {
|
|
1535
|
+
type: "card_json",
|
|
1536
|
+
data: cardJson
|
|
1537
|
+
} });
|
|
1538
|
+
const cardId = res.data?.card_id;
|
|
1539
|
+
if (typeof cardId !== "string" || cardId.length === 0) throw new Error(`cardkit 建卡失败:code=${res.code} msg=${res.msg}`);
|
|
1540
|
+
return cardId;
|
|
1541
|
+
},
|
|
1542
|
+
async sendCardMessage(chatId, cardId) {
|
|
1543
|
+
await client.im.message.create({
|
|
1544
|
+
params: { receive_id_type: "chat_id" },
|
|
1545
|
+
data: {
|
|
1546
|
+
receive_id: chatId,
|
|
1547
|
+
msg_type: "interactive",
|
|
1548
|
+
content: JSON.stringify({
|
|
1549
|
+
type: "card",
|
|
1550
|
+
data: { card_id: cardId }
|
|
1551
|
+
})
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
},
|
|
1555
|
+
async updateCardElement(cardId, elementId, content, sequence) {
|
|
1556
|
+
await client.cardkit.v1.cardElement.content({
|
|
1557
|
+
path: {
|
|
1558
|
+
card_id: cardId,
|
|
1559
|
+
element_id: elementId
|
|
1560
|
+
},
|
|
1561
|
+
data: {
|
|
1562
|
+
content,
|
|
1563
|
+
sequence
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
},
|
|
1567
|
+
async insertElement(cardId, elementJson, targetElementId, sequence) {
|
|
1568
|
+
await client.cardkit.v1.cardElement.create({
|
|
1569
|
+
path: { card_id: cardId },
|
|
1570
|
+
data: {
|
|
1571
|
+
type: "insert_before",
|
|
1572
|
+
target_element_id: targetElementId,
|
|
1573
|
+
elements: `[${elementJson}]`,
|
|
1574
|
+
sequence
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
},
|
|
1578
|
+
async setCardStreaming(cardId, streaming, sequence, summary) {
|
|
1579
|
+
await client.cardkit.v1.card.settings({
|
|
1580
|
+
path: { card_id: cardId },
|
|
1581
|
+
data: {
|
|
1582
|
+
settings: JSON.stringify({ config: {
|
|
1583
|
+
streaming_mode: streaming,
|
|
1584
|
+
...summary !== void 0 ? { summary: { content: summary } } : {}
|
|
1585
|
+
} }),
|
|
1586
|
+
sequence
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
},
|
|
1590
|
+
async replaceCard(cardId, cardJson, sequence) {
|
|
1591
|
+
await client.cardkit.v1.card.update({
|
|
1592
|
+
path: { card_id: cardId },
|
|
1593
|
+
data: {
|
|
1594
|
+
card: {
|
|
1595
|
+
type: "card_json",
|
|
1596
|
+
data: cardJson
|
|
1597
|
+
},
|
|
1598
|
+
sequence
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
},
|
|
1602
|
+
async sendText(chatId, text) {
|
|
1603
|
+
await client.im.message.create({
|
|
1604
|
+
params: { receive_id_type: "chat_id" },
|
|
1605
|
+
data: {
|
|
1606
|
+
receive_id: chatId,
|
|
1607
|
+
msg_type: "text",
|
|
1608
|
+
content: JSON.stringify({ text })
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
},
|
|
1612
|
+
async addReaction(messageId, emojiType) {
|
|
1613
|
+
const res = await client.im.messageReaction.create({
|
|
1614
|
+
path: { message_id: messageId },
|
|
1615
|
+
data: { reaction_type: { emoji_type: emojiType } }
|
|
1616
|
+
});
|
|
1617
|
+
const reactionId = res.data?.reaction_id;
|
|
1618
|
+
if (typeof reactionId !== "string") throw new Error(`加表情失败:code=${res.code} msg=${res.msg}`);
|
|
1619
|
+
return reactionId;
|
|
1620
|
+
},
|
|
1621
|
+
async removeReaction(messageId, reactionId) {
|
|
1622
|
+
await client.im.messageReaction.delete({ path: {
|
|
1623
|
+
message_id: messageId,
|
|
1624
|
+
reaction_id: reactionId
|
|
1625
|
+
} });
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
//#endregion
|
|
1630
|
+
//#region src/channels/directive.ts
|
|
1631
|
+
/** 仅当整条消息就是一个指令时命中;带参数/前后文的按普通消息处理。 */
|
|
1632
|
+
function parseDirective(text) {
|
|
1633
|
+
const t = text.trim().toLowerCase();
|
|
1634
|
+
if (t === "/new") return "new";
|
|
1635
|
+
if (t === "/stop") return "stop";
|
|
1636
|
+
if (t === "/status") return "status";
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
/** 群消息正文中的 @ 占位符(@_user_1 等)剥掉,得到纯净指令文本。 */
|
|
1640
|
+
function stripMentionPlaceholders(text) {
|
|
1641
|
+
return text.replace(/@_user_\d+\s*/g, "").trim();
|
|
1642
|
+
}
|
|
1643
|
+
//#endregion
|
|
1644
|
+
//#region src/channels/feishu/parse.ts
|
|
1645
|
+
/** im.message.receive_v1 事件解析:窄化为渠道无关的 ParsedMessage;message_id 去重。 */
|
|
1646
|
+
/**
|
|
1647
|
+
* SDK handler 收到的 data 即事件体(README 示例 `data.message` 直接解构);
|
|
1648
|
+
* 兼容包一层 { event } 的形态。过滤:机器人消息、非文本、群内未 @机器人、空文本。
|
|
1649
|
+
*/
|
|
1650
|
+
function parseMessageEvent(data) {
|
|
1651
|
+
const wrapped = data;
|
|
1652
|
+
const event = wrapped.event ?? wrapped;
|
|
1653
|
+
if (event.sender?.sender_type !== "user") return null;
|
|
1654
|
+
const userId = event.sender.sender_id?.open_id;
|
|
1655
|
+
const msg = event.message;
|
|
1656
|
+
if (typeof userId !== "string" || msg === void 0) return null;
|
|
1657
|
+
if (msg.message_type !== "text" || typeof msg.content !== "string") return null;
|
|
1658
|
+
if (typeof msg.message_id !== "string" || typeof msg.chat_id !== "string") return null;
|
|
1659
|
+
if (msg.chat_type !== "p2p" && msg.chat_type !== "group") return null;
|
|
1660
|
+
if (msg.chat_type === "group" && !(msg.mentions ?? []).some((m) => m.mentioned_type === "bot")) return null;
|
|
1661
|
+
let text;
|
|
1662
|
+
try {
|
|
1663
|
+
const parsed = JSON.parse(msg.content);
|
|
1664
|
+
if (typeof parsed.text !== "string") return null;
|
|
1665
|
+
text = stripMentionPlaceholders(parsed.text);
|
|
1666
|
+
} catch {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
if (text.length === 0) return null;
|
|
1670
|
+
return {
|
|
1671
|
+
messageId: msg.message_id,
|
|
1672
|
+
chatId: msg.chat_id,
|
|
1673
|
+
chatType: msg.chat_type,
|
|
1674
|
+
userId,
|
|
1675
|
+
text
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
/** message_id 去重(飞书会重推);FIFO 容量淘汰。 */
|
|
1679
|
+
var MessageDedup = class {
|
|
1680
|
+
cap;
|
|
1681
|
+
seen = /* @__PURE__ */ new Set();
|
|
1682
|
+
order = [];
|
|
1683
|
+
constructor(cap = 1e3) {
|
|
1684
|
+
this.cap = cap;
|
|
1685
|
+
}
|
|
1686
|
+
/** true = 新消息。 */
|
|
1687
|
+
check(id) {
|
|
1688
|
+
if (this.seen.has(id)) return false;
|
|
1689
|
+
this.seen.add(id);
|
|
1690
|
+
this.order.push(id);
|
|
1691
|
+
if (this.order.length > this.cap) this.seen.delete(this.order.shift());
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
//#endregion
|
|
1696
|
+
//#region src/channels/feishu/cards.ts
|
|
1697
|
+
const STATUS_ELEMENT_ID = "status";
|
|
1698
|
+
/** create 未返回真实 card_id 前的占位哨兵(executor 赋值;防并发 flush 重复建卡)。 */
|
|
1699
|
+
const PENDING_CARD_ID = "__pending__";
|
|
1700
|
+
/** 过程区截尾后的头部省略标记。 */
|
|
1701
|
+
const PROCESS_OMITTED = "…(已省略前文)\n";
|
|
1702
|
+
/** 输出中状态行文案。 */
|
|
1703
|
+
const STATUS_STREAMING = "⏳ 输出中…";
|
|
1704
|
+
/** 定格状态行文案。 */
|
|
1705
|
+
const STATUS_FINAL = {
|
|
1706
|
+
done: "✅ 输出完成",
|
|
1707
|
+
error: "❌ 输出出错",
|
|
1708
|
+
cancelled: "⏹ 已取消"
|
|
1709
|
+
};
|
|
1710
|
+
/** 新卡固定开销字节数(状态行 + 结构,粗算进预算)。 */
|
|
1711
|
+
const CARD_FIXED_BYTES = 64;
|
|
1712
|
+
/** 单卡组件数安全上限(飞书硬上限 200;面板按 2 计:面板 + 内嵌 markdown)。 */
|
|
1713
|
+
const CARD_ELEMENT_LIMIT = 190;
|
|
1714
|
+
/** 按 UTF-8 字节上限截头(保留头部),不劈开多字节字符与代理对。 */
|
|
1715
|
+
function sliceByBytes(text, maxBytes) {
|
|
1716
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1717
|
+
let lo = 0;
|
|
1718
|
+
let hi = text.length;
|
|
1719
|
+
while (lo < hi) {
|
|
1720
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
1721
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
1722
|
+
else hi = mid - 1;
|
|
1723
|
+
}
|
|
1724
|
+
let cut = lo;
|
|
1725
|
+
if (cut > 0) {
|
|
1726
|
+
const code = text.charCodeAt(cut - 1);
|
|
1727
|
+
if (code >= 55296 && code <= 56319) cut -= 1;
|
|
1728
|
+
}
|
|
1729
|
+
return text.slice(0, cut);
|
|
1730
|
+
}
|
|
1731
|
+
/** 按 UTF-8 字节上限截尾(保留尾部),不劈开多字节字符与代理对;截断时头部加省略标记。 */
|
|
1732
|
+
function sliceTailByBytes(text, maxBytes) {
|
|
1733
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1734
|
+
const budget = maxBytes - Buffer.byteLength(PROCESS_OMITTED, "utf8");
|
|
1735
|
+
if (budget <= 0) throw new Error(`processMaxBytes=${maxBytes} 过小,连省略标记都容纳不了`);
|
|
1736
|
+
let lo = 0;
|
|
1737
|
+
let hi = text.length;
|
|
1738
|
+
while (lo < hi) {
|
|
1739
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
1740
|
+
if (Buffer.byteLength(text.slice(mid), "utf8") <= budget) hi = mid;
|
|
1741
|
+
else lo = mid + 1;
|
|
1742
|
+
}
|
|
1743
|
+
let cut = lo;
|
|
1744
|
+
if (cut < text.length) {
|
|
1745
|
+
const code = text.charCodeAt(cut);
|
|
1746
|
+
if (code >= 56320 && code <= 57343) cut += 1;
|
|
1747
|
+
}
|
|
1748
|
+
return PROCESS_OMITTED + text.slice(cut);
|
|
1749
|
+
}
|
|
1750
|
+
/** 新卡:仅状态行的流式卡;段后续经插入组件 API 动态加入。 */
|
|
1751
|
+
function buildCardJson() {
|
|
1752
|
+
return JSON.stringify({
|
|
1753
|
+
schema: "2.0",
|
|
1754
|
+
config: {
|
|
1755
|
+
streaming_mode: true,
|
|
1756
|
+
summary: { content: "生成中…" },
|
|
1757
|
+
streaming_config: {
|
|
1758
|
+
print_frequency_ms: { default: 70 },
|
|
1759
|
+
print_step: { default: 1 },
|
|
1760
|
+
print_strategy: "fast"
|
|
1761
|
+
}
|
|
1762
|
+
},
|
|
1763
|
+
body: { elements: [{
|
|
1764
|
+
tag: "markdown",
|
|
1765
|
+
content: STATUS_STREAMING,
|
|
1766
|
+
element_id: STATUS_ELEMENT_ID
|
|
1767
|
+
}] }
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
/** 段元素 JSON(insert op 负载):process = 默认收起折叠面板;text = 纯 markdown。 */
|
|
1771
|
+
function buildSegmentJson(kind, elementId, content) {
|
|
1772
|
+
if (kind === "process") return JSON.stringify({
|
|
1773
|
+
tag: "collapsible_panel",
|
|
1774
|
+
expanded: false,
|
|
1775
|
+
header: { title: {
|
|
1776
|
+
tag: "plain_text",
|
|
1777
|
+
content: "思考与工具调用过程"
|
|
1778
|
+
} },
|
|
1779
|
+
elements: [{
|
|
1780
|
+
tag: "markdown",
|
|
1781
|
+
content,
|
|
1782
|
+
element_id: elementId
|
|
1783
|
+
}]
|
|
1784
|
+
});
|
|
1785
|
+
return JSON.stringify({
|
|
1786
|
+
tag: "markdown",
|
|
1787
|
+
content,
|
|
1788
|
+
element_id: elementId
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
const initialStreamState = () => ({
|
|
1792
|
+
cardId: null,
|
|
1793
|
+
seq: 0,
|
|
1794
|
+
cardBytes: 0,
|
|
1795
|
+
cardElements: 0,
|
|
1796
|
+
segCounter: 0,
|
|
1797
|
+
closedSegCount: 0,
|
|
1798
|
+
tail: void 0,
|
|
1799
|
+
carry: void 0
|
|
1800
|
+
});
|
|
1801
|
+
/** 把段序列的新增部分同步到卡片;新段 insert 到状态行之前,尾段增长走元素 update,满卡关流开续卡。 */
|
|
1802
|
+
function planSync(state, segments, maxBytes, processMaxBytes) {
|
|
1803
|
+
const ops = [];
|
|
1804
|
+
let { cardId, seq, cardBytes, cardElements, segCounter, closedSegCount, tail, carry } = state;
|
|
1805
|
+
const ensureCard = () => {
|
|
1806
|
+
if (cardId !== null) return;
|
|
1807
|
+
ops.push({
|
|
1808
|
+
type: "create",
|
|
1809
|
+
cardJson: buildCardJson()
|
|
1810
|
+
});
|
|
1811
|
+
ops.push({ type: "send" });
|
|
1812
|
+
cardId = PENDING_CARD_ID;
|
|
1813
|
+
seq = 0;
|
|
1814
|
+
cardBytes = CARD_FIXED_BYTES;
|
|
1815
|
+
cardElements = 1;
|
|
1816
|
+
};
|
|
1817
|
+
const closeCard = () => {
|
|
1818
|
+
seq += 1;
|
|
1819
|
+
ops.push({
|
|
1820
|
+
type: "settings",
|
|
1821
|
+
streaming: false,
|
|
1822
|
+
sequence: seq
|
|
1823
|
+
});
|
|
1824
|
+
cardId = null;
|
|
1825
|
+
tail = void 0;
|
|
1826
|
+
};
|
|
1827
|
+
let i = tail?.segIndex ?? closedSegCount;
|
|
1828
|
+
while (i < segments.length) {
|
|
1829
|
+
const seg = segments[i];
|
|
1830
|
+
const content = seg.kind === "process" ? sliceTailByBytes(seg.content, processMaxBytes) : seg.content;
|
|
1831
|
+
const base = tail !== void 0 && tail.segIndex === i ? tail.base : carry?.segIndex === i ? carry.base : 0;
|
|
1832
|
+
const elementContent = seg.kind === "text" ? content.slice(base) : content;
|
|
1833
|
+
if (tail !== void 0 && tail.segIndex === i) {
|
|
1834
|
+
if (elementContent !== tail.shownText) {
|
|
1835
|
+
const delta = Buffer.byteLength(elementContent, "utf8") - Buffer.byteLength(tail.shownText, "utf8");
|
|
1836
|
+
if (cardBytes + delta <= maxBytes) {
|
|
1837
|
+
seq += 1;
|
|
1838
|
+
ops.push({
|
|
1839
|
+
type: "update",
|
|
1840
|
+
elementId: tail.elementId,
|
|
1841
|
+
content: elementContent,
|
|
1842
|
+
sequence: seq
|
|
1843
|
+
});
|
|
1844
|
+
cardBytes += delta;
|
|
1845
|
+
tail = {
|
|
1846
|
+
...tail,
|
|
1847
|
+
shownText: elementContent
|
|
1848
|
+
};
|
|
1849
|
+
} else if (seg.kind === "text") {
|
|
1850
|
+
const piece = sliceByBytes(elementContent, Buffer.byteLength(tail.shownText, "utf8") + (maxBytes - cardBytes));
|
|
1851
|
+
if (piece.length > tail.shownText.length) {
|
|
1852
|
+
seq += 1;
|
|
1853
|
+
ops.push({
|
|
1854
|
+
type: "update",
|
|
1855
|
+
elementId: tail.elementId,
|
|
1856
|
+
content: piece,
|
|
1857
|
+
sequence: seq
|
|
1858
|
+
});
|
|
1859
|
+
cardBytes += Buffer.byteLength(piece, "utf8") - Buffer.byteLength(tail.shownText, "utf8");
|
|
1860
|
+
tail = {
|
|
1861
|
+
...tail,
|
|
1862
|
+
shownText: piece
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
carry = {
|
|
1866
|
+
segIndex: i,
|
|
1867
|
+
base: tail.base + tail.shownText.length
|
|
1868
|
+
};
|
|
1869
|
+
closeCard();
|
|
1870
|
+
continue;
|
|
1871
|
+
} else {
|
|
1872
|
+
closeCard();
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (i === segments.length - 1) break;
|
|
1877
|
+
tail = void 0;
|
|
1878
|
+
closedSegCount = i + 1;
|
|
1879
|
+
carry = void 0;
|
|
1880
|
+
i += 1;
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (seg.kind === "process") {
|
|
1884
|
+
const windowBytes = Buffer.byteLength(elementContent, "utf8");
|
|
1885
|
+
if (cardId !== null && (cardBytes + windowBytes > maxBytes || cardElements + 2 > CARD_ELEMENT_LIMIT)) {
|
|
1886
|
+
closeCard();
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
ensureCard();
|
|
1890
|
+
segCounter += 1;
|
|
1891
|
+
const elementId = `seg_${segCounter}`;
|
|
1892
|
+
seq += 1;
|
|
1893
|
+
ops.push({
|
|
1894
|
+
type: "insert",
|
|
1895
|
+
elementJson: buildSegmentJson("process", elementId, elementContent),
|
|
1896
|
+
sequence: seq
|
|
1897
|
+
});
|
|
1898
|
+
cardBytes += windowBytes;
|
|
1899
|
+
cardElements += 2;
|
|
1900
|
+
tail = {
|
|
1901
|
+
segIndex: i,
|
|
1902
|
+
elementId,
|
|
1903
|
+
base: 0,
|
|
1904
|
+
shownText: elementContent
|
|
1905
|
+
};
|
|
1906
|
+
} else {
|
|
1907
|
+
if (elementContent.length === 0) {
|
|
1908
|
+
closedSegCount = i + 1;
|
|
1909
|
+
carry = void 0;
|
|
1910
|
+
i += 1;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (cardId !== null && cardElements + 1 > CARD_ELEMENT_LIMIT) {
|
|
1914
|
+
closeCard();
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
ensureCard();
|
|
1918
|
+
const piece = sliceByBytes(elementContent, maxBytes - cardBytes);
|
|
1919
|
+
if (piece.length === 0) throw new Error(`cardMaxBytes=${maxBytes} 过小,扣固定开销后连一个字符都容纳不了`);
|
|
1920
|
+
segCounter += 1;
|
|
1921
|
+
const elementId = `seg_${segCounter}`;
|
|
1922
|
+
seq += 1;
|
|
1923
|
+
ops.push({
|
|
1924
|
+
type: "insert",
|
|
1925
|
+
elementJson: buildSegmentJson("text", elementId, piece),
|
|
1926
|
+
sequence: seq
|
|
1927
|
+
});
|
|
1928
|
+
cardBytes += Buffer.byteLength(piece, "utf8");
|
|
1929
|
+
cardElements += 1;
|
|
1930
|
+
tail = {
|
|
1931
|
+
segIndex: i,
|
|
1932
|
+
elementId,
|
|
1933
|
+
base,
|
|
1934
|
+
shownText: piece
|
|
1935
|
+
};
|
|
1936
|
+
if (piece.length < elementContent.length) {
|
|
1937
|
+
carry = {
|
|
1938
|
+
segIndex: i,
|
|
1939
|
+
base: base + piece.length
|
|
1940
|
+
};
|
|
1941
|
+
closeCard();
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
carry = void 0;
|
|
1946
|
+
if (i === segments.length - 1) break;
|
|
1947
|
+
tail = void 0;
|
|
1948
|
+
closedSegCount = i + 1;
|
|
1949
|
+
i += 1;
|
|
1950
|
+
}
|
|
1951
|
+
return {
|
|
1952
|
+
state: {
|
|
1953
|
+
cardId,
|
|
1954
|
+
seq,
|
|
1955
|
+
cardBytes,
|
|
1956
|
+
cardElements,
|
|
1957
|
+
segCounter,
|
|
1958
|
+
closedSegCount,
|
|
1959
|
+
tail,
|
|
1960
|
+
carry
|
|
1961
|
+
},
|
|
1962
|
+
ops
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
/** 定格:先 update 状态行(流式还开着),再关闭 + summary。 */
|
|
1966
|
+
function planFinalize(state, status) {
|
|
1967
|
+
if (state.cardId === null) return { ops: [] };
|
|
1968
|
+
return { ops: [{
|
|
1969
|
+
type: "update",
|
|
1970
|
+
elementId: STATUS_ELEMENT_ID,
|
|
1971
|
+
content: STATUS_FINAL[status],
|
|
1972
|
+
sequence: state.seq + 1
|
|
1973
|
+
}, {
|
|
1974
|
+
type: "settings",
|
|
1975
|
+
streaming: false,
|
|
1976
|
+
sequence: state.seq + 2,
|
|
1977
|
+
summary: STATUS_FINAL[status]
|
|
1978
|
+
}] };
|
|
1979
|
+
}
|
|
1980
|
+
//#endregion
|
|
1981
|
+
//#region src/channels/feishu/reply.ts
|
|
1982
|
+
/** 指数退避重试(默认 3 次,300ms 起)。 */
|
|
1983
|
+
async function withRetry(fn, attempts = 3, baseDelayMs = 300) {
|
|
1984
|
+
let lastError;
|
|
1985
|
+
for (let i = 0; i < attempts; i++) try {
|
|
1986
|
+
return await fn();
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
lastError = error;
|
|
1989
|
+
if (i < attempts - 1) await new Promise((resolve) => setTimeout(resolve, baseDelayMs * 2 ** i));
|
|
1990
|
+
}
|
|
1991
|
+
throw lastError;
|
|
1992
|
+
}
|
|
1993
|
+
var FeishuReplyHandle = class {
|
|
1994
|
+
api;
|
|
1995
|
+
chatId;
|
|
1996
|
+
tunables;
|
|
1997
|
+
log;
|
|
1998
|
+
state = initialStreamState();
|
|
1999
|
+
segments = [];
|
|
2000
|
+
tail = Promise.resolve();
|
|
2001
|
+
timer;
|
|
2002
|
+
finalized = false;
|
|
2003
|
+
constructor(api, chatId, tunables, log) {
|
|
2004
|
+
this.api = api;
|
|
2005
|
+
this.chatId = chatId;
|
|
2006
|
+
this.tunables = tunables;
|
|
2007
|
+
this.log = log;
|
|
2008
|
+
}
|
|
2009
|
+
/** 惰性建卡:无文本输出的 turn 不产生空卡片。 */
|
|
2010
|
+
beginTurn() {
|
|
2011
|
+
return Promise.resolve();
|
|
2012
|
+
}
|
|
2013
|
+
update(segments) {
|
|
2014
|
+
if (this.finalized) return Promise.resolve();
|
|
2015
|
+
this.segments = segments;
|
|
2016
|
+
if (this.timer === void 0) this.timer = setTimeout(() => {
|
|
2017
|
+
this.timer = void 0;
|
|
2018
|
+
this.flush();
|
|
2019
|
+
}, this.tunables.cardUpdateThrottleMs);
|
|
2020
|
+
return Promise.resolve();
|
|
2021
|
+
}
|
|
2022
|
+
async finalize(status, detail) {
|
|
2023
|
+
if (this.finalized) {
|
|
2024
|
+
await this.tail;
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
this.finalized = true;
|
|
2028
|
+
if (this.timer !== void 0) {
|
|
2029
|
+
clearTimeout(this.timer);
|
|
2030
|
+
this.timer = void 0;
|
|
2031
|
+
}
|
|
2032
|
+
this.flush();
|
|
2033
|
+
await this.tail;
|
|
2034
|
+
const { ops } = planFinalize(this.state, status);
|
|
2035
|
+
this.enqueue(() => this.exec(ops));
|
|
2036
|
+
if (this.state.cardId === null && detail !== void 0) this.enqueue(() => withRetry(() => this.api.sendText(this.chatId, detail)).then(() => void 0));
|
|
2037
|
+
await this.tail;
|
|
2038
|
+
}
|
|
2039
|
+
notice(text) {
|
|
2040
|
+
this.enqueue(() => withRetry(() => this.api.sendText(this.chatId, text)).then(() => void 0));
|
|
2041
|
+
return this.tail.then(() => void 0);
|
|
2042
|
+
}
|
|
2043
|
+
flush() {
|
|
2044
|
+
const planned = planSync(this.state, this.segments, this.tunables.cardMaxBytes, this.tunables.processMaxBytes);
|
|
2045
|
+
if (planned.ops.length === 0) return;
|
|
2046
|
+
this.state = planned.state;
|
|
2047
|
+
this.enqueue(() => this.exec(planned.ops));
|
|
2048
|
+
}
|
|
2049
|
+
enqueue(task) {
|
|
2050
|
+
this.tail = this.tail.then(task).catch((error) => {
|
|
2051
|
+
if (this.state.cardId === "__pending__") {
|
|
2052
|
+
const t = this.state.tail;
|
|
2053
|
+
this.state = {
|
|
2054
|
+
...this.state,
|
|
2055
|
+
cardId: null,
|
|
2056
|
+
...t !== void 0 ? {
|
|
2057
|
+
tail: void 0,
|
|
2058
|
+
carry: {
|
|
2059
|
+
segIndex: t.segIndex,
|
|
2060
|
+
base: t.base
|
|
2061
|
+
},
|
|
2062
|
+
closedSegCount: t.segIndex
|
|
2063
|
+
} : {}
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
this.log(`[project-bot] 卡片操作失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
async exec(ops) {
|
|
2070
|
+
for (const op of ops) if (op.type === "create") this.state.cardId = await withRetry(() => this.api.createCard(op.cardJson));
|
|
2071
|
+
else if (op.type === "send") await withRetry(() => this.api.sendCardMessage(this.chatId, this.state.cardId));
|
|
2072
|
+
else if (op.type === "insert") await withRetry(() => this.api.insertElement(this.state.cardId, op.elementJson, STATUS_ELEMENT_ID, op.sequence));
|
|
2073
|
+
else if (op.type === "update") await withRetry(() => this.api.updateCardElement(this.state.cardId, op.elementId, op.content, op.sequence));
|
|
2074
|
+
else await withRetry(() => this.api.setCardStreaming(this.state.cardId, op.streaming, op.sequence, op.summary));
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
/** 「处理中」表情:加上后返回删除 disposer;加/删失败都静默(表情残留无害)。 */
|
|
2078
|
+
function makeAck(api, messageId, emojiType) {
|
|
2079
|
+
return async () => {
|
|
2080
|
+
try {
|
|
2081
|
+
const reactionId = await api.addReaction(messageId, emojiType);
|
|
2082
|
+
return () => {
|
|
2083
|
+
api.removeReaction(messageId, reactionId).catch(() => void 0);
|
|
2084
|
+
};
|
|
2085
|
+
} catch {
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
//#endregion
|
|
2091
|
+
//#region src/channels/feishu/index.ts
|
|
2092
|
+
/** 飞书渠道:WSClient 长连接收事件 → 解析 → 核心;出站走 FeishuReplyHandle。 */
|
|
2093
|
+
const feishuChannel = {
|
|
2094
|
+
type: "feishu",
|
|
2095
|
+
async start(bot, io, tunables, log) {
|
|
2096
|
+
const { appId } = bot.record.feishu;
|
|
2097
|
+
const api = createFeishuApi(new lark.Client({
|
|
2098
|
+
appId,
|
|
2099
|
+
appSecret: bot.secret
|
|
2100
|
+
}));
|
|
2101
|
+
const dedup = new MessageDedup();
|
|
2102
|
+
const dispatcher = new lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => {
|
|
2103
|
+
const parsed = parseMessageEvent(data);
|
|
2104
|
+
if (parsed === null || !dedup.check(parsed.messageId)) return;
|
|
2105
|
+
const reply = new FeishuReplyHandle(api, parsed.chatId, tunables, log);
|
|
2106
|
+
io.onMessage({
|
|
2107
|
+
botId: bot.record.id,
|
|
2108
|
+
chatId: parsed.chatId,
|
|
2109
|
+
userId: parsed.userId,
|
|
2110
|
+
messageId: parsed.messageId,
|
|
2111
|
+
text: parsed.text,
|
|
2112
|
+
reply,
|
|
2113
|
+
ackProcessing: makeAck(api, parsed.messageId, tunables.processingReactionEmoji)
|
|
2114
|
+
});
|
|
2115
|
+
} });
|
|
2116
|
+
const ws = new lark.WSClient({
|
|
2117
|
+
appId,
|
|
2118
|
+
appSecret: bot.secret,
|
|
2119
|
+
loggerLevel: lark.LoggerLevel.warn
|
|
2120
|
+
});
|
|
2121
|
+
await ws.start({ eventDispatcher: dispatcher });
|
|
2122
|
+
return {
|
|
2123
|
+
close: () => {
|
|
2124
|
+
ws.close({ force: true });
|
|
2125
|
+
return Promise.resolve();
|
|
2126
|
+
},
|
|
2127
|
+
status: () => ws.getConnectionStatus().state
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
//#endregion
|
|
2132
|
+
//#region src/bots/store.ts
|
|
2133
|
+
/** project-bot 存储域声明:身份、版本、记录 zod schema 的单一来源。 */
|
|
2134
|
+
/** 飞书自建应用 appId 形态(WSClient 同款校验)。 */
|
|
2135
|
+
const FEISHU_APP_ID_RE = /^cli_[0-9a-fA-F]{16}$/;
|
|
2136
|
+
/** CredentialRef 字符集(credentials 服务 credentialRef() 的校验规则)。 */
|
|
2137
|
+
const CREDENTIAL_REF_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2138
|
+
/** bot id:小写 slug。 */
|
|
2139
|
+
const BOT_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
2140
|
+
const FeishuConfigSchema = z$1.object({
|
|
2141
|
+
appId: z$1.string().regex(FEISHU_APP_ID_RE),
|
|
2142
|
+
appSecretRef: z$1.string().regex(CREDENTIAL_REF_RE)
|
|
2143
|
+
});
|
|
2144
|
+
const BotRecordSchema = z$1.object({
|
|
2145
|
+
id: z$1.string().regex(BOT_ID_RE),
|
|
2146
|
+
name: z$1.string().min(1).max(64),
|
|
2147
|
+
channel: z$1.literal("feishu"),
|
|
2148
|
+
feishu: FeishuConfigSchema,
|
|
2149
|
+
/** 绑定项目(agent 的 cwd,绝对路径)。一 bot 一项目。 */
|
|
2150
|
+
project: z$1.string().min(1),
|
|
2151
|
+
/** 透传到 agent 创作期的 persona 提示段。 */
|
|
2152
|
+
persona: z$1.string().max(8e3).optional(),
|
|
2153
|
+
/** 绑定的 Agent('main' 或注册表角色 id,缺省 = 'main')。 */
|
|
2154
|
+
agentRef: z$1.string().min(1).optional(),
|
|
2155
|
+
/** 可用工具白名单(缺省 = 不限制);空数组无意义,直接拒绝。 */
|
|
2156
|
+
tools: z$1.array(z$1.string().min(1)).min(1).optional(),
|
|
2157
|
+
agentOptions: z$1.object({
|
|
2158
|
+
provider: z$1.string().min(1).optional(),
|
|
2159
|
+
model: z$1.string().min(1).optional()
|
|
2160
|
+
}).optional(),
|
|
2161
|
+
createdAt: z$1.number().int().nonnegative(),
|
|
2162
|
+
updatedAt: z$1.number().int().nonnegative()
|
|
2163
|
+
});
|
|
2164
|
+
const BindingSchema = z$1.object({ sessionId: z$1.string().min(1) });
|
|
2165
|
+
/** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
|
|
2166
|
+
const projectBotDomain = defineDomain({
|
|
2167
|
+
name: "project_bot",
|
|
2168
|
+
version: 1,
|
|
2169
|
+
tables: {
|
|
2170
|
+
bots: domainTable(BotRecordSchema),
|
|
2171
|
+
bindings: domainTable(BindingSchema)
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
/** bindings 表 key:(botId, chatId) → sessionId。 */
|
|
2175
|
+
function bindingKey(botId, chatId) {
|
|
2176
|
+
return `${botId}:${chatId}`;
|
|
2177
|
+
}
|
|
2178
|
+
//#endregion
|
|
2179
|
+
//#region src/channels/inbound.ts
|
|
2180
|
+
/** 入站:指令分流 → 路由 → 单 in-flight 准入 → 表情回复 → followup 投递。 */
|
|
2181
|
+
var Inbound = class {
|
|
2182
|
+
deps;
|
|
2183
|
+
constructor(deps) {
|
|
2184
|
+
this.deps = deps;
|
|
2185
|
+
}
|
|
2186
|
+
onMessage(msg) {
|
|
2187
|
+
this.handle(msg).catch(async (error) => {
|
|
2188
|
+
this.deps.onError(`[project-bot] 入站处理失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2189
|
+
await msg.reply.notice("处理失败,请稍后再试").catch(() => void 0);
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
async handle(msg) {
|
|
2193
|
+
const bot = this.deps.bots.get(msg.botId);
|
|
2194
|
+
if (bot === void 0) return;
|
|
2195
|
+
const directive = parseDirective(msg.text);
|
|
2196
|
+
if (directive === "new") {
|
|
2197
|
+
await this.deps.router.reset(bot, msg.chatId, msg.reply);
|
|
2198
|
+
await msg.reply.notice("已开启新会话");
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (directive === "stop") {
|
|
2202
|
+
const rt = this.deps.router.lookup(bot.id, msg.chatId);
|
|
2203
|
+
if (rt?.inflight !== void 0) {
|
|
2204
|
+
rt.agent.cancel();
|
|
2205
|
+
await msg.reply.notice("已请求停止当前任务");
|
|
2206
|
+
} else await msg.reply.notice("当前没有进行中的任务");
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (directive === "status") {
|
|
2210
|
+
const rt = this.deps.router.lookup(bot.id, msg.chatId);
|
|
2211
|
+
await msg.reply.notice(rt === void 0 ? `项目:${bot.project}\n会话:未创建(发送消息即创建)` : `项目:${bot.project}\n会话:${rt.sessionId}\n状态:${rt.inflight !== void 0 ? "处理中" : "空闲"}`);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const rt = await this.deps.router.ensure(bot, msg.chatId, msg.reply);
|
|
2215
|
+
if (rt.inflight !== void 0) {
|
|
2216
|
+
await msg.reply.notice("上一条还在处理中,请稍候(或发送 /stop 取消)");
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
rt.inflight = { ack: void 0 };
|
|
2220
|
+
rt.inflight.ack = await msg.ackProcessing().catch(() => void 0) ?? void 0;
|
|
2221
|
+
const message = createUserMessage({
|
|
2222
|
+
content: [{
|
|
2223
|
+
type: "text",
|
|
2224
|
+
text: msg.text
|
|
2225
|
+
}],
|
|
2226
|
+
source: { kind: "user" }
|
|
2227
|
+
});
|
|
2228
|
+
try {
|
|
2229
|
+
rt.agent.followup(message);
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
const ack = rt.inflight.ack;
|
|
2232
|
+
rt.inflight = void 0;
|
|
2233
|
+
await ack?.();
|
|
2234
|
+
throw error;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
/** 向段序列追加内容:与尾段同类则合并,异类开新段。 */
|
|
2239
|
+
function appendToSegments(segments, kind, text) {
|
|
2240
|
+
const tail = segments[segments.length - 1];
|
|
2241
|
+
if (tail !== void 0 && tail.kind === kind) tail.content += text;
|
|
2242
|
+
else segments.push({
|
|
2243
|
+
kind,
|
|
2244
|
+
content: text
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
function mapTurnEnd(reason) {
|
|
2248
|
+
if (reason.kind === "completed") return "done";
|
|
2249
|
+
if (reason.kind === "aborted" || reason.kind === "interrupted") return "cancelled";
|
|
2250
|
+
return "error";
|
|
2251
|
+
}
|
|
2252
|
+
/** 截断错误摘要到 max 字符,超出追加省略号。 */
|
|
2253
|
+
function truncateDetail(text, max) {
|
|
2254
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
2255
|
+
}
|
|
2256
|
+
/** 从 turn/end reason 提取错误摘要;非 error 或无 message 返回 undefined。 */
|
|
2257
|
+
function errorDetailOf(reason, max) {
|
|
2258
|
+
if (reason.kind !== "error") return void 0;
|
|
2259
|
+
const message = reason.error?.message;
|
|
2260
|
+
return truncateDetail(typeof message === "string" && message.length > 0 ? message : "未知错误", max);
|
|
2261
|
+
}
|
|
2262
|
+
var Outbound = class {
|
|
2263
|
+
sessions;
|
|
2264
|
+
onError;
|
|
2265
|
+
maxErrorDetailChars;
|
|
2266
|
+
constructor(sessions, onError, maxErrorDetailChars = 500) {
|
|
2267
|
+
this.sessions = sessions;
|
|
2268
|
+
this.onError = onError;
|
|
2269
|
+
this.maxErrorDetailChars = maxErrorDetailChars;
|
|
2270
|
+
}
|
|
2271
|
+
handleSessionEvent(sessionId, event) {
|
|
2272
|
+
const rt = this.sessions.get(sessionId);
|
|
2273
|
+
if (rt === void 0) return;
|
|
2274
|
+
if (event.type === "turn/start") {
|
|
2275
|
+
rt.turn = {
|
|
2276
|
+
n: event.data.turn,
|
|
2277
|
+
segments: [],
|
|
2278
|
+
began: false
|
|
2279
|
+
};
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
if (event.type === "assistant/chunk") {
|
|
2283
|
+
const turn = rt.turn;
|
|
2284
|
+
if (turn === void 0 || turn.n !== event.data.turn) return;
|
|
2285
|
+
const chunk = event.data.chunk;
|
|
2286
|
+
if (chunk.type === "text-delta" && typeof chunk.text === "string") appendToSegments(turn.segments, "text", chunk.text);
|
|
2287
|
+
else if (chunk.type === "reasoning-delta" && typeof chunk.text === "string") appendToSegments(turn.segments, "process", chunk.text);
|
|
2288
|
+
else if (chunk.type === "block-end" && chunk.block?.type === "reasoning" && turn.segments[turn.segments.length - 1]?.kind === "process") appendToSegments(turn.segments, "process", "\n\n");
|
|
2289
|
+
else return;
|
|
2290
|
+
const snapshot = turn.segments.map((s) => ({ ...s }));
|
|
2291
|
+
this.enqueue(rt, async () => {
|
|
2292
|
+
if (rt.reply === void 0) return;
|
|
2293
|
+
if (!turn.began) {
|
|
2294
|
+
await rt.reply.beginTurn();
|
|
2295
|
+
turn.began = true;
|
|
2296
|
+
}
|
|
2297
|
+
await rt.reply.update(snapshot);
|
|
2298
|
+
});
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
if (event.type === "tool/call") {
|
|
2302
|
+
const turn = rt.turn;
|
|
2303
|
+
if (turn === void 0 || turn.n !== event.data.turn) return;
|
|
2304
|
+
const name = event.data.name;
|
|
2305
|
+
const args = typeof event.data.arguments === "string" ? event.data.arguments : JSON.stringify(event.data.arguments ?? {});
|
|
2306
|
+
appendToSegments(turn.segments, "process", `🔧 ${name} — ${truncateDetail(args, 120)}\n\n`);
|
|
2307
|
+
const snapshot = turn.segments.map((s) => ({ ...s }));
|
|
2308
|
+
this.enqueue(rt, async () => {
|
|
2309
|
+
if (rt.reply === void 0) return;
|
|
2310
|
+
if (!turn.began) {
|
|
2311
|
+
await rt.reply.beginTurn();
|
|
2312
|
+
turn.began = true;
|
|
2313
|
+
}
|
|
2314
|
+
await rt.reply.update(snapshot);
|
|
2315
|
+
});
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
if (event.type === "turn/end") {
|
|
2319
|
+
const turn = rt.turn;
|
|
2320
|
+
if (turn === void 0 || turn.n !== event.data.turn) return;
|
|
2321
|
+
const reason = event.data.reason;
|
|
2322
|
+
const status = mapTurnEnd(reason);
|
|
2323
|
+
const detail = errorDetailOf(reason, this.maxErrorDetailChars);
|
|
2324
|
+
this.enqueue(rt, async () => {
|
|
2325
|
+
if ((turn.began || detail !== void 0) && rt.reply !== void 0) await rt.reply.finalize(status, detail);
|
|
2326
|
+
const ack = rt.inflight?.ack;
|
|
2327
|
+
rt.inflight = void 0;
|
|
2328
|
+
if (ack !== void 0) await ack();
|
|
2329
|
+
});
|
|
2330
|
+
rt.turn = void 0;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* turn 外错误(agent/error:resume/驱动边界失败等没有 turn/end 的场景):
|
|
2335
|
+
* notice 错误摘要并释放 inflight 槽 + 删除表情;turn 进行中的错误由 turn/end 报告,跳过防双发。
|
|
2336
|
+
*/
|
|
2337
|
+
handleAgentError(sessionId, errorText) {
|
|
2338
|
+
const rt = this.sessions.get(sessionId);
|
|
2339
|
+
if (rt === void 0 || rt.turn !== void 0) return;
|
|
2340
|
+
const detail = truncateDetail(errorText, this.maxErrorDetailChars);
|
|
2341
|
+
this.enqueue(rt, async () => {
|
|
2342
|
+
if (rt.reply !== void 0) await rt.reply.notice(`出错了:${detail}`);
|
|
2343
|
+
const ack = rt.inflight?.ack;
|
|
2344
|
+
rt.inflight = void 0;
|
|
2345
|
+
if (ack !== void 0) await ack();
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
enqueue(rt, task) {
|
|
2349
|
+
rt.tail = rt.tail.then(task).catch((error) => {
|
|
2350
|
+
this.onError(`[project-bot] 出站处理失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
//#endregion
|
|
2355
|
+
//#region src/channels/ports.ts
|
|
2356
|
+
/** 从 bot 记录提取创作期注入(主 Agent 绑定形态)。 */
|
|
2357
|
+
function hooksOf(bot) {
|
|
2358
|
+
return {
|
|
2359
|
+
...bot.persona !== void 0 ? { persona: bot.persona } : {},
|
|
2360
|
+
...bot.tools !== void 0 ? { tools: bot.tools } : {}
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
//#endregion
|
|
2364
|
+
//#region src/channels/router.ts
|
|
2365
|
+
/** 绑定路由:(botId, chatId) → 长期会话;create / resume / reset。 */
|
|
2366
|
+
var Router = class {
|
|
2367
|
+
agents;
|
|
2368
|
+
bindings;
|
|
2369
|
+
sessions;
|
|
2370
|
+
defaultModel;
|
|
2371
|
+
workspace;
|
|
2372
|
+
onWarn;
|
|
2373
|
+
registry;
|
|
2374
|
+
constructor(agents, bindings, sessions, defaultModel, workspace, onWarn, registry) {
|
|
2375
|
+
this.agents = agents;
|
|
2376
|
+
this.bindings = bindings;
|
|
2377
|
+
this.sessions = sessions;
|
|
2378
|
+
this.defaultModel = defaultModel;
|
|
2379
|
+
this.workspace = workspace;
|
|
2380
|
+
this.onWarn = onWarn;
|
|
2381
|
+
this.registry = registry;
|
|
2382
|
+
}
|
|
2383
|
+
/** 取(或建/恢复)该 chat 的会话 runtime;reply 刷新为最近一次入站携带的句柄。 */
|
|
2384
|
+
async ensure(bot, chatId, reply) {
|
|
2385
|
+
const bound = this.bindings.get(bot.id, chatId);
|
|
2386
|
+
if (bound !== void 0) {
|
|
2387
|
+
const existing = this.sessions.get(bound);
|
|
2388
|
+
if (existing !== void 0) {
|
|
2389
|
+
existing.reply = reply;
|
|
2390
|
+
return existing;
|
|
2391
|
+
}
|
|
2392
|
+
const agent = await this.agents.resume({
|
|
2393
|
+
sessionId: bound,
|
|
2394
|
+
...this.resolveSession(bot)
|
|
2395
|
+
});
|
|
2396
|
+
await this.attach(bot.project, bound);
|
|
2397
|
+
return this.adopt(bot.id, chatId, bound, agent, reply);
|
|
2398
|
+
}
|
|
2399
|
+
const sessionId = randomUUID();
|
|
2400
|
+
const agent = await this.agents.create({
|
|
2401
|
+
sessionId,
|
|
2402
|
+
cwd: bot.project,
|
|
2403
|
+
...this.resolveSession(bot)
|
|
2404
|
+
});
|
|
2405
|
+
await this.bindings.set(bot.id, chatId, sessionId);
|
|
2406
|
+
await this.attach(bot.project, sessionId);
|
|
2407
|
+
return this.adopt(bot.id, chatId, sessionId, agent, reply);
|
|
2408
|
+
}
|
|
2409
|
+
/** attach 失败仅告警(会话降级为未分组),不阻塞消息处理。 */
|
|
2410
|
+
async attach(cwd, sessionId) {
|
|
2411
|
+
try {
|
|
2412
|
+
await this.workspace.attach(cwd, sessionId);
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
this.onWarn(`[project-bot] 会话 ${sessionId} 挂载 workspace 失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
/** 有 agentOptions 原样透传;无则回退宿主默认模型(存量 bot 不抛 no provider/model)。 */
|
|
2418
|
+
resolveOptions(bot) {
|
|
2419
|
+
return bot.agentOptions ?? this.defaultModel();
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* 按 bot.agentRef 解析会话组装(agentOptions + 创作期 hooks):
|
|
2423
|
+
* - 缺省/指向 main → 主 Agent 形态:bot 自带 persona/tools + 默认模型回退;
|
|
2424
|
+
* - 指向角色 → 角色形态:persona 单 section + tools.restrict + role.model;
|
|
2425
|
+
* - 指向不存在角色 → warn 并降级为主 Agent 形态。
|
|
2426
|
+
*/
|
|
2427
|
+
resolveSession(bot) {
|
|
2428
|
+
const ref = bot.agentRef ?? "main";
|
|
2429
|
+
const role = this.registry.get(ref);
|
|
2430
|
+
if (role === void 0 || ref === "main") {
|
|
2431
|
+
if (role === void 0 && ref !== "main") this.onWarn(`[project-bot] bot "${bot.id}" 的 agentRef "${ref}" 不存在,降级绑定主 Agent`);
|
|
2432
|
+
return {
|
|
2433
|
+
agentOptions: this.resolveOptions(bot),
|
|
2434
|
+
hooks: hooksOf(bot)
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
const sections = role.persona === void 0 || role.persona.trim().length === 0 ? [] : [{
|
|
2438
|
+
name: "dsh-agent-toolkit:agent:persona",
|
|
2439
|
+
order: 0,
|
|
2440
|
+
text: role.persona
|
|
2441
|
+
}];
|
|
2442
|
+
return {
|
|
2443
|
+
agentOptions: role.model ?? this.resolveOptions(bot),
|
|
2444
|
+
hooks: {
|
|
2445
|
+
...sections.length > 0 ? { sections } : {},
|
|
2446
|
+
...role.tools !== void 0 ? { tools: role.tools.allow } : {}
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
/** /new:取消旧会话、清绑定、开新会话。 */
|
|
2451
|
+
async reset(bot, chatId, reply) {
|
|
2452
|
+
const bound = this.bindings.get(bot.id, chatId);
|
|
2453
|
+
if (bound !== void 0) {
|
|
2454
|
+
this.sessions.get(bound)?.agent.cancel();
|
|
2455
|
+
this.sessions.delete(bound);
|
|
2456
|
+
await this.bindings.delete(bot.id, chatId);
|
|
2457
|
+
}
|
|
2458
|
+
return this.ensure(bot, chatId, reply);
|
|
2459
|
+
}
|
|
2460
|
+
lookup(botId, chatId) {
|
|
2461
|
+
const bound = this.bindings.get(botId, chatId);
|
|
2462
|
+
return bound === void 0 ? void 0 : this.sessions.get(bound);
|
|
2463
|
+
}
|
|
2464
|
+
adopt(botId, chatId, sessionId, agent, reply) {
|
|
2465
|
+
const rt = {
|
|
2466
|
+
botId,
|
|
2467
|
+
chatId,
|
|
2468
|
+
sessionId,
|
|
2469
|
+
agent,
|
|
2470
|
+
reply,
|
|
2471
|
+
inflight: void 0,
|
|
2472
|
+
tail: Promise.resolve(),
|
|
2473
|
+
turn: void 0
|
|
2474
|
+
};
|
|
2475
|
+
this.sessions.set(sessionId, rt);
|
|
2476
|
+
return rt;
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
//#endregion
|
|
2480
|
+
//#region src/channels/runtime.ts
|
|
2481
|
+
var BotRuntime = class {
|
|
2482
|
+
deps;
|
|
2483
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2484
|
+
router;
|
|
2485
|
+
inbound;
|
|
2486
|
+
outbound;
|
|
2487
|
+
handles = /* @__PURE__ */ new Map();
|
|
2488
|
+
constructor(deps) {
|
|
2489
|
+
this.deps = deps;
|
|
2490
|
+
const bindingStore = this.bindingStore();
|
|
2491
|
+
this.router = new Router(deps.agents, bindingStore, this.sessions, deps.defaultModel, deps.workspace, (m) => deps.log.warn(m), deps.registry);
|
|
2492
|
+
this.inbound = new Inbound({
|
|
2493
|
+
router: this.router,
|
|
2494
|
+
bots: deps.bots,
|
|
2495
|
+
onError: (m) => deps.log.warn(m)
|
|
2496
|
+
});
|
|
2497
|
+
this.outbound = new Outbound(this.sessions, (m) => deps.log.warn(m), deps.maxErrorDetailChars);
|
|
2498
|
+
}
|
|
2499
|
+
async startAll() {
|
|
2500
|
+
for (const botId of [...this.deps.bots.keys()]) await this.reconcile(botId);
|
|
2501
|
+
}
|
|
2502
|
+
/** 按最新记录重建该 bot 的渠道(创建/更新后调用;记录已删则纯停止)。 */
|
|
2503
|
+
async reconcile(botId) {
|
|
2504
|
+
await this.stopChannel(botId);
|
|
2505
|
+
const record = this.deps.bots.get(botId);
|
|
2506
|
+
if (record === void 0) return;
|
|
2507
|
+
if (!this.deps.validateProject(record.project)) {
|
|
2508
|
+
this.deps.log.warn(`[project-bot] bot "${botId}" 的项目路径不可用:${record.project}`);
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
const secret = await this.deps.resolveSecret(record.feishu.appSecretRef);
|
|
2512
|
+
if (secret === void 0) {
|
|
2513
|
+
this.deps.log.warn(`[project-bot] bot "${botId}" 的密钥 ${record.feishu.appSecretRef} 未配置`);
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
const channel = this.deps.channels.get(record.channel);
|
|
2517
|
+
if (channel === void 0) {
|
|
2518
|
+
this.deps.log.warn(`[project-bot] bot "${botId}" 的渠道 "${record.channel}" 未实现`);
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
const handle = await channel.start({
|
|
2523
|
+
record,
|
|
2524
|
+
secret
|
|
2525
|
+
}, { onMessage: (msg) => this.inbound.onMessage(msg) }, this.deps.tunables, (m) => this.deps.log.warn(m));
|
|
2526
|
+
this.handles.set(botId, handle);
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
this.deps.log.warn(`[project-bot] bot "${botId}" 渠道启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
/** 删除 bot:停渠道、取消会话、清绑定。 */
|
|
2532
|
+
async stopBot(botId) {
|
|
2533
|
+
await this.stopChannel(botId);
|
|
2534
|
+
for (const [sessionId, rt] of [...this.sessions]) if (rt.botId === botId) {
|
|
2535
|
+
rt.agent.cancel();
|
|
2536
|
+
this.sessions.delete(sessionId);
|
|
2537
|
+
}
|
|
2538
|
+
await this.bindingStore().deleteBot(botId);
|
|
2539
|
+
}
|
|
2540
|
+
statusOf(botId) {
|
|
2541
|
+
return this.handles.get(botId)?.status() ?? "not-running";
|
|
2542
|
+
}
|
|
2543
|
+
/** 卸载时序:取消在飞会话 → 等 idle → drain 出站链(卡片定格)→ 断全部渠道。 */
|
|
2544
|
+
async stopAll() {
|
|
2545
|
+
for (const rt of this.sessions.values()) rt.agent.cancel();
|
|
2546
|
+
await Promise.allSettled([...this.sessions.values()].map(async (rt) => {
|
|
2547
|
+
await rt.agent.whenIdle().catch(() => void 0);
|
|
2548
|
+
await rt.tail;
|
|
2549
|
+
}));
|
|
2550
|
+
await Promise.allSettled([...this.handles.values()].map((h) => h.close()));
|
|
2551
|
+
this.handles.clear();
|
|
2552
|
+
}
|
|
2553
|
+
async stopChannel(botId) {
|
|
2554
|
+
const handle = this.handles.get(botId);
|
|
2555
|
+
if (handle === void 0) return;
|
|
2556
|
+
this.handles.delete(botId);
|
|
2557
|
+
await handle.close().catch((error) => {
|
|
2558
|
+
this.deps.log.warn(`[project-bot] bot "${botId}" 渠道关闭异常:${error instanceof Error ? error.message : String(error)}`);
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
bindingStore() {
|
|
2562
|
+
const { bindings } = this.deps;
|
|
2563
|
+
return {
|
|
2564
|
+
get: (b, c) => bindings.get(bindingKey(b, c))?.sessionId,
|
|
2565
|
+
set: async (b, c, s) => {
|
|
2566
|
+
await bindings.put(bindingKey(b, c), { sessionId: s });
|
|
2567
|
+
},
|
|
2568
|
+
delete: async (b, c) => {
|
|
2569
|
+
await bindings.delete(bindingKey(b, c));
|
|
2570
|
+
},
|
|
2571
|
+
deleteBot: async (b) => {
|
|
2572
|
+
for (const key of [...bindings.keys()]) if (key.startsWith(`${b}:`)) await bindings.delete(key);
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
//#endregion
|
|
2578
|
+
//#region src/bots/api.ts
|
|
2579
|
+
/** 浏览器半 RPC:单前缀路由 /dsh-agent-toolkit/api/bots + 内部路径分发。 */
|
|
2580
|
+
const MAX_BODY_BYTES = 65536;
|
|
2581
|
+
const CreateBodySchema = z$1.object({
|
|
2582
|
+
/** 缺省时后端自动生成(bot-<8 位随机小写字母数字>)。 */
|
|
2583
|
+
id: z$1.string().regex(BOT_ID_RE).optional(),
|
|
2584
|
+
name: z$1.string().min(1).max(64),
|
|
2585
|
+
project: z$1.string().min(1),
|
|
2586
|
+
persona: z$1.string().max(8e3).optional(),
|
|
2587
|
+
/** 绑定的 Agent('main' 或注册表角色 id;缺省 = main)。 */
|
|
2588
|
+
agentRef: z$1.string().min(1).optional(),
|
|
2589
|
+
tools: z$1.array(z$1.string().min(1)).min(1).optional(),
|
|
2590
|
+
agentOptions: z$1.object({
|
|
2591
|
+
provider: z$1.string().min(1).optional(),
|
|
2592
|
+
model: z$1.string().min(1).optional()
|
|
2593
|
+
}).optional(),
|
|
2594
|
+
feishu: z$1.object({
|
|
2595
|
+
appId: z$1.string().regex(FEISHU_APP_ID_RE),
|
|
2596
|
+
/** 手动填写路径:明文密钥(立即入 credentials,不落表)。 */
|
|
2597
|
+
appSecret: z$1.string().min(1).optional(),
|
|
2598
|
+
/** 扫码路径:registerApp 已入库,直接给引用。 */
|
|
2599
|
+
appSecretRef: z$1.string().optional()
|
|
2600
|
+
})
|
|
2601
|
+
});
|
|
2602
|
+
const UpdateBodySchema = z$1.object({
|
|
2603
|
+
name: z$1.string().min(1).max(64).optional(),
|
|
2604
|
+
project: z$1.string().min(1).optional(),
|
|
2605
|
+
persona: z$1.string().max(8e3).nullable().optional(),
|
|
2606
|
+
agentRef: z$1.string().min(1).nullable().optional(),
|
|
2607
|
+
tools: z$1.array(z$1.string().min(1)).min(1).nullable().optional(),
|
|
2608
|
+
agentOptions: z$1.object({
|
|
2609
|
+
provider: z$1.string().min(1).optional(),
|
|
2610
|
+
model: z$1.string().min(1).optional()
|
|
2611
|
+
}).nullable().optional(),
|
|
2612
|
+
/** 换绑应用:明文新密钥(立即入 credentials)。 */
|
|
2613
|
+
feishu: z$1.object({
|
|
2614
|
+
appId: z$1.string().regex(FEISHU_APP_ID_RE),
|
|
2615
|
+
appSecret: z$1.string().min(1)
|
|
2616
|
+
}).optional()
|
|
2617
|
+
});
|
|
2618
|
+
function json(res, code, body) {
|
|
2619
|
+
res.writeHead(code, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
2620
|
+
}
|
|
2621
|
+
const ID_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
2622
|
+
/** 生成 bot-<8 位随机小写字母数字>(符合 BOT_ID_RE);与现有 id 冲突时重试。 */
|
|
2623
|
+
function generateBotId(occupied) {
|
|
2624
|
+
for (;;) {
|
|
2625
|
+
const bytes = randomBytes(8);
|
|
2626
|
+
let id = "bot-";
|
|
2627
|
+
for (let i = 0; i < 8; i++) id += ID_CHARS[bytes[i] % 36];
|
|
2628
|
+
if (!occupied(id)) return id;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
/** 读 JSON body;超限 413 / 非法 JSON 400(已写响应时返回 undefined)。 */
|
|
2632
|
+
async function readJsonBody(req, res) {
|
|
2633
|
+
const chunks = [];
|
|
2634
|
+
let received = 0;
|
|
2635
|
+
for await (const chunk of req) {
|
|
2636
|
+
const buffer = chunk;
|
|
2637
|
+
received += buffer.byteLength;
|
|
2638
|
+
if (received > MAX_BODY_BYTES) {
|
|
2639
|
+
json(res, 413, { error: "body too large" });
|
|
2640
|
+
req.destroy();
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
chunks.push(buffer);
|
|
2644
|
+
}
|
|
2645
|
+
try {
|
|
2646
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2647
|
+
} catch {
|
|
2648
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
function createApiHandler(deps) {
|
|
2653
|
+
return async (req, res) => {
|
|
2654
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2655
|
+
const sub = url.pathname.replace(/^\/dsh-agent-toolkit\/api\/bots/, "") || "/";
|
|
2656
|
+
const method = req.method ?? "GET";
|
|
2657
|
+
if (sub === "/bots" && method === "GET") {
|
|
2658
|
+
json(res, 200, { bots: [...deps.bots.entries()].map(([, record]) => ({
|
|
2659
|
+
...record,
|
|
2660
|
+
status: deps.runtime.statusOf(record.id)
|
|
2661
|
+
})) });
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
if (sub === "/bots" && method === "POST") {
|
|
2665
|
+
const body = await readJsonBody(req, res);
|
|
2666
|
+
if (body === void 0) return;
|
|
2667
|
+
const parsed = CreateBodySchema.safeParse(body);
|
|
2668
|
+
if (!parsed.success) {
|
|
2669
|
+
json(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid body" });
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const input = parsed.data;
|
|
2673
|
+
const id = input.id ?? generateBotId((candidate) => deps.bots.get(candidate) !== void 0);
|
|
2674
|
+
if (deps.bots.get(id) !== void 0) {
|
|
2675
|
+
json(res, 409, { error: `bot id "${id}" 已存在` });
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
for (const [, existing] of deps.bots.entries()) if (existing.feishu.appId === input.feishu.appId) {
|
|
2679
|
+
json(res, 409, { error: `appId 已被 bot "${existing.id}" 使用` });
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
if (!deps.validateProject(input.project)) {
|
|
2683
|
+
json(res, 400, { error: `项目路径不可用:${input.project}` });
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
let appSecretRef = input.feishu.appSecretRef;
|
|
2687
|
+
if (appSecretRef === void 0) {
|
|
2688
|
+
if (input.feishu.appSecret === void 0) {
|
|
2689
|
+
json(res, 400, { error: "缺少 appSecret 或 appSecretRef" });
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
appSecretRef = await deps.storeSecret(id, input.feishu.appSecret);
|
|
2693
|
+
}
|
|
2694
|
+
const record = BotRecordSchema.parse({
|
|
2695
|
+
id,
|
|
2696
|
+
name: input.name,
|
|
2697
|
+
channel: "feishu",
|
|
2698
|
+
feishu: {
|
|
2699
|
+
appId: input.feishu.appId,
|
|
2700
|
+
appSecretRef
|
|
2701
|
+
},
|
|
2702
|
+
project: input.project,
|
|
2703
|
+
...input.persona !== void 0 ? { persona: input.persona } : {},
|
|
2704
|
+
...input.agentRef !== void 0 ? { agentRef: input.agentRef } : {},
|
|
2705
|
+
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
2706
|
+
...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
|
|
2707
|
+
createdAt: deps.now(),
|
|
2708
|
+
updatedAt: deps.now()
|
|
2709
|
+
});
|
|
2710
|
+
await deps.bots.put(record.id, record);
|
|
2711
|
+
await deps.runtime.reconcile(record.id);
|
|
2712
|
+
json(res, 200, { bot: {
|
|
2713
|
+
...record,
|
|
2714
|
+
status: deps.runtime.statusOf(record.id)
|
|
2715
|
+
} });
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
if (sub === "/bots" && method === "PUT") {
|
|
2719
|
+
const id = url.searchParams.get("id") ?? "";
|
|
2720
|
+
const existing = deps.bots.get(id);
|
|
2721
|
+
if (existing === void 0) {
|
|
2722
|
+
json(res, 404, { error: `bot "${id}" 不存在` });
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
const body = await readJsonBody(req, res);
|
|
2726
|
+
if (body === void 0) return;
|
|
2727
|
+
const parsed = UpdateBodySchema.safeParse(body);
|
|
2728
|
+
if (!parsed.success) {
|
|
2729
|
+
json(res, 400, { error: parsed.error.issues[0]?.message ?? "invalid body" });
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const input = parsed.data;
|
|
2733
|
+
let feishu = existing.feishu;
|
|
2734
|
+
if (input.feishu !== void 0) feishu = {
|
|
2735
|
+
appId: input.feishu.appId,
|
|
2736
|
+
appSecretRef: await deps.storeSecret(id, input.feishu.appSecret)
|
|
2737
|
+
};
|
|
2738
|
+
const project = input.project ?? existing.project;
|
|
2739
|
+
if (!deps.validateProject(project)) {
|
|
2740
|
+
json(res, 400, { error: `项目路径不可用:${project}` });
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
const merged = {
|
|
2744
|
+
...existing,
|
|
2745
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
2746
|
+
project,
|
|
2747
|
+
feishu,
|
|
2748
|
+
updatedAt: deps.now()
|
|
2749
|
+
};
|
|
2750
|
+
if (input.persona === null) delete merged.persona;
|
|
2751
|
+
else if (input.persona !== void 0) merged.persona = input.persona;
|
|
2752
|
+
if (input.agentRef === null) delete merged.agentRef;
|
|
2753
|
+
else if (input.agentRef !== void 0) merged.agentRef = input.agentRef;
|
|
2754
|
+
if (input.tools === null) delete merged.tools;
|
|
2755
|
+
else if (input.tools !== void 0) merged.tools = input.tools;
|
|
2756
|
+
if (input.agentOptions === null) delete merged.agentOptions;
|
|
2757
|
+
else if (input.agentOptions !== void 0) merged.agentOptions = input.agentOptions;
|
|
2758
|
+
const record = BotRecordSchema.parse(merged);
|
|
2759
|
+
await deps.bots.put(id, record);
|
|
2760
|
+
await deps.runtime.reconcile(id);
|
|
2761
|
+
json(res, 200, { bot: {
|
|
2762
|
+
...record,
|
|
2763
|
+
status: deps.runtime.statusOf(id)
|
|
2764
|
+
} });
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
if (sub === "/bots" && method === "DELETE") {
|
|
2768
|
+
const id = url.searchParams.get("id") ?? "";
|
|
2769
|
+
const existing = deps.bots.get(id);
|
|
2770
|
+
if (existing === void 0) {
|
|
2771
|
+
json(res, 404, { error: `bot "${id}" 不存在` });
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
await deps.runtime.stopBot(id);
|
|
2775
|
+
await deps.bots.delete(id);
|
|
2776
|
+
await deps.deleteSecret(existing.feishu.appSecretRef);
|
|
2777
|
+
json(res, 200, { ok: true });
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
if (sub === "/register-app" && method === "POST") {
|
|
2781
|
+
json(res, 200, { id: deps.registerApp.start() });
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
if (sub === "/register-app/status" && method === "GET") {
|
|
2785
|
+
const id = url.searchParams.get("id") ?? "";
|
|
2786
|
+
const state = deps.registerApp.get(id);
|
|
2787
|
+
if (state === void 0) {
|
|
2788
|
+
json(res, 404, { error: "register session 不存在" });
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
json(res, 200, { state });
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
if (sub === "/tools" && method === "GET") {
|
|
2795
|
+
json(res, 200, { tools: deps.listTools() });
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (sub === "/providers" && method === "GET") {
|
|
2799
|
+
json(res, 200, { providers: deps.listProviders() });
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
if (sub === "/models" && method === "GET") {
|
|
2803
|
+
let models = [];
|
|
2804
|
+
try {
|
|
2805
|
+
models = await deps.listModels(url.searchParams.get("provider") ?? "");
|
|
2806
|
+
} catch {
|
|
2807
|
+
models = [];
|
|
2808
|
+
}
|
|
2809
|
+
json(res, 200, { models });
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
if ([
|
|
2813
|
+
"/bots",
|
|
2814
|
+
"/register-app",
|
|
2815
|
+
"/register-app/status",
|
|
2816
|
+
"/tools",
|
|
2817
|
+
"/providers",
|
|
2818
|
+
"/models"
|
|
2819
|
+
].includes(sub)) {
|
|
2820
|
+
json(res, 405, { error: "method not allowed" });
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
json(res, 404, { error: "not found" });
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
//#endregion
|
|
2827
|
+
//#region src/bots/register-app.ts
|
|
2828
|
+
/** 扫码一键创建飞书应用:lark.registerApp(OAuth 2.0 Device Authorization Grant)的状态机封装。 */
|
|
2829
|
+
var RegisterAppService = class {
|
|
2830
|
+
deps;
|
|
2831
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2832
|
+
constructor(deps) {
|
|
2833
|
+
this.deps = deps;
|
|
2834
|
+
}
|
|
2835
|
+
/** 发起一轮扫码创建;返回轮询 id。 */
|
|
2836
|
+
start() {
|
|
2837
|
+
const id = (this.deps.newId ?? randomUUID)();
|
|
2838
|
+
const controller = new AbortController();
|
|
2839
|
+
const entry = {
|
|
2840
|
+
state: { status: "pending" },
|
|
2841
|
+
controller,
|
|
2842
|
+
timer: setTimeout(() => {
|
|
2843
|
+
controller.abort();
|
|
2844
|
+
}, this.deps.timeoutMs)
|
|
2845
|
+
};
|
|
2846
|
+
this.sessions.set(id, entry);
|
|
2847
|
+
this.deps.registerApp({
|
|
2848
|
+
signal: controller.signal,
|
|
2849
|
+
onQRCodeReady: (info) => {
|
|
2850
|
+
entry.state = {
|
|
2851
|
+
status: "pending",
|
|
2852
|
+
url: info.url,
|
|
2853
|
+
expireIn: info.expireIn
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
}).then(async (result) => {
|
|
2857
|
+
const credentialRef = await this.deps.storeSecret(result.client_id, result.client_secret);
|
|
2858
|
+
entry.state = {
|
|
2859
|
+
status: "done",
|
|
2860
|
+
appId: result.client_id,
|
|
2861
|
+
credentialRef
|
|
2862
|
+
};
|
|
2863
|
+
}).catch((error) => {
|
|
2864
|
+
const e = error;
|
|
2865
|
+
entry.state = {
|
|
2866
|
+
status: "error",
|
|
2867
|
+
code: typeof e.code === "string" ? e.code : "unknown",
|
|
2868
|
+
...typeof e.description === "string" ? { description: e.description } : {}
|
|
2869
|
+
};
|
|
2870
|
+
}).finally(() => {
|
|
2871
|
+
clearTimeout(entry.timer);
|
|
2872
|
+
});
|
|
2873
|
+
return id;
|
|
2874
|
+
}
|
|
2875
|
+
get(id) {
|
|
2876
|
+
return this.sessions.get(id)?.state;
|
|
2877
|
+
}
|
|
2878
|
+
/** 卸载:中断全部进行中的轮询。 */
|
|
2879
|
+
dispose() {
|
|
2880
|
+
for (const entry of this.sessions.values()) {
|
|
2881
|
+
entry.controller.abort();
|
|
2882
|
+
clearTimeout(entry.timer);
|
|
2883
|
+
}
|
|
2884
|
+
this.sessions.clear();
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
//#endregion
|
|
2888
|
+
//#region src/bots/index.ts
|
|
2889
|
+
/** bots 模块:项目机器人(飞书渠道)——多 bot 作为项目 agent 的交互入口(project-bot Node 半迁移,去 preset 化)。 */
|
|
2890
|
+
function setupBots(ctx, config, deps) {
|
|
2891
|
+
const log = {
|
|
2892
|
+
warn: (m) => ctx.logger.warn(m),
|
|
2893
|
+
info: (m) => ctx.logger.info(m)
|
|
2894
|
+
};
|
|
2895
|
+
const channels = /* @__PURE__ */ new Map([["feishu", feishuChannel]]);
|
|
2896
|
+
const tunables = {
|
|
2897
|
+
cardUpdateThrottleMs: config.cardUpdateThrottleMs,
|
|
2898
|
+
cardMaxBytes: config.cardMaxBytes,
|
|
2899
|
+
processMaxBytes: config.processMaxBytes,
|
|
2900
|
+
processingReactionEmoji: config.processingReactionEmoji
|
|
2901
|
+
};
|
|
2902
|
+
const storeSecret = async (key, secret) => {
|
|
2903
|
+
const ref = `project_bot_${key.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
2904
|
+
await ctx.credentials.set(credentialRef(ref), secret);
|
|
2905
|
+
return ref;
|
|
2906
|
+
};
|
|
2907
|
+
/** 创作期注入已迁至 agent-setup.ts(基础工具 scoped 挂载 + persona/tools),preset 机制整体移除。 */
|
|
2908
|
+
const agentsPort = {
|
|
2909
|
+
async create(input) {
|
|
2910
|
+
return adaptAgent(await ctx.agents.create({
|
|
2911
|
+
sessionId: SessionId(input.sessionId),
|
|
2912
|
+
meta: { cwd: input.cwd },
|
|
2913
|
+
...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
|
|
2914
|
+
setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks)
|
|
2915
|
+
}));
|
|
2916
|
+
},
|
|
2917
|
+
async resume(input) {
|
|
2918
|
+
return adaptAgent(await ctx.agents.resume({
|
|
2919
|
+
resumeSessionId: SessionId(input.sessionId),
|
|
2920
|
+
...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
|
|
2921
|
+
setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks)
|
|
2922
|
+
}));
|
|
2923
|
+
}
|
|
2924
|
+
};
|
|
2925
|
+
function adaptAgent(handle) {
|
|
2926
|
+
const { agent } = handle;
|
|
2927
|
+
return {
|
|
2928
|
+
sessionId: String(agent.id),
|
|
2929
|
+
followup: (message) => agent.followup(message),
|
|
2930
|
+
cancel: () => agent.cancel({ kind: "user" }),
|
|
2931
|
+
whenIdle: () => agent.whenIdle()
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
const workspaceRegistry = ctx.get("workspaceRegistry", false);
|
|
2935
|
+
const workspacePort = { async attach(cwd, sessionId) {
|
|
2936
|
+
if (workspaceRegistry === void 0) throw new Error("workspaceRegistry 服务不可用");
|
|
2937
|
+
await (await workspaceRegistry.create(cwd)).attachSession(SessionId(sessionId));
|
|
2938
|
+
} };
|
|
2939
|
+
let botsTable;
|
|
2940
|
+
let bindingsTable;
|
|
2941
|
+
let runtime;
|
|
2942
|
+
let started = Promise.resolve();
|
|
2943
|
+
const domainReady = openDomainSafely(ctx, projectBotDomain, (msg) => log.warn(msg), async () => {
|
|
2944
|
+
await started.catch(() => void 0);
|
|
2945
|
+
await runtime?.stopAll();
|
|
2946
|
+
}).then((domain) => {
|
|
2947
|
+
botsTable = domain.table("bots");
|
|
2948
|
+
bindingsTable = domain.table("bindings");
|
|
2949
|
+
return domain;
|
|
2950
|
+
});
|
|
2951
|
+
const registerAppService = new RegisterAppService({
|
|
2952
|
+
registerApp: (options) => import("@larksuiteoapi/node-sdk").then((lark) => lark.registerApp(options)),
|
|
2953
|
+
storeSecret,
|
|
2954
|
+
timeoutMs: config.registerAppTimeoutMs
|
|
2955
|
+
});
|
|
2956
|
+
started = domainReady.then(() => {
|
|
2957
|
+
runtime = new BotRuntime({
|
|
2958
|
+
bots: botsTable,
|
|
2959
|
+
bindings: bindingsTable,
|
|
2960
|
+
agents: agentsPort,
|
|
2961
|
+
registry: deps.registry,
|
|
2962
|
+
defaultModel: () => {
|
|
2963
|
+
const selection = ctx.agentDefaultModel.currentSelection();
|
|
2964
|
+
return {
|
|
2965
|
+
provider: selection.provider,
|
|
2966
|
+
model: selection.model
|
|
2967
|
+
};
|
|
2968
|
+
},
|
|
2969
|
+
workspace: workspacePort,
|
|
2970
|
+
channels,
|
|
2971
|
+
tunables,
|
|
2972
|
+
maxErrorDetailChars: config.errorDetailMaxChars,
|
|
2973
|
+
resolveSecret: async (ref) => (await ctx.credentials.resolve(credentialRef(ref)))?.value,
|
|
2974
|
+
validateProject: (path) => existsSync(path),
|
|
2975
|
+
log
|
|
2976
|
+
});
|
|
2977
|
+
return runtime.startAll();
|
|
2978
|
+
});
|
|
2979
|
+
started.catch((error) => {
|
|
2980
|
+
log.warn(`[project-bot] 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2981
|
+
});
|
|
2982
|
+
ctx.on("session/event", (session, event) => {
|
|
2983
|
+
runtime?.outbound.handleSessionEvent(String(session.header.id), event);
|
|
2984
|
+
});
|
|
2985
|
+
ctx.on("agent/error", ({ agent, error }) => {
|
|
2986
|
+
const text = error instanceof Error ? error.message : String(error?.message ?? error);
|
|
2987
|
+
runtime?.outbound.handleAgentError(String(agent.session.id), text);
|
|
2988
|
+
});
|
|
2989
|
+
registerOptionalRoutes(ctx, (webCtx) => {
|
|
2990
|
+
const botsHandler = async (req, res) => {
|
|
2991
|
+
if (runtime === void 0) throw new Error("runtime unavailable");
|
|
2992
|
+
await createApiHandler({
|
|
2993
|
+
bots: botsTable,
|
|
2994
|
+
runtime,
|
|
2995
|
+
registerApp: registerAppService,
|
|
2996
|
+
listTools: () => ctx.tools.schemas().map((s) => s.name),
|
|
2997
|
+
listProviders: () => ctx.llm.listProviders().map(({ id, name }) => ({
|
|
2998
|
+
id,
|
|
2999
|
+
name
|
|
3000
|
+
})),
|
|
3001
|
+
listModels: (provider) => ctx.llm.listModels(provider).then((models) => models.map(({ id, name }) => ({
|
|
3002
|
+
id,
|
|
3003
|
+
name
|
|
3004
|
+
}))),
|
|
3005
|
+
storeSecret,
|
|
3006
|
+
deleteSecret: async (ref) => ctx.credentials.unset(credentialRef(ref)),
|
|
3007
|
+
validateProject: (path) => existsSync(path),
|
|
3008
|
+
now: () => Date.now()
|
|
3009
|
+
})(req, res);
|
|
3010
|
+
};
|
|
3011
|
+
const dispose = webCtx.webServer.register({
|
|
3012
|
+
kind: "prefix",
|
|
3013
|
+
path: "/dsh-agent-toolkit/api/bots",
|
|
3014
|
+
handler: async (req, res) => {
|
|
3015
|
+
try {
|
|
3016
|
+
await started;
|
|
3017
|
+
await botsHandler(req, res);
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
res.writeHead(500, { "content-type": "application/json" }).end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
return () => dispose();
|
|
3024
|
+
});
|
|
3025
|
+
ctx.effect(() => async () => {
|
|
3026
|
+
registerAppService.dispose();
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
//#endregion
|
|
3030
|
+
//#region src/index.ts
|
|
3031
|
+
const name = "dsh-agent-toolkit";
|
|
3032
|
+
const inject = [
|
|
3033
|
+
"storageDomain",
|
|
3034
|
+
"tools",
|
|
3035
|
+
"subagents",
|
|
3036
|
+
"systemPrompt",
|
|
3037
|
+
"commands",
|
|
3038
|
+
"llm",
|
|
3039
|
+
"agentDefaultModel",
|
|
3040
|
+
"agents",
|
|
3041
|
+
"tokenMeter",
|
|
3042
|
+
"credentials"
|
|
3043
|
+
];
|
|
3044
|
+
/** layers/rules 的 schemastery schema 照归档 prompt-stack/src/index.ts:21-41 逐字段平移(含 overrides transform hack)。 */
|
|
3045
|
+
const Config = z.object({
|
|
3046
|
+
modules: z.object({
|
|
3047
|
+
feishu: z.boolean().default(true),
|
|
3048
|
+
usage: z.boolean().default(true)
|
|
3049
|
+
}).default({
|
|
3050
|
+
feishu: true,
|
|
3051
|
+
usage: true
|
|
3052
|
+
}),
|
|
3053
|
+
layers: z.array(z.object({
|
|
3054
|
+
name: z.string().required(),
|
|
3055
|
+
order: z.number().required(),
|
|
3056
|
+
text: z.string().required()
|
|
3057
|
+
})).default(DEFAULT_LAYERS),
|
|
3058
|
+
rules: z.array(z.object({
|
|
3059
|
+
match: z.object({
|
|
3060
|
+
provider: z.string(),
|
|
3061
|
+
model: z.string(),
|
|
3062
|
+
modelPattern: z.string()
|
|
3063
|
+
}).required(),
|
|
3064
|
+
overrides: z.transform(z.dict(z.string()), (value) => Object.keys(value).length === 0 ? void 0 : value),
|
|
3065
|
+
append: z.string()
|
|
3066
|
+
})).default(DEFAULT_RULES),
|
|
3067
|
+
timezone: z.string().default("Asia/Shanghai"),
|
|
3068
|
+
provider: z.string().default("spawn"),
|
|
3069
|
+
toolName: z.string().default("team_delegate"),
|
|
3070
|
+
feishu: z.object({
|
|
3071
|
+
cardUpdateThrottleMs: z.number().default(500),
|
|
3072
|
+
cardMaxBytes: z.number().default(28e3),
|
|
3073
|
+
processMaxBytes: z.number().default(8e3),
|
|
3074
|
+
registerAppTimeoutMs: z.number().default(6e5),
|
|
3075
|
+
processingReactionEmoji: z.string().default("OneSecond"),
|
|
3076
|
+
errorDetailMaxChars: z.number().default(500)
|
|
3077
|
+
}).default({
|
|
3078
|
+
cardUpdateThrottleMs: 500,
|
|
3079
|
+
cardMaxBytes: 28e3,
|
|
3080
|
+
processMaxBytes: 8e3,
|
|
3081
|
+
registerAppTimeoutMs: 6e5,
|
|
3082
|
+
processingReactionEmoji: "OneSecond",
|
|
3083
|
+
errorDetailMaxChars: 500
|
|
3084
|
+
})
|
|
3085
|
+
});
|
|
3086
|
+
async function apply(ctx, config) {
|
|
3087
|
+
validateConfig({
|
|
3088
|
+
layers: config.layers,
|
|
3089
|
+
rules: config.rules
|
|
3090
|
+
});
|
|
3091
|
+
const warn = (msg) => ctx.logger.warn(msg);
|
|
3092
|
+
const domain = await openDomainSafely(ctx, agentToolkitDomain, warn);
|
|
3093
|
+
const tables = {
|
|
3094
|
+
agents: domain.table("agents"),
|
|
3095
|
+
meta: domain.table("meta"),
|
|
3096
|
+
promptLayers: domain.table("prompt_layers")
|
|
3097
|
+
};
|
|
3098
|
+
const registry = await createRegistry(warn, {
|
|
3099
|
+
agents: tables.agents,
|
|
3100
|
+
meta: tables.meta
|
|
3101
|
+
});
|
|
3102
|
+
const layerSource = await openLayerSource({
|
|
3103
|
+
promptLayers: tables.promptLayers,
|
|
3104
|
+
meta: tables.meta
|
|
3105
|
+
}, config.layers);
|
|
3106
|
+
setupPrompt(ctx, {
|
|
3107
|
+
source: layerSource,
|
|
3108
|
+
rules: config.rules
|
|
3109
|
+
});
|
|
3110
|
+
setupDelegate(ctx, {
|
|
3111
|
+
provider: config.provider,
|
|
3112
|
+
toolName: config.toolName,
|
|
3113
|
+
rules: config.rules
|
|
3114
|
+
}, registry);
|
|
3115
|
+
setupAgentsApi(ctx, {
|
|
3116
|
+
registry,
|
|
3117
|
+
listTools: () => ctx.tools.schemas().map((s) => s.name),
|
|
3118
|
+
listProviders: () => ctx.llm.listProviders().map(({ id, name }) => ({
|
|
3119
|
+
id,
|
|
3120
|
+
name
|
|
3121
|
+
})),
|
|
3122
|
+
listModels: (provider) => ctx.llm.listModels(provider).then((models) => models.map(({ id, name }) => ({
|
|
3123
|
+
id,
|
|
3124
|
+
name
|
|
3125
|
+
})))
|
|
3126
|
+
});
|
|
3127
|
+
setupPromptLayersApi(ctx, {
|
|
3128
|
+
source: layerSource,
|
|
3129
|
+
rules: config.rules,
|
|
3130
|
+
seedLayers: config.layers,
|
|
3131
|
+
probe: async () => {
|
|
3132
|
+
const assembly = await ctx.systemPrompt.assemble({});
|
|
3133
|
+
return {
|
|
3134
|
+
sections: assembly.sections.map(({ name: n, text }) => ({
|
|
3135
|
+
name: n,
|
|
3136
|
+
text
|
|
3137
|
+
})),
|
|
3138
|
+
contexts: assembly.contexts.map(({ name: n, text }) => ({
|
|
3139
|
+
name: n,
|
|
3140
|
+
text
|
|
3141
|
+
}))
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
if (config.modules.feishu) setupBots(ctx, config.feishu, { registry });
|
|
3146
|
+
if (config.modules.usage) setupUsage(ctx, { timezone: config.timezone }, name);
|
|
3147
|
+
}
|
|
3148
|
+
//#endregion
|
|
3149
|
+
export { Config, apply, inject, name };
|
|
3150
|
+
|
|
3151
|
+
//# sourceMappingURL=index.js.map
|