dsh-agent-toolkit 0.2.6 → 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
@@ -2404,8 +2404,9 @@ const FeishuConfigSchema = z$1.object({
2404
2404
  const BotRecordSchema = z$1.object({
2405
2405
  id: z$1.string().regex(BOT_ID_RE),
2406
2406
  name: z$1.string().min(1).max(64),
2407
- channel: z$1.literal("feishu"),
2408
- feishu: FeishuConfigSchema,
2407
+ /** 渠道类型;与 feishu 同有或同无(未绑定为双双缺省)。 */
2408
+ channel: z$1.literal("feishu").optional(),
2409
+ feishu: FeishuConfigSchema.optional(),
2409
2410
  /** 绑定项目(agent 的 cwd,绝对路径)。一 bot 一项目。 */
2410
2411
  project: z$1.string().min(1),
2411
2412
  /** 透传到 agent 创作期的 persona 提示段。 */
@@ -2420,7 +2421,7 @@ const BotRecordSchema = z$1.object({
2420
2421
  }).optional(),
2421
2422
  createdAt: z$1.number().int().nonnegative(),
2422
2423
  updatedAt: z$1.number().int().nonnegative()
2423
- });
2424
+ }).refine((r) => r.channel === void 0 === (r.feishu === void 0), { message: "channel 与 feishu 必须同有或同无" });
2424
2425
  const BindingSchema = z$1.object({ sessionId: z$1.string().min(1) });
2425
2426
  /** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
2426
2427
  const projectBotDomain = defineDomain({
@@ -2571,7 +2572,7 @@ var Inbound = class {
2571
2572
  if (bot === void 0) return;
2572
2573
  const directive = parseDirective(msg.text);
2573
2574
  if (directive === "new") {
2574
- await this.deps.router.reset(bot, msg.chatId, msg.reply);
2575
+ await this.deps.router.reset(bot, msg.chatId, msg.reply, msg.userId);
2575
2576
  await msg.reply.notice("已开启新会话");
2576
2577
  return;
2577
2578
  }
@@ -2588,7 +2589,7 @@ var Inbound = class {
2588
2589
  await msg.reply.notice(rt === void 0 ? `项目:${bot.project}\n会话:未创建(发送消息即创建)` : `项目:${bot.project}\n会话:${rt.sessionId}\n状态:${rt.inflight !== void 0 ? "处理中" : "空闲"}`);
2589
2590
  return;
2590
2591
  }
2591
- 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);
2592
2593
  if (rt.inflight !== void 0) {
2593
2594
  await msg.reply.notice("上一条还在处理中,请稍候(或发送 /stop 取消)");
2594
2595
  return;
@@ -2636,6 +2637,12 @@ function hooksOf(bot) {
2636
2637
  //#endregion
2637
2638
  //#region src/channels/router.ts
2638
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
+ }
2639
2646
  var Router = class {
2640
2647
  agents;
2641
2648
  bindings;
@@ -2644,7 +2651,8 @@ var Router = class {
2644
2651
  workspace;
2645
2652
  onWarn;
2646
2653
  registry;
2647
- constructor(agents, bindings, sessions, defaultModel, workspace, onWarn, registry) {
2654
+ injectSender;
2655
+ constructor(agents, bindings, sessions, defaultModel, workspace, onWarn, registry, injectSender = true) {
2648
2656
  this.agents = agents;
2649
2657
  this.bindings = bindings;
2650
2658
  this.sessions = sessions;
@@ -2652,9 +2660,10 @@ var Router = class {
2652
2660
  this.workspace = workspace;
2653
2661
  this.onWarn = onWarn;
2654
2662
  this.registry = registry;
2663
+ this.injectSender = injectSender;
2655
2664
  }
2656
2665
  /** 取(或建/恢复)该 chat 的会话 runtime;reply 刷新为最近一次入站携带的句柄。 */
2657
- async ensure(bot, chatId, reply) {
2666
+ async ensure(bot, chatId, reply, userId) {
2658
2667
  const bound = this.bindings.get(bot.id, chatId);
2659
2668
  if (bound !== void 0) {
2660
2669
  const existing = this.sessions.get(bound);
@@ -2664,7 +2673,7 @@ var Router = class {
2664
2673
  }
2665
2674
  const agent = await this.agents.resume({
2666
2675
  sessionId: bound,
2667
- ...this.resolveSession(bot)
2676
+ ...this.resolveSession(bot, userId)
2668
2677
  });
2669
2678
  await this.attach(bot.project, bound);
2670
2679
  return this.adopt(bot.id, chatId, bound, agent, reply);
@@ -2673,7 +2682,7 @@ var Router = class {
2673
2682
  const agent = await this.agents.create({
2674
2683
  sessionId,
2675
2684
  cwd: bot.project,
2676
- ...this.resolveSession(bot)
2685
+ ...this.resolveSession(bot, userId)
2677
2686
  });
2678
2687
  await this.bindings.set(bot.id, chatId, sessionId);
2679
2688
  await this.attach(bot.project, sessionId);
@@ -2691,20 +2700,33 @@ var Router = class {
2691
2700
  resolveOptions(bot) {
2692
2701
  return bot.agentOptions ?? this.defaultModel();
2693
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
+ }
2694
2716
  /**
2695
2717
  * 按 bot.agentRef 解析会话组装(agentOptions + 创作期 hooks):
2696
2718
  * - 缺省/指向 main → 主 Agent 形态:bot 自带 persona/tools + 默认模型回退;
2697
2719
  * - 指向角色 → 角色形态:persona 单 section + tools.restrict + role.model;
2698
2720
  * - 指向不存在角色 → warn 并降级为主 Agent 形态。
2699
2721
  */
2700
- resolveSession(bot) {
2722
+ resolveSession(bot, userId) {
2701
2723
  const ref = bot.agentRef ?? "main";
2702
2724
  const role = this.registry.get(ref);
2703
2725
  if (role === void 0 || ref === "main") {
2704
2726
  if (role === void 0 && ref !== "main") this.onWarn(`[project-bot] bot "${bot.id}" 的 agentRef "${ref}" 不存在,降级绑定主 Agent`);
2705
2727
  return {
2706
2728
  agentOptions: this.resolveOptions(bot),
2707
- hooks: hooksOf(bot)
2729
+ hooks: this.withSenderSection(hooksOf(bot), bot, userId)
2708
2730
  };
2709
2731
  }
2710
2732
  const sections = role.persona === void 0 || role.persona.trim().length === 0 ? [] : [{
@@ -2714,21 +2736,21 @@ var Router = class {
2714
2736
  }];
2715
2737
  return {
2716
2738
  agentOptions: role.model ?? this.resolveOptions(bot),
2717
- hooks: {
2739
+ hooks: this.withSenderSection({
2718
2740
  ...sections.length > 0 ? { sections } : {},
2719
2741
  ...role.tools !== void 0 ? { tools: role.tools.allow } : {}
2720
- }
2742
+ }, bot, userId)
2721
2743
  };
2722
2744
  }
2723
2745
  /** /new:取消旧会话、清绑定、开新会话。 */
2724
- async reset(bot, chatId, reply) {
2746
+ async reset(bot, chatId, reply, userId) {
2725
2747
  const bound = this.bindings.get(bot.id, chatId);
2726
2748
  if (bound !== void 0) {
2727
2749
  this.sessions.get(bound)?.agent.cancel();
2728
2750
  this.sessions.delete(bound);
2729
2751
  await this.bindings.delete(bot.id, chatId);
2730
2752
  }
2731
- return this.ensure(bot, chatId, reply);
2753
+ return this.ensure(bot, chatId, reply, userId);
2732
2754
  }
2733
2755
  lookup(botId, chatId) {
2734
2756
  const bound = this.bindings.get(botId, chatId);
@@ -2761,7 +2783,7 @@ var BotRuntime = class {
2761
2783
  constructor(deps) {
2762
2784
  this.deps = deps;
2763
2785
  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);
2786
+ this.router = new Router(deps.agents, bindingStore, this.sessions, deps.defaultModel, deps.workspace, (m) => deps.log.warn(m), deps.registry, deps.injectSender ?? true);
2765
2787
  this.inbound = new Inbound({
2766
2788
  router: this.router,
2767
2789
  bots: deps.bots,
@@ -2774,11 +2796,12 @@ var BotRuntime = class {
2774
2796
  async startAll() {
2775
2797
  for (const botId of [...this.deps.bots.keys()]) await this.reconcile(botId);
2776
2798
  }
2777
- /** 按最新记录重建该 bot 的渠道(创建/更新后调用;记录已删则纯停止)。 */
2799
+ /** 按最新记录重建该 bot 的渠道(创建/更新后调用;记录已删或未绑定则纯停止)。 */
2778
2800
  async reconcile(botId) {
2779
2801
  await this.stopChannel(botId);
2780
2802
  const record = this.deps.bots.get(botId);
2781
2803
  if (record === void 0) return;
2804
+ if (record.feishu === void 0 || record.channel === void 0) return;
2782
2805
  if (!this.deps.validateProject(record.project)) {
2783
2806
  this.deps.log.warn(`[project-bot] bot "${botId}" 的项目路径不可用:${record.project}`);
2784
2807
  return;
@@ -2812,7 +2835,17 @@ var BotRuntime = class {
2812
2835
  }
2813
2836
  await this.bindingStore().deleteBot(botId);
2814
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
+ }
2815
2846
  statusOf(botId) {
2847
+ const record = this.deps.bots.get(botId);
2848
+ if (record !== void 0 && record.feishu === void 0) return "unbound";
2816
2849
  return this.handles.get(botId)?.status() ?? "not-running";
2817
2850
  }
2818
2851
  /** 卸载时序:取消在飞会话 → 等 idle → drain 出站链(卡片定格)→ 断全部渠道。 */
@@ -2920,7 +2953,7 @@ const CreateBodySchema = z$1.object({
2920
2953
  /** 手动填写路径:明文密钥(立即入 credentials,不落表)。 */
2921
2954
  appSecret: z$1.string().min(1).optional(),
2922
2955
  /** 扫码路径:registerApp 已入库,直接给引用。 */
2923
- appSecretRef: z$1.string().optional()
2956
+ appSecretRef: z$1.string().regex(CREDENTIAL_REF_RE).optional()
2924
2957
  })
2925
2958
  });
2926
2959
  const UpdateBodySchema = z$1.object({
@@ -2933,11 +2966,12 @@ const UpdateBodySchema = z$1.object({
2933
2966
  provider: z$1.string().min(1).optional(),
2934
2967
  model: z$1.string().min(1).optional()
2935
2968
  }).nullable().optional(),
2936
- /** 换绑应用:明文新密钥(立即入 credentials)。 */
2969
+ /** 重绑:明文新密钥(立即入 credentials)或扫码引用(已入库);null = 解绑渠道(删密钥、保留会话绑定)。 */
2937
2970
  feishu: z$1.object({
2938
2971
  appId: z$1.string().regex(FEISHU_APP_ID_RE),
2939
- appSecret: z$1.string().min(1)
2940
- }).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()
2941
2975
  });
2942
2976
  function json(res, code, body) {
2943
2977
  res.writeHead(code, { "content-type": "application/json" }).end(JSON.stringify(body));
@@ -2999,7 +3033,7 @@ function createApiHandler(deps) {
2999
3033
  json(res, 409, { error: `bot id "${id}" 已存在` });
3000
3034
  return;
3001
3035
  }
3002
- 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) {
3003
3037
  json(res, 409, { error: `appId 已被 bot "${existing.id}" 使用` });
3004
3038
  return;
3005
3039
  }
@@ -3054,23 +3088,40 @@ function createApiHandler(deps) {
3054
3088
  return;
3055
3089
  }
3056
3090
  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
3091
  const project = input.project ?? existing.project;
3063
3092
  if (!deps.validateProject(project)) {
3064
3093
  json(res, 400, { error: `项目路径不可用:${project}` });
3065
3094
  return;
3066
3095
  }
3067
- const merged = {
3068
- ...existing,
3069
- ...input.name !== void 0 ? { name: input.name } : {},
3070
- project,
3071
- feishu,
3072
- updatedAt: deps.now()
3073
- };
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
+ }
3074
3125
  if (input.persona === null) delete merged.persona;
3075
3126
  else if (input.persona !== void 0) merged.persona = input.persona;
3076
3127
  if (input.agentRef === null) delete merged.agentRef;
@@ -3097,7 +3148,7 @@ function createApiHandler(deps) {
3097
3148
  }
3098
3149
  await deps.runtime.stopBot(id);
3099
3150
  await deps.bots.delete(id);
3100
- await deps.deleteSecret(existing.feishu.appSecretRef);
3151
+ if (existing.feishu !== void 0) await deps.deleteSecret(existing.feishu.appSecretRef);
3101
3152
  json(res, 200, { ok: true });
3102
3153
  return;
3103
3154
  }
@@ -3321,6 +3372,7 @@ function setupBots(ctx, config, deps) {
3321
3372
  tunables,
3322
3373
  maxErrorDetailChars: config.errorDetailMaxChars,
3323
3374
  attachments: attachmentsOf,
3375
+ injectSender: config.injectSender,
3324
3376
  resolveSecret: async (ref) => (await ctx.credentials.resolve(credentialRef(ref)))?.value,
3325
3377
  validateProject: (path) => existsSync(path),
3326
3378
  log
@@ -3546,14 +3598,16 @@ const Config = z.object({
3546
3598
  processMaxBytes: z.number().default(8e3),
3547
3599
  registerAppTimeoutMs: z.number().default(6e5),
3548
3600
  processingReactionEmoji: z.string().default("OneSecond"),
3549
- errorDetailMaxChars: z.number().default(500)
3601
+ errorDetailMaxChars: z.number().default(500),
3602
+ injectSender: z.boolean().default(true)
3550
3603
  }).default({
3551
3604
  cardUpdateThrottleMs: 500,
3552
3605
  cardMaxBytes: 28e3,
3553
3606
  processMaxBytes: 8e3,
3554
3607
  registerAppTimeoutMs: 6e5,
3555
3608
  processingReactionEmoji: "OneSecond",
3556
- errorDetailMaxChars: 500
3609
+ errorDetailMaxChars: 500,
3610
+ injectSender: true
3557
3611
  }),
3558
3612
  agentTeamPreset: z.object({
3559
3613
  enabled: z.boolean().default(true),