chattercatcher 0.1.27 → 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.27",
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
@@ -971,6 +1001,7 @@ function getGatewayStatus(config, secrets) {
971
1001
  }
972
1002
 
973
1003
  // src/llm/openai-compatible.ts
1004
+ var OPENAI_EMBEDDING_BATCH_SIZE = 64;
974
1005
  function normalizeBaseUrl(baseUrl) {
975
1006
  return baseUrl.replace(/\/+$/, "");
976
1007
  }
@@ -1002,12 +1033,66 @@ function toOpenAITool(tool) {
1002
1033
  }
1003
1034
  };
1004
1035
  }
1036
+ function parseToolCallArguments(value) {
1037
+ try {
1038
+ return JSON.parse(value);
1039
+ } catch {
1040
+ return {};
1041
+ }
1042
+ }
1043
+ function decodeDsmlValue(value, isString) {
1044
+ const trimmed = value.trim();
1045
+ if (isString) {
1046
+ return trimmed;
1047
+ }
1048
+ if (trimmed === "true") return true;
1049
+ if (trimmed === "false") return false;
1050
+ if (trimmed === "null") return null;
1051
+ const numberValue = Number(trimmed);
1052
+ if (trimmed && Number.isFinite(numberValue)) {
1053
+ return numberValue;
1054
+ }
1055
+ return trimmed;
1056
+ }
1057
+ function parseDsmlToolCalls(content) {
1058
+ if (!content?.includes("DSML")) {
1059
+ return [];
1060
+ }
1061
+ const toolCalls = [];
1062
+ const invokePattern = /<||DSML||invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/||DSML||invoke>/g;
1063
+ const parameterPattern = /<||DSML||parameter\s+name="([^"]+)"\s+string="(true|false)"\s*>([\s\S]*?)<\/||DSML||parameter>/g;
1064
+ for (const invoke of content.matchAll(invokePattern)) {
1065
+ const name = invoke[1];
1066
+ if (!name) {
1067
+ continue;
1068
+ }
1069
+ const input2 = {};
1070
+ const body = invoke[2] ?? "";
1071
+ for (const parameter of body.matchAll(parameterPattern)) {
1072
+ const parameterName = parameter[1];
1073
+ if (!parameterName) {
1074
+ continue;
1075
+ }
1076
+ input2[parameterName] = decodeDsmlValue(parameter[3] ?? "", parameter[2] === "true");
1077
+ }
1078
+ toolCalls.push({
1079
+ id: `dsml_${toolCalls.length + 1}`,
1080
+ name,
1081
+ input: input2
1082
+ });
1083
+ }
1084
+ return toolCalls;
1085
+ }
1005
1086
  function parseToolCalls(message) {
1006
- return message?.tool_calls?.map((toolCall) => ({
1087
+ const standardToolCalls = message?.tool_calls?.map((toolCall) => ({
1007
1088
  id: toolCall.id,
1008
1089
  name: toolCall.function.name,
1009
- input: JSON.parse(toolCall.function.arguments)
1090
+ input: parseToolCallArguments(toolCall.function.arguments)
1010
1091
  })) ?? [];
1092
+ return standardToolCalls.length > 0 ? standardToolCalls : parseDsmlToolCalls(message?.content);
1093
+ }
1094
+ function isDsmlToolCallContent(content) {
1095
+ return parseDsmlToolCalls(content).length > 0;
1011
1096
  }
1012
1097
  var OpenAICompatibleChatModel = class {
1013
1098
  constructor(options) {
@@ -1066,9 +1151,10 @@ var OpenAICompatibleChatModel = class {
1066
1151
  }
1067
1152
  const data2 = await response.json();
1068
1153
  const message = data2.choices?.[0]?.message;
1154
+ const toolCalls = parseToolCalls(message);
1069
1155
  return {
1070
- content: message?.content ?? "",
1071
- toolCalls: parseToolCalls(message),
1156
+ content: toolCalls.length > 0 && isDsmlToolCallContent(message?.content) ? "" : message?.content ?? "",
1157
+ toolCalls,
1072
1158
  reasoningContent: message?.reasoning_content ?? void 0
1073
1159
  };
1074
1160
  }
@@ -1086,6 +1172,13 @@ var OpenAICompatibleEmbeddingModel = class {
1086
1172
  if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
1087
1173
  throw new Error("Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
1088
1174
  }
1175
+ const vectors = [];
1176
+ for (let index2 = 0; index2 < texts.length; index2 += OPENAI_EMBEDDING_BATCH_SIZE) {
1177
+ vectors.push(...await this.fetchEmbeddingBatch(texts.slice(index2, index2 + OPENAI_EMBEDDING_BATCH_SIZE)));
1178
+ }
1179
+ return vectors;
1180
+ }
1181
+ async fetchEmbeddingBatch(texts) {
1089
1182
  const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/embeddings`, {
1090
1183
  method: "POST",
1091
1184
  headers: {
@@ -2838,6 +2931,9 @@ var CronJobRepository = class {
2838
2931
  const schedule = input2.schedule.trim();
2839
2932
  const prompt = input2.prompt.trim();
2840
2933
  const imageFileName = input2.imageFileName?.trim();
2934
+ const mentionTargetName = input2.mentionTargetName?.trim();
2935
+ const mentionOpenId = input2.mentionOpenId?.trim();
2936
+ const mentionUserId = input2.mentionUserId?.trim();
2841
2937
  if (!isValidCronSchedule(schedule)) {
2842
2938
  throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
2843
2939
  }
@@ -2856,6 +2952,9 @@ var CronJobRepository = class {
2856
2952
  schedule,
2857
2953
  prompt,
2858
2954
  ...imageFileName ? { imageFileName } : {},
2955
+ ...mentionTargetName ? { mentionTargetName } : {},
2956
+ ...mentionOpenId ? { mentionOpenId } : {},
2957
+ ...mentionUserId ? { mentionUserId } : {},
2859
2958
  status: "active",
2860
2959
  nextRunAt: nextRunAt.toISOString(),
2861
2960
  createdAt: now.toISOString(),
@@ -2864,17 +2963,22 @@ var CronJobRepository = class {
2864
2963
  this.database.prepare(
2865
2964
  `
2866
2965
  INSERT INTO cron_jobs (
2867
- 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,
2868
2968
  last_run_at, next_run_at, last_error, created_at, updated_at
2869
2969
  )
2870
2970
  VALUES (
2871
- @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName, @status,
2971
+ @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName,
2972
+ @mentionTargetName, @mentionOpenId, @mentionUserId, @status,
2872
2973
  NULL, @nextRunAt, NULL, @createdAt, @updatedAt
2873
2974
  )
2874
2975
  `
2875
2976
  ).run({
2876
2977
  ...record,
2877
- imageFileName: record.imageFileName ?? null
2978
+ imageFileName: record.imageFileName ?? null,
2979
+ mentionTargetName: record.mentionTargetName ?? null,
2980
+ mentionOpenId: record.mentionOpenId ?? null,
2981
+ mentionUserId: record.mentionUserId ?? null
2878
2982
  });
2879
2983
  return record;
2880
2984
  }
@@ -2901,6 +3005,9 @@ var CronJobRepository = class {
2901
3005
  schedule,
2902
3006
  prompt,
2903
3007
  image_file_name AS imageFileName,
3008
+ mention_target_name AS mentionTargetName,
3009
+ mention_open_id AS mentionOpenId,
3010
+ mention_user_id AS mentionUserId,
2904
3011
  status,
2905
3012
  last_run_at AS lastRunAt,
2906
3013
  next_run_at AS nextRunAt,
@@ -2920,6 +3027,9 @@ var CronJobRepository = class {
2920
3027
  schedule: row.schedule,
2921
3028
  prompt: row.prompt,
2922
3029
  imageFileName: row.imageFileName ?? void 0,
3030
+ mentionTargetName: row.mentionTargetName ?? void 0,
3031
+ mentionOpenId: row.mentionOpenId ?? void 0,
3032
+ mentionUserId: row.mentionUserId ?? void 0,
2923
3033
  status: row.status,
2924
3034
  lastRunAt: row.lastRunAt ?? void 0,
2925
3035
  nextRunAt: row.nextRunAt,
@@ -2994,6 +3104,9 @@ var CronJobRepository = class {
2994
3104
  schedule,
2995
3105
  prompt,
2996
3106
  image_file_name AS imageFileName,
3107
+ mention_target_name AS mentionTargetName,
3108
+ mention_open_id AS mentionOpenId,
3109
+ mention_user_id AS mentionUserId,
2997
3110
  status,
2998
3111
  last_run_at AS lastRunAt,
2999
3112
  next_run_at AS nextRunAt,
@@ -3013,6 +3126,9 @@ var CronJobRepository = class {
3013
3126
  schedule: row.schedule,
3014
3127
  prompt: row.prompt,
3015
3128
  imageFileName: row.imageFileName ?? void 0,
3129
+ mentionTargetName: row.mentionTargetName ?? void 0,
3130
+ mentionOpenId: row.mentionOpenId ?? void 0,
3131
+ mentionUserId: row.mentionUserId ?? void 0,
3016
3132
  status: row.status,
3017
3133
  lastRunAt: row.lastRunAt ?? void 0,
3018
3134
  nextRunAt: row.nextRunAt,
@@ -3038,8 +3154,12 @@ async function generateCronJobMessage(input2) {
3038
3154
  if (!input2.model.completeWithTools) {
3039
3155
  throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
3040
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;
3041
3161
  const messages = [
3042
- { role: "system", content: SYSTEM_PROMPT },
3162
+ { role: "system", content: systemPrompt },
3043
3163
  { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3044
3164
  \u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}` }
3045
3165
  ];
@@ -3057,7 +3177,7 @@ async function generateCronJobMessage(input2) {
3057
3177
  for (const call of result.toolCalls) {
3058
3178
  if (toolCallsUsed >= maxToolCalls) {
3059
3179
  return input2.model.complete([
3060
- { role: "system", content: SYSTEM_PROMPT },
3180
+ { role: "system", content: systemPrompt },
3061
3181
  {
3062
3182
  role: "user",
3063
3183
  content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
@@ -3116,7 +3236,12 @@ function createCronJobScheduler(options) {
3116
3236
  for (const job of jobs) {
3117
3237
  try {
3118
3238
  const text = await options.generateMessage(job, startedAt);
3119
- 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
+ }
3120
3245
  if (job.imageFileName) {
3121
3246
  if (!options.sendImageToChat) {
3122
3247
  throw new Error("\u5F53\u524D\u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\u3002");
@@ -3263,12 +3388,20 @@ function parseExactNumber2(field, min, max) {
3263
3388
  }
3264
3389
 
3265
3390
  // src/rag/indexer.ts
3391
+ var EMBEDDING_INDEX_BATCH_SIZE = 64;
3266
3392
  async function indexMessageChunks(input2) {
3267
3393
  const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
3268
3394
  if (chunks.length === 0) {
3269
3395
  return { chunks: 0, vectors: 0 };
3270
3396
  }
3271
- const vectors = await input2.embedding.embedBatch(chunks.map((chunk) => chunk.text));
3397
+ const vectors = [];
3398
+ for (let index2 = 0; index2 < chunks.length; index2 += EMBEDDING_INDEX_BATCH_SIZE) {
3399
+ vectors.push(
3400
+ ...await input2.embedding.embedBatch(
3401
+ chunks.slice(index2, index2 + EMBEDDING_INDEX_BATCH_SIZE).map((chunk) => chunk.text)
3402
+ )
3403
+ );
3404
+ }
3272
3405
  const records = [];
3273
3406
  for (const [index2, chunk] of chunks.entries()) {
3274
3407
  const vector = vectors[index2];
@@ -3606,6 +3739,185 @@ var ImageMultimodalWorker = class {
3606
3739
  }
3607
3740
  };
3608
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
+
3609
3921
  // src/cron/tools.ts
3610
3922
  function readString(input2, key) {
3611
3923
  const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
@@ -3644,18 +3956,27 @@ function createCronJobTools(input2) {
3644
3956
  imageFileName: {
3645
3957
  type: "string",
3646
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."
3647
3963
  }
3648
3964
  },
3649
3965
  required: ["schedule", "prompt"],
3650
3966
  additionalProperties: false
3651
3967
  },
3652
3968
  execute: async (rawInput) => {
3969
+ const mentionTargetName = readOptionalString(rawInput, "mentionTargetName");
3970
+ const mentionTarget = mentionTargetName && input2.memberResolver ? await input2.memberResolver.resolveUniqueName(input2.chatId, mentionTargetName) : null;
3653
3971
  const job = input2.repository.create({
3654
3972
  chatId: input2.chatId,
3655
3973
  createdByOpenId: input2.createdByOpenId,
3656
3974
  schedule: readString(rawInput, "schedule"),
3657
3975
  prompt: readString(rawInput, "prompt"),
3658
- imageFileName: readOptionalString(rawInput, "imageFileName")
3976
+ imageFileName: readOptionalString(rawInput, "imageFileName"),
3977
+ mentionTargetName,
3978
+ mentionOpenId: mentionTarget?.openId,
3979
+ mentionUserId: mentionTarget?.userId
3659
3980
  });
3660
3981
  return JSON.stringify({ ok: true, job });
3661
3982
  }
@@ -3823,6 +4144,9 @@ var DEFAULT_MAX_MODEL_TURNS = 4;
3823
4144
  var DEFAULT_MAX_TOOL_CALLS = 8;
3824
4145
  var FEISHU_TOOL_LOOP_FALLBACK = "\u5B9A\u65F6\u4EFB\u52A1\u64CD\u4F5C\u5DF2\u63D0\u4EA4\uFF0C\u4F46\u6A21\u578B\u6CA1\u6709\u751F\u6210\u6700\u7EC8\u56DE\u590D\u3002";
3825
4146
  var FEISHU_TOOL_LOOP_LIMIT_REACHED = "\u5DE5\u5177\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u5230\u4E0A\u9650\uFF0C\u8BF7\u7F29\u5C0F\u8BF7\u6C42\u540E\u91CD\u8BD5\u3002";
4147
+ function containsRawToolCallMarkup(content) {
4148
+ return /<||DSML||tool_calls>|<||DSML||invoke\s+name=|<tool_call>|<tool_calls>/i.test(content);
4149
+ }
3826
4150
  function toToolResultContent(value) {
3827
4151
  if (typeof value === "string") return value;
3828
4152
  return JSON.stringify(value);
@@ -3856,8 +4180,12 @@ async function runFeishuToolLoop(input2) {
3856
4180
  }
3857
4181
  const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
3858
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;
3859
4187
  const messages = [
3860
- { role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
4188
+ { role: "system", content: systemPrompt },
3861
4189
  { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3862
4190
  \u95EE\u9898\uFF1A${input2.question}` }
3863
4191
  ];
@@ -3865,6 +4193,7 @@ async function runFeishuToolLoop(input2) {
3865
4193
  let toolCallsUsed = 0;
3866
4194
  for (let turn = 0; turn < maxModelTurns; turn += 1) {
3867
4195
  const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
4196
+ const hasRawToolCallMarkup = containsRawToolCallMarkup(assistantResult.content);
3868
4197
  messages.push({
3869
4198
  role: "assistant",
3870
4199
  content: assistantResult.content,
@@ -3872,6 +4201,9 @@ async function runFeishuToolLoop(input2) {
3872
4201
  reasoningContent: assistantResult.reasoningContent
3873
4202
  });
3874
4203
  if (assistantResult.toolCalls.length === 0) {
4204
+ if (hasRawToolCallMarkup) {
4205
+ break;
4206
+ }
3875
4207
  return assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
3876
4208
  }
3877
4209
  for (const toolCall of assistantResult.toolCalls) {
@@ -3965,8 +4297,13 @@ function getFeishuQuestionDecision(payload, config) {
3965
4297
  var FeishuQuestionHandler = class {
3966
4298
  constructor(options) {
3967
4299
  this.options = options;
4300
+ this.memberResolver = options.memberResolver;
3968
4301
  }
3969
4302
  options;
4303
+ memberResolver;
4304
+ setMemberResolver(memberResolver) {
4305
+ this.memberResolver = memberResolver;
4306
+ }
3970
4307
  async sendResponse(chatId, messageId, text) {
3971
4308
  if (messageId && this.options.sender.replyTextToMessage) {
3972
4309
  try {
@@ -3984,7 +4321,7 @@ var FeishuQuestionHandler = class {
3984
4321
  }
3985
4322
  if (this.options.sender.addReactionToMessage) {
3986
4323
  try {
3987
- await this.options.sender.addReactionToMessage(messageId, this.options.thinkingEmojiType ?? "keyboard");
4324
+ await this.options.sender.addReactionToMessage(messageId, this.options.thinkingEmojiType ?? "OK");
3988
4325
  return;
3989
4326
  } catch (error) {
3990
4327
  console.log(`\u98DE\u4E66\u63D0\u95EE\u8868\u60C5\u53CD\u9988\u5931\u8D25\uFF0C\u6539\u7528\u6587\u5B57\u53CD\u9988\uFF1A${error instanceof Error ? error.message : String(error)}`);
@@ -4013,14 +4350,18 @@ var FeishuQuestionHandler = class {
4013
4350
  const cronTools = createCronJobTools({
4014
4351
  repository: new CronJobRepository(this.options.database),
4015
4352
  chatId: decision.chatId,
4016
- createdByOpenId: payload.event?.sender?.sender_id?.open_id
4353
+ createdByOpenId: payload.event?.sender?.sender_id?.open_id,
4354
+ memberResolver: this.memberResolver
4017
4355
  });
4018
4356
  const allTools = [...tools, ...cronTools];
4357
+ const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
4358
+ const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
4019
4359
  const answer = await runFeishuToolLoop({
4020
4360
  question: decision.question,
4021
4361
  now,
4022
4362
  tools: allTools,
4023
- model: this.options.model
4363
+ model: this.options.model,
4364
+ memberPrompt
4024
4365
  });
4025
4366
  qaLogs.create({
4026
4367
  chatId: decision.chatId,
@@ -4073,6 +4414,15 @@ function extractImageKey(response) {
4073
4414
  }
4074
4415
  throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
4075
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
+ }
4076
4426
  var FeishuMessageSender = class _FeishuMessageSender {
4077
4427
  constructor(client) {
4078
4428
  this.client = client;
@@ -4086,12 +4436,12 @@ var FeishuMessageSender = class _FeishuMessageSender {
4086
4436
  });
4087
4437
  return new _FeishuMessageSender(client);
4088
4438
  }
4089
- async sendTextToChat(chatId, text) {
4439
+ async sendTextToChat(chatId, text, options) {
4090
4440
  const payload = {
4091
4441
  data: {
4092
4442
  receive_id: chatId,
4093
4443
  msg_type: "text",
4094
- content: JSON.stringify({ text })
4444
+ content: JSON.stringify({ text: formatTextWithMentions(text, options) })
4095
4445
  },
4096
4446
  params: {
4097
4447
  receive_id_type: "chat_id"
@@ -4213,13 +4563,24 @@ function createFeishuEventDispatcher(options) {
4213
4563
  return;
4214
4564
  }
4215
4565
  }
4216
- const result = options.resourceDownloader ? await options.ingestor.ingestFeishuEventAndDownloadAttachments({
4217
- payload,
4218
- downloader: options.resourceDownloader,
4219
- config: options.config,
4220
- secrets: options.secrets,
4221
- vectorIndexMessage: options.attachmentVectorIndexer
4222
- }) : 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
+ }
4223
4584
  if (!result.accepted) {
4224
4585
  console.log(`\u98DE\u4E66\u6D88\u606F\u672A\u5165\u5E93\uFF1A${result.reason}`);
4225
4586
  return;
@@ -4310,10 +4671,20 @@ function createFeishuGateway(options) {
4310
4671
  onReconnecting: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u6B63\u5728\u91CD\u8FDE\u3002"),
4311
4672
  onReconnected: () => console.log("\u98DE\u4E66\u957F\u8FDE\u63A5\u5DF2\u91CD\u8FDE\u3002")
4312
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);
4313
4683
  const eventDispatcher = createFeishuEventDispatcher({
4314
4684
  config: options.config,
4315
4685
  secrets: options.secrets,
4316
4686
  ingestor: options.ingestor,
4687
+ memberResolver,
4317
4688
  questionHandler: options.questionHandler,
4318
4689
  resourceDownloader: options.resourceDownloader,
4319
4690
  attachmentVectorIndexer: options.attachmentVectorIndexer,
@@ -4333,7 +4704,7 @@ function createFeishuGateway(options) {
4333
4704
  }) : void 0);
4334
4705
  const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4335
4706
  repository: new CronJobRepository(options.cronJobProcessor.database),
4336
- sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
4707
+ sendTextToChat: (chatId, text, sendOptions) => options.cronJobProcessor.sender.sendTextToChat(chatId, text, sendOptions),
4337
4708
  sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
4338
4709
  chatId,
4339
4710
  resolveFeishuImagePath(options.config, imageFileName)
@@ -4347,7 +4718,16 @@ function createFeishuGateway(options) {
4347
4718
  scope: { platform: "feishu", platformChatId: job.chatId }
4348
4719
  });
4349
4720
  try {
4350
- 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
+ });
4351
4731
  } finally {
4352
4732
  close();
4353
4733
  }
@@ -4705,7 +5085,9 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4705
5085
  if (!text) {
4706
5086
  return null;
4707
5087
  }
4708
- 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";
4709
5091
  return {
4710
5092
  platform: "feishu",
4711
5093
  platformChatId: message.chat_id,
@@ -4720,12 +5102,24 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4720
5102
  platform: "feishu",
4721
5103
  raw: payload,
4722
5104
  content,
5105
+ sender: {
5106
+ ...senderOpenId ? { openId: senderOpenId } : {},
5107
+ ...senderUserId ? { userId: senderUserId } : {}
5108
+ },
4723
5109
  attachment: extractFeishuAttachment(messageType, content)
4724
5110
  }
4725
5111
  };
4726
5112
  }
4727
5113
 
4728
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
+ }
4729
5123
  function extractAttachment(message) {
4730
5124
  const raw = message.rawPayload;
4731
5125
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -4745,14 +5139,16 @@ function isMultimodalReady(config, secrets) {
4745
5139
  return Boolean(config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey);
4746
5140
  }
4747
5141
  var GatewayIngestor = class {
4748
- messages;
4749
- jobs;
4750
- imageTasks;
4751
5142
  constructor(database) {
5143
+ this.database = database;
4752
5144
  this.messages = new MessageRepository(database);
4753
5145
  this.jobs = new FileJobRepository(database);
4754
5146
  this.imageTasks = new ImageMultimodalTaskRepository(database);
4755
5147
  }
5148
+ database;
5149
+ messages;
5150
+ jobs;
5151
+ imageTasks;
4756
5152
  ingestFeishuEvent(payload) {
4757
5153
  const normalized = normalizeFeishuReceiveMessageEvent(payload);
4758
5154
  if (!normalized) {
@@ -4770,8 +5166,28 @@ var GatewayIngestor = class {
4770
5166
  duplicate
4771
5167
  };
4772
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
+ }
4773
5189
  async ingestFeishuEventAndDownloadAttachments(input2) {
4774
- const result = this.ingestFeishuEvent(input2.payload);
5190
+ const result = input2.memberResolver ? await this.ingestFeishuEventWithMembers({ payload: input2.payload, memberResolver: input2.memberResolver }) : this.ingestFeishuEvent(input2.payload);
4775
5191
  if (!result.accepted || !result.messageId || !result.message || result.duplicate) {
4776
5192
  return result;
4777
5193
  }
@@ -5026,6 +5442,9 @@ function createMultimodalModel(config, secrets) {
5026
5442
  });
5027
5443
  }
5028
5444
 
5445
+ // src/cli.ts
5446
+ import * as lark4 from "@larksuiteoapi/node-sdk";
5447
+
5029
5448
  // src/rag/answer.ts
5030
5449
  var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
5031
5450
  var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
@@ -6123,6 +6542,14 @@ async function startGatewayForegroundCommand() {
6123
6542
  const database = openDatabase(config);
6124
6543
  const chatModel = createChatModel(config, secrets);
6125
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
+ });
6126
6553
  const vectorStore = hasEmbeddingConfig(config, secrets) ? new SqliteVectorStore(database, { model: config.embedding.model }) : null;
6127
6554
  const gatewayRuntime = createFeishuGateway({
6128
6555
  config,
@@ -6155,6 +6582,7 @@ async function startGatewayForegroundCommand() {
6155
6582
  config,
6156
6583
  secrets,
6157
6584
  database,
6585
+ memberResolver,
6158
6586
  sender,
6159
6587
  model: chatModel
6160
6588
  })