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/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" || typeof msg.content !== "string") return null;
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
- try {
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 (text.length === 0) return null;
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
- channel: z$1.literal("feishu"),
2322
- feishu: FeishuConfigSchema,
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
- constructor(agents, bindings, sessions, defaultModel, workspace, onWarn, registry) {
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
- /** 换绑应用:明文新密钥(立即入 credentials)。 */
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
- }).optional()
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
- const merged = {
2969
- ...existing,
2970
- ...input.name !== void 0 ? { name: input.name } : {},
2971
- project,
2972
- feishu,
2973
- updatedAt: deps.now()
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),