dsh-agent-toolkit 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +133 -94
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +212 -46
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
package/lib/index.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ interface BotsModuleConfig {
|
|
|
45
45
|
processingReactionEmoji: string;
|
|
46
46
|
/** 回传飞书的错误摘要最大字符数。 */
|
|
47
47
|
errorDetailMaxChars: number;
|
|
48
|
+
/** 会话创建/恢复时注入「渠道 + 发起人 open_id」提示段(dsh-agent-toolkit:channel:sender)。 */
|
|
49
|
+
injectSender: boolean;
|
|
48
50
|
}
|
|
49
51
|
//#endregion
|
|
50
52
|
//#region src/agents/team-preset.d.ts
|
package/lib/index.js
CHANGED
|
@@ -1698,6 +1698,22 @@ async function setupAgentScope(agentCtx, hooks, toolsScope) {
|
|
|
1698
1698
|
}
|
|
1699
1699
|
//#endregion
|
|
1700
1700
|
//#region src/channels/feishu/api.ts
|
|
1701
|
+
/** 附件服务接受的图片媒体类型(与宿主 attachment v1 一致)。 */
|
|
1702
|
+
const IMAGE_MEDIA_TYPES = [
|
|
1703
|
+
"image/png",
|
|
1704
|
+
"image/jpeg",
|
|
1705
|
+
"image/webp",
|
|
1706
|
+
"image/gif"
|
|
1707
|
+
];
|
|
1708
|
+
/** 按魔数判定图片媒体类型;未知字节时回退响应头 content-type(限接受集合),否则 undefined。 */
|
|
1709
|
+
function sniffImageMediaType(data, contentType) {
|
|
1710
|
+
if (data.length >= 4 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
|
|
1711
|
+
if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
1712
|
+
if (data.length >= 4 && data[0] === 71 && data[1] === 73 && data[2] === 70 && data[3] === 56) return "image/gif";
|
|
1713
|
+
if (data.length >= 12 && data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70 && data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) return "image/webp";
|
|
1714
|
+
const declared = typeof contentType === "string" ? contentType.split(";")[0].trim().toLowerCase() : "";
|
|
1715
|
+
return IMAGE_MEDIA_TYPES.includes(declared) ? declared : void 0;
|
|
1716
|
+
}
|
|
1701
1717
|
/** SDK 薄封装:tenant_access_token 由 SDK 自动管理;错误带 code/msg 上下文。 */
|
|
1702
1718
|
function createFeishuApi(client) {
|
|
1703
1719
|
return {
|
|
@@ -1794,6 +1810,31 @@ function createFeishuApi(client) {
|
|
|
1794
1810
|
message_id: messageId,
|
|
1795
1811
|
reaction_id: reactionId
|
|
1796
1812
|
} });
|
|
1813
|
+
},
|
|
1814
|
+
async downloadImage(messageId, fileKey) {
|
|
1815
|
+
const res = await client.im.messageResource.get({
|
|
1816
|
+
params: { type: "image" },
|
|
1817
|
+
path: {
|
|
1818
|
+
message_id: messageId,
|
|
1819
|
+
file_key: fileKey
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
const chunks = [];
|
|
1823
|
+
await new Promise((resolve, reject) => {
|
|
1824
|
+
const stream = res.getReadableStream();
|
|
1825
|
+
stream.on("data", (chunk) => {
|
|
1826
|
+
chunks.push(chunk);
|
|
1827
|
+
});
|
|
1828
|
+
stream.on("end", () => resolve());
|
|
1829
|
+
stream.on("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
1830
|
+
});
|
|
1831
|
+
const data = new Uint8Array(Buffer.concat(chunks));
|
|
1832
|
+
const mediaType = sniffImageMediaType(data, res.headers?.["content-type"]);
|
|
1833
|
+
if (mediaType === void 0) throw new Error(`图片资源格式不受支持(file_key=${fileKey},content-type=${String(res.headers?.["content-type"] ?? "未知")})`);
|
|
1834
|
+
return {
|
|
1835
|
+
data,
|
|
1836
|
+
mediaType
|
|
1837
|
+
};
|
|
1797
1838
|
}
|
|
1798
1839
|
};
|
|
1799
1840
|
}
|
|
@@ -1814,9 +1855,39 @@ function stripMentionPlaceholders(text) {
|
|
|
1814
1855
|
//#endregion
|
|
1815
1856
|
//#region src/channels/feishu/parse.ts
|
|
1816
1857
|
/** im.message.receive_v1 事件解析:窄化为渠道无关的 ParsedMessage;message_id 去重。 */
|
|
1858
|
+
/** 从 post content(段落数组)抽取文本与图片 key;段落间以换行连接。 */
|
|
1859
|
+
function parsePostContent(content) {
|
|
1860
|
+
const imageKeys = [];
|
|
1861
|
+
const paragraphs = [];
|
|
1862
|
+
try {
|
|
1863
|
+
const parsed = JSON.parse(content);
|
|
1864
|
+
const list = Array.isArray(parsed.content) ? parsed.content : [];
|
|
1865
|
+
for (const paragraph of list) {
|
|
1866
|
+
if (!Array.isArray(paragraph)) continue;
|
|
1867
|
+
const parts = [];
|
|
1868
|
+
for (const node of paragraph) {
|
|
1869
|
+
if (node === null || typeof node !== "object") continue;
|
|
1870
|
+
if (node.tag === "img") {
|
|
1871
|
+
if (typeof node.image_key === "string" && node.image_key.length > 0) imageKeys.push(node.image_key);
|
|
1872
|
+
} else if ((node.tag === "text" || node.tag === "a") && typeof node.text === "string") parts.push(node.text);
|
|
1873
|
+
}
|
|
1874
|
+
paragraphs.push(parts.join(""));
|
|
1875
|
+
}
|
|
1876
|
+
} catch {
|
|
1877
|
+
return {
|
|
1878
|
+
text: "",
|
|
1879
|
+
imageKeys: []
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
return {
|
|
1883
|
+
text: paragraphs.join("\n"),
|
|
1884
|
+
imageKeys
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1817
1887
|
/**
|
|
1818
1888
|
* SDK handler 收到的 data 即事件体(README 示例 `data.message` 直接解构);
|
|
1819
|
-
* 兼容包一层 { event }
|
|
1889
|
+
* 兼容包一层 { event } 的形态。过滤:机器人消息、非 text/post/image 类型、
|
|
1890
|
+
* 群内未 @机器人、无文本且无图片。
|
|
1820
1891
|
*/
|
|
1821
1892
|
function parseMessageEvent(data) {
|
|
1822
1893
|
const wrapped = data;
|
|
@@ -1825,25 +1896,38 @@ function parseMessageEvent(data) {
|
|
|
1825
1896
|
const userId = event.sender.sender_id?.open_id;
|
|
1826
1897
|
const msg = event.message;
|
|
1827
1898
|
if (typeof userId !== "string" || msg === void 0) return null;
|
|
1828
|
-
if (msg.message_type !== "text"
|
|
1899
|
+
if (msg.message_type !== "text" && msg.message_type !== "post" && msg.message_type !== "image") return null;
|
|
1900
|
+
if (typeof msg.content !== "string") return null;
|
|
1829
1901
|
if (typeof msg.message_id !== "string" || typeof msg.chat_id !== "string") return null;
|
|
1830
1902
|
if (msg.chat_type !== "p2p" && msg.chat_type !== "group") return null;
|
|
1831
1903
|
if (msg.chat_type === "group" && !(msg.mentions ?? []).some((m) => m.mentioned_type === "bot")) return null;
|
|
1832
|
-
let text;
|
|
1833
|
-
|
|
1904
|
+
let text = "";
|
|
1905
|
+
let imageKeys = [];
|
|
1906
|
+
if (msg.message_type === "text") try {
|
|
1834
1907
|
const parsed = JSON.parse(msg.content);
|
|
1835
1908
|
if (typeof parsed.text !== "string") return null;
|
|
1836
1909
|
text = stripMentionPlaceholders(parsed.text);
|
|
1837
1910
|
} catch {
|
|
1838
1911
|
return null;
|
|
1839
1912
|
}
|
|
1840
|
-
if (
|
|
1913
|
+
else if (msg.message_type === "post") {
|
|
1914
|
+
const post = parsePostContent(msg.content);
|
|
1915
|
+
text = stripMentionPlaceholders(post.text);
|
|
1916
|
+
imageKeys = post.imageKeys;
|
|
1917
|
+
} else try {
|
|
1918
|
+
const parsed = JSON.parse(msg.content);
|
|
1919
|
+
if (typeof parsed.image_key === "string" && parsed.image_key.length > 0) imageKeys.push(parsed.image_key);
|
|
1920
|
+
} catch {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
if (text.length === 0 && imageKeys.length === 0) return null;
|
|
1841
1924
|
return {
|
|
1842
1925
|
messageId: msg.message_id,
|
|
1843
1926
|
chatId: msg.chat_id,
|
|
1844
1927
|
chatType: msg.chat_type,
|
|
1845
1928
|
userId,
|
|
1846
|
-
text
|
|
1929
|
+
text,
|
|
1930
|
+
imageKeys
|
|
1847
1931
|
};
|
|
1848
1932
|
}
|
|
1849
1933
|
/** message_id 去重(飞书会重推);FIFO 容量淘汰。 */
|
|
@@ -2276,12 +2360,14 @@ const feishuChannel = {
|
|
|
2276
2360
|
const parsed = parseMessageEvent(data);
|
|
2277
2361
|
if (parsed === null || !dedup.check(parsed.messageId)) return;
|
|
2278
2362
|
const reply = new FeishuReplyHandle(api, parsed.chatId, tunables, log);
|
|
2363
|
+
const loadImages = parsed.imageKeys.length > 0 ? async () => Promise.all(parsed.imageKeys.map(async (key) => api.downloadImage(parsed.messageId, key))) : void 0;
|
|
2279
2364
|
io.onMessage({
|
|
2280
2365
|
botId: bot.record.id,
|
|
2281
2366
|
chatId: parsed.chatId,
|
|
2282
2367
|
userId: parsed.userId,
|
|
2283
2368
|
messageId: parsed.messageId,
|
|
2284
2369
|
text: parsed.text,
|
|
2370
|
+
...loadImages !== void 0 ? { loadImages } : {},
|
|
2285
2371
|
reply,
|
|
2286
2372
|
ackProcessing: makeAck(api, parsed.messageId, tunables.processingReactionEmoji)
|
|
2287
2373
|
});
|
|
@@ -2318,8 +2404,9 @@ const FeishuConfigSchema = z$1.object({
|
|
|
2318
2404
|
const BotRecordSchema = z$1.object({
|
|
2319
2405
|
id: z$1.string().regex(BOT_ID_RE),
|
|
2320
2406
|
name: z$1.string().min(1).max(64),
|
|
2321
|
-
|
|
2322
|
-
|
|
2407
|
+
/** 渠道类型;与 feishu 同有或同无(未绑定为双双缺省)。 */
|
|
2408
|
+
channel: z$1.literal("feishu").optional(),
|
|
2409
|
+
feishu: FeishuConfigSchema.optional(),
|
|
2323
2410
|
/** 绑定项目(agent 的 cwd,绝对路径)。一 bot 一项目。 */
|
|
2324
2411
|
project: z$1.string().min(1),
|
|
2325
2412
|
/** 透传到 agent 创作期的 persona 提示段。 */
|
|
@@ -2334,7 +2421,7 @@ const BotRecordSchema = z$1.object({
|
|
|
2334
2421
|
}).optional(),
|
|
2335
2422
|
createdAt: z$1.number().int().nonnegative(),
|
|
2336
2423
|
updatedAt: z$1.number().int().nonnegative()
|
|
2337
|
-
});
|
|
2424
|
+
}).refine((r) => r.channel === void 0 === (r.feishu === void 0), { message: "channel 与 feishu 必须同有或同无" });
|
|
2338
2425
|
const BindingSchema = z$1.object({ sessionId: z$1.string().min(1) });
|
|
2339
2426
|
/** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
|
|
2340
2427
|
const projectBotDomain = defineDomain({
|
|
@@ -2467,7 +2554,7 @@ var Outbound = class {
|
|
|
2467
2554
|
};
|
|
2468
2555
|
//#endregion
|
|
2469
2556
|
//#region src/channels/inbound.ts
|
|
2470
|
-
/** 入站:指令分流 → 路由 → 单 in-flight 准入 → 表情回复 → followup 投递。 */
|
|
2557
|
+
/** 入站:指令分流 → 路由 → 单 in-flight 准入 → 表情回复 → 图片落附件库 → followup 投递。 */
|
|
2471
2558
|
var Inbound = class {
|
|
2472
2559
|
deps;
|
|
2473
2560
|
constructor(deps) {
|
|
@@ -2485,7 +2572,7 @@ var Inbound = class {
|
|
|
2485
2572
|
if (bot === void 0) return;
|
|
2486
2573
|
const directive = parseDirective(msg.text);
|
|
2487
2574
|
if (directive === "new") {
|
|
2488
|
-
await this.deps.router.reset(bot, msg.chatId, msg.reply);
|
|
2575
|
+
await this.deps.router.reset(bot, msg.chatId, msg.reply, msg.userId);
|
|
2489
2576
|
await msg.reply.notice("已开启新会话");
|
|
2490
2577
|
return;
|
|
2491
2578
|
}
|
|
@@ -2502,18 +2589,30 @@ var Inbound = class {
|
|
|
2502
2589
|
await msg.reply.notice(rt === void 0 ? `项目:${bot.project}\n会话:未创建(发送消息即创建)` : `项目:${bot.project}\n会话:${rt.sessionId}\n状态:${rt.inflight !== void 0 ? "处理中" : "空闲"}`);
|
|
2503
2590
|
return;
|
|
2504
2591
|
}
|
|
2505
|
-
const rt = await this.deps.router.ensure(bot, msg.chatId, msg.reply);
|
|
2592
|
+
const rt = await this.deps.router.ensure(bot, msg.chatId, msg.reply, msg.userId);
|
|
2506
2593
|
if (rt.inflight !== void 0) {
|
|
2507
2594
|
await msg.reply.notice("上一条还在处理中,请稍候(或发送 /stop 取消)");
|
|
2508
2595
|
return;
|
|
2509
2596
|
}
|
|
2510
2597
|
rt.inflight = { ack: void 0 };
|
|
2511
2598
|
rt.inflight.ack = await msg.ackProcessing().catch(() => void 0) ?? void 0;
|
|
2599
|
+
const imageRefs = [];
|
|
2600
|
+
if (msg.loadImages !== void 0) {
|
|
2601
|
+
const images = await msg.loadImages();
|
|
2602
|
+
if (images.length > 0) {
|
|
2603
|
+
const attachments = this.deps.attachments?.();
|
|
2604
|
+
if (attachments === void 0) await msg.reply.notice("当前环境暂不支持图片消息(附件服务不可用),已按文字部分处理");
|
|
2605
|
+
else imageRefs.push(...await attachments.saveImages(images));
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2512
2608
|
const message = createUserMessage({
|
|
2513
|
-
content: [{
|
|
2609
|
+
content: [...msg.text.length > 0 ? [{
|
|
2514
2610
|
type: "text",
|
|
2515
2611
|
text: msg.text
|
|
2516
|
-
}],
|
|
2612
|
+
}] : [], ...imageRefs.map((ref) => ({
|
|
2613
|
+
type: "image",
|
|
2614
|
+
attachment: ref
|
|
2615
|
+
}))],
|
|
2517
2616
|
source: { kind: "user" }
|
|
2518
2617
|
});
|
|
2519
2618
|
try {
|
|
@@ -2538,6 +2637,12 @@ function hooksOf(bot) {
|
|
|
2538
2637
|
//#endregion
|
|
2539
2638
|
//#region src/channels/router.ts
|
|
2540
2639
|
/** 绑定路由:(botId, chatId) → 长期会话;create / resume / reset。 */
|
|
2640
|
+
/** 发起人提示段名:bot 会话声明来源渠道与发起人 open_id。 */
|
|
2641
|
+
const SENDER_SECTION_NAME = "dsh-agent-toolkit:channel:sender";
|
|
2642
|
+
/** sender 段文本(单聊语义;channel 取 BotRecord.channel,未来新渠道零改动透传)。 */
|
|
2643
|
+
function senderSectionText(channel, userId) {
|
|
2644
|
+
return `本会话由 ${channel} 渠道的单聊会话发起。发起人 ID(${channel} open_id):\`${userId}\`。`;
|
|
2645
|
+
}
|
|
2541
2646
|
var Router = class {
|
|
2542
2647
|
agents;
|
|
2543
2648
|
bindings;
|
|
@@ -2546,7 +2651,8 @@ var Router = class {
|
|
|
2546
2651
|
workspace;
|
|
2547
2652
|
onWarn;
|
|
2548
2653
|
registry;
|
|
2549
|
-
|
|
2654
|
+
injectSender;
|
|
2655
|
+
constructor(agents, bindings, sessions, defaultModel, workspace, onWarn, registry, injectSender = true) {
|
|
2550
2656
|
this.agents = agents;
|
|
2551
2657
|
this.bindings = bindings;
|
|
2552
2658
|
this.sessions = sessions;
|
|
@@ -2554,9 +2660,10 @@ var Router = class {
|
|
|
2554
2660
|
this.workspace = workspace;
|
|
2555
2661
|
this.onWarn = onWarn;
|
|
2556
2662
|
this.registry = registry;
|
|
2663
|
+
this.injectSender = injectSender;
|
|
2557
2664
|
}
|
|
2558
2665
|
/** 取(或建/恢复)该 chat 的会话 runtime;reply 刷新为最近一次入站携带的句柄。 */
|
|
2559
|
-
async ensure(bot, chatId, reply) {
|
|
2666
|
+
async ensure(bot, chatId, reply, userId) {
|
|
2560
2667
|
const bound = this.bindings.get(bot.id, chatId);
|
|
2561
2668
|
if (bound !== void 0) {
|
|
2562
2669
|
const existing = this.sessions.get(bound);
|
|
@@ -2566,7 +2673,7 @@ var Router = class {
|
|
|
2566
2673
|
}
|
|
2567
2674
|
const agent = await this.agents.resume({
|
|
2568
2675
|
sessionId: bound,
|
|
2569
|
-
...this.resolveSession(bot)
|
|
2676
|
+
...this.resolveSession(bot, userId)
|
|
2570
2677
|
});
|
|
2571
2678
|
await this.attach(bot.project, bound);
|
|
2572
2679
|
return this.adopt(bot.id, chatId, bound, agent, reply);
|
|
@@ -2575,7 +2682,7 @@ var Router = class {
|
|
|
2575
2682
|
const agent = await this.agents.create({
|
|
2576
2683
|
sessionId,
|
|
2577
2684
|
cwd: bot.project,
|
|
2578
|
-
...this.resolveSession(bot)
|
|
2685
|
+
...this.resolveSession(bot, userId)
|
|
2579
2686
|
});
|
|
2580
2687
|
await this.bindings.set(bot.id, chatId, sessionId);
|
|
2581
2688
|
await this.attach(bot.project, sessionId);
|
|
@@ -2593,20 +2700,33 @@ var Router = class {
|
|
|
2593
2700
|
resolveOptions(bot) {
|
|
2594
2701
|
return bot.agentOptions ?? this.defaultModel();
|
|
2595
2702
|
}
|
|
2703
|
+
/** injectSender 开启时向 hooks.sections 末尾追加 sender 段(主/角色形态通用)。 */
|
|
2704
|
+
withSenderSection(hooks, bot, userId) {
|
|
2705
|
+
if (!this.injectSender) return hooks;
|
|
2706
|
+
const section = {
|
|
2707
|
+
name: SENDER_SECTION_NAME,
|
|
2708
|
+
order: 20,
|
|
2709
|
+
text: senderSectionText(bot.channel ?? "unknown", userId)
|
|
2710
|
+
};
|
|
2711
|
+
return {
|
|
2712
|
+
...hooks,
|
|
2713
|
+
sections: [...hooks.sections ?? [], section]
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2596
2716
|
/**
|
|
2597
2717
|
* 按 bot.agentRef 解析会话组装(agentOptions + 创作期 hooks):
|
|
2598
2718
|
* - 缺省/指向 main → 主 Agent 形态:bot 自带 persona/tools + 默认模型回退;
|
|
2599
2719
|
* - 指向角色 → 角色形态:persona 单 section + tools.restrict + role.model;
|
|
2600
2720
|
* - 指向不存在角色 → warn 并降级为主 Agent 形态。
|
|
2601
2721
|
*/
|
|
2602
|
-
resolveSession(bot) {
|
|
2722
|
+
resolveSession(bot, userId) {
|
|
2603
2723
|
const ref = bot.agentRef ?? "main";
|
|
2604
2724
|
const role = this.registry.get(ref);
|
|
2605
2725
|
if (role === void 0 || ref === "main") {
|
|
2606
2726
|
if (role === void 0 && ref !== "main") this.onWarn(`[project-bot] bot "${bot.id}" 的 agentRef "${ref}" 不存在,降级绑定主 Agent`);
|
|
2607
2727
|
return {
|
|
2608
2728
|
agentOptions: this.resolveOptions(bot),
|
|
2609
|
-
hooks: hooksOf(bot)
|
|
2729
|
+
hooks: this.withSenderSection(hooksOf(bot), bot, userId)
|
|
2610
2730
|
};
|
|
2611
2731
|
}
|
|
2612
2732
|
const sections = role.persona === void 0 || role.persona.trim().length === 0 ? [] : [{
|
|
@@ -2616,21 +2736,21 @@ var Router = class {
|
|
|
2616
2736
|
}];
|
|
2617
2737
|
return {
|
|
2618
2738
|
agentOptions: role.model ?? this.resolveOptions(bot),
|
|
2619
|
-
hooks: {
|
|
2739
|
+
hooks: this.withSenderSection({
|
|
2620
2740
|
...sections.length > 0 ? { sections } : {},
|
|
2621
2741
|
...role.tools !== void 0 ? { tools: role.tools.allow } : {}
|
|
2622
|
-
}
|
|
2742
|
+
}, bot, userId)
|
|
2623
2743
|
};
|
|
2624
2744
|
}
|
|
2625
2745
|
/** /new:取消旧会话、清绑定、开新会话。 */
|
|
2626
|
-
async reset(bot, chatId, reply) {
|
|
2746
|
+
async reset(bot, chatId, reply, userId) {
|
|
2627
2747
|
const bound = this.bindings.get(bot.id, chatId);
|
|
2628
2748
|
if (bound !== void 0) {
|
|
2629
2749
|
this.sessions.get(bound)?.agent.cancel();
|
|
2630
2750
|
this.sessions.delete(bound);
|
|
2631
2751
|
await this.bindings.delete(bot.id, chatId);
|
|
2632
2752
|
}
|
|
2633
|
-
return this.ensure(bot, chatId, reply);
|
|
2753
|
+
return this.ensure(bot, chatId, reply, userId);
|
|
2634
2754
|
}
|
|
2635
2755
|
lookup(botId, chatId) {
|
|
2636
2756
|
const bound = this.bindings.get(botId, chatId);
|
|
@@ -2663,11 +2783,12 @@ var BotRuntime = class {
|
|
|
2663
2783
|
constructor(deps) {
|
|
2664
2784
|
this.deps = deps;
|
|
2665
2785
|
const bindingStore = this.bindingStore();
|
|
2666
|
-
this.router = new Router(deps.agents, bindingStore, this.sessions, deps.defaultModel, deps.workspace, (m) => deps.log.warn(m), deps.registry);
|
|
2786
|
+
this.router = new Router(deps.agents, bindingStore, this.sessions, deps.defaultModel, deps.workspace, (m) => deps.log.warn(m), deps.registry, deps.injectSender ?? true);
|
|
2667
2787
|
this.inbound = new Inbound({
|
|
2668
2788
|
router: this.router,
|
|
2669
2789
|
bots: deps.bots,
|
|
2670
2790
|
maxErrorDetailChars: deps.maxErrorDetailChars,
|
|
2791
|
+
...deps.attachments !== void 0 ? { attachments: deps.attachments } : {},
|
|
2671
2792
|
onError: (m) => deps.log.warn(m)
|
|
2672
2793
|
});
|
|
2673
2794
|
this.outbound = new Outbound(this.sessions, (m) => deps.log.warn(m), deps.maxErrorDetailChars);
|
|
@@ -2675,11 +2796,12 @@ var BotRuntime = class {
|
|
|
2675
2796
|
async startAll() {
|
|
2676
2797
|
for (const botId of [...this.deps.bots.keys()]) await this.reconcile(botId);
|
|
2677
2798
|
}
|
|
2678
|
-
/** 按最新记录重建该 bot
|
|
2799
|
+
/** 按最新记录重建该 bot 的渠道(创建/更新后调用;记录已删或未绑定则纯停止)。 */
|
|
2679
2800
|
async reconcile(botId) {
|
|
2680
2801
|
await this.stopChannel(botId);
|
|
2681
2802
|
const record = this.deps.bots.get(botId);
|
|
2682
2803
|
if (record === void 0) return;
|
|
2804
|
+
if (record.feishu === void 0 || record.channel === void 0) return;
|
|
2683
2805
|
if (!this.deps.validateProject(record.project)) {
|
|
2684
2806
|
this.deps.log.warn(`[project-bot] bot "${botId}" 的项目路径不可用:${record.project}`);
|
|
2685
2807
|
return;
|
|
@@ -2713,7 +2835,17 @@ var BotRuntime = class {
|
|
|
2713
2835
|
}
|
|
2714
2836
|
await this.bindingStore().deleteBot(botId);
|
|
2715
2837
|
}
|
|
2838
|
+
/** 解绑渠道:停渠道、取消在飞会话;绑定表与持久会话保留(重绑后 resume 接续)。 */
|
|
2839
|
+
async unbindBot(botId) {
|
|
2840
|
+
await this.stopChannel(botId);
|
|
2841
|
+
for (const [sessionId, rt] of [...this.sessions]) if (rt.botId === botId) {
|
|
2842
|
+
rt.agent.cancel();
|
|
2843
|
+
this.sessions.delete(sessionId);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2716
2846
|
statusOf(botId) {
|
|
2847
|
+
const record = this.deps.bots.get(botId);
|
|
2848
|
+
if (record !== void 0 && record.feishu === void 0) return "unbound";
|
|
2717
2849
|
return this.handles.get(botId)?.status() ?? "not-running";
|
|
2718
2850
|
}
|
|
2719
2851
|
/** 卸载时序:取消在飞会话 → 等 idle → drain 出站链(卡片定格)→ 断全部渠道。 */
|
|
@@ -2821,7 +2953,7 @@ const CreateBodySchema = z$1.object({
|
|
|
2821
2953
|
/** 手动填写路径:明文密钥(立即入 credentials,不落表)。 */
|
|
2822
2954
|
appSecret: z$1.string().min(1).optional(),
|
|
2823
2955
|
/** 扫码路径:registerApp 已入库,直接给引用。 */
|
|
2824
|
-
appSecretRef: z$1.string().optional()
|
|
2956
|
+
appSecretRef: z$1.string().regex(CREDENTIAL_REF_RE).optional()
|
|
2825
2957
|
})
|
|
2826
2958
|
});
|
|
2827
2959
|
const UpdateBodySchema = z$1.object({
|
|
@@ -2834,11 +2966,12 @@ const UpdateBodySchema = z$1.object({
|
|
|
2834
2966
|
provider: z$1.string().min(1).optional(),
|
|
2835
2967
|
model: z$1.string().min(1).optional()
|
|
2836
2968
|
}).nullable().optional(),
|
|
2837
|
-
/**
|
|
2969
|
+
/** 重绑:明文新密钥(立即入 credentials)或扫码引用(已入库);null = 解绑渠道(删密钥、保留会话绑定)。 */
|
|
2838
2970
|
feishu: z$1.object({
|
|
2839
2971
|
appId: z$1.string().regex(FEISHU_APP_ID_RE),
|
|
2840
|
-
appSecret: z$1.string().min(1)
|
|
2841
|
-
|
|
2972
|
+
appSecret: z$1.string().min(1).optional(),
|
|
2973
|
+
appSecretRef: z$1.string().regex(CREDENTIAL_REF_RE).optional()
|
|
2974
|
+
}).refine((f) => f.appSecret !== void 0 || f.appSecretRef !== void 0, { message: "缺少 appSecret 或 appSecretRef" }).nullable().optional()
|
|
2842
2975
|
});
|
|
2843
2976
|
function json(res, code, body) {
|
|
2844
2977
|
res.writeHead(code, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
@@ -2900,7 +3033,7 @@ function createApiHandler(deps) {
|
|
|
2900
3033
|
json(res, 409, { error: `bot id "${id}" 已存在` });
|
|
2901
3034
|
return;
|
|
2902
3035
|
}
|
|
2903
|
-
for (const [, existing] of deps.bots.entries()) if (existing.feishu.appId === input.feishu.appId) {
|
|
3036
|
+
for (const [, existing] of deps.bots.entries()) if (existing.feishu !== void 0 && existing.feishu.appId === input.feishu.appId) {
|
|
2904
3037
|
json(res, 409, { error: `appId 已被 bot "${existing.id}" 使用` });
|
|
2905
3038
|
return;
|
|
2906
3039
|
}
|
|
@@ -2955,23 +3088,40 @@ function createApiHandler(deps) {
|
|
|
2955
3088
|
return;
|
|
2956
3089
|
}
|
|
2957
3090
|
const input = parsed.data;
|
|
2958
|
-
let feishu = existing.feishu;
|
|
2959
|
-
if (input.feishu !== void 0) feishu = {
|
|
2960
|
-
appId: input.feishu.appId,
|
|
2961
|
-
appSecretRef: await deps.storeSecret(id, input.feishu.appSecret)
|
|
2962
|
-
};
|
|
2963
3091
|
const project = input.project ?? existing.project;
|
|
2964
3092
|
if (!deps.validateProject(project)) {
|
|
2965
3093
|
json(res, 400, { error: `项目路径不可用:${project}` });
|
|
2966
3094
|
return;
|
|
2967
3095
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3096
|
+
if (input.feishu !== null && input.feishu !== void 0) {
|
|
3097
|
+
for (const [, other] of deps.bots.entries()) if (other.id !== id && other.feishu?.appId === input.feishu.appId) {
|
|
3098
|
+
json(res, 409, { error: `appId 已被 bot "${other.id}" 使用` });
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
let appSecretRef;
|
|
3103
|
+
if (input.feishu === null) {
|
|
3104
|
+
await deps.runtime.unbindBot(id);
|
|
3105
|
+
if (existing.feishu !== void 0) await deps.deleteSecret(existing.feishu.appSecretRef);
|
|
3106
|
+
} else if (input.feishu !== void 0) {
|
|
3107
|
+
if (input.feishu.appSecret !== void 0) appSecretRef = await deps.storeSecret(id, input.feishu.appSecret);
|
|
3108
|
+
else appSecretRef = input.feishu.appSecretRef;
|
|
3109
|
+
if (existing.feishu !== void 0 && existing.feishu.appSecretRef !== appSecretRef) await deps.deleteSecret(existing.feishu.appSecretRef);
|
|
3110
|
+
}
|
|
3111
|
+
const merged = { ...existing };
|
|
3112
|
+
if (input.name !== void 0) merged.name = input.name;
|
|
3113
|
+
merged.project = project;
|
|
3114
|
+
merged.updatedAt = deps.now();
|
|
3115
|
+
if (input.feishu === null) {
|
|
3116
|
+
delete merged.channel;
|
|
3117
|
+
delete merged.feishu;
|
|
3118
|
+
} else if (input.feishu !== void 0 && appSecretRef !== void 0) {
|
|
3119
|
+
merged.channel = "feishu";
|
|
3120
|
+
merged.feishu = {
|
|
3121
|
+
appId: input.feishu.appId,
|
|
3122
|
+
appSecretRef
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
2975
3125
|
if (input.persona === null) delete merged.persona;
|
|
2976
3126
|
else if (input.persona !== void 0) merged.persona = input.persona;
|
|
2977
3127
|
if (input.agentRef === null) delete merged.agentRef;
|
|
@@ -2998,7 +3148,7 @@ function createApiHandler(deps) {
|
|
|
2998
3148
|
}
|
|
2999
3149
|
await deps.runtime.stopBot(id);
|
|
3000
3150
|
await deps.bots.delete(id);
|
|
3001
|
-
await deps.deleteSecret(existing.feishu.appSecretRef);
|
|
3151
|
+
if (existing.feishu !== void 0) await deps.deleteSecret(existing.feishu.appSecretRef);
|
|
3002
3152
|
json(res, 200, { ok: true });
|
|
3003
3153
|
return;
|
|
3004
3154
|
}
|
|
@@ -3175,6 +3325,18 @@ function setupBots(ctx, config, deps) {
|
|
|
3175
3325
|
if (workspaceRegistry === void 0) throw new Error("workspaceRegistry 服务不可用");
|
|
3176
3326
|
await (await workspaceRegistry.create(cwd)).attachSession(SessionId(sessionId));
|
|
3177
3327
|
} };
|
|
3328
|
+
const attachmentsOf = () => {
|
|
3329
|
+
if (ctx.get("attachments", false) === void 0) return void 0;
|
|
3330
|
+
return { async saveImages(inputs) {
|
|
3331
|
+
const current = ctx.get("attachments", false);
|
|
3332
|
+
if (current === void 0) throw new Error("attachments 服务不可用");
|
|
3333
|
+
return current.saveImages(inputs.map(({ data, mediaType, name }) => ({
|
|
3334
|
+
data,
|
|
3335
|
+
mediaType,
|
|
3336
|
+
...name !== void 0 ? { name } : {}
|
|
3337
|
+
})));
|
|
3338
|
+
} };
|
|
3339
|
+
};
|
|
3178
3340
|
let botsTable;
|
|
3179
3341
|
let bindingsTable;
|
|
3180
3342
|
let runtime;
|
|
@@ -3209,6 +3371,8 @@ function setupBots(ctx, config, deps) {
|
|
|
3209
3371
|
channels,
|
|
3210
3372
|
tunables,
|
|
3211
3373
|
maxErrorDetailChars: config.errorDetailMaxChars,
|
|
3374
|
+
attachments: attachmentsOf,
|
|
3375
|
+
injectSender: config.injectSender,
|
|
3212
3376
|
resolveSecret: async (ref) => (await ctx.credentials.resolve(credentialRef(ref)))?.value,
|
|
3213
3377
|
validateProject: (path) => existsSync(path),
|
|
3214
3378
|
log
|
|
@@ -3434,14 +3598,16 @@ const Config = z.object({
|
|
|
3434
3598
|
processMaxBytes: z.number().default(8e3),
|
|
3435
3599
|
registerAppTimeoutMs: z.number().default(6e5),
|
|
3436
3600
|
processingReactionEmoji: z.string().default("OneSecond"),
|
|
3437
|
-
errorDetailMaxChars: z.number().default(500)
|
|
3601
|
+
errorDetailMaxChars: z.number().default(500),
|
|
3602
|
+
injectSender: z.boolean().default(true)
|
|
3438
3603
|
}).default({
|
|
3439
3604
|
cardUpdateThrottleMs: 500,
|
|
3440
3605
|
cardMaxBytes: 28e3,
|
|
3441
3606
|
processMaxBytes: 8e3,
|
|
3442
3607
|
registerAppTimeoutMs: 6e5,
|
|
3443
3608
|
processingReactionEmoji: "OneSecond",
|
|
3444
|
-
errorDetailMaxChars: 500
|
|
3609
|
+
errorDetailMaxChars: 500,
|
|
3610
|
+
injectSender: true
|
|
3445
3611
|
}),
|
|
3446
3612
|
agentTeamPreset: z.object({
|
|
3447
3613
|
enabled: z.boolean().default(true),
|