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/index.d.ts CHANGED
@@ -134,6 +134,7 @@ interface IngestMessageInput {
134
134
  interface CreateImageSummaryMessageInput {
135
135
  sourceMessageId: string;
136
136
  imageKey: string;
137
+ imageFileName?: string;
137
138
  summary: string;
138
139
  reason?: string;
139
140
  multimodalModel: string;
@@ -237,6 +238,7 @@ interface CronJobRecord {
237
238
  createdByOpenId?: string;
238
239
  schedule: string;
239
240
  prompt: string;
241
+ imageFileName?: string;
240
242
  status: CronJobStatus;
241
243
  lastRunAt?: string;
242
244
  nextRunAt: string;
@@ -256,6 +258,7 @@ declare class CronJobRepository {
256
258
  createdByOpenId?: string;
257
259
  schedule: string;
258
260
  prompt: string;
261
+ imageFileName?: string;
259
262
  }): CronJobRecord;
260
263
  get(id: string): CronJobRecord | null;
261
264
  list(limit?: number): CronJobRecord[];
@@ -280,6 +283,7 @@ interface CreateCronJobSchedulerOptions {
280
283
  repository: Pick<CronJobRepository, "listDue" | "markSuccess" | "markFailure">;
281
284
  generateMessage: (job: CronJobRecord, now: Date) => Promise<string>;
282
285
  sendTextToChat: (chatId: string, text: string) => Promise<void>;
286
+ sendImageToChat?: (chatId: string, imageFileName: string) => Promise<void>;
283
287
  now?: () => Date;
284
288
  setIntervalFn?: typeof setInterval;
285
289
  clearIntervalFn?: typeof clearInterval;
@@ -594,6 +598,7 @@ interface IndexingScheduler {
594
598
 
595
599
  interface MessageSender {
596
600
  sendTextToChat(chatId: string, text: string): Promise<void>;
601
+ sendImageToChat?(chatId: string, imagePath: string): Promise<void>;
597
602
  replyTextToMessage?(messageId: string, text: string): Promise<void>;
598
603
  addReactionToMessage?(messageId: string, emojiType: string): Promise<void>;
599
604
  }
@@ -621,6 +626,14 @@ interface FeishuClientLike {
621
626
  };
622
627
  }) => Promise<unknown>;
623
628
  };
629
+ image?: {
630
+ create(payload: {
631
+ data: {
632
+ image_type: "message";
633
+ image: Buffer;
634
+ };
635
+ }): Promise<unknown>;
636
+ };
624
637
  messageReaction?: {
625
638
  create(payload: {
626
639
  path: {
@@ -663,6 +676,7 @@ declare class FeishuMessageSender implements MessageSender {
663
676
  constructor(client: FeishuClientLike);
664
677
  static fromConfig(config: AppConfig, secrets: AppSecrets): FeishuMessageSender;
665
678
  sendTextToChat(chatId: string, text: string): Promise<void>;
679
+ sendImageToChat(chatId: string, imagePath: string): Promise<void>;
666
680
  replyTextToMessage(messageId: string, text: string): Promise<void>;
667
681
  addReactionToMessage(messageId: string, emojiType: string): Promise<void>;
668
682
  }
@@ -731,7 +745,7 @@ interface FeishuGatewayOptions {
731
745
  cronJobProcessor?: {
732
746
  database: SqliteDatabase;
733
747
  model: ChatModel;
734
- sender: Pick<MessageSender, "sendTextToChat">;
748
+ sender: Pick<MessageSender, "sendTextToChat" | "sendImageToChat">;
735
749
  };
736
750
  cronJobScheduler?: CronJobScheduler;
737
751
  wsClientFactory?: (params: {
package/dist/index.js CHANGED
@@ -362,6 +362,7 @@ var CronJobRepository = class {
362
362
  create(input) {
363
363
  const schedule = input.schedule.trim();
364
364
  const prompt = input.prompt.trim();
365
+ const imageFileName = input.imageFileName?.trim();
365
366
  if (!isValidCronSchedule(schedule)) {
366
367
  throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
367
368
  }
@@ -379,6 +380,7 @@ var CronJobRepository = class {
379
380
  createdByOpenId: input.createdByOpenId,
380
381
  schedule,
381
382
  prompt,
383
+ ...imageFileName ? { imageFileName } : {},
382
384
  status: "active",
383
385
  nextRunAt: nextRunAt.toISOString(),
384
386
  createdAt: now.toISOString(),
@@ -387,15 +389,18 @@ var CronJobRepository = class {
387
389
  this.database.prepare(
388
390
  `
389
391
  INSERT INTO cron_jobs (
390
- id, chat_id, created_by_open_id, schedule, prompt, status,
392
+ id, chat_id, created_by_open_id, schedule, prompt, image_file_name, status,
391
393
  last_run_at, next_run_at, last_error, created_at, updated_at
392
394
  )
393
395
  VALUES (
394
- @id, @chatId, @createdByOpenId, @schedule, @prompt, @status,
396
+ @id, @chatId, @createdByOpenId, @schedule, @prompt, @imageFileName, @status,
395
397
  NULL, @nextRunAt, NULL, @createdAt, @updatedAt
396
398
  )
397
399
  `
398
- ).run(record);
400
+ ).run({
401
+ ...record,
402
+ imageFileName: record.imageFileName ?? null
403
+ });
399
404
  return record;
400
405
  }
401
406
  get(id) {
@@ -420,6 +425,7 @@ var CronJobRepository = class {
420
425
  created_by_open_id AS createdByOpenId,
421
426
  schedule,
422
427
  prompt,
428
+ image_file_name AS imageFileName,
423
429
  status,
424
430
  last_run_at AS lastRunAt,
425
431
  next_run_at AS nextRunAt,
@@ -438,6 +444,7 @@ var CronJobRepository = class {
438
444
  createdByOpenId: row.createdByOpenId ?? void 0,
439
445
  schedule: row.schedule,
440
446
  prompt: row.prompt,
447
+ imageFileName: row.imageFileName ?? void 0,
441
448
  status: row.status,
442
449
  lastRunAt: row.lastRunAt ?? void 0,
443
450
  nextRunAt: row.nextRunAt,
@@ -511,6 +518,7 @@ var CronJobRepository = class {
511
518
  created_by_open_id AS createdByOpenId,
512
519
  schedule,
513
520
  prompt,
521
+ image_file_name AS imageFileName,
514
522
  status,
515
523
  last_run_at AS lastRunAt,
516
524
  next_run_at AS nextRunAt,
@@ -529,6 +537,7 @@ var CronJobRepository = class {
529
537
  createdByOpenId: row.createdByOpenId ?? void 0,
530
538
  schedule: row.schedule,
531
539
  prompt: row.prompt,
540
+ imageFileName: row.imageFileName ?? void 0,
532
541
  status: row.status,
533
542
  lastRunAt: row.lastRunAt ?? void 0,
534
543
  nextRunAt: row.nextRunAt,
@@ -559,6 +568,12 @@ function createCronJobScheduler(options) {
559
568
  try {
560
569
  const text = await options.generateMessage(job, startedAt);
561
570
  await options.sendTextToChat(job.chatId, text);
571
+ if (job.imageFileName) {
572
+ if (!options.sendImageToChat) {
573
+ throw new Error("\u5F53\u524D\u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\u3002");
574
+ }
575
+ await options.sendImageToChat(job.chatId, job.imageFileName);
576
+ }
562
577
  options.repository.markSuccess(job.id, startedAt);
563
578
  } catch (error) {
564
579
  const message = error instanceof Error ? error.message : String(error);
@@ -599,6 +614,17 @@ function readString(input, key) {
599
614
  }
600
615
  return value.trim();
601
616
  }
617
+ function readOptionalString(input, key) {
618
+ const value = typeof input === "object" && input !== null && key in input ? input[key] : void 0;
619
+ if (value === void 0 || value === null) {
620
+ return void 0;
621
+ }
622
+ if (typeof value !== "string") {
623
+ throw new Error(`${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
624
+ }
625
+ const trimmed = value.trim();
626
+ return trimmed || void 0;
627
+ }
602
628
  function createCronJobTools(input) {
603
629
  return [
604
630
  {
@@ -614,6 +640,10 @@ function createCronJobTools(input) {
614
640
  prompt: {
615
641
  type: "string",
616
642
  description: "Prompt used later to generate the scheduled message."
643
+ },
644
+ imageFileName: {
645
+ type: "string",
646
+ description: "Optional image filename already stored from the current chat, for example om_xxx-image.jpg."
617
647
  }
618
648
  },
619
649
  required: ["schedule", "prompt"],
@@ -624,7 +654,8 @@ function createCronJobTools(input) {
624
654
  chatId: input.chatId,
625
655
  createdByOpenId: input.createdByOpenId,
626
656
  schedule: readString(rawInput, "schedule"),
627
- prompt: readString(rawInput, "prompt")
657
+ prompt: readString(rawInput, "prompt"),
658
+ imageFileName: readOptionalString(rawInput, "imageFileName")
628
659
  });
629
660
  return JSON.stringify({ ok: true, job });
630
661
  }
@@ -954,12 +985,17 @@ function migrateDatabase(database) {
954
985
  next_run_at TEXT NOT NULL,
955
986
  last_error TEXT,
956
987
  created_at TEXT NOT NULL,
957
- updated_at TEXT NOT NULL
988
+ updated_at TEXT NOT NULL,
989
+ image_file_name TEXT
958
990
  );
959
991
 
960
992
  CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
961
993
  CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
962
994
  `);
995
+ const cronJobColumns = database.prepare("PRAGMA table_info(cron_jobs)").all();
996
+ if (!cronJobColumns.some((column) => column.name === "image_file_name")) {
997
+ database.prepare("ALTER TABLE cron_jobs ADD COLUMN image_file_name TEXT").run();
998
+ }
963
999
  }
964
1000
 
965
1001
  // src/doctor/checks.ts
@@ -1710,6 +1746,9 @@ var MessageRepository = class {
1710
1746
  throw new Error("\u539F\u59CB\u56FE\u7247\u6D88\u606F\u4E0D\u5B58\u5728\u3002");
1711
1747
  }
1712
1748
  const derivedPlatformMessageId = `${source.platformMessageId}:image-summary:${input.imageKey}`;
1749
+ const imageFileName = input.imageFileName?.trim();
1750
+ const summaryText = imageFileName ? `[\u56FE\u7247\u8F6C\u8FF0] \u6587\u4EF6\u540D\uFF1A${imageFileName}
1751
+ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
1713
1752
  return this.ingest({
1714
1753
  platform: source.platform,
1715
1754
  platformChatId: source.platformChatId,
@@ -1718,12 +1757,13 @@ var MessageRepository = class {
1718
1757
  senderId: source.senderId,
1719
1758
  senderName: source.senderName,
1720
1759
  messageType: "image_summary",
1721
- text: `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`,
1760
+ text: summaryText,
1722
1761
  sentAt: source.sentAt,
1723
1762
  rawPayload: {
1724
1763
  derivedFromMessageId: input.sourceMessageId,
1725
1764
  sourceAttachmentKind: "image",
1726
1765
  sourceResourceKey: input.imageKey,
1766
+ ...imageFileName ? { imageFileName } : {},
1727
1767
  multimodalModel: input.multimodalModel,
1728
1768
  isMeaningful: true,
1729
1769
  ...input.reason?.trim() ? { reason: input.reason.trim() } : {},
@@ -3024,7 +3064,7 @@ async function summarizeEpisodeWindow(window, model, now) {
3024
3064
  const summary = await model.complete([
3025
3065
  {
3026
3066
  role: "system",
3027
- 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"
3067
+ 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"
3028
3068
  },
3029
3069
  {
3030
3070
  role: "user",
@@ -3055,6 +3095,7 @@ async function processEpisodesNow(input) {
3055
3095
 
3056
3096
  // src/feishu/gateway.ts
3057
3097
  import * as lark2 from "@larksuiteoapi/node-sdk";
3098
+ import path12 from "path";
3058
3099
 
3059
3100
  // src/gateway/indexing-scheduler.ts
3060
3101
  function createIndexingScheduler(options) {
@@ -3440,6 +3481,7 @@ var ImageMultimodalTaskRepository = class {
3440
3481
  };
3441
3482
 
3442
3483
  // src/multimodal/worker.ts
3484
+ import path11 from "path";
3443
3485
  var ImageMultimodalWorker = class {
3444
3486
  constructor(options) {
3445
3487
  this.options = options;
@@ -3466,9 +3508,11 @@ var ImageMultimodalWorker = class {
3466
3508
  throw error;
3467
3509
  }
3468
3510
  try {
3511
+ const imageFileName = path11.basename(running.storedPath);
3469
3512
  const described = await this.options.model.describeImage({
3470
3513
  imagePath: running.storedPath,
3471
- mimeType: running.mimeType
3514
+ mimeType: running.mimeType,
3515
+ context: `\u56FE\u7247\u6587\u4EF6\u540D\uFF1A${imageFileName}`
3472
3516
  });
3473
3517
  if (!described.isMeaningful) {
3474
3518
  this.options.tasks.markSkipped(running.id, described.reason || "\u591A\u6A21\u6001\u6A21\u578B\u5224\u5B9A\u56FE\u7247\u65E0\u610F\u4E49\u3002");
@@ -3478,6 +3522,7 @@ var ImageMultimodalWorker = class {
3478
3522
  const derivedMessageId = this.options.messages.createImageSummaryMessage({
3479
3523
  sourceMessageId: running.sourceMessageId,
3480
3524
  imageKey: running.imageKey,
3525
+ imageFileName,
3481
3526
  summary: described.summary,
3482
3527
  reason: described.reason,
3483
3528
  multimodalModel: this.options.multimodalModelName,
@@ -3628,7 +3673,7 @@ function stripMentions(text, mentions) {
3628
3673
  }
3629
3674
  return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
3630
3675
  }
3631
- 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`;
3676
+ 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`;
3632
3677
  var DEFAULT_MAX_MODEL_TURNS = 4;
3633
3678
  var DEFAULT_MAX_TOOL_CALLS = 8;
3634
3679
  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";
@@ -3867,9 +3912,22 @@ var FeishuQuestionHandler = class {
3867
3912
 
3868
3913
  // src/feishu/sender.ts
3869
3914
  import * as lark from "@larksuiteoapi/node-sdk";
3915
+ import fs9 from "fs/promises";
3870
3916
  function mapDomain(domain) {
3871
3917
  return domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu;
3872
3918
  }
3919
+ function extractImageKey(response) {
3920
+ const data = response && typeof response === "object" ? response : {};
3921
+ const direct = data.image_key;
3922
+ if (typeof direct === "string" && direct.trim()) {
3923
+ return direct.trim();
3924
+ }
3925
+ const nested = data.data && typeof data.data === "object" ? data.data.image_key : void 0;
3926
+ if (typeof nested === "string" && nested.trim()) {
3927
+ return nested.trim();
3928
+ }
3929
+ throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
3930
+ }
3873
3931
  var FeishuMessageSender = class _FeishuMessageSender {
3874
3932
  constructor(client) {
3875
3933
  this.client = client;
@@ -3906,6 +3964,39 @@ var FeishuMessageSender = class _FeishuMessageSender {
3906
3964
  throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
3907
3965
  }
3908
3966
  }
3967
+ async sendImageToChat(chatId, imagePath) {
3968
+ const imageCreate = this.client.im.v1?.image?.create;
3969
+ if (!imageCreate) {
3970
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u56FE\u7247\u4E0A\u4F20\u63A5\u53E3\u3002");
3971
+ }
3972
+ const image = await fs9.readFile(imagePath);
3973
+ const uploaded = await imageCreate({
3974
+ data: {
3975
+ image_type: "message",
3976
+ image
3977
+ }
3978
+ });
3979
+ const imageKey = extractImageKey(uploaded);
3980
+ const payload = {
3981
+ data: {
3982
+ receive_id: chatId,
3983
+ msg_type: "image",
3984
+ content: JSON.stringify({ image_key: imageKey })
3985
+ },
3986
+ params: {
3987
+ receive_id_type: "chat_id"
3988
+ }
3989
+ };
3990
+ if (this.client.im.v1?.message.create) {
3991
+ await this.client.im.v1.message.create(payload);
3992
+ return;
3993
+ }
3994
+ if (this.client.im.message?.create) {
3995
+ await this.client.im.message.create(payload);
3996
+ return;
3997
+ }
3998
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
3999
+ }
3909
4000
  async replyTextToMessage(messageId, text) {
3910
4001
  const payload = {
3911
4002
  path: {
@@ -4046,6 +4137,13 @@ function createFeishuEventDispatcher(options) {
4046
4137
  }
4047
4138
  });
4048
4139
  }
4140
+ function resolveFeishuImagePath(config, imageFileName) {
4141
+ const fileName = path12.basename(imageFileName.trim());
4142
+ if (!fileName || fileName !== imageFileName.trim()) {
4143
+ throw new Error("\u56FE\u7247\u6587\u4EF6\u540D\u65E0\u6548\u3002");
4144
+ }
4145
+ return path12.join(resolveHomePath(config.storage.dataDir), "files", "feishu", fileName);
4146
+ }
4049
4147
  function createFeishuGateway(options) {
4050
4148
  assertFeishuConfig(options.config, options.secrets);
4051
4149
  const wsClient = options.wsClientFactory?.({
@@ -4091,6 +4189,10 @@ function createFeishuGateway(options) {
4091
4189
  const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4092
4190
  repository: new CronJobRepository(options.cronJobProcessor.database),
4093
4191
  sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
4192
+ sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
4193
+ chatId,
4194
+ resolveFeishuImagePath(options.config, imageFileName)
4195
+ ) : void 0,
4094
4196
  generateMessage: async (job, now) => {
4095
4197
  const { tools, close } = await createAgenticRagSearchTools({
4096
4198
  config: options.config,
@@ -4278,8 +4380,8 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4278
4380
 
4279
4381
  // src/feishu/resource-downloader.ts
4280
4382
  import * as lark3 from "@larksuiteoapi/node-sdk";
4281
- import fs9 from "fs/promises";
4282
- import path11 from "path";
4383
+ import fs10 from "fs/promises";
4384
+ import path13 from "path";
4283
4385
  var RESOURCE_TYPE_BY_KIND = {
4284
4386
  file: "file",
4285
4387
  image: "image",
@@ -4317,10 +4419,10 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4317
4419
  }
4318
4420
  async download(input) {
4319
4421
  const resourceType = RESOURCE_TYPE_BY_KIND[input.attachment.kind];
4320
- const targetDir = path11.join(this.dataDir, "files", "feishu");
4321
- await fs9.mkdir(targetDir, { recursive: true });
4422
+ const targetDir = path13.join(this.dataDir, "files", "feishu");
4423
+ await fs10.mkdir(targetDir, { recursive: true });
4322
4424
  const fileName = buildStoredFileName(input);
4323
- const storedPath = path11.join(targetDir, fileName);
4425
+ const storedPath = path13.join(targetDir, fileName);
4324
4426
  const payload = {
4325
4427
  params: { type: resourceType },
4326
4428
  path: { message_id: input.messageId, file_key: input.attachment.fileKey }
@@ -4343,29 +4445,29 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4343
4445
 
4344
4446
  // src/files/ingest.ts
4345
4447
  import crypto7 from "crypto";
4346
- import fs11 from "fs/promises";
4347
- import path13 from "path";
4448
+ import fs12 from "fs/promises";
4449
+ import path15 from "path";
4348
4450
 
4349
4451
  // src/files/parser.ts
4350
- import fs10 from "fs/promises";
4351
- import path12 from "path";
4452
+ import fs11 from "fs/promises";
4453
+ import path14 from "path";
4352
4454
  import mammoth from "mammoth";
4353
4455
  import { PDFParse } from "pdf-parse";
4354
4456
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".txt", ".md", ".markdown", ".json", ".csv", ".tsv", ".log"]);
4355
4457
  var DOCX_EXTENSIONS = /* @__PURE__ */ new Set([".docx"]);
4356
4458
  var PDF_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
4357
4459
  function isSupportedParseFile(filePath) {
4358
- const extension = path12.extname(filePath).toLowerCase();
4460
+ const extension = path14.extname(filePath).toLowerCase();
4359
4461
  return TEXT_EXTENSIONS.has(extension) || DOCX_EXTENSIONS.has(extension) || PDF_EXTENSIONS.has(extension);
4360
4462
  }
4361
4463
  function describeSupportedParseTypes() {
4362
4464
  return "txt\u3001md\u3001json\u3001csv\u3001tsv\u3001log\u3001docx\u3001pdf";
4363
4465
  }
4364
4466
  async function parseFileToText(filePath) {
4365
- const extension = path12.extname(filePath).toLowerCase();
4467
+ const extension = path14.extname(filePath).toLowerCase();
4366
4468
  if (TEXT_EXTENSIONS.has(extension)) {
4367
4469
  return {
4368
- text: await fs10.readFile(filePath, "utf8"),
4470
+ text: await fs11.readFile(filePath, "utf8"),
4369
4471
  parser: "text",
4370
4472
  warnings: []
4371
4473
  };
@@ -4379,7 +4481,7 @@ async function parseFileToText(filePath) {
4379
4481
  };
4380
4482
  }
4381
4483
  if (PDF_EXTENSIONS.has(extension)) {
4382
- const buffer = await fs10.readFile(filePath);
4484
+ const buffer = await fs11.readFile(filePath);
4383
4485
  const parser = new PDFParse({ data: buffer });
4384
4486
  try {
4385
4487
  const result = await parser.getText();
@@ -4401,7 +4503,7 @@ function isSupportedTextFile(filePath) {
4401
4503
  }
4402
4504
  function ensureSupportedTextFile(filePath) {
4403
4505
  if (!isSupportedTextFile(filePath)) {
4404
- const extension = path13.extname(filePath).toLowerCase();
4506
+ const extension = path15.extname(filePath).toLowerCase();
4405
4507
  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`);
4406
4508
  }
4407
4509
  }
@@ -4410,12 +4512,12 @@ function stableStoredName(sourcePath, fileName) {
4410
4512
  return `${digest}-${fileName}`;
4411
4513
  }
4412
4514
  async function ingestLocalFile(input) {
4413
- const sourcePath = path13.resolve(input.filePath);
4414
- const fileName = path13.basename(sourcePath);
4515
+ const sourcePath = path15.resolve(input.filePath);
4516
+ const fileName = path15.basename(sourcePath);
4415
4517
  const jobId = input.jobs?.start({ sourcePath, fileName });
4416
4518
  try {
4417
4519
  ensureSupportedTextFile(sourcePath);
4418
- const stat = await fs11.stat(sourcePath);
4520
+ const stat = await fs12.stat(sourcePath);
4419
4521
  if (!stat.isFile()) {
4420
4522
  throw new Error(`\u4E0D\u662F\u6587\u4EF6\uFF1A${sourcePath}`);
4421
4523
  }
@@ -4424,10 +4526,10 @@ async function ingestLocalFile(input) {
4424
4526
  if (!text) {
4425
4527
  throw new Error(`\u6587\u4EF6\u6CA1\u6709\u53EF\u7D22\u5F15\u6587\u672C\uFF1A${sourcePath}`);
4426
4528
  }
4427
- const fileDir = path13.join(resolveHomePath(input.config.storage.dataDir), "files");
4428
- await fs11.mkdir(fileDir, { recursive: true });
4429
- const storedPath = path13.join(fileDir, stableStoredName(sourcePath, fileName));
4430
- await fs11.copyFile(sourcePath, storedPath);
4529
+ const fileDir = path15.join(resolveHomePath(input.config.storage.dataDir), "files");
4530
+ await fs12.mkdir(fileDir, { recursive: true });
4531
+ const storedPath = path15.join(fileDir, stableStoredName(sourcePath, fileName));
4532
+ await fs12.copyFile(sourcePath, storedPath);
4431
4533
  const messageId = input.messages.ingest({
4432
4534
  platform: "local-file",
4433
4535
  platformChatId: "local-files",