chattercatcher 0.1.28 → 0.1.29

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/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import fs15 from "fs/promises";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "chattercatcher",
11
- version: "0.1.28",
11
+ version: "0.1.29",
12
12
  description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
13
13
  type: "module",
14
14
  main: "dist/index.js",
@@ -537,6 +537,18 @@ function migrateDatabase(database) {
537
537
 
538
538
  CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
539
539
 
540
+ CREATE TABLE IF NOT EXISTS feishu_chat_members (
541
+ chat_id TEXT NOT NULL,
542
+ open_id TEXT NOT NULL,
543
+ user_id TEXT,
544
+ user_name TEXT,
545
+ updated_at TEXT NOT NULL,
546
+ PRIMARY KEY (chat_id, open_id)
547
+ );
548
+
549
+ CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
550
+ ON feishu_chat_members(chat_id, user_name);
551
+
540
552
  CREATE TABLE IF NOT EXISTS cron_jobs (
541
553
  id TEXT PRIMARY KEY,
542
554
  chat_id TEXT NOT NULL,
@@ -554,11 +566,29 @@ function migrateDatabase(database) {
554
566
 
555
567
  CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
556
568
  CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
569
+
570
+ CREATE TABLE IF NOT EXISTS feishu_chat_members (
571
+ chat_id TEXT NOT NULL,
572
+ open_id TEXT NOT NULL,
573
+ user_id TEXT,
574
+ user_name TEXT NOT NULL,
575
+ updated_at TEXT NOT NULL,
576
+ PRIMARY KEY (chat_id, open_id)
577
+ );
578
+
579
+ CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
580
+ ON feishu_chat_members(chat_id, user_name);
557
581
  `);
558
582
  const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
559
- if (!cronJobColumns.some((column) => column.name === "image_file_name")) {
560
- database.prepare("ALTER TABLE cron_jobs ADD COLUMN image_file_name TEXT").run();
561
- }
583
+ const ensureCronJobColumn = (name, definition) => {
584
+ if (!cronJobColumns.some((column) => column.name === name)) {
585
+ database.prepare(`ALTER TABLE cron_jobs ADD COLUMN ${definition}`).run();
586
+ }
587
+ };
588
+ ensureCronJobColumn("image_file_name", "image_file_name TEXT");
589
+ ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
590
+ ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
591
+ ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
562
592
  }
563
593
 
564
594
  // src/doctor/checks.ts
@@ -2901,6 +2931,9 @@ var CronJobRepository = class {
2901
2931
  const schedule = input2.schedule.trim();
2902
2932
  const prompt = input2.prompt.trim();
2903
2933
  const imageFileName = input2.imageFileName?.trim();
2934
+ const mentionTargetName = input2.mentionTargetName?.trim();
2935
+ const mentionOpenId = input2.mentionOpenId?.trim();
2936
+ const mentionUserId = input2.mentionUserId?.trim();
2904
2937
  if (!isValidCronSchedule(schedule)) {
2905
2938
  throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
2906
2939
  }
@@ -2919,6 +2952,9 @@ var CronJobRepository = class {
2919
2952
  schedule,
2920
2953
  prompt,
2921
2954
  ...imageFileName ? { imageFileName } : {},
2955
+ ...mentionTargetName ? { mentionTargetName } : {},
2956
+ ...mentionOpenId ? { mentionOpenId } : {},
2957
+ ...mentionUserId ? { mentionUserId } : {},
2922
2958
  status: "active",
2923
2959
  nextRunAt: nextRunAt.toISOString(),
2924
2960
  createdAt: now.toISOString(),
@@ -2927,17 +2963,22 @@ var CronJobRepository = class {
2927
2963
  this.database.prepare(
2928
2964
  `
2929
2965
  INSERT INTO cron_jobs (
2930
- id, chat_id, created_by_open_id, schedule, prompt, image_file_name, status,
2966
+ id, chat_id, created_by_open_id, schedule, prompt, image_file_name,
2967
+ mention_target_name, mention_open_id, mention_user_id, status,
2931
2968
  last_run_at, next_run_at, last_error, created_at, updated_at
2932
2969
  )
2933
2970
  VALUES (
2934
- @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName, @status,
2971
+ @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName,
2972
+ @mentionTargetName, @mentionOpenId, @mentionUserId, @status,
2935
2973
  NULL, @nextRunAt, NULL, @createdAt, @updatedAt
2936
2974
  )
2937
2975
  `
2938
2976
  ).run({
2939
2977
  ...record,
2940
- imageFileName: record.imageFileName ?? null
2978
+ imageFileName: record.imageFileName ?? null,
2979
+ mentionTargetName: record.mentionTargetName ?? null,
2980
+ mentionOpenId: record.mentionOpenId ?? null,
2981
+ mentionUserId: record.mentionUserId ?? null
2941
2982
  });
2942
2983
  return record;
2943
2984
  }
@@ -2964,6 +3005,9 @@ var CronJobRepository = class {
2964
3005
  schedule,
2965
3006
  prompt,
2966
3007
  image_file_name AS imageFileName,
3008
+ mention_target_name AS mentionTargetName,
3009
+ mention_open_id AS mentionOpenId,
3010
+ mention_user_id AS mentionUserId,
2967
3011
  status,
2968
3012
  last_run_at AS lastRunAt,
2969
3013
  next_run_at AS nextRunAt,
@@ -2983,6 +3027,9 @@ var CronJobRepository = class {
2983
3027
  schedule: row.schedule,
2984
3028
  prompt: row.prompt,
2985
3029
  imageFileName: row.imageFileName ?? void 0,
3030
+ mentionTargetName: row.mentionTargetName ?? void 0,
3031
+ mentionOpenId: row.mentionOpenId ?? void 0,
3032
+ mentionUserId: row.mentionUserId ?? void 0,
2986
3033
  status: row.status,
2987
3034
  lastRunAt: row.lastRunAt ?? void 0,
2988
3035
  nextRunAt: row.nextRunAt,
@@ -3057,6 +3104,9 @@ var CronJobRepository = class {
3057
3104
  schedule,
3058
3105
  prompt,
3059
3106
  image_file_name AS imageFileName,
3107
+ mention_target_name AS mentionTargetName,
3108
+ mention_open_id AS mentionOpenId,
3109
+ mention_user_id AS mentionUserId,
3060
3110
  status,
3061
3111
  last_run_at AS lastRunAt,
3062
3112
  next_run_at AS nextRunAt,
@@ -3076,6 +3126,9 @@ var CronJobRepository = class {
3076
3126
  schedule: row.schedule,
3077
3127
  prompt: row.prompt,
3078
3128
  imageFileName: row.imageFileName ?? void 0,
3129
+ mentionTargetName: row.mentionTargetName ?? void 0,
3130
+ mentionOpenId: row.mentionOpenId ?? void 0,
3131
+ mentionUserId: row.mentionUserId ?? void 0,
3079
3132
  status: row.status,
3080
3133
  lastRunAt: row.lastRunAt ?? void 0,
3081
3134
  nextRunAt: row.nextRunAt,
@@ -3101,8 +3154,12 @@ async function generateCronJobMessage(input2) {
3101
3154
  if (!input2.model.completeWithTools) {
3102
3155
  throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
3103
3156
  }
3157
+ const systemPrompt = input2.memberPrompt ? `${SYSTEM_PROMPT}
3158
+
3159
+ ${input2.memberPrompt}
3160
+ \u751F\u6210\u6D88\u606F\u65F6\u9047\u5230\u4E0A\u8FF0 ID \u65F6\u4F18\u5148\u4F7F\u7528\u5BF9\u5E94\u7FA4\u6635\u79F0\uFF1B\u6CA1\u6709\u6620\u5C04\u65F6\u4FDD\u7559\u539F ID\uFF0C\u4E0D\u8981\u7F16\u9020\u6635\u79F0\u3002` : SYSTEM_PROMPT;
3104
3161
  const messages = [
3105
- { role: "system", content: SYSTEM_PROMPT },
3162
+ { role: "system", content: systemPrompt },
3106
3163
  { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3107
3164
  \u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}` }
3108
3165
  ];
@@ -3120,7 +3177,7 @@ async function generateCronJobMessage(input2) {
3120
3177
  for (const call of result.toolCalls) {
3121
3178
  if (toolCallsUsed >= maxToolCalls) {
3122
3179
  return input2.model.complete([
3123
- { role: "system", content: SYSTEM_PROMPT },
3180
+ { role: "system", content: systemPrompt },
3124
3181
  {
3125
3182
  role: "user",
3126
3183
  content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
@@ -3179,7 +3236,12 @@ function createCronJobScheduler(options) {
3179
3236
  for (const job of jobs) {
3180
3237
  try {
3181
3238
  const text = await options.generateMessage(job, startedAt);
3182
- await options.sendTextToChat(job.chatId, text);
3239
+ const sendOptions = job.mentionOpenId && job.mentionTargetName ? { mentions: [{ openId: job.mentionOpenId, name: job.mentionTargetName }] } : void 0;
3240
+ if (sendOptions) {
3241
+ await options.sendTextToChat(job.chatId, text, sendOptions);
3242
+ } else {
3243
+ await options.sendTextToChat(job.chatId, text);
3244
+ }
3183
3245
  if (job.imageFileName) {
3184
3246
  if (!options.sendImageToChat) {
3185
3247
  throw new Error("\u5F53\u524D\u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\u3002");
@@ -3677,6 +3739,185 @@ var ImageMultimodalWorker = class {
3677
3739
  }
3678
3740
  };
3679
3741
 
3742
+ // src/feishu/members.ts
3743
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
3744
+ var FeishuMemberRepository = class {
3745
+ constructor(database) {
3746
+ this.database = database;
3747
+ }
3748
+ database;
3749
+ upsert(record) {
3750
+ this.database.prepare(
3751
+ `
3752
+ INSERT INTO feishu_chat_members (chat_id, open_id, user_id, user_name, updated_at)
3753
+ VALUES (@chatId, @openId, @userId, @userName, @updatedAt)
3754
+ ON CONFLICT(chat_id, open_id)
3755
+ DO UPDATE SET
3756
+ user_id = excluded.user_id,
3757
+ user_name = excluded.user_name,
3758
+ updated_at = excluded.updated_at
3759
+ `
3760
+ ).run({
3761
+ chatId: record.chatId,
3762
+ openId: record.openId,
3763
+ userId: record.userId ?? null,
3764
+ userName: record.userName,
3765
+ updatedAt: record.updatedAt
3766
+ });
3767
+ }
3768
+ get(chatId, openId) {
3769
+ const row = this.database.prepare(
3770
+ `
3771
+ SELECT
3772
+ chat_id AS chatId,
3773
+ open_id AS openId,
3774
+ user_id AS userId,
3775
+ user_name AS userName,
3776
+ updated_at AS updatedAt
3777
+ FROM feishu_chat_members
3778
+ WHERE chat_id = ? AND open_id = ?
3779
+ `
3780
+ ).get(chatId, openId);
3781
+ return row ?? null;
3782
+ }
3783
+ listByChat(chatId) {
3784
+ return this.database.prepare(
3785
+ `
3786
+ SELECT
3787
+ chat_id AS chatId,
3788
+ open_id AS openId,
3789
+ user_id AS userId,
3790
+ user_name AS userName,
3791
+ updated_at AS updatedAt
3792
+ FROM feishu_chat_members
3793
+ WHERE chat_id = ?
3794
+ ORDER BY user_name ASC, open_id ASC
3795
+ `
3796
+ ).all(chatId);
3797
+ }
3798
+ findUniqueByName(chatId, userName) {
3799
+ const rows = this.database.prepare(
3800
+ `
3801
+ SELECT
3802
+ chat_id AS chatId,
3803
+ open_id AS openId,
3804
+ user_id AS userId,
3805
+ user_name AS userName,
3806
+ updated_at AS updatedAt
3807
+ FROM feishu_chat_members
3808
+ WHERE chat_id = ? AND user_name = ?
3809
+ ORDER BY open_id ASC
3810
+ LIMIT 2
3811
+ `
3812
+ ).all(chatId, userName);
3813
+ return rows.length === 1 ? rows[0] : null;
3814
+ }
3815
+ };
3816
+ var FeishuMemberResolver = class {
3817
+ constructor(options) {
3818
+ this.options = options;
3819
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3820
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
3821
+ this.logger = options.logger;
3822
+ }
3823
+ options;
3824
+ now;
3825
+ ttlMs;
3826
+ logger;
3827
+ async resolveOpenIdName(chatId, openId) {
3828
+ const cached = this.options.repository.get(chatId, openId);
3829
+ if (!cached || this.isExpired(cached.updatedAt)) {
3830
+ try {
3831
+ await this.refreshChatMembers(chatId);
3832
+ } catch (error) {
3833
+ this.logger?.warn("Failed to refresh Feishu chat members for open id resolution", {
3834
+ chatId,
3835
+ openId,
3836
+ error
3837
+ });
3838
+ return cached?.userName ?? openId;
3839
+ }
3840
+ }
3841
+ return this.options.repository.get(chatId, openId)?.userName ?? openId;
3842
+ }
3843
+ async resolveUniqueName(chatId, userName) {
3844
+ const cached = this.options.repository.findUniqueByName(chatId, userName);
3845
+ if (cached && !this.isExpired(cached.updatedAt)) {
3846
+ return cached;
3847
+ }
3848
+ try {
3849
+ await this.refreshChatMembers(chatId);
3850
+ } catch (error) {
3851
+ this.logger?.warn("Failed to refresh Feishu chat members for unique name resolution", {
3852
+ chatId,
3853
+ userName,
3854
+ error
3855
+ });
3856
+ return cached ?? null;
3857
+ }
3858
+ return this.options.repository.findUniqueByName(chatId, userName);
3859
+ }
3860
+ isExpired(updatedAt) {
3861
+ const updatedAtMs = Date.parse(updatedAt);
3862
+ if (Number.isNaN(updatedAtMs)) {
3863
+ return true;
3864
+ }
3865
+ return this.now().getTime() - updatedAtMs >= this.ttlMs;
3866
+ }
3867
+ async refreshChatMembers(chatId) {
3868
+ const members = await this.options.client.listChatMembers({ chatId, memberIdType: "open_id" });
3869
+ const updatedAt = this.now().toISOString();
3870
+ for (const member of members) {
3871
+ this.options.repository.upsert({
3872
+ chatId,
3873
+ openId: member.openId,
3874
+ userId: member.userId,
3875
+ userName: member.userName,
3876
+ updatedAt
3877
+ });
3878
+ }
3879
+ }
3880
+ };
3881
+ function formatFeishuMemberPrompt(members, limit = 80) {
3882
+ const lines = members.filter((member) => member.userName).slice(0, limit).map((member) => `${member.openId} = ${member.userName}`);
3883
+ return lines.length ? `\u5F53\u524D\u7FA4\u804A\u6210\u5458 ID \u4E0E\u7FA4\u6635\u79F0\u6620\u5C04\uFF1A
3884
+ ${lines.join("\n")}` : "";
3885
+ }
3886
+ function createFeishuChatMembersClient(client) {
3887
+ return {
3888
+ async listChatMembers(payload) {
3889
+ const api = client.im.v1?.chatMembers?.get;
3890
+ if (!api) {
3891
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301 chatMembers.get\uFF0C\u65E0\u6CD5\u83B7\u53D6\u7FA4\u6210\u5458\u3002");
3892
+ }
3893
+ const members = [];
3894
+ let pageToken;
3895
+ do {
3896
+ const response = await api({
3897
+ path: { chat_id: payload.chatId },
3898
+ params: {
3899
+ member_id_type: payload.memberIdType,
3900
+ ...pageToken ? { page_token: pageToken } : {}
3901
+ }
3902
+ });
3903
+ const items = response.data?.items ?? [];
3904
+ for (const item of items) {
3905
+ if (!item.member_id || !item.name) {
3906
+ continue;
3907
+ }
3908
+ members.push({
3909
+ openId: item.member_id,
3910
+ userId: item.user_id,
3911
+ userName: item.name
3912
+ });
3913
+ }
3914
+ pageToken = response.data?.has_more ? response.data.page_token : void 0;
3915
+ } while (pageToken);
3916
+ return members;
3917
+ }
3918
+ };
3919
+ }
3920
+
3680
3921
  // src/cron/tools.ts
3681
3922
  function readString(input2, key) {
3682
3923
  const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
@@ -3715,18 +3956,27 @@ function createCronJobTools(input2) {
3715
3956
  imageFileName: {
3716
3957
  type: "string",
3717
3958
  description: "Optional image filename already stored from the current chat, for example om_xxx-image.jpg."
3959
+ },
3960
+ mentionTargetName: {
3961
+ type: "string",
3962
+ description: "Optional exact Feishu chat nickname to @ when the scheduled message is sent."
3718
3963
  }
3719
3964
  },
3720
3965
  required: ["schedule", "prompt"],
3721
3966
  additionalProperties: false
3722
3967
  },
3723
3968
  execute: async (rawInput) => {
3969
+ const mentionTargetName = readOptionalString(rawInput, "mentionTargetName");
3970
+ const mentionTarget = mentionTargetName && input2.memberResolver ? await input2.memberResolver.resolveUniqueName(input2.chatId, mentionTargetName) : null;
3724
3971
  const job = input2.repository.create({
3725
3972
  chatId: input2.chatId,
3726
3973
  createdByOpenId: input2.createdByOpenId,
3727
3974
  schedule: readString(rawInput, "schedule"),
3728
3975
  prompt: readString(rawInput, "prompt"),
3729
- imageFileName: readOptionalString(rawInput, "imageFileName")
3976
+ imageFileName: readOptionalString(rawInput, "imageFileName"),
3977
+ mentionTargetName,
3978
+ mentionOpenId: mentionTarget?.openId,
3979
+ mentionUserId: mentionTarget?.userId
3730
3980
  });
3731
3981
  return JSON.stringify({ ok: true, job });
3732
3982
  }
@@ -3930,8 +4180,12 @@ async function runFeishuToolLoop(input2) {
3930
4180
  }
3931
4181
  const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
3932
4182
  const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
4183
+ const systemPrompt = input2.memberPrompt ? `${FEISHU_TOOL_SYSTEM_PROMPT}
4184
+
4185
+ ${input2.memberPrompt}
4186
+ \u56DE\u7B54\u4E2D\u9047\u5230\u4E0A\u8FF0 ID \u65F6\u4F18\u5148\u4F7F\u7528\u5BF9\u5E94\u7FA4\u6635\u79F0\uFF1B\u6CA1\u6709\u6620\u5C04\u65F6\u4FDD\u7559\u539F ID\uFF0C\u4E0D\u8981\u7F16\u9020\u6635\u79F0\u3002` : FEISHU_TOOL_SYSTEM_PROMPT;
3933
4187
  const messages = [
3934
- { role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
4188
+ { role: "system", content: systemPrompt },
3935
4189
  { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3936
4190
  \u95EE\u9898\uFF1A${input2.question}` }
3937
4191
  ];
@@ -4043,8 +4297,13 @@ function getFeishuQuestionDecision(payload, config) {
4043
4297
  var FeishuQuestionHandler = class {
4044
4298
  constructor(options) {
4045
4299
  this.options = options;
4300
+ this.memberResolver = options.memberResolver;
4046
4301
  }
4047
4302
  options;
4303
+ memberResolver;
4304
+ setMemberResolver(memberResolver) {
4305
+ this.memberResolver = memberResolver;
4306
+ }
4048
4307
  async sendResponse(chatId, messageId, text) {
4049
4308
  if (messageId && this.options.sender.replyTextToMessage) {
4050
4309
  try {
@@ -4091,14 +4350,18 @@ var FeishuQuestionHandler = class {
4091
4350
  const cronTools = createCronJobTools({
4092
4351
  repository: new CronJobRepository(this.options.database),
4093
4352
  chatId: decision.chatId,
4094
- createdByOpenId: payload.event?.sender?.sender_id?.open_id
4353
+ createdByOpenId: payload.event?.sender?.sender_id?.open_id,
4354
+ memberResolver: this.memberResolver
4095
4355
  });
4096
4356
  const allTools = [...tools, ...cronTools];
4357
+ const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
4358
+ const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
4097
4359
  const answer = await runFeishuToolLoop({
4098
4360
  question: decision.question,
4099
4361
  now,
4100
4362
  tools: allTools,
4101
- model: this.options.model
4363
+ model: this.options.model,
4364
+ memberPrompt
4102
4365
  });
4103
4366
  qaLogs.create({
4104
4367
  chatId: decision.chatId,
@@ -4151,6 +4414,15 @@ function extractImageKey(response) {
4151
4414
  }
4152
4415
  throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
4153
4416
  }
4417
+ function escapeAtText(value) {
4418
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4419
+ }
4420
+ function formatTextWithMentions(text, options) {
4421
+ const mentions = options?.mentions ?? [];
4422
+ if (mentions.length === 0) return text;
4423
+ const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
4424
+ return `${prefix} ${text}`.trim();
4425
+ }
4154
4426
  var FeishuMessageSender = class _FeishuMessageSender {
4155
4427
  constructor(client) {
4156
4428
  this.client = client;
@@ -4164,12 +4436,12 @@ var FeishuMessageSender = class _FeishuMessageSender {
4164
4436
  });
4165
4437
  return new _FeishuMessageSender(client);
4166
4438
  }
4167
- async sendTextToChat(chatId, text) {
4439
+ async sendTextToChat(chatId, text, options) {
4168
4440
  const payload = {
4169
4441
  data: {
4170
4442
  receive_id: chatId,
4171
4443
  msg_type: "text",
4172
- content: JSON.stringify({ text })
4444
+ content: JSON.stringify({ text: formatTextWithMentions(text, options) })
4173
4445
  },
4174
4446
  params: {
4175
4447
  receive_id_type: "chat_id"
@@ -4291,13 +4563,24 @@ function createFeishuEventDispatcher(options) {
4291
4563
  return;
4292
4564
  }
4293
4565
  }
4294
- const result = options.resourceDownloader ? await options.ingestor.ingestFeishuEventAndDownloadAttachments({
4295
- payload,
4296
- downloader: options.resourceDownloader,
4297
- config: options.config,
4298
- secrets: options.secrets,
4299
- vectorIndexMessage: options.attachmentVectorIndexer
4300
- }) : options.ingestor.ingestFeishuEvent(payload);
4566
+ let result;
4567
+ if (options.resourceDownloader) {
4568
+ result = await options.ingestor.ingestFeishuEventAndDownloadAttachments({
4569
+ payload,
4570
+ downloader: options.resourceDownloader,
4571
+ config: options.config,
4572
+ secrets: options.secrets,
4573
+ vectorIndexMessage: options.attachmentVectorIndexer,
4574
+ memberResolver: options.memberResolver
4575
+ });
4576
+ } else if (options.memberResolver) {
4577
+ result = await options.ingestor.ingestFeishuEventWithMembers({
4578
+ payload,
4579
+ memberResolver: options.memberResolver
4580
+ });
4581
+ } else {
4582
+ result = options.ingestor.ingestFeishuEvent(payload);
4583
+ }
4301
4584
  if (!result.accepted) {
4302
4585
  console.log(`\u98DE\u4E66\u6D88\u606F\u672A\u5165\u5E93\uFF1A${result.reason}`);
4303
4586
  return;
@@ -4388,10 +4671,20 @@ function createFeishuGateway(options) {
4388
4671
  onReconnecting: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u6B63\u5728\u91CD\u8FDE\u3002"),
4389
4672
  onReconnected: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u91CD\u8FDE\u3002")
4390
4673
  });
4674
+ const memberResolver = new FeishuMemberResolver({
4675
+ repository: new FeishuMemberRepository(options.ingestor.database),
4676
+ client: createFeishuChatMembersClient(new lark2.Client({
4677
+ appId: options.config.feishu.appId,
4678
+ appSecret: options.secrets.feishu.appSecret,
4679
+ domain: mapDomain(options.config.feishu.domain)
4680
+ }))
4681
+ });
4682
+ options.questionHandler?.setMemberResolver?.(memberResolver);
4391
4683
  const eventDispatcher = createFeishuEventDispatcher({
4392
4684
  config: options.config,
4393
4685
  secrets: options.secrets,
4394
4686
  ingestor: options.ingestor,
4687
+ memberResolver,
4395
4688
  questionHandler: options.questionHandler,
4396
4689
  resourceDownloader: options.resourceDownloader,
4397
4690
  attachmentVectorIndexer: options.attachmentVectorIndexer,
@@ -4411,7 +4704,7 @@ function createFeishuGateway(options) {
4411
4704
  }) : void 0);
4412
4705
  const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4413
4706
  repository: new CronJobRepository(options.cronJobProcessor.database),
4414
- sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
4707
+ sendTextToChat: (chatId, text, sendOptions) => options.cronJobProcessor.sender.sendTextToChat(chatId, text, sendOptions),
4415
4708
  sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
4416
4709
  chatId,
4417
4710
  resolveFeishuImagePath(options.config, imageFileName)
@@ -4425,7 +4718,16 @@ function createFeishuGateway(options) {
4425
4718
  scope: { platform: "feishu", platformChatId: job.chatId }
4426
4719
  });
4427
4720
  try {
4428
- return await generateCronJobMessage({ prompt: job.prompt, model: options.cronJobProcessor.model, tools, now });
4721
+ const memberPrompt = formatFeishuMemberPrompt(
4722
+ new FeishuMemberRepository(options.cronJobProcessor.database).listByChat(job.chatId)
4723
+ );
4724
+ return await generateCronJobMessage({
4725
+ prompt: job.prompt,
4726
+ model: options.cronJobProcessor.model,
4727
+ tools,
4728
+ now,
4729
+ memberPrompt
4730
+ });
4429
4731
  } finally {
4430
4732
  close();
4431
4733
  }
@@ -4783,7 +5085,9 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4783
5085
  if (!text) {
4784
5086
  return null;
4785
5087
  }
4786
- const senderId = event.sender?.sender_id?.open_id || event.sender?.sender_id?.user_id || event.sender?.sender_id?.union_id || "unknown";
5088
+ const senderOpenId = event.sender?.sender_id?.open_id;
5089
+ const senderUserId = event.sender?.sender_id?.user_id;
5090
+ const senderId = senderOpenId || senderUserId || event.sender?.sender_id?.union_id || "unknown";
4787
5091
  return {
4788
5092
  platform: "feishu",
4789
5093
  platformChatId: message.chat_id,
@@ -4798,12 +5102,24 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4798
5102
  platform: "feishu",
4799
5103
  raw: payload,
4800
5104
  content,
5105
+ sender: {
5106
+ ...senderOpenId ? { openId: senderOpenId } : {},
5107
+ ...senderUserId ? { userId: senderUserId } : {}
5108
+ },
4801
5109
  attachment: extractFeishuAttachment(messageType, content)
4802
5110
  }
4803
5111
  };
4804
5112
  }
4805
5113
 
4806
5114
  // src/gateway/ingest.ts
5115
+ function extractFeishuSenderOpenId(message) {
5116
+ const raw = message.rawPayload;
5117
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
5118
+ const sender = raw.sender;
5119
+ if (!sender || typeof sender !== "object" || Array.isArray(sender)) return void 0;
5120
+ const openId = sender.openId;
5121
+ return typeof openId === "string" && openId.trim() ? openId.trim() : void 0;
5122
+ }
4807
5123
  function extractAttachment(message) {
4808
5124
  const raw = message.rawPayload;
4809
5125
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -4823,14 +5139,16 @@ function isMultimodalReady(config, secrets) {
4823
5139
  return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
4824
5140
  }
4825
5141
  var GatewayIngestor = class {
4826
- messages;
4827
- jobs;
4828
- imageTasks;
4829
5142
  constructor(database) {
5143
+ this.database = database;
4830
5144
  this.messages = new MessageRepository(database);
4831
5145
  this.jobs = new FileJobRepository(database);
4832
5146
  this.imageTasks = new ImageMultimodalTaskRepository(database);
4833
5147
  }
5148
+ database;
5149
+ messages;
5150
+ jobs;
5151
+ imageTasks;
4834
5152
  ingestFeishuEvent(payload) {
4835
5153
  const normalized = normalizeFeishuReceiveMessageEvent(payload);
4836
5154
  if (!normalized) {
@@ -4848,8 +5166,28 @@ var GatewayIngestor = class {
4848
5166
  duplicate
4849
5167
  };
4850
5168
  }
5169
+ async ingestFeishuEventWithMembers(input2) {
5170
+ const normalized = normalizeFeishuReceiveMessageEvent(input2.payload);
5171
+ if (!normalized) {
5172
+ return {
5173
+ accepted: false,
5174
+ reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
5175
+ };
5176
+ }
5177
+ const openId = extractFeishuSenderOpenId(normalized);
5178
+ const senderName = openId ? await input2.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
5179
+ const enriched = { ...normalized, senderName };
5180
+ const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
5181
+ const messageId = this.messages.ingest(enriched);
5182
+ return {
5183
+ accepted: true,
5184
+ messageId,
5185
+ message: enriched,
5186
+ duplicate
5187
+ };
5188
+ }
4851
5189
  async ingestFeishuEventAndDownloadAttachments(input2) {
4852
- const result = this.ingestFeishuEvent(input2.payload);
5190
+ const result = input2.memberResolver ? await this.ingestFeishuEventWithMembers({ payload: input2.payload, memberResolver: input2.memberResolver }) : this.ingestFeishuEvent(input2.payload);
4853
5191
  if (!result.accepted || !result.messageId || !result.message || result.duplicate) {
4854
5192
  return result;
4855
5193
  }
@@ -5104,6 +5442,9 @@ function createMultimodalModel(config, secrets) {
5104
5442
  });
5105
5443
  }
5106
5444
 
5445
+ // src/cli.ts
5446
+ import * as lark4 from "@larksuiteoapi/node-sdk";
5447
+
5107
5448
  // src/rag/answer.ts
5108
5449
  var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
5109
5450
  var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
@@ -6201,6 +6542,14 @@ async function startGatewayForegroundCommand() {
6201
6542
  const database = openDatabase(config);
6202
6543
  const chatModel = createChatModel(config, secrets);
6203
6544
  const sender = FeishuMessageSender.fromConfig(config, secrets);
6545
+ const memberResolver = new FeishuMemberResolver({
6546
+ repository: new FeishuMemberRepository(database),
6547
+ client: createFeishuChatMembersClient(new lark4.Client({
6548
+ appId: config.feishu.appId,
6549
+ appSecret: secrets.feishu.appSecret,
6550
+ domain: mapDomain(config.feishu.domain)
6551
+ }))
6552
+ });
6204
6553
  const vectorStore = hasEmbeddingConfig(config, secrets) ? new SqliteVectorStore(database, { model: config.embedding.model }) : null;
6205
6554
  const gatewayRuntime = createFeishuGateway({
6206
6555
  config,
@@ -6233,6 +6582,7 @@ async function startGatewayForegroundCommand() {
6233
6582
  config,
6234
6583
  secrets,
6235
6584
  database,
6585
+ memberResolver,
6236
6586
  sender,
6237
6587
  model: chatModel
6238
6588
  })