chattercatcher 0.1.28 → 0.1.30

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/index.js CHANGED
@@ -193,8 +193,12 @@ async function generateCronJobMessage(input) {
193
193
  if (!input.model.completeWithTools) {
194
194
  throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
195
195
  }
196
+ const systemPrompt = input.memberPrompt ? `${SYSTEM_PROMPT}
197
+
198
+ ${input.memberPrompt}
199
+ \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;
196
200
  const messages = [
197
- { role: "system", content: SYSTEM_PROMPT },
201
+ { role: "system", content: systemPrompt },
198
202
  { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
199
203
  \u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input.prompt}` }
200
204
  ];
@@ -212,7 +216,7 @@ async function generateCronJobMessage(input) {
212
216
  for (const call of result.toolCalls) {
213
217
  if (toolCallsUsed >= maxToolCalls) {
214
218
  return input.model.complete([
215
- { role: "system", content: SYSTEM_PROMPT },
219
+ { role: "system", content: systemPrompt },
216
220
  {
217
221
  role: "user",
218
222
  content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
@@ -363,6 +367,9 @@ var CronJobRepository = class {
363
367
  const schedule = input.schedule.trim();
364
368
  const prompt = input.prompt.trim();
365
369
  const imageFileName = input.imageFileName?.trim();
370
+ const mentionTargetName = input.mentionTargetName?.trim();
371
+ const mentionOpenId = input.mentionOpenId?.trim();
372
+ const mentionUserId = input.mentionUserId?.trim();
366
373
  if (!isValidCronSchedule(schedule)) {
367
374
  throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
368
375
  }
@@ -381,6 +388,9 @@ var CronJobRepository = class {
381
388
  schedule,
382
389
  prompt,
383
390
  ...imageFileName ? { imageFileName } : {},
391
+ ...mentionTargetName ? { mentionTargetName } : {},
392
+ ...mentionOpenId ? { mentionOpenId } : {},
393
+ ...mentionUserId ? { mentionUserId } : {},
384
394
  status: "active",
385
395
  nextRunAt: nextRunAt.toISOString(),
386
396
  createdAt: now.toISOString(),
@@ -389,17 +399,22 @@ var CronJobRepository = class {
389
399
  this.database.prepare(
390
400
  `
391
401
  INSERT INTO cron_jobs (
392
- id, chat_id, created_by_open_id, schedule, prompt, image_file_name, status,
402
+ id, chat_id, created_by_open_id, schedule, prompt, image_file_name,
403
+ mention_target_name, mention_open_id, mention_user_id, status,
393
404
  last_run_at, next_run_at, last_error, created_at, updated_at
394
405
  )
395
406
  VALUES (
396
- @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName, @status,
407
+ @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName,
408
+ @mentionTargetName, @mentionOpenId, @mentionUserId, @status,
397
409
  NULL, @nextRunAt, NULL, @createdAt, @updatedAt
398
410
  )
399
411
  `
400
412
  ).run({
401
413
  ...record,
402
- imageFileName: record.imageFileName ?? null
414
+ imageFileName: record.imageFileName ?? null,
415
+ mentionTargetName: record.mentionTargetName ?? null,
416
+ mentionOpenId: record.mentionOpenId ?? null,
417
+ mentionUserId: record.mentionUserId ?? null
403
418
  });
404
419
  return record;
405
420
  }
@@ -426,6 +441,9 @@ var CronJobRepository = class {
426
441
  schedule,
427
442
  prompt,
428
443
  image_file_name AS imageFileName,
444
+ mention_target_name AS mentionTargetName,
445
+ mention_open_id AS mentionOpenId,
446
+ mention_user_id AS mentionUserId,
429
447
  status,
430
448
  last_run_at AS lastRunAt,
431
449
  next_run_at AS nextRunAt,
@@ -445,6 +463,9 @@ var CronJobRepository = class {
445
463
  schedule: row.schedule,
446
464
  prompt: row.prompt,
447
465
  imageFileName: row.imageFileName ?? void 0,
466
+ mentionTargetName: row.mentionTargetName ?? void 0,
467
+ mentionOpenId: row.mentionOpenId ?? void 0,
468
+ mentionUserId: row.mentionUserId ?? void 0,
448
469
  status: row.status,
449
470
  lastRunAt: row.lastRunAt ?? void 0,
450
471
  nextRunAt: row.nextRunAt,
@@ -519,6 +540,9 @@ var CronJobRepository = class {
519
540
  schedule,
520
541
  prompt,
521
542
  image_file_name AS imageFileName,
543
+ mention_target_name AS mentionTargetName,
544
+ mention_open_id AS mentionOpenId,
545
+ mention_user_id AS mentionUserId,
522
546
  status,
523
547
  last_run_at AS lastRunAt,
524
548
  next_run_at AS nextRunAt,
@@ -538,6 +562,9 @@ var CronJobRepository = class {
538
562
  schedule: row.schedule,
539
563
  prompt: row.prompt,
540
564
  imageFileName: row.imageFileName ?? void 0,
565
+ mentionTargetName: row.mentionTargetName ?? void 0,
566
+ mentionOpenId: row.mentionOpenId ?? void 0,
567
+ mentionUserId: row.mentionUserId ?? void 0,
541
568
  status: row.status,
542
569
  lastRunAt: row.lastRunAt ?? void 0,
543
570
  nextRunAt: row.nextRunAt,
@@ -567,7 +594,12 @@ function createCronJobScheduler(options) {
567
594
  for (const job of jobs) {
568
595
  try {
569
596
  const text = await options.generateMessage(job, startedAt);
570
- await options.sendTextToChat(job.chatId, text);
597
+ const sendOptions = job.mentionOpenId && job.mentionTargetName ? { mentions: [{ openId: job.mentionOpenId, name: job.mentionTargetName }] } : void 0;
598
+ if (sendOptions) {
599
+ await options.sendTextToChat(job.chatId, text, sendOptions);
600
+ } else {
601
+ await options.sendTextToChat(job.chatId, text);
602
+ }
571
603
  if (job.imageFileName) {
572
604
  if (!options.sendImageToChat) {
573
605
  throw new Error("\u5F53\u524D\u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\u3002");
@@ -644,18 +676,27 @@ function createCronJobTools(input) {
644
676
  imageFileName: {
645
677
  type: "string",
646
678
  description: "Optional image filename already stored from the current chat, for example om_xxx-image.jpg."
679
+ },
680
+ mentionTargetName: {
681
+ type: "string",
682
+ description: "Optional exact Feishu chat nickname to @ when the scheduled message is sent."
647
683
  }
648
684
  },
649
685
  required: ["schedule", "prompt"],
650
686
  additionalProperties: false
651
687
  },
652
688
  execute: async (rawInput) => {
689
+ const mentionTargetName = readOptionalString(rawInput, "mentionTargetName");
690
+ const mentionTarget = mentionTargetName && input.memberResolver ? await input.memberResolver.resolveUniqueName(input.chatId, mentionTargetName) : null;
653
691
  const job = input.repository.create({
654
692
  chatId: input.chatId,
655
693
  createdByOpenId: input.createdByOpenId,
656
694
  schedule: readString(rawInput, "schedule"),
657
695
  prompt: readString(rawInput, "prompt"),
658
- imageFileName: readOptionalString(rawInput, "imageFileName")
696
+ imageFileName: readOptionalString(rawInput, "imageFileName"),
697
+ mentionTargetName,
698
+ mentionOpenId: mentionTarget?.openId,
699
+ mentionUserId: mentionTarget?.userId
659
700
  });
660
701
  return JSON.stringify({ ok: true, job });
661
702
  }
@@ -974,6 +1015,18 @@ function migrateDatabase(database) {
974
1015
 
975
1016
  CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
976
1017
 
1018
+ CREATE TABLE IF NOT EXISTS feishu_chat_members (
1019
+ chat_id TEXT NOT NULL,
1020
+ open_id TEXT NOT NULL,
1021
+ user_id TEXT,
1022
+ user_name TEXT,
1023
+ updated_at TEXT NOT NULL,
1024
+ PRIMARY KEY (chat_id, open_id)
1025
+ );
1026
+
1027
+ CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
1028
+ ON feishu_chat_members(chat_id, user_name);
1029
+
977
1030
  CREATE TABLE IF NOT EXISTS cron_jobs (
978
1031
  id TEXT PRIMARY KEY,
979
1032
  chat_id TEXT NOT NULL,
@@ -991,11 +1044,29 @@ function migrateDatabase(database) {
991
1044
 
992
1045
  CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
993
1046
  CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
1047
+
1048
+ CREATE TABLE IF NOT EXISTS feishu_chat_members (
1049
+ chat_id TEXT NOT NULL,
1050
+ open_id TEXT NOT NULL,
1051
+ user_id TEXT,
1052
+ user_name TEXT NOT NULL,
1053
+ updated_at TEXT NOT NULL,
1054
+ PRIMARY KEY (chat_id, open_id)
1055
+ );
1056
+
1057
+ CREATE INDEX IF NOT EXISTS feishu_chat_members_chat_name_idx
1058
+ ON feishu_chat_members(chat_id, user_name);
994
1059
  `);
995
1060
  const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
996
- if (!cronJobColumns.some((column) => column.name === "image_file_name")) {
997
- database.prepare("ALTER TABLE cron_jobs ADD COLUMN image_file_name TEXT").run();
998
- }
1061
+ const ensureCronJobColumn = (name, definition) => {
1062
+ if (!cronJobColumns.some((column) => column.name === name)) {
1063
+ database.prepare(`ALTER TABLE cron_jobs ADD COLUMN ${definition}`).run();
1064
+ }
1065
+ };
1066
+ ensureCronJobColumn("image_file_name", "image_file_name TEXT");
1067
+ ensureCronJobColumn("mention_target_name", "mention_target_name TEXT");
1068
+ ensureCronJobColumn("mention_open_id", "mention_open_id TEXT");
1069
+ ensureCronJobColumn("mention_user_id", "mention_user_id TEXT");
999
1070
  }
1000
1071
 
1001
1072
  // src/doctor/checks.ts
@@ -3619,11 +3690,204 @@ var ImageMultimodalWorker = class {
3619
3690
  }
3620
3691
  };
3621
3692
 
3693
+ // src/feishu/members.ts
3694
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
3695
+ var FeishuMemberRepository = class {
3696
+ constructor(database) {
3697
+ this.database = database;
3698
+ }
3699
+ database;
3700
+ upsert(record) {
3701
+ this.database.prepare(
3702
+ `
3703
+ INSERT INTO feishu_chat_members (chat_id, open_id, user_id, user_name, updated_at)
3704
+ VALUES (@chatId, @openId, @userId, @userName, @updatedAt)
3705
+ ON CONFLICT(chat_id, open_id)
3706
+ DO UPDATE SET
3707
+ user_id = excluded.user_id,
3708
+ user_name = excluded.user_name,
3709
+ updated_at = excluded.updated_at
3710
+ `
3711
+ ).run({
3712
+ chatId: record.chatId,
3713
+ openId: record.openId,
3714
+ userId: record.userId ?? null,
3715
+ userName: record.userName,
3716
+ updatedAt: record.updatedAt
3717
+ });
3718
+ }
3719
+ get(chatId, openId) {
3720
+ const row = this.database.prepare(
3721
+ `
3722
+ SELECT
3723
+ chat_id AS chatId,
3724
+ open_id AS openId,
3725
+ user_id AS userId,
3726
+ user_name AS userName,
3727
+ updated_at AS updatedAt
3728
+ FROM feishu_chat_members
3729
+ WHERE chat_id = ? AND open_id = ?
3730
+ `
3731
+ ).get(chatId, openId);
3732
+ return row ?? null;
3733
+ }
3734
+ listByChat(chatId) {
3735
+ return this.database.prepare(
3736
+ `
3737
+ SELECT
3738
+ chat_id AS chatId,
3739
+ open_id AS openId,
3740
+ user_id AS userId,
3741
+ user_name AS userName,
3742
+ updated_at AS updatedAt
3743
+ FROM feishu_chat_members
3744
+ WHERE chat_id = ?
3745
+ ORDER BY user_name ASC, open_id ASC
3746
+ `
3747
+ ).all(chatId);
3748
+ }
3749
+ findUniqueByName(chatId, userName) {
3750
+ const rows = this.database.prepare(
3751
+ `
3752
+ SELECT
3753
+ chat_id AS chatId,
3754
+ open_id AS openId,
3755
+ user_id AS userId,
3756
+ user_name AS userName,
3757
+ updated_at AS updatedAt
3758
+ FROM feishu_chat_members
3759
+ WHERE chat_id = ? AND user_name = ?
3760
+ ORDER BY open_id ASC
3761
+ LIMIT 2
3762
+ `
3763
+ ).all(chatId, userName);
3764
+ return rows.length === 1 ? rows[0] : null;
3765
+ }
3766
+ };
3767
+ var FeishuMemberResolver = class {
3768
+ constructor(options) {
3769
+ this.options = options;
3770
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3771
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
3772
+ this.logger = options.logger;
3773
+ }
3774
+ options;
3775
+ now;
3776
+ ttlMs;
3777
+ logger;
3778
+ async resolveOpenIdName(chatId, openId) {
3779
+ const cached = this.options.repository.get(chatId, openId);
3780
+ if (!cached || this.isExpired(cached.updatedAt)) {
3781
+ try {
3782
+ await this.refreshChatMembers(chatId);
3783
+ } catch (error) {
3784
+ this.logger?.warn("Failed to refresh Feishu chat members for open id resolution", {
3785
+ chatId,
3786
+ openId,
3787
+ error
3788
+ });
3789
+ return cached?.userName ?? openId;
3790
+ }
3791
+ }
3792
+ return this.options.repository.get(chatId, openId)?.userName ?? openId;
3793
+ }
3794
+ async resolveUniqueName(chatId, userName) {
3795
+ const cached = this.options.repository.findUniqueByName(chatId, userName);
3796
+ if (cached && !this.isExpired(cached.updatedAt)) {
3797
+ return cached;
3798
+ }
3799
+ try {
3800
+ await this.refreshChatMembers(chatId);
3801
+ } catch (error) {
3802
+ this.logger?.warn("Failed to refresh Feishu chat members for unique name resolution", {
3803
+ chatId,
3804
+ userName,
3805
+ error
3806
+ });
3807
+ return cached ?? null;
3808
+ }
3809
+ return this.options.repository.findUniqueByName(chatId, userName);
3810
+ }
3811
+ isExpired(updatedAt) {
3812
+ const updatedAtMs = Date.parse(updatedAt);
3813
+ if (Number.isNaN(updatedAtMs)) {
3814
+ return true;
3815
+ }
3816
+ return this.now().getTime() - updatedAtMs >= this.ttlMs;
3817
+ }
3818
+ async refreshChatMembers(chatId) {
3819
+ const members = await this.options.client.listChatMembers({ chatId, memberIdType: "open_id" });
3820
+ const updatedAt = this.now().toISOString();
3821
+ for (const member of members) {
3822
+ this.options.repository.upsert({
3823
+ chatId,
3824
+ openId: member.openId,
3825
+ userId: member.userId,
3826
+ userName: member.userName,
3827
+ updatedAt
3828
+ });
3829
+ }
3830
+ }
3831
+ };
3832
+ function formatFeishuMemberPrompt(members, limit = 80) {
3833
+ const lines = members.filter((member) => member.userName).slice(0, limit).map((member) => `${member.openId} = ${member.userName}`);
3834
+ return lines.length ? `\u5F53\u524D\u7FA4\u804A\u6210\u5458 ID \u4E0E\u7FA4\u6635\u79F0\u6620\u5C04\uFF1A
3835
+ ${lines.join("\n")}` : "";
3836
+ }
3837
+ function createFeishuChatMembersClient(client) {
3838
+ return {
3839
+ async listChatMembers(payload) {
3840
+ const api = client.im.v1?.chatMembers?.get;
3841
+ if (!api) {
3842
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301 chatMembers.get\uFF0C\u65E0\u6CD5\u83B7\u53D6\u7FA4\u6210\u5458\u3002");
3843
+ }
3844
+ const members = [];
3845
+ let pageToken;
3846
+ do {
3847
+ const response = await api({
3848
+ path: { chat_id: payload.chatId },
3849
+ params: {
3850
+ member_id_type: payload.memberIdType,
3851
+ ...pageToken ? { page_token: pageToken } : {}
3852
+ }
3853
+ });
3854
+ const items = response.data?.items ?? [];
3855
+ for (const item of items) {
3856
+ if (!item.member_id || !item.name) {
3857
+ continue;
3858
+ }
3859
+ members.push({
3860
+ openId: item.member_id,
3861
+ userId: item.user_id,
3862
+ userName: item.name
3863
+ });
3864
+ }
3865
+ pageToken = response.data?.has_more ? response.data.page_token : void 0;
3866
+ } while (pageToken);
3867
+ return members;
3868
+ }
3869
+ };
3870
+ }
3871
+
3622
3872
  // src/rag/qa-logs.ts
3623
3873
  import crypto6 from "crypto";
3624
3874
  function clampLimit(limit) {
3625
3875
  return Math.max(1, Math.min(200, Math.trunc(limit)));
3626
3876
  }
3877
+ function mapQaLogRow(row) {
3878
+ return {
3879
+ id: row.id,
3880
+ chatId: row.chat_id,
3881
+ questionMessageId: row.question_message_id,
3882
+ question: row.question,
3883
+ answer: row.answer,
3884
+ citations: JSON.parse(row.citations_json),
3885
+ retrievalDebug: JSON.parse(row.retrieval_debug_json),
3886
+ status: row.status,
3887
+ error: row.error,
3888
+ createdAt: row.created_at
3889
+ };
3890
+ }
3627
3891
  var QaLogRepository = class {
3628
3892
  constructor(database) {
3629
3893
  this.database = database;
@@ -3702,18 +3966,29 @@ var QaLogRepository = class {
3702
3966
  LIMIT ?
3703
3967
  `
3704
3968
  ).all(clampLimit(limit));
3705
- return rows.map((row) => ({
3706
- id: row.id,
3707
- chatId: row.chat_id,
3708
- questionMessageId: row.question_message_id,
3709
- question: row.question,
3710
- answer: row.answer,
3711
- citations: JSON.parse(row.citations_json),
3712
- retrievalDebug: JSON.parse(row.retrieval_debug_json),
3713
- status: row.status,
3714
- error: row.error,
3715
- createdAt: row.created_at
3716
- }));
3969
+ return rows.map(mapQaLogRow);
3970
+ }
3971
+ listRecentByChat(chatId, limit) {
3972
+ const rows = this.database.prepare(
3973
+ `
3974
+ SELECT
3975
+ id,
3976
+ chat_id,
3977
+ question_message_id,
3978
+ question,
3979
+ answer,
3980
+ citations_json,
3981
+ retrieval_debug_json,
3982
+ status,
3983
+ error,
3984
+ created_at
3985
+ FROM qa_logs
3986
+ WHERE chat_id = ? AND status = 'answered'
3987
+ ORDER BY created_at DESC
3988
+ LIMIT ?
3989
+ `
3990
+ ).all(chatId, clampLimit(limit));
3991
+ return rows.map(mapQaLogRow);
3717
3992
  }
3718
3993
  getCount() {
3719
3994
  const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
@@ -3785,8 +4060,18 @@ async function runFeishuToolLoop(input) {
3785
4060
  }
3786
4061
  const maxModelTurns = input.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
3787
4062
  const maxToolCalls = input.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
4063
+ const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
4064
+ if (input.memberPrompt) {
4065
+ systemPromptParts.push(`${input.memberPrompt}
4066
+ \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`);
4067
+ }
4068
+ if (input.conversationContext) {
4069
+ systemPromptParts.push(`${input.conversationContext}
4070
+ \u8FD9\u4E9B\u662F\u5F53\u524D\u7FA4\u804A\u91CC\u6700\u8FD1\u51E0\u8F6E\u4F60\u548C\u7528\u6237\u7684\u95EE\u7B54\uFF0C\u53EA\u4F5C\u4E3A\u7406\u89E3\u7701\u7565\u6307\u4EE3\u548C\u8FDE\u7EED\u8FFD\u95EE\u7684\u4E0A\u4E0B\u6587\uFF1B\u5982\u679C\u4E0E\u68C0\u7D22\u8BC1\u636E\u51B2\u7A81\uFF0C\u4EE5\u68C0\u7D22\u8BC1\u636E\u4E3A\u51C6\u3002`);
4071
+ }
4072
+ const systemPrompt = systemPromptParts.join("\n\n");
3788
4073
  const messages = [
3789
- { role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
4074
+ { role: "system", content: systemPrompt },
3790
4075
  { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input.now.toISOString()}
3791
4076
  \u95EE\u9898\uFF1A${input.question}` }
3792
4077
  ];
@@ -3848,6 +4133,13 @@ async function runFeishuToolLoop(input) {
3848
4133
  return "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
3849
4134
  }
3850
4135
  }
4136
+ function formatConversationContext(records) {
4137
+ const lines = records.slice().reverse().map((record, index) => `\u7B2C ${index + 1} \u8F6E
4138
+ \u7528\u6237\uFF1A${record.question}
4139
+ \u52A9\u624B\uFF1A${record.answer}`);
4140
+ return lines.length ? `\u8FD1\u671F\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1A
4141
+ ${lines.join("\n\n")}` : "";
4142
+ }
3851
4143
  function isMentionForBot(mention, config) {
3852
4144
  if (!config.feishu.botOpenId) {
3853
4145
  return false;
@@ -3898,8 +4190,13 @@ function getFeishuQuestionDecision(payload, config) {
3898
4190
  var FeishuQuestionHandler = class {
3899
4191
  constructor(options) {
3900
4192
  this.options = options;
4193
+ this.memberResolver = options.memberResolver;
3901
4194
  }
3902
4195
  options;
4196
+ memberResolver;
4197
+ setMemberResolver(memberResolver) {
4198
+ this.memberResolver = memberResolver;
4199
+ }
3903
4200
  async sendResponse(chatId, messageId, text) {
3904
4201
  if (messageId && this.options.sender.replyTextToMessage) {
3905
4202
  try {
@@ -3946,14 +4243,20 @@ var FeishuQuestionHandler = class {
3946
4243
  const cronTools = createCronJobTools({
3947
4244
  repository: new CronJobRepository(this.options.database),
3948
4245
  chatId: decision.chatId,
3949
- createdByOpenId: payload.event?.sender?.sender_id?.open_id
4246
+ createdByOpenId: payload.event?.sender?.sender_id?.open_id,
4247
+ memberResolver: this.memberResolver
3950
4248
  });
3951
4249
  const allTools = [...tools, ...cronTools];
4250
+ const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
4251
+ const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
4252
+ const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
3952
4253
  const answer = await runFeishuToolLoop({
3953
4254
  question: decision.question,
3954
4255
  now,
3955
4256
  tools: allTools,
3956
- model: this.options.model
4257
+ model: this.options.model,
4258
+ memberPrompt,
4259
+ conversationContext
3957
4260
  });
3958
4261
  qaLogs.create({
3959
4262
  chatId: decision.chatId,
@@ -4006,6 +4309,15 @@ function extractImageKey(response) {
4006
4309
  }
4007
4310
  throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
4008
4311
  }
4312
+ function escapeAtText(value) {
4313
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4314
+ }
4315
+ function formatTextWithMentions(text, options) {
4316
+ const mentions = options?.mentions ?? [];
4317
+ if (mentions.length === 0) return text;
4318
+ const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
4319
+ return `${prefix} ${text}`.trim();
4320
+ }
4009
4321
  var FeishuMessageSender = class _FeishuMessageSender {
4010
4322
  constructor(client) {
4011
4323
  this.client = client;
@@ -4019,12 +4331,12 @@ var FeishuMessageSender = class _FeishuMessageSender {
4019
4331
  });
4020
4332
  return new _FeishuMessageSender(client);
4021
4333
  }
4022
- async sendTextToChat(chatId, text) {
4334
+ async sendTextToChat(chatId, text, options) {
4023
4335
  const payload = {
4024
4336
  data: {
4025
4337
  receive_id: chatId,
4026
4338
  msg_type: "text",
4027
- content: JSON.stringify({ text })
4339
+ content: JSON.stringify({ text: formatTextWithMentions(text, options) })
4028
4340
  },
4029
4341
  params: {
4030
4342
  receive_id_type: "chat_id"
@@ -4146,13 +4458,24 @@ function createFeishuEventDispatcher(options) {
4146
4458
  return;
4147
4459
  }
4148
4460
  }
4149
- const result = options.resourceDownloader ? await options.ingestor.ingestFeishuEventAndDownloadAttachments({
4150
- payload,
4151
- downloader: options.resourceDownloader,
4152
- config: options.config,
4153
- secrets: options.secrets,
4154
- vectorIndexMessage: options.attachmentVectorIndexer
4155
- }) : options.ingestor.ingestFeishuEvent(payload);
4461
+ let result;
4462
+ if (options.resourceDownloader) {
4463
+ result = await options.ingestor.ingestFeishuEventAndDownloadAttachments({
4464
+ payload,
4465
+ downloader: options.resourceDownloader,
4466
+ config: options.config,
4467
+ secrets: options.secrets,
4468
+ vectorIndexMessage: options.attachmentVectorIndexer,
4469
+ memberResolver: options.memberResolver
4470
+ });
4471
+ } else if (options.memberResolver) {
4472
+ result = await options.ingestor.ingestFeishuEventWithMembers({
4473
+ payload,
4474
+ memberResolver: options.memberResolver
4475
+ });
4476
+ } else {
4477
+ result = options.ingestor.ingestFeishuEvent(payload);
4478
+ }
4156
4479
  if (!result.accepted) {
4157
4480
  console.log(`\u98DE\u4E66\u6D88\u606F\u672A\u5165\u5E93\uFF1A${result.reason}`);
4158
4481
  return;
@@ -4243,10 +4566,20 @@ function createFeishuGateway(options) {
4243
4566
  onReconnecting: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u6B63\u5728\u91CD\u8FDE\u3002"),
4244
4567
  onReconnected: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u91CD\u8FDE\u3002")
4245
4568
  });
4569
+ const memberResolver = new FeishuMemberResolver({
4570
+ repository: new FeishuMemberRepository(options.ingestor.database),
4571
+ client: createFeishuChatMembersClient(new lark2.Client({
4572
+ appId: options.config.feishu.appId,
4573
+ appSecret: options.secrets.feishu.appSecret,
4574
+ domain: mapDomain(options.config.feishu.domain)
4575
+ }))
4576
+ });
4577
+ options.questionHandler?.setMemberResolver?.(memberResolver);
4246
4578
  const eventDispatcher = createFeishuEventDispatcher({
4247
4579
  config: options.config,
4248
4580
  secrets: options.secrets,
4249
4581
  ingestor: options.ingestor,
4582
+ memberResolver,
4250
4583
  questionHandler: options.questionHandler,
4251
4584
  resourceDownloader: options.resourceDownloader,
4252
4585
  attachmentVectorIndexer: options.attachmentVectorIndexer,
@@ -4266,7 +4599,7 @@ function createFeishuGateway(options) {
4266
4599
  }) : void 0);
4267
4600
  const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4268
4601
  repository: new CronJobRepository(options.cronJobProcessor.database),
4269
- sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
4602
+ sendTextToChat: (chatId, text, sendOptions) => options.cronJobProcessor.sender.sendTextToChat(chatId, text, sendOptions),
4270
4603
  sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
4271
4604
  chatId,
4272
4605
  resolveFeishuImagePath(options.config, imageFileName)
@@ -4280,7 +4613,16 @@ function createFeishuGateway(options) {
4280
4613
  scope: { platform: "feishu", platformChatId: job.chatId }
4281
4614
  });
4282
4615
  try {
4283
- return await generateCronJobMessage({ prompt: job.prompt, model: options.cronJobProcessor.model, tools, now });
4616
+ const memberPrompt = formatFeishuMemberPrompt(
4617
+ new FeishuMemberRepository(options.cronJobProcessor.database).listByChat(job.chatId)
4618
+ );
4619
+ return await generateCronJobMessage({
4620
+ prompt: job.prompt,
4621
+ model: options.cronJobProcessor.model,
4622
+ tools,
4623
+ now,
4624
+ memberPrompt
4625
+ });
4284
4626
  } finally {
4285
4627
  close();
4286
4628
  }
@@ -4436,7 +4778,9 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4436
4778
  if (!text) {
4437
4779
  return null;
4438
4780
  }
4439
- const senderId = event.sender?.sender_id?.open_id || event.sender?.sender_id?.user_id || event.sender?.sender_id?.union_id || "unknown";
4781
+ const senderOpenId = event.sender?.sender_id?.open_id;
4782
+ const senderUserId = event.sender?.sender_id?.user_id;
4783
+ const senderId = senderOpenId || senderUserId || event.sender?.sender_id?.union_id || "unknown";
4440
4784
  return {
4441
4785
  platform: "feishu",
4442
4786
  platformChatId: message.chat_id,
@@ -4451,6 +4795,10 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4451
4795
  platform: "feishu",
4452
4796
  raw: payload,
4453
4797
  content,
4798
+ sender: {
4799
+ ...senderOpenId ? { openId: senderOpenId } : {},
4800
+ ...senderUserId ? { userId: senderUserId } : {}
4801
+ },
4454
4802
  attachment: extractFeishuAttachment(messageType, content)
4455
4803
  }
4456
4804
  };
@@ -4659,6 +5007,14 @@ async function ingestLocalFile(input) {
4659
5007
  }
4660
5008
 
4661
5009
  // src/gateway/ingest.ts
5010
+ function extractFeishuSenderOpenId(message) {
5011
+ const raw = message.rawPayload;
5012
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
5013
+ const sender = raw.sender;
5014
+ if (!sender || typeof sender !== "object" || Array.isArray(sender)) return void 0;
5015
+ const openId = sender.openId;
5016
+ return typeof openId === "string" && openId.trim() ? openId.trim() : void 0;
5017
+ }
4662
5018
  function extractAttachment(message) {
4663
5019
  const raw = message.rawPayload;
4664
5020
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -4678,14 +5034,16 @@ function isMultimodalReady(config, secrets) {
4678
5034
  return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
4679
5035
  }
4680
5036
  var GatewayIngestor = class {
4681
- messages;
4682
- jobs;
4683
- imageTasks;
4684
5037
  constructor(database) {
5038
+ this.database = database;
4685
5039
  this.messages = new MessageRepository(database);
4686
5040
  this.jobs = new FileJobRepository(database);
4687
5041
  this.imageTasks = new ImageMultimodalTaskRepository(database);
4688
5042
  }
5043
+ database;
5044
+ messages;
5045
+ jobs;
5046
+ imageTasks;
4689
5047
  ingestFeishuEvent(payload) {
4690
5048
  const normalized = normalizeFeishuReceiveMessageEvent(payload);
4691
5049
  if (!normalized) {
@@ -4703,8 +5061,28 @@ var GatewayIngestor = class {
4703
5061
  duplicate
4704
5062
  };
4705
5063
  }
5064
+ async ingestFeishuEventWithMembers(input) {
5065
+ const normalized = normalizeFeishuReceiveMessageEvent(input.payload);
5066
+ if (!normalized) {
5067
+ return {
5068
+ accepted: false,
5069
+ reason: "\u4E8B\u4EF6\u4E0D\u662F\u53EF\u5165\u5E93\u7684\u98DE\u4E66\u6D88\u606F\u3002"
5070
+ };
5071
+ }
5072
+ const openId = extractFeishuSenderOpenId(normalized);
5073
+ const senderName = openId ? await input.memberResolver.resolveOpenIdName(normalized.platformChatId, openId) : normalized.senderName;
5074
+ const enriched = { ...normalized, senderName };
5075
+ const duplicate = this.messages.hasPlatformMessage(enriched.platform, enriched.platformMessageId);
5076
+ const messageId = this.messages.ingest(enriched);
5077
+ return {
5078
+ accepted: true,
5079
+ messageId,
5080
+ message: enriched,
5081
+ duplicate
5082
+ };
5083
+ }
4706
5084
  async ingestFeishuEventAndDownloadAttachments(input) {
4707
- const result = this.ingestFeishuEvent(input.payload);
5085
+ const result = input.memberResolver ? await this.ingestFeishuEventWithMembers({ payload: input.payload, memberResolver: input.memberResolver }) : this.ingestFeishuEvent(input.payload);
4708
5086
  if (!result.accepted || !result.messageId || !result.message || result.duplicate) {
4709
5087
  return result;
4710
5088
  }