dsh-agent-toolkit 0.2.6 → 0.2.8

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
@@ -1835,6 +1835,15 @@ function createFeishuApi(client) {
1835
1835
  data,
1836
1836
  mediaType
1837
1837
  };
1838
+ },
1839
+ async getBotOpenId() {
1840
+ const res = await client.request({
1841
+ method: "GET",
1842
+ url: "https://open.feishu.cn/open-apis/bot/v3/info"
1843
+ });
1844
+ const openId = res.bot?.open_id;
1845
+ if (typeof openId !== "string" || openId.length === 0) throw new Error(`获取机器人信息失败:code=${res.code} msg=${res.msg}`);
1846
+ return openId;
1838
1847
  }
1839
1848
  };
1840
1849
  }
@@ -1888,8 +1897,10 @@ function parsePostContent(content) {
1888
1897
  * SDK handler 收到的 data 即事件体(README 示例 `data.message` 直接解构);
1889
1898
  * 兼容包一层 { event } 的形态。过滤:机器人消息、非 text/post/image 类型、
1890
1899
  * 群内未 @机器人、无文本且无图片。
1900
+ * 群消息 @ 校验身份:mention 的 open_id 必须等于本机器人(应用若持全部群消息权限,
1901
+ * 会收到 @ 其他机器人的消息,不能误触发)。
1891
1902
  */
1892
- function parseMessageEvent(data) {
1903
+ function parseMessageEvent(data, botOpenId) {
1893
1904
  const wrapped = data;
1894
1905
  const event = wrapped.event ?? wrapped;
1895
1906
  if (event.sender?.sender_type !== "user") return null;
@@ -1900,7 +1911,7 @@ function parseMessageEvent(data) {
1900
1911
  if (typeof msg.content !== "string") return null;
1901
1912
  if (typeof msg.message_id !== "string" || typeof msg.chat_id !== "string") return null;
1902
1913
  if (msg.chat_type !== "p2p" && msg.chat_type !== "group") return null;
1903
- if (msg.chat_type === "group" && !(msg.mentions ?? []).some((m) => m.mentioned_type === "bot")) return null;
1914
+ if (msg.chat_type === "group" && !(msg.mentions ?? []).some((m) => m.mentioned_type === "bot" && m.id?.open_id === botOpenId)) return null;
1904
1915
  let text = "";
1905
1916
  let imageKeys = [];
1906
1917
  if (msg.message_type === "text") try {
@@ -2353,11 +2364,12 @@ const feishuChannel = {
2353
2364
  appId,
2354
2365
  appSecret: bot.secret
2355
2366
  }));
2367
+ const botOpenId = await api.getBotOpenId();
2356
2368
  const dedup = new MessageDedup();
2357
2369
  const dispatcher = new lark.EventDispatcher({}).register({
2358
2370
  "im.message.message_read_v1": async () => void 0,
2359
2371
  "im.message.receive_v1": async (data) => {
2360
- const parsed = parseMessageEvent(data);
2372
+ const parsed = parseMessageEvent(data, botOpenId);
2361
2373
  if (parsed === null || !dedup.check(parsed.messageId)) return;
2362
2374
  const reply = new FeishuReplyHandle(api, parsed.chatId, tunables, log);
2363
2375
  const loadImages = parsed.imageKeys.length > 0 ? async () => Promise.all(parsed.imageKeys.map(async (key) => api.downloadImage(parsed.messageId, key))) : void 0;
@@ -2404,8 +2416,9 @@ const FeishuConfigSchema = z$1.object({
2404
2416
  const BotRecordSchema = z$1.object({
2405
2417
  id: z$1.string().regex(BOT_ID_RE),
2406
2418
  name: z$1.string().min(1).max(64),
2407
- channel: z$1.literal("feishu"),
2408
- feishu: FeishuConfigSchema,
2419
+ /** 渠道类型;与 feishu 同有或同无(未绑定为双双缺省)。 */
2420
+ channel: z$1.literal("feishu").optional(),
2421
+ feishu: FeishuConfigSchema.optional(),
2409
2422
  /** 绑定项目(agent 的 cwd,绝对路径)。一 bot 一项目。 */
2410
2423
  project: z$1.string().min(1),
2411
2424
  /** 透传到 agent 创作期的 persona 提示段。 */
@@ -2420,7 +2433,7 @@ const BotRecordSchema = z$1.object({
2420
2433
  }).optional(),
2421
2434
  createdAt: z$1.number().int().nonnegative(),
2422
2435
  updatedAt: z$1.number().int().nonnegative()
2423
- });
2436
+ }).refine((r) => r.channel === void 0 === (r.feishu === void 0), { message: "channel 与 feishu 必须同有或同无" });
2424
2437
  const BindingSchema = z$1.object({ sessionId: z$1.string().min(1) });
2425
2438
  /** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
2426
2439
  const projectBotDomain = defineDomain({
@@ -2571,7 +2584,7 @@ var Inbound = class {
2571
2584
  if (bot === void 0) return;
2572
2585
  const directive = parseDirective(msg.text);
2573
2586
  if (directive === "new") {
2574
- await this.deps.router.reset(bot, msg.chatId, msg.reply);
2587
+ await this.deps.router.reset(bot, msg.chatId, msg.reply, msg.userId);
2575
2588
  await msg.reply.notice("已开启新会话");
2576
2589
  return;
2577
2590
  }
@@ -2588,7 +2601,7 @@ var Inbound = class {
2588
2601
  await msg.reply.notice(rt === void 0 ? `项目:${bot.project}\n会话:未创建(发送消息即创建)` : `项目:${bot.project}\n会话:${rt.sessionId}\n状态:${rt.inflight !== void 0 ? "处理中" : "空闲"}`);
2589
2602
  return;
2590
2603
  }
2591
- const rt = await this.deps.router.ensure(bot, msg.chatId, msg.reply);
2604
+ const rt = await this.deps.router.ensure(bot, msg.chatId, msg.reply, msg.userId);
2592
2605
  if (rt.inflight !== void 0) {
2593
2606
  await msg.reply.notice("上一条还在处理中,请稍候(或发送 /stop 取消)");
2594
2607
  return;
@@ -2636,6 +2649,12 @@ function hooksOf(bot) {
2636
2649
  //#endregion
2637
2650
  //#region src/channels/router.ts
2638
2651
  /** 绑定路由:(botId, chatId) → 长期会话;create / resume / reset。 */
2652
+ /** 发起人提示段名:bot 会话声明来源渠道与发起人 open_id。 */
2653
+ const SENDER_SECTION_NAME = "dsh-agent-toolkit:channel:sender";
2654
+ /** sender 段文本(单聊语义;channel 取 BotRecord.channel,未来新渠道零改动透传)。 */
2655
+ function senderSectionText(channel, userId) {
2656
+ return `本会话由 ${channel} 渠道的单聊会话发起。发起人 ID(${channel} open_id):\`${userId}\`。`;
2657
+ }
2639
2658
  var Router = class {
2640
2659
  agents;
2641
2660
  bindings;
@@ -2644,7 +2663,8 @@ var Router = class {
2644
2663
  workspace;
2645
2664
  onWarn;
2646
2665
  registry;
2647
- constructor(agents, bindings, sessions, defaultModel, workspace, onWarn, registry) {
2666
+ injectSender;
2667
+ constructor(agents, bindings, sessions, defaultModel, workspace, onWarn, registry, injectSender = true) {
2648
2668
  this.agents = agents;
2649
2669
  this.bindings = bindings;
2650
2670
  this.sessions = sessions;
@@ -2652,9 +2672,10 @@ var Router = class {
2652
2672
  this.workspace = workspace;
2653
2673
  this.onWarn = onWarn;
2654
2674
  this.registry = registry;
2675
+ this.injectSender = injectSender;
2655
2676
  }
2656
2677
  /** 取(或建/恢复)该 chat 的会话 runtime;reply 刷新为最近一次入站携带的句柄。 */
2657
- async ensure(bot, chatId, reply) {
2678
+ async ensure(bot, chatId, reply, userId) {
2658
2679
  const bound = this.bindings.get(bot.id, chatId);
2659
2680
  if (bound !== void 0) {
2660
2681
  const existing = this.sessions.get(bound);
@@ -2664,7 +2685,7 @@ var Router = class {
2664
2685
  }
2665
2686
  const agent = await this.agents.resume({
2666
2687
  sessionId: bound,
2667
- ...this.resolveSession(bot)
2688
+ ...this.resolveSession(bot, userId)
2668
2689
  });
2669
2690
  await this.attach(bot.project, bound);
2670
2691
  return this.adopt(bot.id, chatId, bound, agent, reply);
@@ -2673,7 +2694,7 @@ var Router = class {
2673
2694
  const agent = await this.agents.create({
2674
2695
  sessionId,
2675
2696
  cwd: bot.project,
2676
- ...this.resolveSession(bot)
2697
+ ...this.resolveSession(bot, userId)
2677
2698
  });
2678
2699
  await this.bindings.set(bot.id, chatId, sessionId);
2679
2700
  await this.attach(bot.project, sessionId);
@@ -2687,24 +2708,33 @@ var Router = class {
2687
2708
  this.onWarn(`[project-bot] 会话 ${sessionId} 挂载 workspace 失败:${error instanceof Error ? error.message : String(error)}`);
2688
2709
  }
2689
2710
  }
2690
- /** agentOptions 原样透传;无则回退宿主默认模型(存量 bot 不抛 no provider/model)。 */
2691
- resolveOptions(bot) {
2692
- return bot.agentOptions ?? this.defaultModel();
2711
+ /** injectSender 开启时向 hooks.sections 末尾追加 sender 段(主/角色形态通用)。 */
2712
+ withSenderSection(hooks, bot, userId) {
2713
+ if (!this.injectSender) return hooks;
2714
+ const section = {
2715
+ name: SENDER_SECTION_NAME,
2716
+ order: 20,
2717
+ text: senderSectionText(bot.channel ?? "unknown", userId)
2718
+ };
2719
+ return {
2720
+ ...hooks,
2721
+ sections: [...hooks.sections ?? [], section]
2722
+ };
2693
2723
  }
2694
2724
  /**
2695
2725
  * 按 bot.agentRef 解析会话组装(agentOptions + 创作期 hooks):
2696
- * - 缺省/指向 main → 主 Agent 形态:bot 自带 persona/tools + 默认模型回退;
2726
+ * - 缺省/指向 main → 主 Agent 形态:bot 自带 persona/tools + 模型(自配 agentOptions 优先,缺省回退宿主默认模型);
2697
2727
  * - 指向角色 → 角色形态:persona 单 section + tools.restrict + role.model;
2698
2728
  * - 指向不存在角色 → warn 并降级为主 Agent 形态。
2699
2729
  */
2700
- resolveSession(bot) {
2730
+ resolveSession(bot, userId) {
2701
2731
  const ref = bot.agentRef ?? "main";
2702
2732
  const role = this.registry.get(ref);
2703
2733
  if (role === void 0 || ref === "main") {
2704
2734
  if (role === void 0 && ref !== "main") this.onWarn(`[project-bot] bot "${bot.id}" 的 agentRef "${ref}" 不存在,降级绑定主 Agent`);
2705
2735
  return {
2706
- agentOptions: this.resolveOptions(bot),
2707
- hooks: hooksOf(bot)
2736
+ agentOptions: bot.agentOptions ?? this.defaultModel(),
2737
+ hooks: this.withSenderSection(hooksOf(bot), bot, userId)
2708
2738
  };
2709
2739
  }
2710
2740
  const sections = role.persona === void 0 || role.persona.trim().length === 0 ? [] : [{
@@ -2713,22 +2743,22 @@ var Router = class {
2713
2743
  text: role.persona
2714
2744
  }];
2715
2745
  return {
2716
- agentOptions: role.model ?? this.resolveOptions(bot),
2717
- hooks: {
2746
+ agentOptions: role.model ?? this.defaultModel(),
2747
+ hooks: this.withSenderSection({
2718
2748
  ...sections.length > 0 ? { sections } : {},
2719
2749
  ...role.tools !== void 0 ? { tools: role.tools.allow } : {}
2720
- }
2750
+ }, bot, userId)
2721
2751
  };
2722
2752
  }
2723
2753
  /** /new:取消旧会话、清绑定、开新会话。 */
2724
- async reset(bot, chatId, reply) {
2754
+ async reset(bot, chatId, reply, userId) {
2725
2755
  const bound = this.bindings.get(bot.id, chatId);
2726
2756
  if (bound !== void 0) {
2727
2757
  this.sessions.get(bound)?.agent.cancel();
2728
2758
  this.sessions.delete(bound);
2729
2759
  await this.bindings.delete(bot.id, chatId);
2730
2760
  }
2731
- return this.ensure(bot, chatId, reply);
2761
+ return this.ensure(bot, chatId, reply, userId);
2732
2762
  }
2733
2763
  lookup(botId, chatId) {
2734
2764
  const bound = this.bindings.get(botId, chatId);
@@ -2761,7 +2791,7 @@ var BotRuntime = class {
2761
2791
  constructor(deps) {
2762
2792
  this.deps = deps;
2763
2793
  const bindingStore = this.bindingStore();
2764
- this.router = new Router(deps.agents, bindingStore, this.sessions, deps.defaultModel, deps.workspace, (m) => deps.log.warn(m), deps.registry);
2794
+ this.router = new Router(deps.agents, bindingStore, this.sessions, deps.defaultModel, deps.workspace, (m) => deps.log.warn(m), deps.registry, deps.injectSender ?? true);
2765
2795
  this.inbound = new Inbound({
2766
2796
  router: this.router,
2767
2797
  bots: deps.bots,
@@ -2774,11 +2804,12 @@ var BotRuntime = class {
2774
2804
  async startAll() {
2775
2805
  for (const botId of [...this.deps.bots.keys()]) await this.reconcile(botId);
2776
2806
  }
2777
- /** 按最新记录重建该 bot 的渠道(创建/更新后调用;记录已删则纯停止)。 */
2807
+ /** 按最新记录重建该 bot 的渠道(创建/更新后调用;记录已删或未绑定则纯停止)。 */
2778
2808
  async reconcile(botId) {
2779
2809
  await this.stopChannel(botId);
2780
2810
  const record = this.deps.bots.get(botId);
2781
2811
  if (record === void 0) return;
2812
+ if (record.feishu === void 0 || record.channel === void 0) return;
2782
2813
  if (!this.deps.validateProject(record.project)) {
2783
2814
  this.deps.log.warn(`[project-bot] bot "${botId}" 的项目路径不可用:${record.project}`);
2784
2815
  return;
@@ -2812,7 +2843,17 @@ var BotRuntime = class {
2812
2843
  }
2813
2844
  await this.bindingStore().deleteBot(botId);
2814
2845
  }
2846
+ /** 解绑渠道:停渠道、取消在飞会话;绑定表与持久会话保留(重绑后 resume 接续)。 */
2847
+ async unbindBot(botId) {
2848
+ await this.stopChannel(botId);
2849
+ for (const [sessionId, rt] of [...this.sessions]) if (rt.botId === botId) {
2850
+ rt.agent.cancel();
2851
+ this.sessions.delete(sessionId);
2852
+ }
2853
+ }
2815
2854
  statusOf(botId) {
2855
+ const record = this.deps.bots.get(botId);
2856
+ if (record !== void 0 && record.feishu === void 0) return "unbound";
2816
2857
  return this.handles.get(botId)?.status() ?? "not-running";
2817
2858
  }
2818
2859
  /** 卸载时序:取消在飞会话 → 等 idle → drain 出站链(卡片定格)→ 断全部渠道。 */
@@ -2920,7 +2961,7 @@ const CreateBodySchema = z$1.object({
2920
2961
  /** 手动填写路径:明文密钥(立即入 credentials,不落表)。 */
2921
2962
  appSecret: z$1.string().min(1).optional(),
2922
2963
  /** 扫码路径:registerApp 已入库,直接给引用。 */
2923
- appSecretRef: z$1.string().optional()
2964
+ appSecretRef: z$1.string().regex(CREDENTIAL_REF_RE).optional()
2924
2965
  })
2925
2966
  });
2926
2967
  const UpdateBodySchema = z$1.object({
@@ -2933,11 +2974,12 @@ const UpdateBodySchema = z$1.object({
2933
2974
  provider: z$1.string().min(1).optional(),
2934
2975
  model: z$1.string().min(1).optional()
2935
2976
  }).nullable().optional(),
2936
- /** 换绑应用:明文新密钥(立即入 credentials)。 */
2977
+ /** 重绑:明文新密钥(立即入 credentials)或扫码引用(已入库);null = 解绑渠道(删密钥、保留会话绑定)。 */
2937
2978
  feishu: z$1.object({
2938
2979
  appId: z$1.string().regex(FEISHU_APP_ID_RE),
2939
- appSecret: z$1.string().min(1)
2940
- }).optional()
2980
+ appSecret: z$1.string().min(1).optional(),
2981
+ appSecretRef: z$1.string().regex(CREDENTIAL_REF_RE).optional()
2982
+ }).refine((f) => f.appSecret !== void 0 || f.appSecretRef !== void 0, { message: "缺少 appSecret 或 appSecretRef" }).nullable().optional()
2941
2983
  });
2942
2984
  function json(res, code, body) {
2943
2985
  res.writeHead(code, { "content-type": "application/json" }).end(JSON.stringify(body));
@@ -2999,7 +3041,7 @@ function createApiHandler(deps) {
2999
3041
  json(res, 409, { error: `bot id "${id}" 已存在` });
3000
3042
  return;
3001
3043
  }
3002
- for (const [, existing] of deps.bots.entries()) if (existing.feishu.appId === input.feishu.appId) {
3044
+ for (const [, existing] of deps.bots.entries()) if (existing.feishu !== void 0 && existing.feishu.appId === input.feishu.appId) {
3003
3045
  json(res, 409, { error: `appId 已被 bot "${existing.id}" 使用` });
3004
3046
  return;
3005
3047
  }
@@ -3054,23 +3096,40 @@ function createApiHandler(deps) {
3054
3096
  return;
3055
3097
  }
3056
3098
  const input = parsed.data;
3057
- let feishu = existing.feishu;
3058
- if (input.feishu !== void 0) feishu = {
3059
- appId: input.feishu.appId,
3060
- appSecretRef: await deps.storeSecret(id, input.feishu.appSecret)
3061
- };
3062
3099
  const project = input.project ?? existing.project;
3063
3100
  if (!deps.validateProject(project)) {
3064
3101
  json(res, 400, { error: `项目路径不可用:${project}` });
3065
3102
  return;
3066
3103
  }
3067
- const merged = {
3068
- ...existing,
3069
- ...input.name !== void 0 ? { name: input.name } : {},
3070
- project,
3071
- feishu,
3072
- updatedAt: deps.now()
3073
- };
3104
+ if (input.feishu !== null && input.feishu !== void 0) {
3105
+ for (const [, other] of deps.bots.entries()) if (other.id !== id && other.feishu?.appId === input.feishu.appId) {
3106
+ json(res, 409, { error: `appId 已被 bot "${other.id}" 使用` });
3107
+ return;
3108
+ }
3109
+ }
3110
+ let appSecretRef;
3111
+ if (input.feishu === null) {
3112
+ await deps.runtime.unbindBot(id);
3113
+ if (existing.feishu !== void 0) await deps.deleteSecret(existing.feishu.appSecretRef);
3114
+ } else if (input.feishu !== void 0) {
3115
+ if (input.feishu.appSecret !== void 0) appSecretRef = await deps.storeSecret(id, input.feishu.appSecret);
3116
+ else appSecretRef = input.feishu.appSecretRef;
3117
+ if (existing.feishu !== void 0 && existing.feishu.appSecretRef !== appSecretRef) await deps.deleteSecret(existing.feishu.appSecretRef);
3118
+ }
3119
+ const merged = { ...existing };
3120
+ if (input.name !== void 0) merged.name = input.name;
3121
+ merged.project = project;
3122
+ merged.updatedAt = deps.now();
3123
+ if (input.feishu === null) {
3124
+ delete merged.channel;
3125
+ delete merged.feishu;
3126
+ } else if (input.feishu !== void 0 && appSecretRef !== void 0) {
3127
+ merged.channel = "feishu";
3128
+ merged.feishu = {
3129
+ appId: input.feishu.appId,
3130
+ appSecretRef
3131
+ };
3132
+ }
3074
3133
  if (input.persona === null) delete merged.persona;
3075
3134
  else if (input.persona !== void 0) merged.persona = input.persona;
3076
3135
  if (input.agentRef === null) delete merged.agentRef;
@@ -3097,7 +3156,7 @@ function createApiHandler(deps) {
3097
3156
  }
3098
3157
  await deps.runtime.stopBot(id);
3099
3158
  await deps.bots.delete(id);
3100
- await deps.deleteSecret(existing.feishu.appSecretRef);
3159
+ if (existing.feishu !== void 0) await deps.deleteSecret(existing.feishu.appSecretRef);
3101
3160
  json(res, 200, { ok: true });
3102
3161
  return;
3103
3162
  }
@@ -3321,6 +3380,7 @@ function setupBots(ctx, config, deps) {
3321
3380
  tunables,
3322
3381
  maxErrorDetailChars: config.errorDetailMaxChars,
3323
3382
  attachments: attachmentsOf,
3383
+ injectSender: config.injectSender,
3324
3384
  resolveSecret: async (ref) => (await ctx.credentials.resolve(credentialRef(ref)))?.value,
3325
3385
  validateProject: (path) => existsSync(path),
3326
3386
  log
@@ -3546,14 +3606,16 @@ const Config = z.object({
3546
3606
  processMaxBytes: z.number().default(8e3),
3547
3607
  registerAppTimeoutMs: z.number().default(6e5),
3548
3608
  processingReactionEmoji: z.string().default("OneSecond"),
3549
- errorDetailMaxChars: z.number().default(500)
3609
+ errorDetailMaxChars: z.number().default(500),
3610
+ injectSender: z.boolean().default(true)
3550
3611
  }).default({
3551
3612
  cardUpdateThrottleMs: 500,
3552
3613
  cardMaxBytes: 28e3,
3553
3614
  processMaxBytes: 8e3,
3554
3615
  registerAppTimeoutMs: 6e5,
3555
3616
  processingReactionEmoji: "OneSecond",
3556
- errorDetailMaxChars: 500
3617
+ errorDetailMaxChars: 500,
3618
+ injectSender: true
3557
3619
  }),
3558
3620
  agentTeamPreset: z.object({
3559
3621
  enabled: z.boolean().default(true),