chattercatcher 0.1.26 → 0.1.28

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: {
@@ -910,6 +924,7 @@ declare class OpenAICompatibleEmbeddingModel implements EmbeddingModel {
910
924
  constructor(options: OpenAICompatibleEmbeddingOptions);
911
925
  embed(text: string): Promise<number[]>;
912
926
  embedBatch(texts: string[]): Promise<number[][]>;
927
+ private fetchEmbeddingBatch;
913
928
  }
914
929
  declare function createChatModel(config: AppConfig, secrets: AppSecrets): OpenAICompatibleChatModel;
915
930
  declare function createEmbeddingModel(config: AppConfig, secrets: AppSecrets): OpenAICompatibleEmbeddingModel;
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
@@ -1372,6 +1408,7 @@ function getGatewayStatus(config, secrets) {
1372
1408
  }
1373
1409
 
1374
1410
  // src/llm/openai-compatible.ts
1411
+ var OPENAI_EMBEDDING_BATCH_SIZE = 64;
1375
1412
  function normalizeBaseUrl(baseUrl) {
1376
1413
  return baseUrl.replace(/\/+$/, "");
1377
1414
  }
@@ -1403,12 +1440,66 @@ function toOpenAITool(tool) {
1403
1440
  }
1404
1441
  };
1405
1442
  }
1443
+ function parseToolCallArguments(value) {
1444
+ try {
1445
+ return JSON.parse(value);
1446
+ } catch {
1447
+ return {};
1448
+ }
1449
+ }
1450
+ function decodeDsmlValue(value, isString) {
1451
+ const trimmed = value.trim();
1452
+ if (isString) {
1453
+ return trimmed;
1454
+ }
1455
+ if (trimmed === "true") return true;
1456
+ if (trimmed === "false") return false;
1457
+ if (trimmed === "null") return null;
1458
+ const numberValue = Number(trimmed);
1459
+ if (trimmed && Number.isFinite(numberValue)) {
1460
+ return numberValue;
1461
+ }
1462
+ return trimmed;
1463
+ }
1464
+ function parseDsmlToolCalls(content) {
1465
+ if (!content?.includes("DSML")) {
1466
+ return [];
1467
+ }
1468
+ const toolCalls = [];
1469
+ const invokePattern = /<||DSML||invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/||DSML||invoke>/g;
1470
+ const parameterPattern = /<||DSML||parameter\s+name="([^"]+)"\s+string="(true|false)"\s*>([\s\S]*?)<\/||DSML||parameter>/g;
1471
+ for (const invoke of content.matchAll(invokePattern)) {
1472
+ const name = invoke[1];
1473
+ if (!name) {
1474
+ continue;
1475
+ }
1476
+ const input = {};
1477
+ const body = invoke[2] ?? "";
1478
+ for (const parameter of body.matchAll(parameterPattern)) {
1479
+ const parameterName = parameter[1];
1480
+ if (!parameterName) {
1481
+ continue;
1482
+ }
1483
+ input[parameterName] = decodeDsmlValue(parameter[3] ?? "", parameter[2] === "true");
1484
+ }
1485
+ toolCalls.push({
1486
+ id: `dsml_${toolCalls.length + 1}`,
1487
+ name,
1488
+ input
1489
+ });
1490
+ }
1491
+ return toolCalls;
1492
+ }
1406
1493
  function parseToolCalls(message) {
1407
- return message?.tool_calls?.map((toolCall) => ({
1494
+ const standardToolCalls = message?.tool_calls?.map((toolCall) => ({
1408
1495
  id: toolCall.id,
1409
1496
  name: toolCall.function.name,
1410
- input: JSON.parse(toolCall.function.arguments)
1497
+ input: parseToolCallArguments(toolCall.function.arguments)
1411
1498
  })) ?? [];
1499
+ return standardToolCalls.length > 0 ? standardToolCalls : parseDsmlToolCalls(message?.content);
1500
+ }
1501
+ function isDsmlToolCallContent(content) {
1502
+ return parseDsmlToolCalls(content).length > 0;
1412
1503
  }
1413
1504
  var OpenAICompatibleChatModel = class {
1414
1505
  constructor(options) {
@@ -1467,9 +1558,10 @@ var OpenAICompatibleChatModel = class {
1467
1558
  }
1468
1559
  const data = await response.json();
1469
1560
  const message = data.choices?.[0]?.message;
1561
+ const toolCalls = parseToolCalls(message);
1470
1562
  return {
1471
- content: message?.content ?? "",
1472
- toolCalls: parseToolCalls(message),
1563
+ content: toolCalls.length > 0 && isDsmlToolCallContent(message?.content) ? "" : message?.content ?? "",
1564
+ toolCalls,
1473
1565
  reasoningContent: message?.reasoning_content ?? void 0
1474
1566
  };
1475
1567
  }
@@ -1487,6 +1579,13 @@ var OpenAICompatibleEmbeddingModel = class {
1487
1579
  if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
1488
1580
  throw new Error("Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
1489
1581
  }
1582
+ const vectors = [];
1583
+ for (let index = 0; index < texts.length; index += OPENAI_EMBEDDING_BATCH_SIZE) {
1584
+ vectors.push(...await this.fetchEmbeddingBatch(texts.slice(index, index + OPENAI_EMBEDDING_BATCH_SIZE)));
1585
+ }
1586
+ return vectors;
1587
+ }
1588
+ async fetchEmbeddingBatch(texts) {
1490
1589
  const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/embeddings`, {
1491
1590
  method: "POST",
1492
1591
  headers: {
@@ -1710,6 +1809,9 @@ var MessageRepository = class {
1710
1809
  throw new Error("\u539F\u59CB\u56FE\u7247\u6D88\u606F\u4E0D\u5B58\u5728\u3002");
1711
1810
  }
1712
1811
  const derivedPlatformMessageId = `${source.platformMessageId}:image-summary:${input.imageKey}`;
1812
+ const imageFileName = input.imageFileName?.trim();
1813
+ const summaryText = imageFileName ? `[\u56FE\u7247\u8F6C\u8FF0] \u6587\u4EF6\u540D\uFF1A${imageFileName}
1814
+ ${input.summary.trim()}` : `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`;
1713
1815
  return this.ingest({
1714
1816
  platform: source.platform,
1715
1817
  platformChatId: source.platformChatId,
@@ -1718,12 +1820,13 @@ var MessageRepository = class {
1718
1820
  senderId: source.senderId,
1719
1821
  senderName: source.senderName,
1720
1822
  messageType: "image_summary",
1721
- text: `[\u56FE\u7247\u8F6C\u8FF0] ${input.summary.trim()}`,
1823
+ text: summaryText,
1722
1824
  sentAt: source.sentAt,
1723
1825
  rawPayload: {
1724
1826
  derivedFromMessageId: input.sourceMessageId,
1725
1827
  sourceAttachmentKind: "image",
1726
1828
  sourceResourceKey: input.imageKey,
1829
+ ...imageFileName ? { imageFileName } : {},
1727
1830
  multimodalModel: input.multimodalModel,
1728
1831
  isMeaningful: true,
1729
1832
  ...input.reason?.trim() ? { reason: input.reason.trim() } : {},
@@ -3024,7 +3127,7 @@ async function summarizeEpisodeWindow(window, model, now) {
3024
3127
  const summary = await model.complete([
3025
3128
  {
3026
3129
  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"
3130
+ 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
3131
  },
3029
3132
  {
3030
3133
  role: "user",
@@ -3055,6 +3158,7 @@ async function processEpisodesNow(input) {
3055
3158
 
3056
3159
  // src/feishu/gateway.ts
3057
3160
  import * as lark2 from "@larksuiteoapi/node-sdk";
3161
+ import path12 from "path";
3058
3162
 
3059
3163
  // src/gateway/indexing-scheduler.ts
3060
3164
  function createIndexingScheduler(options) {
@@ -3164,12 +3268,20 @@ function parseExactNumber2(field, min, max) {
3164
3268
  }
3165
3269
 
3166
3270
  // src/rag/indexer.ts
3271
+ var EMBEDDING_INDEX_BATCH_SIZE = 64;
3167
3272
  async function indexMessageChunks(input) {
3168
3273
  const chunks = input.messageIds ? input.messages.listMessageChunksByMessageIds(input.messageIds, input.limit ?? 1e4) : input.messages.listAllMessageChunks(input.limit ?? 1e4);
3169
3274
  if (chunks.length === 0) {
3170
3275
  return { chunks: 0, vectors: 0 };
3171
3276
  }
3172
- const vectors = await input.embedding.embedBatch(chunks.map((chunk) => chunk.text));
3277
+ const vectors = [];
3278
+ for (let index = 0; index < chunks.length; index += EMBEDDING_INDEX_BATCH_SIZE) {
3279
+ vectors.push(
3280
+ ...await input.embedding.embedBatch(
3281
+ chunks.slice(index, index + EMBEDDING_INDEX_BATCH_SIZE).map((chunk) => chunk.text)
3282
+ )
3283
+ );
3284
+ }
3173
3285
  const records = [];
3174
3286
  for (const [index, chunk] of chunks.entries()) {
3175
3287
  const vector = vectors[index];
@@ -3440,6 +3552,7 @@ var ImageMultimodalTaskRepository = class {
3440
3552
  };
3441
3553
 
3442
3554
  // src/multimodal/worker.ts
3555
+ import path11 from "path";
3443
3556
  var ImageMultimodalWorker = class {
3444
3557
  constructor(options) {
3445
3558
  this.options = options;
@@ -3466,9 +3579,11 @@ var ImageMultimodalWorker = class {
3466
3579
  throw error;
3467
3580
  }
3468
3581
  try {
3582
+ const imageFileName = path11.basename(running.storedPath);
3469
3583
  const described = await this.options.model.describeImage({
3470
3584
  imagePath: running.storedPath,
3471
- mimeType: running.mimeType
3585
+ mimeType: running.mimeType,
3586
+ context: `\u56FE\u7247\u6587\u4EF6\u540D\uFF1A${imageFileName}`
3472
3587
  });
3473
3588
  if (!described.isMeaningful) {
3474
3589
  this.options.tasks.markSkipped(running.id, described.reason || "\u591A\u6A21\u6001\u6A21\u578B\u5224\u5B9A\u56FE\u7247\u65E0\u610F\u4E49\u3002");
@@ -3478,6 +3593,7 @@ var ImageMultimodalWorker = class {
3478
3593
  const derivedMessageId = this.options.messages.createImageSummaryMessage({
3479
3594
  sourceMessageId: running.sourceMessageId,
3480
3595
  imageKey: running.imageKey,
3596
+ imageFileName,
3481
3597
  summary: described.summary,
3482
3598
  reason: described.reason,
3483
3599
  multimodalModel: this.options.multimodalModelName,
@@ -3628,11 +3744,14 @@ function stripMentions(text, mentions) {
3628
3744
  }
3629
3745
  return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
3630
3746
  }
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`;
3747
+ 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
3748
  var DEFAULT_MAX_MODEL_TURNS = 4;
3633
3749
  var DEFAULT_MAX_TOOL_CALLS = 8;
3634
3750
  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";
3635
3751
  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";
3752
+ function containsRawToolCallMarkup(content) {
3753
+ return /<||DSML||tool_calls>|<||DSML||invoke\s+name=|<tool_call>|<tool_calls>/i.test(content);
3754
+ }
3636
3755
  function toToolResultContent(value) {
3637
3756
  if (typeof value === "string") return value;
3638
3757
  return JSON.stringify(value);
@@ -3675,6 +3794,7 @@ async function runFeishuToolLoop(input) {
3675
3794
  let toolCallsUsed = 0;
3676
3795
  for (let turn = 0; turn < maxModelTurns; turn += 1) {
3677
3796
  const assistantResult = await input.model.completeWithTools(messages, input.tools);
3797
+ const hasRawToolCallMarkup = containsRawToolCallMarkup(assistantResult.content);
3678
3798
  messages.push({
3679
3799
  role: "assistant",
3680
3800
  content: assistantResult.content,
@@ -3682,6 +3802,9 @@ async function runFeishuToolLoop(input) {
3682
3802
  reasoningContent: assistantResult.reasoningContent
3683
3803
  });
3684
3804
  if (assistantResult.toolCalls.length === 0) {
3805
+ if (hasRawToolCallMarkup) {
3806
+ break;
3807
+ }
3685
3808
  return assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
3686
3809
  }
3687
3810
  for (const toolCall of assistantResult.toolCalls) {
@@ -3794,7 +3917,7 @@ var FeishuQuestionHandler = class {
3794
3917
  }
3795
3918
  if (this.options.sender.addReactionToMessage) {
3796
3919
  try {
3797
- await this.options.sender.addReactionToMessage(messageId, this.options.thinkingEmojiType ?? "keyboard");
3920
+ await this.options.sender.addReactionToMessage(messageId, this.options.thinkingEmojiType ?? "OK");
3798
3921
  return;
3799
3922
  } catch (error) {
3800
3923
  console.log(`\u98DE\u4E66\u63D0\u95EE\u8868\u60C5\u53CD\u9988\u5931\u8D25\uFF0C\u6539\u7528\u6587\u5B57\u53CD\u9988\uFF1A${error instanceof Error ? error.message : String(error)}`);
@@ -3867,9 +3990,22 @@ var FeishuQuestionHandler = class {
3867
3990
 
3868
3991
  // src/feishu/sender.ts
3869
3992
  import * as lark from "@larksuiteoapi/node-sdk";
3993
+ import fs9 from "fs/promises";
3870
3994
  function mapDomain(domain) {
3871
3995
  return domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu;
3872
3996
  }
3997
+ function extractImageKey(response) {
3998
+ const data = response && typeof response === "object" ? response : {};
3999
+ const direct = data.image_key;
4000
+ if (typeof direct === "string" && direct.trim()) {
4001
+ return direct.trim();
4002
+ }
4003
+ const nested = data.data && typeof data.data === "object" ? data.data.image_key : void 0;
4004
+ if (typeof nested === "string" && nested.trim()) {
4005
+ return nested.trim();
4006
+ }
4007
+ throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
4008
+ }
3873
4009
  var FeishuMessageSender = class _FeishuMessageSender {
3874
4010
  constructor(client) {
3875
4011
  this.client = client;
@@ -3906,6 +4042,39 @@ var FeishuMessageSender = class _FeishuMessageSender {
3906
4042
  throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
3907
4043
  }
3908
4044
  }
4045
+ async sendImageToChat(chatId, imagePath) {
4046
+ const imageCreate = this.client.im.v1?.image?.create;
4047
+ if (!imageCreate) {
4048
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u56FE\u7247\u4E0A\u4F20\u63A5\u53E3\u3002");
4049
+ }
4050
+ const image = await fs9.readFile(imagePath);
4051
+ const uploaded = await imageCreate({
4052
+ data: {
4053
+ image_type: "message",
4054
+ image
4055
+ }
4056
+ });
4057
+ const imageKey = extractImageKey(uploaded);
4058
+ const payload = {
4059
+ data: {
4060
+ receive_id: chatId,
4061
+ msg_type: "image",
4062
+ content: JSON.stringify({ image_key: imageKey })
4063
+ },
4064
+ params: {
4065
+ receive_id_type: "chat_id"
4066
+ }
4067
+ };
4068
+ if (this.client.im.v1?.message.create) {
4069
+ await this.client.im.v1.message.create(payload);
4070
+ return;
4071
+ }
4072
+ if (this.client.im.message?.create) {
4073
+ await this.client.im.message.create(payload);
4074
+ return;
4075
+ }
4076
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
4077
+ }
3909
4078
  async replyTextToMessage(messageId, text) {
3910
4079
  const payload = {
3911
4080
  path: {
@@ -4046,6 +4215,13 @@ function createFeishuEventDispatcher(options) {
4046
4215
  }
4047
4216
  });
4048
4217
  }
4218
+ function resolveFeishuImagePath(config, imageFileName) {
4219
+ const fileName = path12.basename(imageFileName.trim());
4220
+ if (!fileName || fileName !== imageFileName.trim()) {
4221
+ throw new Error("\u56FE\u7247\u6587\u4EF6\u540D\u65E0\u6548\u3002");
4222
+ }
4223
+ return path12.join(resolveHomePath(config.storage.dataDir), "files", "feishu", fileName);
4224
+ }
4049
4225
  function createFeishuGateway(options) {
4050
4226
  assertFeishuConfig(options.config, options.secrets);
4051
4227
  const wsClient = options.wsClientFactory?.({
@@ -4091,6 +4267,10 @@ function createFeishuGateway(options) {
4091
4267
  const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4092
4268
  repository: new CronJobRepository(options.cronJobProcessor.database),
4093
4269
  sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
4270
+ sendImageToChat: options.cronJobProcessor.sender.sendImageToChat ? (chatId, imageFileName) => options.cronJobProcessor.sender.sendImageToChat(
4271
+ chatId,
4272
+ resolveFeishuImagePath(options.config, imageFileName)
4273
+ ) : void 0,
4094
4274
  generateMessage: async (job, now) => {
4095
4275
  const { tools, close } = await createAgenticRagSearchTools({
4096
4276
  config: options.config,
@@ -4278,8 +4458,8 @@ function normalizeFeishuReceiveMessageEvent(payload) {
4278
4458
 
4279
4459
  // src/feishu/resource-downloader.ts
4280
4460
  import * as lark3 from "@larksuiteoapi/node-sdk";
4281
- import fs9 from "fs/promises";
4282
- import path11 from "path";
4461
+ import fs10 from "fs/promises";
4462
+ import path13 from "path";
4283
4463
  var RESOURCE_TYPE_BY_KIND = {
4284
4464
  file: "file",
4285
4465
  image: "image",
@@ -4317,10 +4497,10 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4317
4497
  }
4318
4498
  async download(input) {
4319
4499
  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 });
4500
+ const targetDir = path13.join(this.dataDir, "files", "feishu");
4501
+ await fs10.mkdir(targetDir, { recursive: true });
4322
4502
  const fileName = buildStoredFileName(input);
4323
- const storedPath = path11.join(targetDir, fileName);
4503
+ const storedPath = path13.join(targetDir, fileName);
4324
4504
  const payload = {
4325
4505
  params: { type: resourceType },
4326
4506
  path: { message_id: input.messageId, file_key: input.attachment.fileKey }
@@ -4343,29 +4523,29 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
4343
4523
 
4344
4524
  // src/files/ingest.ts
4345
4525
  import crypto7 from "crypto";
4346
- import fs11 from "fs/promises";
4347
- import path13 from "path";
4526
+ import fs12 from "fs/promises";
4527
+ import path15 from "path";
4348
4528
 
4349
4529
  // src/files/parser.ts
4350
- import fs10 from "fs/promises";
4351
- import path12 from "path";
4530
+ import fs11 from "fs/promises";
4531
+ import path14 from "path";
4352
4532
  import mammoth from "mammoth";
4353
4533
  import { PDFParse } from "pdf-parse";
4354
4534
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".txt", ".md", ".markdown", ".json", ".csv", ".tsv", ".log"]);
4355
4535
  var DOCX_EXTENSIONS = /* @__PURE__ */ new Set([".docx"]);
4356
4536
  var PDF_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
4357
4537
  function isSupportedParseFile(filePath) {
4358
- const extension = path12.extname(filePath).toLowerCase();
4538
+ const extension = path14.extname(filePath).toLowerCase();
4359
4539
  return TEXT_EXTENSIONS.has(extension) || DOCX_EXTENSIONS.has(extension) || PDF_EXTENSIONS.has(extension);
4360
4540
  }
4361
4541
  function describeSupportedParseTypes() {
4362
4542
  return "txt\u3001md\u3001json\u3001csv\u3001tsv\u3001log\u3001docx\u3001pdf";
4363
4543
  }
4364
4544
  async function parseFileToText(filePath) {
4365
- const extension = path12.extname(filePath).toLowerCase();
4545
+ const extension = path14.extname(filePath).toLowerCase();
4366
4546
  if (TEXT_EXTENSIONS.has(extension)) {
4367
4547
  return {
4368
- text: await fs10.readFile(filePath, "utf8"),
4548
+ text: await fs11.readFile(filePath, "utf8"),
4369
4549
  parser: "text",
4370
4550
  warnings: []
4371
4551
  };
@@ -4379,7 +4559,7 @@ async function parseFileToText(filePath) {
4379
4559
  };
4380
4560
  }
4381
4561
  if (PDF_EXTENSIONS.has(extension)) {
4382
- const buffer = await fs10.readFile(filePath);
4562
+ const buffer = await fs11.readFile(filePath);
4383
4563
  const parser = new PDFParse({ data: buffer });
4384
4564
  try {
4385
4565
  const result = await parser.getText();
@@ -4401,7 +4581,7 @@ function isSupportedTextFile(filePath) {
4401
4581
  }
4402
4582
  function ensureSupportedTextFile(filePath) {
4403
4583
  if (!isSupportedTextFile(filePath)) {
4404
- const extension = path13.extname(filePath).toLowerCase();
4584
+ const extension = path15.extname(filePath).toLowerCase();
4405
4585
  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
4586
  }
4407
4587
  }
@@ -4410,12 +4590,12 @@ function stableStoredName(sourcePath, fileName) {
4410
4590
  return `${digest}-${fileName}`;
4411
4591
  }
4412
4592
  async function ingestLocalFile(input) {
4413
- const sourcePath = path13.resolve(input.filePath);
4414
- const fileName = path13.basename(sourcePath);
4593
+ const sourcePath = path15.resolve(input.filePath);
4594
+ const fileName = path15.basename(sourcePath);
4415
4595
  const jobId = input.jobs?.start({ sourcePath, fileName });
4416
4596
  try {
4417
4597
  ensureSupportedTextFile(sourcePath);
4418
- const stat = await fs11.stat(sourcePath);
4598
+ const stat = await fs12.stat(sourcePath);
4419
4599
  if (!stat.isFile()) {
4420
4600
  throw new Error(`\u4E0D\u662F\u6587\u4EF6\uFF1A${sourcePath}`);
4421
4601
  }
@@ -4424,10 +4604,10 @@ async function ingestLocalFile(input) {
4424
4604
  if (!text) {
4425
4605
  throw new Error(`\u6587\u4EF6\u6CA1\u6709\u53EF\u7D22\u5F15\u6587\u672C\uFF1A${sourcePath}`);
4426
4606
  }
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);
4607
+ const fileDir = path15.join(resolveHomePath(input.config.storage.dataDir), "files");
4608
+ await fs12.mkdir(fileDir, { recursive: true });
4609
+ const storedPath = path15.join(fileDir, stableStoredName(sourcePath, fileName));
4610
+ await fs12.copyFile(sourcePath, storedPath);
4431
4611
  const messageId = input.messages.ingest({
4432
4612
  platform: "local-file",
4433
4613
  platformChatId: "local-files",