chattercatcher 0.1.26 → 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.26",
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,7 +3818,7 @@ 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\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`;
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";
@@ -4012,9 +4057,22 @@ var FeishuQuestionHandler = class {
4012
4057
 
4013
4058
  // src/feishu/sender.ts
4014
4059
  import * as lark from "@larksuiteoapi/node-sdk";
4060
+ import fs9 from "fs/promises";
4015
4061
  function mapDomain(domain) {
4016
4062
  return domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu;
4017
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
+ }
4018
4076
  var FeishuMessageSender = class _FeishuMessageSender {
4019
4077
  constructor(client) {
4020
4078
  this.client = client;
@@ -4051,6 +4109,39 @@ var FeishuMessageSender = class _FeishuMessageSender {
4051
4109
  throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
4052
4110
  }
4053
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
+ }
4054
4145
  async replyTextToMessage(messageId, text) {
4055
4146
  const payload = {
4056
4147
  path: {
@@ -4191,6 +4282,13 @@ function createFeishuEventDispatcher(options) {
4191
4282
  }
4192
4283
  });
4193
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
+ }
4194
4292
  function createFeishuGateway(options) {
4195
4293
  assertFeishuConfig(options.config, options.secrets);
4196
4294
  const wsClient = options.wsClientFactory?.({
@@ -4236,6 +4334,10 @@ function createFeishuGateway(options) {
4236
4334
  const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4237
4335
  repository: new CronJobRepository(options.cronJobProcessor.database),
4238
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,
4239
4341
  generateMessage: async (job, now) => {
4240
4342
  const { tools, close } = await createAgenticRagSearchTools({
4241
4343
  config: options.config,
@@ -4273,8 +4375,8 @@ function createFeishuGateway(options) {
4273
4375
 
4274
4376
  // src/feishu/resource-downloader.ts
4275
4377
  import * as lark3 from "@larksuiteoapi/node-sdk";
4276
- import fs9 from "fs/promises";
4277
- import path11 from "path";
4378
+ import fs10 from "fs/promises";
4379
+ import path13 from "path";
4278
4380
  var RESOURCE_TYPE_BY_KIND = {
4279
4381
  file: "file",
4280
4382
  image: "image",
@@ -4312,10 +4414,10 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4312
4414
  }
4313
4415
  async download(input2) {
4314
4416
  const resourceType = RESOURCE_TYPE_BY_KIND[input2.attachment.kind];
4315
- const targetDir = path11.join(this.dataDir, "files", "feishu");
4316
- await fs9.mkdir(targetDir, { recursive: true });
4417
+ const targetDir = path13.join(this.dataDir, "files", "feishu");
4418
+ await fs10.mkdir(targetDir, { recursive: true });
4317
4419
  const fileName = buildStoredFileName(input2);
4318
- const storedPath = path11.join(targetDir, fileName);
4420
+ const storedPath = path13.join(targetDir, fileName);
4319
4421
  const payload = {
4320
4422
  params: { type: resourceType },
4321
4423
  path: { message_id: input2.messageId, file_key: input2.attachment.fileKey }
@@ -4338,29 +4440,29 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4338
4440
 
4339
4441
  // src/files/ingest.ts
4340
4442
  import crypto7 from "crypto";
4341
- import fs11 from "fs/promises";
4342
- import path13 from "path";
4443
+ import fs12 from "fs/promises";
4444
+ import path15 from "path";
4343
4445
 
4344
4446
  // src/files/parser.ts
4345
- import fs10 from "fs/promises";
4346
- import path12 from "path";
4447
+ import fs11 from "fs/promises";
4448
+ import path14 from "path";
4347
4449
  import mammoth from "mammoth";
4348
4450
  import { PDFParse } from "pdf-parse";
4349
4451
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".txt", ".md", ".markdown", ".json", ".csv", ".tsv", ".log"]);
4350
4452
  var DOCX_EXTENSIONS = /* @__PURE__ */ new Set([".docx"]);
4351
4453
  var PDF_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
4352
4454
  function isSupportedParseFile(filePath) {
4353
- const extension = path12.extname(filePath).toLowerCase();
4455
+ const extension = path14.extname(filePath).toLowerCase();
4354
4456
  return TEXT_EXTENSIONS.has(extension) || DOCX_EXTENSIONS.has(extension) || PDF_EXTENSIONS.has(extension);
4355
4457
  }
4356
4458
  function describeSupportedParseTypes() {
4357
4459
  return "txt\u3001md\u3001json\u3001csv\u3001tsv\u3001log\u3001docx\u3001pdf";
4358
4460
  }
4359
4461
  async function parseFileToText(filePath) {
4360
- const extension = path12.extname(filePath).toLowerCase();
4462
+ const extension = path14.extname(filePath).toLowerCase();
4361
4463
  if (TEXT_EXTENSIONS.has(extension)) {
4362
4464
  return {
4363
- text: await fs10.readFile(filePath, "utf8"),
4465
+ text: await fs11.readFile(filePath, "utf8"),
4364
4466
  parser: "text",
4365
4467
  warnings: []
4366
4468
  };
@@ -4374,7 +4476,7 @@ async function parseFileToText(filePath) {
4374
4476
  };
4375
4477
  }
4376
4478
  if (PDF_EXTENSIONS.has(extension)) {
4377
- const buffer = await fs10.readFile(filePath);
4479
+ const buffer = await fs11.readFile(filePath);
4378
4480
  const parser = new PDFParse({ data: buffer });
4379
4481
  try {
4380
4482
  const result = await parser.getText();
@@ -4396,7 +4498,7 @@ function isSupportedTextFile(filePath) {
4396
4498
  }
4397
4499
  function ensureSupportedTextFile(filePath) {
4398
4500
  if (!isSupportedTextFile(filePath)) {
4399
- const extension = path13.extname(filePath).toLowerCase();
4501
+ const extension = path15.extname(filePath).toLowerCase();
4400
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`);
4401
4503
  }
4402
4504
  }
@@ -4405,12 +4507,12 @@ function stableStoredName(sourcePath, fileName) {
4405
4507
  return `${digest}-${fileName}`;
4406
4508
  }
4407
4509
  async function ingestLocalFile(input2) {
4408
- const sourcePath = path13.resolve(input2.filePath);
4409
- const fileName = path13.basename(sourcePath);
4510
+ const sourcePath = path15.resolve(input2.filePath);
4511
+ const fileName = path15.basename(sourcePath);
4410
4512
  const jobId = input2.jobs?.start({ sourcePath, fileName });
4411
4513
  try {
4412
4514
  ensureSupportedTextFile(sourcePath);
4413
- const stat = await fs11.stat(sourcePath);
4515
+ const stat = await fs12.stat(sourcePath);
4414
4516
  if (!stat.isFile()) {
4415
4517
  throw new Error(`\u4E0D\u662F\u6587\u4EF6\uFF1A${sourcePath}`);
4416
4518
  }
@@ -4419,10 +4521,10 @@ async function ingestLocalFile(input2) {
4419
4521
  if (!text) {
4420
4522
  throw new Error(`\u6587\u4EF6\u6CA1\u6709\u53EF\u7D22\u5F15\u6587\u672C\uFF1A${sourcePath}`);
4421
4523
  }
4422
- const fileDir = path13.join(resolveHomePath(input2.config.storage.dataDir), "files");
4423
- await fs11.mkdir(fileDir, { recursive: true });
4424
- const storedPath = path13.join(fileDir, stableStoredName(sourcePath, fileName));
4425
- 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);
4426
4528
  const messageId = input2.messages.ingest({
4427
4529
  platform: "local-file",
4428
4530
  platformChatId: "local-files",
@@ -4727,8 +4829,8 @@ var GatewayIngestor = class {
4727
4829
 
4728
4830
  // src/gateway/detached.ts
4729
4831
  import { spawn } from "child_process";
4730
- import fs12 from "fs";
4731
- import path14 from "path";
4832
+ import fs13 from "fs";
4833
+ import path16 from "path";
4732
4834
  var START_FAILURE_GRACE_MS = 250;
4733
4835
  function buildGatewayForegroundSpawnCommand(argv = process.argv) {
4734
4836
  const [command = process.execPath, ...rawArgs] = argv;
@@ -4785,7 +4887,7 @@ async function startDetachedGateway(input2) {
4785
4887
  ...status.pid ? { pid: status.pid } : {}
4786
4888
  };
4787
4889
  }
4788
- fs12.mkdirSync(path14.dirname(logFile), { recursive: true });
4890
+ fs13.mkdirSync(path16.dirname(logFile), { recursive: true });
4789
4891
  let out;
4790
4892
  let err;
4791
4893
  let stdioClosed = false;
@@ -4795,15 +4897,15 @@ async function startDetachedGateway(input2) {
4795
4897
  }
4796
4898
  stdioClosed = true;
4797
4899
  if (typeof out === "number") {
4798
- fs12.closeSync(out);
4900
+ fs13.closeSync(out);
4799
4901
  }
4800
4902
  if (typeof err === "number") {
4801
- fs12.closeSync(err);
4903
+ fs13.closeSync(err);
4802
4904
  }
4803
4905
  };
4804
4906
  try {
4805
- out = fs12.openSync(logFile, "a");
4806
- err = fs12.openSync(logFile, "a");
4907
+ out = fs13.openSync(logFile, "a");
4908
+ err = fs13.openSync(logFile, "a");
4807
4909
  const foreground = buildGatewayForegroundSpawnCommand(input2.argv);
4808
4910
  const child = spawn(foreground.command, foreground.args, {
4809
4911
  detached: true,
@@ -4834,7 +4936,7 @@ async function startDetachedGateway(input2) {
4834
4936
  }
4835
4937
 
4836
4938
  // src/multimodal/openai-compatible.ts
4837
- import fs13 from "fs/promises";
4939
+ import fs14 from "fs/promises";
4838
4940
  function normalizeBaseUrl2(baseUrl) {
4839
4941
  return baseUrl.replace(/\/+$/, "");
4840
4942
  }
@@ -4844,6 +4946,7 @@ function buildPrompt(context) {
4844
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",
4845
4947
  '\u8BF7\u53EA\u8F93\u51FA JSON\uFF0C\u683C\u5F0F\u4E3A {"summary": string, "isMeaningful": boolean, "reason": string}\u3002',
4846
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",
4847
4950
  contextText ? `\u4E0A\u4E0B\u6587\uFF1A${contextText}` : void 0
4848
4951
  ].filter(Boolean).join("\n");
4849
4952
  }
@@ -4881,7 +4984,7 @@ var OpenAICompatibleMultimodalModel = class {
4881
4984
  if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
4882
4985
  throw new Error("\u591A\u6A21\u6001\u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
4883
4986
  }
4884
- const image = await fs13.readFile(input2.imagePath);
4987
+ const image = await fs14.readFile(input2.imagePath);
4885
4988
  const response = await fetch(`${normalizeBaseUrl2(this.options.baseUrl)}/chat/completions`, {
4886
4989
  method: "POST",
4887
4990
  headers: {
@@ -6415,7 +6518,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
6415
6518
  const config = await loadConfig();
6416
6519
  const database = openDatabase(config);
6417
6520
  try {
6418
- const raw = await fs14.readFile(options.file, "utf8");
6521
+ const raw = await fs15.readFile(options.file, "utf8");
6419
6522
  const payload = JSON.parse(raw);
6420
6523
  const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
6421
6524
  if (!result.accepted) {