chattercatcher 0.1.25 → 0.1.27

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
@@ -3,12 +3,12 @@
3
3
  // src/cli.ts
4
4
  import { input, password, select, confirm, number } from "@inquirer/prompts";
5
5
  import { Command } from "commander";
6
- import fs14 from "fs/promises";
6
+ import fs15 from "fs/promises";
7
7
 
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "chattercatcher",
11
- version: "0.1.25",
11
+ version: "0.1.27",
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",
@@ -548,12 +548,17 @@ function migrateDatabase(database) {
548
548
  next_run_at TEXT NOT NULL,
549
549
  last_error TEXT,
550
550
  created_at TEXT NOT NULL,
551
- updated_at TEXT NOT NULL
551
+ updated_at TEXT NOT NULL,
552
+ image_file_name TEXT
552
553
  );
553
554
 
554
555
  CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
555
556
  CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
556
557
  `);
558
+ 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
+ }
557
562
  }
558
563
 
559
564
  // src/doctor/checks.ts
@@ -1304,6 +1309,9 @@ var MessageRepository = class {
1304
1309
  throw new Error("\u539F\u59CB\u56FE\u7247\u6D88\u606F\u4E0D\u5B58\u5728\u3002");
1305
1310
  }
1306
1311
  const derivedPlatformMessageId = `${source.platformMessageId}:image-summary:${input2.imageKey}`;
1312
+ const imageFileName = input2.imageFileName?.trim();
1313
+ const summaryText = imageFileName ? `[\u56FE\u7247\u8F6C\u8FF0] \u6587\u4EF6\u540D\uFF1A${imageFileName}
1314
+ ${input2.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}`;
1307
1315
  return this.ingest({
1308
1316
  platform: source.platform,
1309
1317
  platformChatId: source.platformChatId,
@@ -1312,12 +1320,13 @@ var MessageRepository = class {
1312
1320
  senderId: source.senderId,
1313
1321
  senderName: source.senderName,
1314
1322
  messageType: "image_summary",
1315
- text: `[\u56FE\u7247\u8F6C\u8FF0] ${input2.summary.trim()}`,
1323
+ text: summaryText,
1316
1324
  sentAt: source.sentAt,
1317
1325
  rawPayload: {
1318
1326
  derivedFromMessageId: input2.sourceMessageId,
1319
1327
  sourceAttachmentKind: "image",
1320
1328
  sourceResourceKey: input2.imageKey,
1329
+ ...imageFileName ? { imageFileName } : {},
1321
1330
  multimodalModel: input2.multimodalModel,
1322
1331
  isMeaningful: true,
1323
1332
  ...input2.reason?.trim() ? { reason: input2.reason.trim() } : {},
@@ -2618,7 +2627,7 @@ async function summarizeEpisodeWindow(window, model, now) {
2618
2627
  const summary = await model.complete([
2619
2628
  {
2620
2629
  role: "system",
2621
- content: "\u4F60\u662F ChatterCatcher \u7684\u4F1A\u8BDD\u8BB0\u5FC6\u6574\u7406\u6A21\u5757\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u628A\u788E\u7247\u5316\u95F2\u804A\u6574\u7406\u6210\u53EF\u68C0\u7D22\u4E8B\u5B9E\uFF0C\u8865\u5168\u77ED\u6D88\u606F\u3001\u4EE3\u8BCD\u3001\u7F29\u5199\u4E0E\u4E0A\u4E0B\u6587\u4E4B\u95F4\u7684\u5173\u7CFB\u3002\u53EA\u603B\u7ED3\u660E\u786E\u4E8B\u5B9E\uFF0C\u4E0D\u8981\u7F16\u9020\u3002\u4FDD\u7559\u91CD\u8981\u6570\u5B57\u3001\u65E5\u671F\u3001\u94FE\u63A5\u548C\u4EE3\u7801\uFF1B\u5982\u679C\u5185\u5BB9\u50CF\u5BC6\u7801\u3001API key\u3001token \u6216\u5BC6\u94A5\uFF0C\u53EA\u63CF\u8FF0\u5176\u4E0A\u4E0B\u6587\u5173\u7CFB\uFF0C\u4E0D\u8981\u5728\u6458\u8981\u4E2D\u590D\u5199\u539F\u6587\u3002\u6D88\u606F\u91CC\u7684\u201C\u4ECA\u5929\u201D\u201C\u660E\u5929\u201D\u201C\u6628\u665A\u201D\u201C\u4E0B\u5468\u4E09\u201D\u7B49\u76F8\u5BF9\u65F6\u95F4\u8868\u8FF0\uFF0C\u8BF7\u57FA\u4E8E\u6BCF\u6761\u6D88\u606F\u524D\u7684\u53D1\u9001\u65F6\u95F4\u6233\u63A8\u5BFC\u4E3A\u5177\u4F53\u65E5\u671F\u5199\u5165\u6458\u8981\u3002\u4F8B\u5982 [2026-05-05T20:00:00.000Z] \u5988\u5988\u8BF4\u201C\u660E\u5929\u8981\u7528\u4E1D\u4E1D\u9732\u201D\uFF0C\u6458\u8981\u5E94\u5199\u4E3A\u201C2026-05-06 \u8981\u7528\u4E1D\u4E1D\u9732\u201D\u3002"
2630
+ content: "\u4F60\u662F ChatterCatcher \u7684\u4F1A\u8BDD\u8BB0\u5FC6\u6574\u7406\u6A21\u5757\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u628A\u788E\u7247\u5316\u95F2\u804A\u6574\u7406\u6210\u53EF\u68C0\u7D22\u4E8B\u5B9E\uFF0C\u8865\u5168\u77ED\u6D88\u606F\u3001\u4EE3\u8BCD\u3001\u7F29\u5199\u4E0E\u4E0A\u4E0B\u6587\u4E4B\u95F4\u7684\u5173\u7CFB\u3002\u53EA\u603B\u7ED3\u660E\u786E\u4E8B\u5B9E\uFF0C\u4E0D\u8981\u7F16\u9020\u3002\u4FDD\u7559\u91CD\u8981\u6570\u5B57\u3001\u65E5\u671F\u3001\u94FE\u63A5\u3001\u6587\u4EF6\u540D\u548C\u4EE3\u7801\uFF1B\u5982\u679C\u56FE\u7247\u8F6C\u8FF0\u91CC\u51FA\u73B0\u6587\u4EF6\u540D\uFF0C\u5FC5\u987B\u5728\u6458\u8981\u4E2D\u539F\u6837\u4FDD\u7559\u8BE5\u6587\u4EF6\u540D\uFF0C\u65B9\u4FBF\u4E4B\u540E\u6309\u6587\u4EF6\u540D\u627E\u56DE\u56FE\u7247\u3002\u5982\u679C\u5185\u5BB9\u50CF\u5BC6\u7801\u3001API key\u3001token \u6216\u5BC6\u94A5\uFF0C\u53EA\u63CF\u8FF0\u5176\u4E0A\u4E0B\u6587\u5173\u7CFB\uFF0C\u4E0D\u8981\u5728\u6458\u8981\u4E2D\u590D\u5199\u539F\u6587\u3002\u6D88\u606F\u91CC\u7684\u201C\u4ECA\u5929\u201D\u201C\u660E\u5929\u201D\u201C\u6628\u665A\u201D\u201C\u4E0B\u5468\u4E09\u201D\u7B49\u76F8\u5BF9\u65F6\u95F4\u8868\u8FF0\uFF0C\u8BF7\u57FA\u4E8E\u6BCF\u6761\u6D88\u606F\u524D\u7684\u53D1\u9001\u65F6\u95F4\u6233\u63A8\u5BFC\u4E3A\u5177\u4F53\u65E5\u671F\u5199\u5165\u6458\u8981\u3002\u4F8B\u5982 [2026-05-05T20:00:00.000Z] \u5988\u5988\u8BF4\u201C\u660E\u5929\u8981\u7528\u4E1D\u4E1D\u9732\u201D\uFF0C\u6458\u8981\u5E94\u5199\u4E3A\u201C2026-05-06 \u8981\u7528\u4E1D\u4E1D\u9732\u201D\u3002"
2622
2631
  },
2623
2632
  {
2624
2633
  role: "user",
@@ -2723,6 +2732,7 @@ async function ensureFeishuBotOpenId(config, secrets, options = {}) {
2723
2732
 
2724
2733
  // src/feishu/gateway.ts
2725
2734
  import * as lark2 from "@larksuiteoapi/node-sdk";
2735
+ import path12 from "path";
2726
2736
 
2727
2737
  // src/cron/jobs.ts
2728
2738
  import crypto4 from "crypto";
@@ -2827,6 +2837,7 @@ var CronJobRepository = class {
2827
2837
  create(input2) {
2828
2838
  const schedule = input2.schedule.trim();
2829
2839
  const prompt = input2.prompt.trim();
2840
+ const imageFileName = input2.imageFileName?.trim();
2830
2841
  if (!isValidCronSchedule(schedule)) {
2831
2842
  throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
2832
2843
  }
@@ -2844,6 +2855,7 @@ var CronJobRepository = class {
2844
2855
  createdByOpenId: input2.createdByOpenId,
2845
2856
  schedule,
2846
2857
  prompt,
2858
+ ...imageFileName ? { imageFileName } : {},
2847
2859
  status: "active",
2848
2860
  nextRunAt: nextRunAt.toISOString(),
2849
2861
  createdAt: now.toISOString(),
@@ -2852,15 +2864,18 @@ var CronJobRepository = class {
2852
2864
  this.database.prepare(
2853
2865
  `
2854
2866
  INSERT INTO cron_jobs (
2855
- id, chat_id, created_by_open_id, schedule, prompt, status,
2867
+ id, chat_id, created_by_open_id, schedule, prompt, image_file_name, status,
2856
2868
  last_run_at, next_run_at, last_error, created_at, updated_at
2857
2869
  )
2858
2870
  VALUES (
2859
- @id, @chatId, @createdByOpenId, @schedule, @prompt, @status,
2871
+ @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName, @status,
2860
2872
  NULL, @nextRunAt, NULL, @createdAt, @updatedAt
2861
2873
  )
2862
2874
  `
2863
- ).run(record);
2875
+ ).run({
2876
+ ...record,
2877
+ imageFileName: record.imageFileName ?? null
2878
+ });
2864
2879
  return record;
2865
2880
  }
2866
2881
  get(id) {
@@ -2885,6 +2900,7 @@ var CronJobRepository = class {
2885
2900
  created_by_open_id AS createdByOpenId,
2886
2901
  schedule,
2887
2902
  prompt,
2903
+ image_file_name AS imageFileName,
2888
2904
  status,
2889
2905
  last_run_at AS lastRunAt,
2890
2906
  next_run_at AS nextRunAt,
@@ -2903,6 +2919,7 @@ var CronJobRepository = class {
2903
2919
  createdByOpenId: row.createdByOpenId ?? void 0,
2904
2920
  schedule: row.schedule,
2905
2921
  prompt: row.prompt,
2922
+ imageFileName: row.imageFileName ?? void 0,
2906
2923
  status: row.status,
2907
2924
  lastRunAt: row.lastRunAt ?? void 0,
2908
2925
  nextRunAt: row.nextRunAt,
@@ -2976,6 +2993,7 @@ var CronJobRepository = class {
2976
2993
  created_by_open_id AS createdByOpenId,
2977
2994
  schedule,
2978
2995
  prompt,
2996
+ image_file_name AS imageFileName,
2979
2997
  status,
2980
2998
  last_run_at AS lastRunAt,
2981
2999
  next_run_at AS nextRunAt,
@@ -2994,6 +3012,7 @@ var CronJobRepository = class {
2994
3012
  createdByOpenId: row.createdByOpenId ?? void 0,
2995
3013
  schedule: row.schedule,
2996
3014
  prompt: row.prompt,
3015
+ imageFileName: row.imageFileName ?? void 0,
2997
3016
  status: row.status,
2998
3017
  lastRunAt: row.lastRunAt ?? void 0,
2999
3018
  nextRunAt: row.nextRunAt,
@@ -3098,6 +3117,12 @@ function createCronJobScheduler(options) {
3098
3117
  try {
3099
3118
  const text = await options.generateMessage(job, startedAt);
3100
3119
  await options.sendTextToChat(job.chatId, text);
3120
+ if (job.imageFileName) {
3121
+ if (!options.sendImageToChat) {
3122
+ throw new Error("\u5F53\u524D\u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\u3002");
3123
+ }
3124
+ await options.sendImageToChat(job.chatId, job.imageFileName);
3125
+ }
3101
3126
  options.repository.markSuccess(job.id, startedAt);
3102
3127
  } catch (error) {
3103
3128
  const message = error instanceof Error ? error.message : String(error);
@@ -3514,6 +3539,7 @@ var ImageMultimodalTaskRepository = class {
3514
3539
  };
3515
3540
 
3516
3541
  // src/multimodal/worker.ts
3542
+ import path11 from "path";
3517
3543
  var ImageMultimodalWorker = class {
3518
3544
  constructor(options) {
3519
3545
  this.options = options;
@@ -3540,9 +3566,11 @@ var ImageMultimodalWorker = class {
3540
3566
  throw error;
3541
3567
  }
3542
3568
  try {
3569
+ const imageFileName = path11.basename(running.storedPath);
3543
3570
  const described = await this.options.model.describeImage({
3544
3571
  imagePath: running.storedPath,
3545
- mimeType: running.mimeType
3572
+ mimeType: running.mimeType,
3573
+ context: `\u56FE\u7247\u6587\u4EF6\u540D\uFF1A${imageFileName}`
3546
3574
  });
3547
3575
  if (!described.isMeaningful) {
3548
3576
  this.options.tasks.markSkipped(running.id, described.reason || "\u591A\u6A21\u6001\u6A21\u578B\u5224\u5B9A\u56FE\u7247\u65E0\u610F\u4E49\u3002");
@@ -3552,6 +3580,7 @@ var ImageMultimodalWorker = class {
3552
3580
  const derivedMessageId = this.options.messages.createImageSummaryMessage({
3553
3581
  sourceMessageId: running.sourceMessageId,
3554
3582
  imageKey: running.imageKey,
3583
+ imageFileName,
3555
3584
  summary: described.summary,
3556
3585
  reason: described.reason,
3557
3586
  multimodalModel: this.options.multimodalModelName,
@@ -3585,6 +3614,17 @@ function readString(input2, key) {
3585
3614
  }
3586
3615
  return value.trim();
3587
3616
  }
3617
+ function readOptionalString(input2, key) {
3618
+ const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
3619
+ if (value === void 0 || value === null) {
3620
+ return void 0;
3621
+ }
3622
+ if (typeof value !== "string") {
3623
+ throw new Error(`${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
3624
+ }
3625
+ const trimmed = value.trim();
3626
+ return trimmed || void 0;
3627
+ }
3588
3628
  function createCronJobTools(input2) {
3589
3629
  return [
3590
3630
  {
@@ -3600,6 +3640,10 @@ function createCronJobTools(input2) {
3600
3640
  prompt: {
3601
3641
  type: "string",
3602
3642
  description: "Prompt used later to generate the scheduled message."
3643
+ },
3644
+ imageFileName: {
3645
+ type: "string",
3646
+ description: "Optional image filename already stored from the current chat, for example om_xxx-image.jpg."
3603
3647
  }
3604
3648
  },
3605
3649
  required: ["schedule", "prompt"],
@@ -3610,7 +3654,8 @@ function createCronJobTools(input2) {
3610
3654
  chatId: input2.chatId,
3611
3655
  createdByOpenId: input2.createdByOpenId,
3612
3656
  schedule: readString(rawInput, "schedule"),
3613
- prompt: readString(rawInput, "prompt")
3657
+ prompt: readString(rawInput, "prompt"),
3658
+ imageFileName: readOptionalString(rawInput, "imageFileName")
3614
3659
  });
3615
3660
  return JSON.stringify({ ok: true, job });
3616
3661
  }
@@ -3773,19 +3818,36 @@ function stripMentions(text, mentions) {
3773
3818
  }
3774
3819
  return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
3775
3820
  }
3776
- var FEISHU_TOOL_SYSTEM_PROMPT = "\u4F60\u662F\u98DE\u4E66\u7FA4\u804A\u52A9\u624B\u3002\u4F60\u53EF\u4EE5\u5148\u641C\u7D22\u672C\u5730\u77E5\u8BC6\u6765\u56DE\u7B54\u95EE\u9898\uFF1B\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u521B\u5EFA\u3001\u67E5\u770B\u6216\u5220\u9664\u7FA4\u6D88\u606F\u5B9A\u65F6\u4EFB\u52A1\u65F6\uFF0C\u4E5F\u53EF\u4EE5\u8C03\u7528\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u3002\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u53EA\u7BA1\u7406\u5F53\u524D\u7FA4\u804A\uFF0C\u4E0D\u80FD\u8DE8\u7FA4\u64CD\u4F5C\u3002\u82E5\u7528\u6237\u7528\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u65F6\u95F4\uFF0C\u4F60\u9700\u8981\u5148\u5C06\u5176\u8F6C\u6362\u4E3A\u4E94\u5B57\u6BB5 cron \u8868\u8FBE\u5F0F\uFF08\u5206 \u65F6 \u65E5 \u6708 \u5468\uFF09\uFF0C\u518D\u8C03\u7528\u5DE5\u5177\u3002\u5F53\u524D\u65F6\u95F4\u4F1A\u63D0\u4F9B\u7ED9\u4F60\u3002\u68C0\u7D22\u8BC1\u636E\u4E2D\u7684\u65F6\u95F4\u6233\u662F\u6D88\u606F\u88AB\u53D1\u9001\u65F6\u7684\u771F\u5B9E\u65F6\u95F4\u3002\u56DE\u7B54\u65F6\u82E5\u6D89\u53CA\u76F8\u5BF9\u65F6\u95F4\u8868\u8FF0\uFF08\u5982\u6D88\u606F\u4E2D\u8BF4\u201C\u660E\u5929\u201D\u201C\u4ECA\u665A\u201D\uFF09\uFF0C\u5FC5\u987B\u57FA\u4E8E\u8BC1\u636E\u4E2D\u6BCF\u6761\u6D88\u606F\u7684\u65F6\u95F4\u6233\u63A8\u5BFC\u4E3A\u5177\u4F53\u65E5\u671F\uFF0C\u4E0D\u8981\u7167\u642C\u539F\u6587\u7684\u76F8\u5BF9\u8868\u8FF0\u3002\u5BF9\u4E8E\u4E00\u822C\u95EE\u7B54\uFF0C\u5148\u6309\u9700\u8C03\u7528\u641C\u7D22\u5DE5\u5177\uFF0C\u518D\u57FA\u4E8E\u5DE5\u5177\u8FD4\u56DE\u7684\u8BC1\u636E\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\uFF1B\u82E5\u5F15\u7528\u4E86\u68C0\u7D22\u7ED3\u679C\uFF0C\u8981\u5728\u7B54\u6848\u91CC\u76F4\u63A5\u5199\u51FA\u5F15\u7528\u5185\u5BB9\u3002\u4E0D\u8981\u58F0\u79F0\u5B8C\u6210\u4E86\u672A\u5B9E\u9645\u8C03\u7528\u7684\u64CD\u4F5C\u3002";
3821
+ var FEISHU_TOOL_SYSTEM_PROMPT = `\u4F60\u662F\u98DE\u4E66\u7FA4\u804A\u52A9\u624B\u3002\u4F60\u53EF\u4EE5\u5148\u641C\u7D22\u672C\u5730\u77E5\u8BC6\u6765\u56DE\u7B54\u95EE\u9898\uFF1B\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u521B\u5EFA\u3001\u67E5\u770B\u6216\u5220\u9664\u7FA4\u6D88\u606F\u5B9A\u65F6\u4EFB\u52A1\u65F6\uFF0C\u4E5F\u53EF\u4EE5\u8C03\u7528\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u3002\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u53EA\u7BA1\u7406\u5F53\u524D\u7FA4\u804A\uFF0C\u4E0D\u80FD\u8DE8\u7FA4\u64CD\u4F5C\u3002\u82E5\u7528\u6237\u8981\u6C42\u5B9A\u65F6\u4EFB\u52A1\u53D1\u9001\u56FE\u7247\uFF0C\u53EA\u80FD\u4F7F\u7528\u5F53\u524D\u7FA4\u804A\u91CC\u5DF2\u7ECF\u4E0B\u8F7D\u5165\u5E93\u7684\u56FE\u7247\u6587\u4EF6\u540D\uFF0C\u5E76\u5728\u521B\u5EFA\u5B9A\u65F6\u4EFB\u52A1\u65F6\u628A\u6587\u4EF6\u540D\u586B\u5165 imageFileName\uFF1B\u4E0D\u8981\u7F16\u9020\u672C\u5730\u8DEF\u5F84\u3002\u82E5\u7528\u6237\u7528\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u65F6\u95F4\uFF0C\u4F60\u9700\u8981\u5148\u5C06\u5176\u8F6C\u6362\u4E3A\u4E94\u5B57\u6BB5 cron \u8868\u8FBE\u5F0F\uFF08\u5206 \u65F6 \u65E5 \u6708 \u5468\uFF09\uFF0C\u518D\u8C03\u7528\u5DE5\u5177\u3002\u5F53\u524D\u65F6\u95F4\u4F1A\u63D0\u4F9B\u7ED9\u4F60\u3002\u68C0\u7D22\u8BC1\u636E\u4E2D\u7684\u65F6\u95F4\u6233\u662F\u6D88\u606F\u88AB\u53D1\u9001\u65F6\u7684\u771F\u5B9E\u65F6\u95F4\u3002\u56DE\u7B54\u65F6\u82E5\u6D89\u53CA\u76F8\u5BF9\u65F6\u95F4\u8868\u8FF0\uFF08\u5982\u6D88\u606F\u4E2D\u8BF4\u201D\u660E\u5929\u201D\u201D\u4ECA\u665A\u201D\uFF09\uFF0C\u5FC5\u987B\u57FA\u4E8E\u8BC1\u636E\u4E2D\u6BCF\u6761\u6D88\u606F\u7684\u65F6\u95F4\u6233\u63A8\u5BFC\u4E3A\u5177\u4F53\u65E5\u671F\uFF0C\u4E0D\u8981\u7167\u642C\u539F\u6587\u7684\u76F8\u5BF9\u8868\u8FF0\u3002\u5BF9\u4E8E\u4E00\u822C\u95EE\u7B54\uFF0C\u5148\u6309\u9700\u8C03\u7528\u641C\u7D22\u5DE5\u5177\uFF0C\u518D\u57FA\u4E8E\u5DE5\u5177\u8FD4\u56DE\u7684\u8BC1\u636E\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\uFF1B\u82E5\u5F15\u7528\u4E86\u68C0\u7D22\u7ED3\u679C\uFF0C\u8981\u5728\u7B54\u6848\u91CC\u76F4\u63A5\u5199\u51FA\u5F15\u7528\u5185\u5BB9\u3002\u4E0D\u8981\u58F0\u79F0\u5B8C\u6210\u4E86\u672A\u5B9E\u9645\u8C03\u7528\u7684\u64CD\u4F5C\u3002\u91CD\u8981\uFF1A\u4F60\u7684\u56DE\u7B54\u5FC5\u987B\u662F\u9762\u5411\u7FA4\u6210\u5458\u7684\u81EA\u7136\u8BED\u8A00\uFF0C\u7EDD\u5BF9\u4E0D\u80FD\u8F93\u51FA JSON\u3001\u5DE5\u5177\u8C03\u7528\u7EC6\u8282\u6216\u539F\u59CB\u7684\u641C\u7D22\u7ED3\u679C\u683C\u5F0F\u3002\u7528\u6237\u53EA\u5E94\u770B\u5230\u4F60\u6574\u5408\u540E\u7684\u6700\u7EC8\u7B54\u6848\u3002`;
3777
3822
  var DEFAULT_MAX_MODEL_TURNS = 4;
3778
3823
  var DEFAULT_MAX_TOOL_CALLS = 8;
3779
3824
  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";
3780
3825
  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";
3781
3826
  function toToolResultContent(value) {
3782
- return typeof value === "string" ? value : JSON.stringify(value);
3827
+ if (typeof value === "string") return value;
3828
+ return JSON.stringify(value);
3829
+ }
3830
+ function isEvidenceBlockArray(value) {
3831
+ return Array.isArray(value) && value.length > 0 && typeof value[0]?.text === "string";
3832
+ }
3833
+ function formatEvidenceBlocks(blocks) {
3834
+ return blocks.map((block, index2) => {
3835
+ const source = block.source;
3836
+ const sender = source.sender ? `${source.sender} ` : "";
3837
+ const timestamp = source.timestamp ? `(${source.timestamp.slice(0, 19).replace("T", " ")})` : "";
3838
+ const header = `[\u8BC1\u636E${index2 + 1}] ${sender}${timestamp}:`;
3839
+ return `${header}
3840
+ ${block.text}`;
3841
+ }).join("\n\n");
3783
3842
  }
3784
3843
  function toToolErrorContent(message) {
3785
3844
  return JSON.stringify({ ok: false, error: message });
3786
3845
  }
3787
3846
  async function executeFeishuTool(tool, input2) {
3788
3847
  const result = await tool.execute(input2);
3848
+ if (isEvidenceBlockArray(result)) {
3849
+ return formatEvidenceBlocks(result);
3850
+ }
3789
3851
  return toToolResultContent(result);
3790
3852
  }
3791
3853
  async function runFeishuToolLoop(input2) {
@@ -3995,9 +4057,22 @@ var FeishuQuestionHandler = class {
3995
4057
 
3996
4058
  // src/feishu/sender.ts
3997
4059
  import * as lark from "@larksuiteoapi/node-sdk";
4060
+ import fs9 from "fs/promises";
3998
4061
  function mapDomain(domain) {
3999
4062
  return domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu;
4000
4063
  }
4064
+ function extractImageKey(response) {
4065
+ const data2 = response && typeof response === "object" ? response : {};
4066
+ const direct = data2.image_key;
4067
+ if (typeof direct === "string" && direct.trim()) {
4068
+ return direct.trim();
4069
+ }
4070
+ const nested = data2.data && typeof data2.data === "object" ? data2.data.image_key : void 0;
4071
+ if (typeof nested === "string" && nested.trim()) {
4072
+ return nested.trim();
4073
+ }
4074
+ throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
4075
+ }
4001
4076
  var FeishuMessageSender = class _FeishuMessageSender {
4002
4077
  constructor(client) {
4003
4078
  this.client = client;
@@ -4034,6 +4109,39 @@ var FeishuMessageSender = class _FeishuMessageSender {
4034
4109
  throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
4035
4110
  }
4036
4111
  }
4112
+ async sendImageToChat(chatId, imagePath) {
4113
+ const imageCreate = this.client.im.v1?.image?.create;
4114
+ if (!imageCreate) {
4115
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u56FE\u7247\u4E0A\u4F20\u63A5\u53E3\u3002");
4116
+ }
4117
+ const image = await fs9.readFile(imagePath);
4118
+ const uploaded = await imageCreate({
4119
+ data: {
4120
+ image_type: "message",
4121
+ image
4122
+ }
4123
+ });
4124
+ const imageKey = extractImageKey(uploaded);
4125
+ const payload = {
4126
+ data: {
4127
+ receive_id: chatId,
4128
+ msg_type: "image",
4129
+ content: JSON.stringify({ image_key: imageKey })
4130
+ },
4131
+ params: {
4132
+ receive_id_type: "chat_id"
4133
+ }
4134
+ };
4135
+ if (this.client.im.v1?.message.create) {
4136
+ await this.client.im.v1.message.create(payload);
4137
+ return;
4138
+ }
4139
+ if (this.client.im.message?.create) {
4140
+ await this.client.im.message.create(payload);
4141
+ return;
4142
+ }
4143
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
4144
+ }
4037
4145
  async replyTextToMessage(messageId, text) {
4038
4146
  const payload = {
4039
4147
  path: {
@@ -4174,6 +4282,13 @@ function createFeishuEventDispatcher(options) {
4174
4282
  }
4175
4283
  });
4176
4284
  }
4285
+ function resolveFeishuImagePath(config, imageFileName) {
4286
+ const fileName = path12.basename(imageFileName.trim());
4287
+ if (!fileName || fileName !== imageFileName.trim()) {
4288
+ throw new Error("\u56FE\u7247\u6587\u4EF6\u540D\u65E0\u6548\u3002");
4289
+ }
4290
+ return path12.join(resolveHomePath(config.storage.dataDir), "files", "feishu", fileName);
4291
+ }
4177
4292
  function createFeishuGateway(options) {
4178
4293
  assertFeishuConfig(options.config, options.secrets);
4179
4294
  const wsClient = options.wsClientFactory?.({
@@ -4219,6 +4334,10 @@ function createFeishuGateway(options) {
4219
4334
  const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4220
4335
  repository: new CronJobRepository(options.cronJobProcessor.database),
4221
4336
  sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
4337
+ sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
4338
+ chatId,
4339
+ resolveFeishuImagePath(options.config, imageFileName)
4340
+ ) : void 0,
4222
4341
  generateMessage: async (job, now) => {
4223
4342
  const { tools, close } = await createAgenticRagSearchTools({
4224
4343
  config: options.config,
@@ -4256,8 +4375,8 @@ function createFeishuGateway(options) {
4256
4375
 
4257
4376
  // src/feishu/resource-downloader.ts
4258
4377
  import * as lark3 from "@larksuiteoapi/node-sdk";
4259
- import fs9 from "fs/promises";
4260
- import path11 from "path";
4378
+ import fs10 from "fs/promises";
4379
+ import path13 from "path";
4261
4380
  var RESOURCE_TYPE_BY_KIND = {
4262
4381
  file: "file",
4263
4382
  image: "image",
@@ -4295,10 +4414,10 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4295
4414
  }
4296
4415
  async download(input2) {
4297
4416
  const resourceType = RESOURCE_TYPE_BY_KIND[input2.attachment.kind];
4298
- const targetDir = path11.join(this.dataDir, "files", "feishu");
4299
- await fs9.mkdir(targetDir, { recursive: true });
4417
+ const targetDir = path13.join(this.dataDir, "files", "feishu");
4418
+ await fs10.mkdir(targetDir, { recursive: true });
4300
4419
  const fileName = buildStoredFileName(input2);
4301
- const storedPath = path11.join(targetDir, fileName);
4420
+ const storedPath = path13.join(targetDir, fileName);
4302
4421
  const payload = {
4303
4422
  params: { type: resourceType },
4304
4423
  path: { message_id: input2.messageId, file_key: input2.attachment.fileKey }
@@ -4321,29 +4440,29 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4321
4440
 
4322
4441
  // src/files/ingest.ts
4323
4442
  import crypto7 from "crypto";
4324
- import fs11 from "fs/promises";
4325
- import path13 from "path";
4443
+ import fs12 from "fs/promises";
4444
+ import path15 from "path";
4326
4445
 
4327
4446
  // src/files/parser.ts
4328
- import fs10 from "fs/promises";
4329
- import path12 from "path";
4447
+ import fs11 from "fs/promises";
4448
+ import path14 from "path";
4330
4449
  import mammoth from "mammoth";
4331
4450
  import { PDFParse } from "pdf-parse";
4332
4451
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".txt", ".md", ".markdown", ".json", ".csv", ".tsv", ".log"]);
4333
4452
  var DOCX_EXTENSIONS = /* @__PURE__ */ new Set([".docx"]);
4334
4453
  var PDF_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
4335
4454
  function isSupportedParseFile(filePath) {
4336
- const extension = path12.extname(filePath).toLowerCase();
4455
+ const extension = path14.extname(filePath).toLowerCase();
4337
4456
  return TEXT_EXTENSIONS.has(extension) || DOCX_EXTENSIONS.has(extension) || PDF_EXTENSIONS.has(extension);
4338
4457
  }
4339
4458
  function describeSupportedParseTypes() {
4340
4459
  return "txt\u3001md\u3001json\u3001csv\u3001tsv\u3001log\u3001docx\u3001pdf";
4341
4460
  }
4342
4461
  async function parseFileToText(filePath) {
4343
- const extension = path12.extname(filePath).toLowerCase();
4462
+ const extension = path14.extname(filePath).toLowerCase();
4344
4463
  if (TEXT_EXTENSIONS.has(extension)) {
4345
4464
  return {
4346
- text: await fs10.readFile(filePath, "utf8"),
4465
+ text: await fs11.readFile(filePath, "utf8"),
4347
4466
  parser: "text",
4348
4467
  warnings: []
4349
4468
  };
@@ -4357,7 +4476,7 @@ async function parseFileToText(filePath) {
4357
4476
  };
4358
4477
  }
4359
4478
  if (PDF_EXTENSIONS.has(extension)) {
4360
- const buffer = await fs10.readFile(filePath);
4479
+ const buffer = await fs11.readFile(filePath);
4361
4480
  const parser = new PDFParse({ data: buffer });
4362
4481
  try {
4363
4482
  const result = await parser.getText();
@@ -4379,7 +4498,7 @@ function isSupportedTextFile(filePath) {
4379
4498
  }
4380
4499
  function ensureSupportedTextFile(filePath) {
4381
4500
  if (!isSupportedTextFile(filePath)) {
4382
- const extension = path13.extname(filePath).toLowerCase();
4501
+ const extension = path15.extname(filePath).toLowerCase();
4383
4502
  throw new Error(`\u6682\u4E0D\u652F\u6301\u8BE5\u6587\u4EF6\u7C7B\u578B\uFF1A${extension || "\u65E0\u6269\u5C55\u540D"}\u3002\u5F53\u524D\u652F\u6301 ${describeSupportedParseTypes()}\u3002`);
4384
4503
  }
4385
4504
  }
@@ -4388,12 +4507,12 @@ function stableStoredName(sourcePath, fileName) {
4388
4507
  return `${digest}-${fileName}`;
4389
4508
  }
4390
4509
  async function ingestLocalFile(input2) {
4391
- const sourcePath = path13.resolve(input2.filePath);
4392
- const fileName = path13.basename(sourcePath);
4510
+ const sourcePath = path15.resolve(input2.filePath);
4511
+ const fileName = path15.basename(sourcePath);
4393
4512
  const jobId = input2.jobs?.start({ sourcePath, fileName });
4394
4513
  try {
4395
4514
  ensureSupportedTextFile(sourcePath);
4396
- const stat = await fs11.stat(sourcePath);
4515
+ const stat = await fs12.stat(sourcePath);
4397
4516
  if (!stat.isFile()) {
4398
4517
  throw new Error(`\u4E0D\u662F\u6587\u4EF6\uFF1A${sourcePath}`);
4399
4518
  }
@@ -4402,10 +4521,10 @@ async function ingestLocalFile(input2) {
4402
4521
  if (!text) {
4403
4522
  throw new Error(`\u6587\u4EF6\u6CA1\u6709\u53EF\u7D22\u5F15\u6587\u672C\uFF1A${sourcePath}`);
4404
4523
  }
4405
- const fileDir = path13.join(resolveHomePath(input2.config.storage.dataDir), "files");
4406
- await fs11.mkdir(fileDir, { recursive: true });
4407
- const storedPath = path13.join(fileDir, stableStoredName(sourcePath, fileName));
4408
- await fs11.copyFile(sourcePath, storedPath);
4524
+ const fileDir = path15.join(resolveHomePath(input2.config.storage.dataDir), "files");
4525
+ await fs12.mkdir(fileDir, { recursive: true });
4526
+ const storedPath = path15.join(fileDir, stableStoredName(sourcePath, fileName));
4527
+ await fs12.copyFile(sourcePath, storedPath);
4409
4528
  const messageId = input2.messages.ingest({
4410
4529
  platform: "local-file",
4411
4530
  platformChatId: "local-files",
@@ -4710,8 +4829,8 @@ var GatewayIngestor = class {
4710
4829
 
4711
4830
  // src/gateway/detached.ts
4712
4831
  import { spawn } from "child_process";
4713
- import fs12 from "fs";
4714
- import path14 from "path";
4832
+ import fs13 from "fs";
4833
+ import path16 from "path";
4715
4834
  var START_FAILURE_GRACE_MS = 250;
4716
4835
  function buildGatewayForegroundSpawnCommand(argv = process.argv) {
4717
4836
  const [command = process.execPath, ...rawArgs] = argv;
@@ -4768,7 +4887,7 @@ async function startDetachedGateway(input2) {
4768
4887
  ...status.pid ? { pid: status.pid } : {}
4769
4888
  };
4770
4889
  }
4771
- fs12.mkdirSync(path14.dirname(logFile), { recursive: true });
4890
+ fs13.mkdirSync(path16.dirname(logFile), { recursive: true });
4772
4891
  let out;
4773
4892
  let err;
4774
4893
  let stdioClosed = false;
@@ -4778,15 +4897,15 @@ async function startDetachedGateway(input2) {
4778
4897
  }
4779
4898
  stdioClosed = true;
4780
4899
  if (typeof out === "number") {
4781
- fs12.closeSync(out);
4900
+ fs13.closeSync(out);
4782
4901
  }
4783
4902
  if (typeof err === "number") {
4784
- fs12.closeSync(err);
4903
+ fs13.closeSync(err);
4785
4904
  }
4786
4905
  };
4787
4906
  try {
4788
- out = fs12.openSync(logFile, "a");
4789
- err = fs12.openSync(logFile, "a");
4907
+ out = fs13.openSync(logFile, "a");
4908
+ err = fs13.openSync(logFile, "a");
4790
4909
  const foreground = buildGatewayForegroundSpawnCommand(input2.argv);
4791
4910
  const child = spawn(foreground.command, foreground.args, {
4792
4911
  detached: true,
@@ -4817,7 +4936,7 @@ async function startDetachedGateway(input2) {
4817
4936
  }
4818
4937
 
4819
4938
  // src/multimodal/openai-compatible.ts
4820
- import fs13 from "fs/promises";
4939
+ import fs14 from "fs/promises";
4821
4940
  function normalizeBaseUrl2(baseUrl) {
4822
4941
  return baseUrl.replace(/\/+$/, "");
4823
4942
  }
@@ -4827,6 +4946,7 @@ function buildPrompt(context) {
4827
4946
  "\u8BF7\u7406\u89E3\u8FD9\u5F20\u56FE\u7247\uFF0C\u5224\u65AD\u5B83\u662F\u5426\u5305\u542B\u503C\u5F97\u8FDB\u5165\u77E5\u8BC6\u5E93\u548C\u4F1A\u8BDD\u8BB0\u5FC6\u7684\u6709\u610F\u4E49\u4FE1\u606F\u3002",
4828
4947
  '\u8BF7\u53EA\u8F93\u51FA JSON\uFF0C\u683C\u5F0F\u4E3A {"summary": string, "isMeaningful": boolean, "reason": string}\u3002',
4829
4948
  "summary \u4F7F\u7528\u7B80\u6D01\u4E2D\u6587\u8F6C\u8FF0\u56FE\u7247\u4E2D\u7684\u5173\u952E\u4FE1\u606F\uFF1B\u65E0\u610F\u4E49\u56FE\u7247\u4E5F\u8981\u7ED9\u51FA\u7B80\u77ED summary\u3002",
4949
+ "\u5982\u679C\u4E0A\u4E0B\u6587\u63D0\u4F9B\u4E86\u56FE\u7247\u6587\u4EF6\u540D\uFF0Csummary \u5FC5\u987B\u539F\u6837\u5305\u542B\u8BE5\u6587\u4EF6\u540D\uFF0C\u4FBF\u4E8E\u4E4B\u540E\u6309\u6587\u4EF6\u540D\u68C0\u7D22\u548C\u53D1\u9001\u56FE\u7247\u3002",
4830
4950
  contextText ? `\u4E0A\u4E0B\u6587\uFF1A${contextText}` : void 0
4831
4951
  ].filter(Boolean).join("\n");
4832
4952
  }
@@ -4864,7 +4984,7 @@ var OpenAICompatibleMultimodalModel = class {
4864
4984
  if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
4865
4985
  throw new Error("\u591A\u6A21\u6001\u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
4866
4986
  }
4867
- const image = await fs13.readFile(input2.imagePath);
4987
+ const image = await fs14.readFile(input2.imagePath);
4868
4988
  const response = await fetch(`${normalizeBaseUrl2(this.options.baseUrl)}/chat/completions`, {
4869
4989
  method: "POST",
4870
4990
  headers: {
@@ -6398,7 +6518,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
6398
6518
  const config = await loadConfig();
6399
6519
  const database = openDatabase(config);
6400
6520
  try {
6401
- const raw = await fs14.readFile(options.file, "utf8");
6521
+ const raw = await fs15.readFile(options.file, "utf8");
6402
6522
  const payload = JSON.parse(raw);
6403
6523
  const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
6404
6524
  if (!result.accepted) {