chattercatcher 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import fs14 from "fs/promises";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "chattercatcher",
11
- version: "0.1.19",
11
+ version: "0.1.20",
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",
@@ -141,6 +141,12 @@ var appSecretsSchema = z.object({
141
141
  z.object({
142
142
  apiKey: z.string().default("")
143
143
  })
144
+ ),
145
+ web: z.preprocess(
146
+ (value) => value ?? {},
147
+ z.object({
148
+ actionToken: z.string().default("")
149
+ })
144
150
  )
145
151
  });
146
152
  function createDefaultConfig() {
@@ -160,7 +166,8 @@ function createDefaultSecrets() {
160
166
  feishu: {},
161
167
  llm: {},
162
168
  embedding: {},
163
- multimodal: {}
169
+ multimodal: {},
170
+ web: {}
164
171
  });
165
172
  }
166
173
 
@@ -529,6 +536,23 @@ function migrateDatabase(database) {
529
536
  );
530
537
 
531
538
  CREATE INDEX IF NOT EXISTS image_multimodal_tasks_status_idx ON image_multimodal_tasks(status, updated_at);
539
+
540
+ CREATE TABLE IF NOT EXISTS cron_jobs (
541
+ id TEXT PRIMARY KEY,
542
+ chat_id TEXT NOT NULL,
543
+ created_by_open_id TEXT,
544
+ schedule TEXT NOT NULL,
545
+ prompt TEXT NOT NULL,
546
+ status TEXT NOT NULL CHECK(status IN ('active','deleted')),
547
+ last_run_at TEXT,
548
+ next_run_at TEXT NOT NULL,
549
+ last_error TEXT,
550
+ created_at TEXT NOT NULL,
551
+ updated_at TEXT NOT NULL
552
+ );
553
+
554
+ CREATE INDEX IF NOT EXISTS cron_jobs_chat_status_idx ON cron_jobs(chat_id, status, updated_at);
555
+ CREATE INDEX IF NOT EXISTS cron_jobs_due_idx ON cron_jobs(status, next_run_at);
532
556
  `);
533
557
  }
534
558
 
@@ -1148,6 +1172,22 @@ function buildSearchTerms(query) {
1148
1172
  }
1149
1173
  return [trimmed];
1150
1174
  }
1175
+ function buildScopeWhere(scope) {
1176
+ const clauses = [];
1177
+ const params = [];
1178
+ if (scope?.platform) {
1179
+ clauses.push("m.platform = ?");
1180
+ params.push(scope.platform);
1181
+ }
1182
+ if (scope?.platformChatId) {
1183
+ clauses.push("c.platform_chat_id = ?");
1184
+ params.push(scope.platformChatId);
1185
+ }
1186
+ return {
1187
+ where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
1188
+ params
1189
+ };
1190
+ }
1151
1191
  function parseRawPayload(value) {
1152
1192
  try {
1153
1193
  const parsed = JSON.parse(value);
@@ -1353,6 +1393,7 @@ var MessageRepository = class {
1353
1393
  const ftsQuery = escapeFtsQuery(query);
1354
1394
  const excludedIds = options.excludeMessageIds ?? [];
1355
1395
  const excludedWhere = excludedIds.length > 0 ? `AND fts.message_id NOT IN (${excludedIds.map(() => "?").join(", ")})` : "";
1396
+ const scope = buildScopeWhere(options.scope);
1356
1397
  const ftsResults = this.database.prepare(
1357
1398
  `
1358
1399
  SELECT
@@ -1371,10 +1412,11 @@ var MessageRepository = class {
1371
1412
  JOIN chats c ON c.id = m.chat_id
1372
1413
  WHERE message_chunks_fts MATCH ?
1373
1414
  ${excludedWhere}
1415
+ ${scope.where}
1374
1416
  ORDER BY bm25(message_chunks_fts)
1375
1417
  LIMIT ?
1376
1418
  `
1377
- ).all(ftsQuery, ...excludedIds, limit);
1419
+ ).all(ftsQuery, ...excludedIds, ...scope.params, limit);
1378
1420
  if (ftsResults.length > 0) {
1379
1421
  return ftsResults;
1380
1422
  }
@@ -1402,10 +1444,11 @@ var MessageRepository = class {
1402
1444
  JOIN chats c ON c.id = m.chat_id
1403
1445
  WHERE (${where})
1404
1446
  ${likeExcludedWhere}
1447
+ ${scope.where}
1405
1448
  ORDER BY m.sent_at DESC
1406
1449
  LIMIT ?
1407
1450
  `
1408
- ).all(...params, ...excludedIds, limit);
1451
+ ).all(...params, ...excludedIds, ...scope.params, limit);
1409
1452
  }
1410
1453
  getChatCount() {
1411
1454
  return this.database.prepare("SELECT COUNT(*) AS count FROM chats").get().count;
@@ -1505,6 +1548,22 @@ function toMillis(value) {
1505
1548
  const time = Date.parse(value);
1506
1549
  return Number.isFinite(time) ? time : 0;
1507
1550
  }
1551
+ function buildScopeWhere2(scope) {
1552
+ const clauses = [];
1553
+ const params = [];
1554
+ if (scope?.platform) {
1555
+ clauses.push("c.platform = ?");
1556
+ params.push(scope.platform);
1557
+ }
1558
+ if (scope?.platformChatId) {
1559
+ clauses.push("c.platform_chat_id = ?");
1560
+ params.push(scope.platformChatId);
1561
+ }
1562
+ return {
1563
+ where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
1564
+ params
1565
+ };
1566
+ }
1508
1567
  var EpisodeRepository = class {
1509
1568
  constructor(database) {
1510
1569
  this.database = database;
@@ -1687,8 +1746,9 @@ var EpisodeRepository = class {
1687
1746
  `
1688
1747
  ).all(limit);
1689
1748
  }
1690
- searchEpisodes(query, limit = 8) {
1749
+ searchEpisodes(query, limit = 8, scope) {
1691
1750
  const ftsQuery = escapeFtsQuery2(query);
1751
+ const scopeWhere = buildScopeWhere2(scope);
1692
1752
  return this.database.prepare(
1693
1753
  `
1694
1754
  SELECT
@@ -1716,11 +1776,12 @@ var EpisodeRepository = class {
1716
1776
  JOIN memory_episodes e ON e.id = fts.episode_id
1717
1777
  JOIN chats c ON c.id = e.chat_id
1718
1778
  WHERE memory_episodes_fts MATCH ?
1779
+ ${scopeWhere.where}
1719
1780
  GROUP BY e.id
1720
1781
  ORDER BY e.ended_at DESC
1721
1782
  LIMIT ?
1722
1783
  `
1723
- ).all(ftsQuery, limit).map((row) => {
1784
+ ).all(ftsQuery, ...scopeWhere.params, limit).map((row) => {
1724
1785
  const item = row;
1725
1786
  return {
1726
1787
  ...item,
@@ -1750,8 +1811,8 @@ var EpisodeFtsRetriever = class {
1750
1811
  this.episodes = episodes;
1751
1812
  }
1752
1813
  episodes;
1753
- async retrieve(question) {
1754
- return this.episodes.searchEpisodes(question, 8).map(toEpisodeEvidence);
1814
+ async retrieve(question, scope) {
1815
+ return this.episodes.searchEpisodes(question, 8, scope).map(toEpisodeEvidence);
1755
1816
  }
1756
1817
  };
1757
1818
 
@@ -1777,8 +1838,9 @@ var HybridRetriever = class {
1777
1838
  }
1778
1839
  retrievers;
1779
1840
  options;
1780
- async retrieve(question) {
1781
- const results = await Promise.all(this.retrievers.map((retriever) => retriever.retrieve(question)));
1841
+ async retrieve(question, scope) {
1842
+ const effectiveScope = scope ?? this.options.scope;
1843
+ const results = await Promise.all(this.retrievers.map((retriever) => retriever.retrieve(question, effectiveScope)));
1782
1844
  const merged = /* @__PURE__ */ new Map();
1783
1845
  for (const evidenceList of results) {
1784
1846
  for (const evidence of evidenceList) {
@@ -1819,9 +1881,10 @@ var MessageFtsRetriever = class {
1819
1881
  }
1820
1882
  messages;
1821
1883
  options;
1822
- async retrieve(question) {
1884
+ async retrieve(question, scope) {
1823
1885
  const results = this.messages.searchMessages(question, 8, {
1824
- excludeMessageIds: this.options.excludeMessageIds
1886
+ excludeMessageIds: this.options.excludeMessageIds,
1887
+ scope
1825
1888
  });
1826
1889
  return results.map((result) => ({
1827
1890
  id: result.chunkId,
@@ -1856,17 +1919,17 @@ function parseSearchInput(input2) {
1856
1919
  const limit = Math.min(12, Math.max(1, Math.floor(numericLimit)));
1857
1920
  return { query, limit };
1858
1921
  }
1859
- async function runRetriever(retriever, input2) {
1922
+ async function runRetriever(retriever, input2, scope) {
1860
1923
  const { query, limit } = parseSearchInput(input2);
1861
- const results = await retriever.retrieve(query);
1924
+ const results = await retriever.retrieve(query, scope);
1862
1925
  return results.slice(0, limit);
1863
1926
  }
1864
- function createSearchTool(name, description, retriever) {
1927
+ function createSearchTool(name, description, retriever, scope) {
1865
1928
  return {
1866
1929
  name,
1867
1930
  description,
1868
1931
  inputSchema: searchInputSchema,
1869
- execute: (input2) => runRetriever(retriever, input2)
1932
+ execute: (input2) => runRetriever(retriever, input2, scope)
1870
1933
  };
1871
1934
  }
1872
1935
  function createRagSearchTools(input2) {
@@ -1874,17 +1937,20 @@ function createRagSearchTools(input2) {
1874
1937
  createSearchTool(
1875
1938
  "hybrid_search",
1876
1939
  "Search across all indexed RAG evidence using the default hybrid retrieval strategy.",
1877
- input2.hybrid
1940
+ input2.hybrid,
1941
+ input2.scope
1878
1942
  ),
1879
1943
  createSearchTool(
1880
1944
  "search_messages",
1881
1945
  "Search chat messages only when the answer likely depends on message-level evidence.",
1882
- input2.messages
1946
+ input2.messages,
1947
+ input2.scope
1883
1948
  ),
1884
1949
  createSearchTool(
1885
1950
  "search_episodes",
1886
1951
  "Search episode summaries only when the answer likely depends on longer-running context.",
1887
- input2.episodes
1952
+ input2.episodes,
1953
+ input2.scope
1888
1954
  )
1889
1955
  ];
1890
1956
  if (input2.semantic) {
@@ -1892,7 +1958,8 @@ function createRagSearchTools(input2) {
1892
1958
  createSearchTool(
1893
1959
  "semantic_search",
1894
1960
  "Search semantic vector evidence only when broader conceptual recall is needed.",
1895
- input2.semantic
1961
+ input2.semantic,
1962
+ input2.scope
1896
1963
  )
1897
1964
  );
1898
1965
  }
@@ -1937,6 +2004,22 @@ function toEvidenceSource2(row) {
1937
2004
  timestamp: row.sentAt
1938
2005
  };
1939
2006
  }
2007
+ function buildScopeWhere3(scope) {
2008
+ const clauses = [];
2009
+ const params = [];
2010
+ if (scope?.platform) {
2011
+ clauses.push("m.platform = ?");
2012
+ params.push(scope.platform);
2013
+ }
2014
+ if (scope?.platformChatId) {
2015
+ clauses.push("c.platform_chat_id = ?");
2016
+ params.push(scope.platformChatId);
2017
+ }
2018
+ return {
2019
+ where: clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "",
2020
+ params
2021
+ };
2022
+ }
1940
2023
  var SqliteVectorStore = class {
1941
2024
  constructor(database, options) {
1942
2025
  this.database = database;
@@ -1971,10 +2054,11 @@ var SqliteVectorStore = class {
1971
2054
  });
1972
2055
  transaction(records);
1973
2056
  }
1974
- async search(vector, limit) {
2057
+ async search(vector, limit, scope) {
1975
2058
  if (limit <= 0) {
1976
2059
  return [];
1977
2060
  }
2061
+ const scopeWhere = buildScopeWhere3(scope);
1978
2062
  const rows = this.database.prepare(
1979
2063
  `
1980
2064
  SELECT
@@ -1989,8 +2073,9 @@ var SqliteVectorStore = class {
1989
2073
  JOIN messages m ON m.id = mc.message_id
1990
2074
  JOIN chats c ON c.id = m.chat_id
1991
2075
  WHERE e.model = ?
2076
+ ${scopeWhere.where}
1992
2077
  `
1993
- ).all(this.options.model);
2078
+ ).all(this.options.model, ...scopeWhere.params);
1994
2079
  return rows.flatMap((row) => {
1995
2080
  const storedVector = parseEmbeddingJson(row.embeddingJson);
1996
2081
  if (storedVector.length === 0) {
@@ -2022,9 +2107,9 @@ var VectorRetriever = class {
2022
2107
  embedding;
2023
2108
  store;
2024
2109
  limit;
2025
- async retrieve(question) {
2110
+ async retrieve(question, scope) {
2026
2111
  const vector = await this.embedding.embed(question);
2027
- return this.store.search(vector, this.limit);
2112
+ return this.store.search(vector, this.limit, scope);
2028
2113
  }
2029
2114
  };
2030
2115
 
@@ -2045,7 +2130,7 @@ async function createHybridRetriever(input2) {
2045
2130
  retrievers.push(new VectorRetriever(createEmbeddingModel(input2.config, input2.secrets), vectorStore));
2046
2131
  }
2047
2132
  return {
2048
- retriever: new HybridRetriever(retrievers),
2133
+ retriever: new HybridRetriever(retrievers, { scope: input2.scope }),
2049
2134
  close: () => {
2050
2135
  for (const closer of closers) {
2051
2136
  closer();
@@ -2062,7 +2147,7 @@ async function createAgenticRagSearchTools(input2) {
2062
2147
  ) : void 0;
2063
2148
  const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
2064
2149
  return {
2065
- tools: createRagSearchTools({ hybrid, messages, episodes, semantic }),
2150
+ tools: createRagSearchTools({ hybrid, messages, episodes, semantic, scope: input2.scope }),
2066
2151
  close: () => {
2067
2152
  }
2068
2153
  };
@@ -2635,50 +2720,35 @@ async function ensureFeishuBotOpenId(config, secrets, options = {}) {
2635
2720
  // src/feishu/gateway.ts
2636
2721
  import * as lark2 from "@larksuiteoapi/node-sdk";
2637
2722
 
2638
- // src/gateway/indexing-scheduler.ts
2639
- function createIndexingScheduler(options) {
2640
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
2641
- const setIntervalFn = options.setIntervalFn ?? setInterval;
2642
- const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
2643
- const logger = options.logger ?? console;
2644
- const parsed = parseCronSchedule(options.schedule);
2645
- let timer;
2646
- let running = false;
2647
- const runDueNow = async () => {
2648
- if (!parsed || running || !matchesParsedSchedule(parsed, now())) {
2649
- return;
2650
- }
2651
- running = true;
2652
- try {
2653
- await options.work();
2654
- } catch (error) {
2655
- const message = error instanceof Error ? error.message : String(error);
2656
- logger.error(`\u5B9A\u65F6\u6D88\u606F\u7D22\u5F15\u5931\u8D25\uFF1A${message}`);
2657
- } finally {
2658
- running = false;
2723
+ // src/cron/jobs.ts
2724
+ import crypto4 from "crypto";
2725
+
2726
+ // src/cron/schedule.ts
2727
+ function isValidCronSchedule(schedule) {
2728
+ return parseCronSchedule(schedule) !== null;
2729
+ }
2730
+ function getNextCronRun(schedule, after) {
2731
+ const parsed = parseCronSchedule(schedule);
2732
+ if (!parsed) {
2733
+ return null;
2734
+ }
2735
+ const candidate = new Date(after);
2736
+ candidate.setSeconds(0, 0);
2737
+ candidate.setMinutes(candidate.getMinutes() + 1);
2738
+ const maxMinutes = 5 * 366 * 24 * 60;
2739
+ for (let i = 0; i < maxMinutes; i += 1) {
2740
+ if (matchesParsedSchedule(parsed, candidate)) {
2741
+ return new Date(candidate);
2659
2742
  }
2660
- };
2661
- return {
2662
- start() {
2663
- if (!parsed || timer) {
2664
- return;
2665
- }
2666
- timer = setIntervalFn(() => {
2667
- void runDueNow();
2668
- }, 6e4);
2669
- },
2670
- stop() {
2671
- if (!timer) {
2672
- return;
2673
- }
2674
- clearIntervalFn(timer);
2675
- timer = void 0;
2676
- },
2677
- runDueNow
2678
- };
2743
+ candidate.setMinutes(candidate.getMinutes() + 1);
2744
+ }
2745
+ return null;
2679
2746
  }
2680
2747
  function matchesParsedSchedule(schedule, date) {
2681
- return schedule.minute(date.getMinutes()) && schedule.hour(date.getHours()) && schedule.dayOfMonth(date.getDate()) && schedule.month(date.getMonth() + 1) && schedule.dayOfWeek(date.getDay());
2748
+ const dayOfMonthMatches = schedule.dayOfMonth.matches(date.getDate());
2749
+ const dayOfWeekMatches = schedule.dayOfWeek.matches(date.getDay());
2750
+ const dayMatches = schedule.dayOfMonth.wildcard || schedule.dayOfWeek.wildcard ? dayOfMonthMatches && dayOfWeekMatches : dayOfMonthMatches || dayOfWeekMatches;
2751
+ return schedule.minute.matches(date.getMinutes()) && schedule.hour.matches(date.getHours()) && dayMatches && schedule.month.matches(date.getMonth() + 1);
2682
2752
  }
2683
2753
  function parseCronSchedule(schedule) {
2684
2754
  const fields = schedule.trim().split(/\s+/);
@@ -2697,7 +2767,7 @@ function parseCronSchedule(schedule) {
2697
2767
  }
2698
2768
  function parseMinuteField(field) {
2699
2769
  if (field === "*") {
2700
- return () => true;
2770
+ return { wildcard: true, matches: () => true };
2701
2771
  }
2702
2772
  const stepMatch = /^\*\/(\d+)$/.exec(field);
2703
2773
  if (stepMatch) {
@@ -2705,7 +2775,7 @@ function parseMinuteField(field) {
2705
2775
  if (!Number.isInteger(step) || step <= 0 || step > 59) {
2706
2776
  return null;
2707
2777
  }
2708
- return (value) => value % step === 0;
2778
+ return { wildcard: false, matches: (value) => value % step === 0 };
2709
2779
  }
2710
2780
  if (field.includes(",")) {
2711
2781
  const values = field.split(",").map((part) => parseExactNumber(part, 0, 59));
@@ -2713,23 +2783,23 @@ function parseMinuteField(field) {
2713
2783
  return null;
2714
2784
  }
2715
2785
  const allowed = new Set(values);
2716
- return (value) => allowed.has(value);
2786
+ return { wildcard: false, matches: (value) => allowed.has(value) };
2717
2787
  }
2718
2788
  const exact = parseExactNumber(field, 0, 59);
2719
2789
  if (exact === null) {
2720
2790
  return null;
2721
2791
  }
2722
- return (value) => value === exact;
2792
+ return { wildcard: false, matches: (value) => value === exact };
2723
2793
  }
2724
2794
  function parseExactOrWildcardField(field, min, max) {
2725
2795
  if (field === "*") {
2726
- return () => true;
2796
+ return { wildcard: true, matches: () => true };
2727
2797
  }
2728
2798
  const exact = parseExactNumber(field, min, max);
2729
2799
  if (exact === null) {
2730
2800
  return null;
2731
2801
  }
2732
- return (value) => value === exact;
2802
+ return { wildcard: false, matches: (value) => value === exact };
2733
2803
  }
2734
2804
  function parseExactNumber(field, min, max) {
2735
2805
  if (!/^\d+$/.test(field)) {
@@ -2742,110 +2812,531 @@ function parseExactNumber(field, min, max) {
2742
2812
  return value;
2743
2813
  }
2744
2814
 
2745
- // src/rag/indexer.ts
2746
- async function indexMessageChunks(input2) {
2747
- const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
2748
- if (chunks.length === 0) {
2749
- return { chunks: 0, vectors: 0 };
2815
+ // src/cron/jobs.ts
2816
+ var CronJobRepository = class {
2817
+ constructor(database, options = {}) {
2818
+ this.database = database;
2819
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2750
2820
  }
2751
- const vectors = await input2.embedding.embedBatch(chunks.map((chunk) => chunk.text));
2752
- const records = [];
2753
- for (const [index2, chunk] of chunks.entries()) {
2754
- const vector = vectors[index2];
2755
- if (!vector || vector.length === 0) {
2756
- continue;
2821
+ database;
2822
+ now;
2823
+ create(input2) {
2824
+ const schedule = input2.schedule.trim();
2825
+ const prompt = input2.prompt.trim();
2826
+ if (!isValidCronSchedule(schedule)) {
2827
+ throw new Error("cron \u8868\u8FBE\u5F0F\u65E0\u6548\u3002");
2757
2828
  }
2758
- records.push({
2759
- id: chunk.chunkId,
2760
- vector,
2761
- evidence: {
2762
- id: chunk.chunkId,
2763
- text: chunk.text,
2764
- score: 1,
2765
- source: toEvidenceSource3(chunk)
2766
- }
2829
+ if (!prompt) {
2830
+ throw new Error("\u5B9A\u65F6\u4EFB\u52A1 prompt \u4E0D\u80FD\u4E3A\u7A7A\u3002");
2831
+ }
2832
+ const now = this.now();
2833
+ const nextRunAt = getNextCronRun(schedule, now);
2834
+ if (!nextRunAt) {
2835
+ throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
2836
+ }
2837
+ const record = {
2838
+ id: crypto4.randomUUID(),
2839
+ chatId: input2.chatId,
2840
+ createdByOpenId: input2.createdByOpenId,
2841
+ schedule,
2842
+ prompt,
2843
+ status: "active",
2844
+ nextRunAt: nextRunAt.toISOString(),
2845
+ createdAt: now.toISOString(),
2846
+ updatedAt: now.toISOString()
2847
+ };
2848
+ this.database.prepare(
2849
+ `
2850
+ INSERT INTO cron_jobs (
2851
+ id, chat_id, created_by_open_id, schedule, prompt, status,
2852
+ last_run_at, next_run_at, last_error, created_at, updated_at
2853
+ )
2854
+ VALUES (
2855
+ @id, @chatId, @createdByOpenId, @schedule, @prompt, @status,
2856
+ NULL, @nextRunAt, NULL, @createdAt, @updatedAt
2857
+ )
2858
+ `
2859
+ ).run(record);
2860
+ return record;
2861
+ }
2862
+ get(id) {
2863
+ return this.listByWhere("WHERE id = ?", [id], 1)[0] ?? null;
2864
+ }
2865
+ list(limit = 100) {
2866
+ return this.listByWhere("", [], limit);
2867
+ }
2868
+ listByChat(chatId, limit = 50) {
2869
+ return this.listByWhere(
2870
+ "WHERE chat_id = ? AND status = 'active'",
2871
+ [chatId],
2872
+ limit
2873
+ );
2874
+ }
2875
+ listDue(now, limit = 20) {
2876
+ const rows = this.database.prepare(
2877
+ `
2878
+ SELECT
2879
+ id,
2880
+ chat_id AS chatId,
2881
+ created_by_open_id AS createdByOpenId,
2882
+ schedule,
2883
+ prompt,
2884
+ status,
2885
+ last_run_at AS lastRunAt,
2886
+ next_run_at AS nextRunAt,
2887
+ last_error AS lastError,
2888
+ created_at AS createdAt,
2889
+ updated_at AS updatedAt
2890
+ FROM cron_jobs
2891
+ WHERE status = 'active' AND next_run_at <= ?
2892
+ ORDER BY next_run_at ASC, updated_at ASC
2893
+ LIMIT ?
2894
+ `
2895
+ ).all(now.toISOString(), limit);
2896
+ return rows.map((row) => ({
2897
+ id: row.id,
2898
+ chatId: row.chatId,
2899
+ createdByOpenId: row.createdByOpenId ?? void 0,
2900
+ schedule: row.schedule,
2901
+ prompt: row.prompt,
2902
+ status: row.status,
2903
+ lastRunAt: row.lastRunAt ?? void 0,
2904
+ nextRunAt: row.nextRunAt,
2905
+ lastError: row.lastError ?? void 0,
2906
+ createdAt: row.createdAt,
2907
+ updatedAt: row.updatedAt
2908
+ }));
2909
+ }
2910
+ deleteByChat(id, chatId) {
2911
+ const now = this.now().toISOString();
2912
+ const result = this.database.prepare(
2913
+ `
2914
+ UPDATE cron_jobs
2915
+ SET status = 'deleted', updated_at = @updatedAt
2916
+ WHERE id = @id AND chat_id = @chatId AND status = 'active'
2917
+ `
2918
+ ).run({ id, chatId, updatedAt: now });
2919
+ return result.changes > 0;
2920
+ }
2921
+ markSuccess(id, ranAt) {
2922
+ const job = this.get(id);
2923
+ if (!job) {
2924
+ return;
2925
+ }
2926
+ const nextRunAt = getNextCronRun(job.schedule, ranAt);
2927
+ if (!nextRunAt) {
2928
+ throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
2929
+ }
2930
+ this.database.prepare(
2931
+ `
2932
+ UPDATE cron_jobs
2933
+ SET last_run_at = @lastRunAt, next_run_at = @nextRunAt, last_error = NULL, updated_at = @updatedAt
2934
+ WHERE id = @id AND status = 'active'
2935
+ `
2936
+ ).run({
2937
+ id,
2938
+ lastRunAt: ranAt.toISOString(),
2939
+ nextRunAt: nextRunAt.toISOString(),
2940
+ updatedAt: ranAt.toISOString()
2767
2941
  });
2768
2942
  }
2769
- await input2.store.upsert(records);
2770
- return {
2771
- chunks: chunks.length,
2772
- vectors: records.length
2773
- };
2774
- }
2775
- function toEvidenceSource3(chunk) {
2776
- if (chunk.messageType === "file") {
2777
- return {
2778
- type: "file",
2779
- label: chunk.senderName,
2780
- timestamp: chunk.sentAt
2781
- };
2943
+ markFailure(id, error, failedAt) {
2944
+ const job = this.get(id);
2945
+ if (!job) {
2946
+ return;
2947
+ }
2948
+ const nextRunAt = getNextCronRun(job.schedule, failedAt);
2949
+ if (!nextRunAt) {
2950
+ throw new Error("\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u6267\u884C\u65F6\u95F4\u3002");
2951
+ }
2952
+ this.database.prepare(
2953
+ `
2954
+ UPDATE cron_jobs
2955
+ SET last_run_at = @lastRunAt, last_error = @lastError, next_run_at = @nextRunAt, updated_at = @updatedAt
2956
+ WHERE id = @id AND status = 'active'
2957
+ `
2958
+ ).run({
2959
+ id,
2960
+ lastRunAt: failedAt.toISOString(),
2961
+ lastError: error,
2962
+ nextRunAt: nextRunAt.toISOString(),
2963
+ updatedAt: failedAt.toISOString()
2964
+ });
2782
2965
  }
2783
- return {
2784
- type: "message",
2785
- label: chunk.chatName,
2786
- sender: chunk.senderName,
2787
- timestamp: chunk.sentAt
2788
- };
2789
- }
2790
-
2791
- // src/rag/manual-index.ts
2792
- async function processMessagesNow(input2) {
2793
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2794
- if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
2795
- return {
2796
- status: "skipped",
2797
- reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
2798
- chunks: 0,
2799
- vectors: 0,
2800
- startedAt,
2801
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
2802
- };
2966
+ listByWhere(whereSql, params, limit) {
2967
+ const rows = this.database.prepare(
2968
+ `
2969
+ SELECT
2970
+ id,
2971
+ chat_id AS chatId,
2972
+ created_by_open_id AS createdByOpenId,
2973
+ schedule,
2974
+ prompt,
2975
+ status,
2976
+ last_run_at AS lastRunAt,
2977
+ next_run_at AS nextRunAt,
2978
+ last_error AS lastError,
2979
+ created_at AS createdAt,
2980
+ updated_at AS updatedAt
2981
+ FROM cron_jobs
2982
+ ${whereSql}
2983
+ ORDER BY updated_at DESC
2984
+ LIMIT ?
2985
+ `
2986
+ ).all(...params, limit);
2987
+ return rows.map((row) => ({
2988
+ id: row.id,
2989
+ chatId: row.chatId,
2990
+ createdByOpenId: row.createdByOpenId ?? void 0,
2991
+ schedule: row.schedule,
2992
+ prompt: row.prompt,
2993
+ status: row.status,
2994
+ lastRunAt: row.lastRunAt ?? void 0,
2995
+ nextRunAt: row.nextRunAt,
2996
+ lastError: row.lastError ?? void 0,
2997
+ createdAt: row.createdAt,
2998
+ updatedAt: row.updatedAt
2999
+ }));
2803
3000
  }
2804
- const vectorStore = new SqliteVectorStore(input2.database, {
2805
- model: input2.config.embedding.model
2806
- });
2807
- const embedding = input2.embedding ?? createEmbeddingModel(input2.config, input2.secrets);
2808
- const stats = await indexMessageChunks({
2809
- messages: new MessageRepository(input2.database),
2810
- embedding,
2811
- store: vectorStore,
2812
- limit: input2.limit
2813
- });
2814
- return {
2815
- status: "completed",
2816
- chunks: stats.chunks,
2817
- vectors: stats.vectors,
2818
- startedAt,
2819
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
2820
- };
2821
- }
3001
+ };
2822
3002
 
2823
- // src/multimodal/tasks.ts
2824
- import crypto4 from "crypto";
2825
- function nowIso4() {
2826
- return (/* @__PURE__ */ new Date()).toISOString();
3003
+ // src/cron/generator.ts
3004
+ var SYSTEM_PROMPT = "\u4F60\u6B63\u5728\u4E3A\u98DE\u4E66\u7FA4\u751F\u6210\u4E00\u6761\u5B9A\u65F6\u6D88\u606F\u3002\u53EF\u4EE5\u5148\u8C03\u7528\u641C\u7D22\u5DE5\u5177\u68C0\u7D22\u672C\u5730\u7FA4\u804A\u77E5\u8BC6\u5E93\u3002\u6700\u7EC8\u8F93\u51FA\u5FC5\u987B\u662F\u53EF\u4EE5\u76F4\u63A5\u53D1\u5230\u7FA4\u91CC\u7684\u7EAF\u6587\u672C\uFF0C\u4E0D\u8981\u8F93\u51FA\u5DE5\u5177\u8C03\u7528\u8BF4\u660E\u3002";
3005
+ function evidenceToText(evidence) {
3006
+ if (evidence.length === 0) {
3007
+ return "\u65E0\u68C0\u7D22\u8BC1\u636E\u3002";
3008
+ }
3009
+ return evidence.map((item, index2) => `${index2 + 1}. ${item.text}`).join("\n");
2827
3010
  }
2828
- function stableId3(sourceMessageId, imageKey) {
2829
- return crypto4.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
3011
+ function toolResultContent(results) {
3012
+ return JSON.stringify(results.map((item) => ({ id: item.id, text: item.text, score: item.score, source: item.source })));
2830
3013
  }
2831
- function mapRow(row) {
2832
- if (!row) {
2833
- return void 0;
3014
+ async function generateCronJobMessage(input2) {
3015
+ if (!input2.model.completeWithTools) {
3016
+ throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
2834
3017
  }
2835
- return {
2836
- id: row.id,
2837
- sourceMessageId: row.source_message_id,
2838
- platformMessageId: row.platform_message_id,
2839
- imageKey: row.image_key,
2840
- storedPath: row.stored_path,
2841
- mimeType: row.mime_type,
2842
- status: row.status,
2843
- attempts: row.attempts,
2844
- ...row.last_error ? { lastError: row.last_error } : {},
2845
- ...row.derived_message_id ? { derivedMessageId: row.derived_message_id } : {},
2846
- createdAt: row.created_at,
2847
- updatedAt: row.updated_at
2848
- };
3018
+ const messages = [
3019
+ { role: "system", content: SYSTEM_PROMPT },
3020
+ { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3021
+ \u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}` }
3022
+ ];
3023
+ const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
3024
+ const evidence = [];
3025
+ const maxModelTurns = input2.maxModelTurns ?? 3;
3026
+ const maxToolCalls = input2.maxToolCalls ?? 6;
3027
+ let toolCallsUsed = 0;
3028
+ for (let turn = 0; turn < maxModelTurns; turn += 1) {
3029
+ const result = await input2.model.completeWithTools(messages, input2.tools);
3030
+ messages.push({ role: "assistant", content: result.content, toolCalls: result.toolCalls });
3031
+ if (result.toolCalls.length === 0) {
3032
+ break;
3033
+ }
3034
+ for (const call of result.toolCalls) {
3035
+ if (toolCallsUsed >= maxToolCalls) {
3036
+ return input2.model.complete([
3037
+ { role: "system", content: SYSTEM_PROMPT },
3038
+ {
3039
+ role: "user",
3040
+ content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3041
+ \u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}
3042
+
3043
+ \u8BC1\u636E\uFF1A
3044
+ ${evidenceToText(evidence)}`
3045
+ }
3046
+ ]);
3047
+ }
3048
+ toolCallsUsed += 1;
3049
+ const tool = toolsByName.get(call.name);
3050
+ if (!tool) {
3051
+ messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify({ error: `\u672A\u77E5\u5DE5\u5177\uFF1A${call.name}` }) });
3052
+ continue;
3053
+ }
3054
+ try {
3055
+ const results = await tool.execute(call.input);
3056
+ evidence.push(...results);
3057
+ messages.push({ role: "tool", toolCallId: call.id, content: toolResultContent(results) });
3058
+ } catch (error) {
3059
+ const message = error instanceof Error ? error.message : String(error);
3060
+ messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify({ error: message }) });
3061
+ }
3062
+ }
3063
+ }
3064
+ return input2.model.complete([
3065
+ { role: "system", content: SYSTEM_PROMPT },
3066
+ {
3067
+ role: "user",
3068
+ content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3069
+ \u4EFB\u52A1\u63D0\u793A\u8BCD\uFF1A${input2.prompt}
3070
+
3071
+ \u8BC1\u636E\uFF1A
3072
+ ${evidenceToText(evidence)}`
3073
+ }
3074
+ ]);
3075
+ }
3076
+
3077
+ // src/cron/scheduler.ts
3078
+ function createCronJobScheduler(options) {
3079
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
3080
+ const setIntervalFn = options.setIntervalFn ?? setInterval;
3081
+ const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
3082
+ const logger = options.logger ?? console;
3083
+ let timer;
3084
+ let running = false;
3085
+ const runDueNow = async () => {
3086
+ if (running) {
3087
+ return;
3088
+ }
3089
+ running = true;
3090
+ const startedAt = now();
3091
+ try {
3092
+ const jobs = options.repository.listDue(startedAt);
3093
+ for (const job of jobs) {
3094
+ try {
3095
+ const text = await options.generateMessage(job, startedAt);
3096
+ await options.sendTextToChat(job.chatId, text);
3097
+ options.repository.markSuccess(job.id, startedAt);
3098
+ } catch (error) {
3099
+ const message = error instanceof Error ? error.message : String(error);
3100
+ options.repository.markFailure(job.id, message, startedAt);
3101
+ logger.error(`CRONJob \u6267\u884C\u5931\u8D25\uFF1A${job.id} ${message}`);
3102
+ }
3103
+ }
3104
+ } finally {
3105
+ running = false;
3106
+ }
3107
+ };
3108
+ return {
3109
+ start() {
3110
+ if (timer) {
3111
+ return;
3112
+ }
3113
+ void runDueNow();
3114
+ timer = setIntervalFn(() => {
3115
+ void runDueNow();
3116
+ }, 6e4);
3117
+ },
3118
+ stop() {
3119
+ if (!timer) {
3120
+ return;
3121
+ }
3122
+ clearIntervalFn(timer);
3123
+ timer = void 0;
3124
+ },
3125
+ runDueNow
3126
+ };
3127
+ }
3128
+
3129
+ // src/gateway/indexing-scheduler.ts
3130
+ function createIndexingScheduler(options) {
3131
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
3132
+ const setIntervalFn = options.setIntervalFn ?? setInterval;
3133
+ const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
3134
+ const logger = options.logger ?? console;
3135
+ const parsed = parseCronSchedule2(options.schedule);
3136
+ let timer;
3137
+ let running = false;
3138
+ const runDueNow = async () => {
3139
+ if (!parsed || running || !matchesParsedSchedule2(parsed, now())) {
3140
+ return;
3141
+ }
3142
+ running = true;
3143
+ try {
3144
+ await options.work();
3145
+ } catch (error) {
3146
+ const message = error instanceof Error ? error.message : String(error);
3147
+ logger.error(`\u5B9A\u65F6\u6D88\u606F\u7D22\u5F15\u5931\u8D25\uFF1A${message}`);
3148
+ } finally {
3149
+ running = false;
3150
+ }
3151
+ };
3152
+ return {
3153
+ start() {
3154
+ if (!parsed || timer) {
3155
+ return;
3156
+ }
3157
+ timer = setIntervalFn(() => {
3158
+ void runDueNow();
3159
+ }, 6e4);
3160
+ },
3161
+ stop() {
3162
+ if (!timer) {
3163
+ return;
3164
+ }
3165
+ clearIntervalFn(timer);
3166
+ timer = void 0;
3167
+ },
3168
+ runDueNow
3169
+ };
3170
+ }
3171
+ function matchesParsedSchedule2(schedule, date) {
3172
+ return schedule.minute(date.getMinutes()) && schedule.hour(date.getHours()) && schedule.dayOfMonth(date.getDate()) && schedule.month(date.getMonth() + 1) && schedule.dayOfWeek(date.getDay());
3173
+ }
3174
+ function parseCronSchedule2(schedule) {
3175
+ const fields = schedule.trim().split(/\s+/);
3176
+ if (fields.length !== 5) {
3177
+ return null;
3178
+ }
3179
+ const minute = parseMinuteField2(fields[0]);
3180
+ const hour = parseExactOrWildcardField2(fields[1], 0, 23);
3181
+ const dayOfMonth = parseExactOrWildcardField2(fields[2], 1, 31);
3182
+ const month = parseExactOrWildcardField2(fields[3], 1, 12);
3183
+ const dayOfWeek = parseExactOrWildcardField2(fields[4], 0, 6);
3184
+ if (!minute || !hour || !dayOfMonth || !month || !dayOfWeek) {
3185
+ return null;
3186
+ }
3187
+ return { minute, hour, dayOfMonth, month, dayOfWeek };
3188
+ }
3189
+ function parseMinuteField2(field) {
3190
+ if (field === "*") {
3191
+ return () => true;
3192
+ }
3193
+ const stepMatch = /^\*\/(\d+)$/.exec(field);
3194
+ if (stepMatch) {
3195
+ const step = Number(stepMatch[1]);
3196
+ if (!Number.isInteger(step) || step <= 0 || step > 59) {
3197
+ return null;
3198
+ }
3199
+ return (value) => value % step === 0;
3200
+ }
3201
+ if (field.includes(",")) {
3202
+ const values = field.split(",").map((part) => parseExactNumber2(part, 0, 59));
3203
+ if (values.some((value) => value === null)) {
3204
+ return null;
3205
+ }
3206
+ const allowed = new Set(values);
3207
+ return (value) => allowed.has(value);
3208
+ }
3209
+ const exact = parseExactNumber2(field, 0, 59);
3210
+ if (exact === null) {
3211
+ return null;
3212
+ }
3213
+ return (value) => value === exact;
3214
+ }
3215
+ function parseExactOrWildcardField2(field, min, max) {
3216
+ if (field === "*") {
3217
+ return () => true;
3218
+ }
3219
+ const exact = parseExactNumber2(field, min, max);
3220
+ if (exact === null) {
3221
+ return null;
3222
+ }
3223
+ return (value) => value === exact;
3224
+ }
3225
+ function parseExactNumber2(field, min, max) {
3226
+ if (!/^\d+$/.test(field)) {
3227
+ return null;
3228
+ }
3229
+ const value = Number(field);
3230
+ if (!Number.isInteger(value) || value < min || value > max) {
3231
+ return null;
3232
+ }
3233
+ return value;
3234
+ }
3235
+
3236
+ // src/rag/indexer.ts
3237
+ async function indexMessageChunks(input2) {
3238
+ const chunks = input2.messageIds ? input2.messages.listMessageChunksByMessageIds(input2.messageIds, input2.limit ?? 1e4) : input2.messages.listAllMessageChunks(input2.limit ?? 1e4);
3239
+ if (chunks.length === 0) {
3240
+ return { chunks: 0, vectors: 0 };
3241
+ }
3242
+ const vectors = await input2.embedding.embedBatch(chunks.map((chunk) => chunk.text));
3243
+ const records = [];
3244
+ for (const [index2, chunk] of chunks.entries()) {
3245
+ const vector = vectors[index2];
3246
+ if (!vector || vector.length === 0) {
3247
+ continue;
3248
+ }
3249
+ records.push({
3250
+ id: chunk.chunkId,
3251
+ vector,
3252
+ evidence: {
3253
+ id: chunk.chunkId,
3254
+ text: chunk.text,
3255
+ score: 1,
3256
+ source: toEvidenceSource3(chunk)
3257
+ }
3258
+ });
3259
+ }
3260
+ await input2.store.upsert(records);
3261
+ return {
3262
+ chunks: chunks.length,
3263
+ vectors: records.length
3264
+ };
3265
+ }
3266
+ function toEvidenceSource3(chunk) {
3267
+ if (chunk.messageType === "file") {
3268
+ return {
3269
+ type: "file",
3270
+ label: chunk.senderName,
3271
+ timestamp: chunk.sentAt
3272
+ };
3273
+ }
3274
+ return {
3275
+ type: "message",
3276
+ label: chunk.chatName,
3277
+ sender: chunk.senderName,
3278
+ timestamp: chunk.sentAt
3279
+ };
3280
+ }
3281
+
3282
+ // src/rag/manual-index.ts
3283
+ async function processMessagesNow(input2) {
3284
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3285
+ if (!hasEmbeddingConfig(input2.config, input2.secrets)) {
3286
+ return {
3287
+ status: "skipped",
3288
+ reason: "Embedding \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1BSQLite FTS \u5DF2\u5728\u6D88\u606F\u5165\u5E93\u65F6\u5373\u65F6\u66F4\u65B0\u3002",
3289
+ chunks: 0,
3290
+ vectors: 0,
3291
+ startedAt,
3292
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
3293
+ };
3294
+ }
3295
+ const vectorStore = new SqliteVectorStore(input2.database, {
3296
+ model: input2.config.embedding.model
3297
+ });
3298
+ const embedding = input2.embedding ?? createEmbeddingModel(input2.config, input2.secrets);
3299
+ const stats = await indexMessageChunks({
3300
+ messages: new MessageRepository(input2.database),
3301
+ embedding,
3302
+ store: vectorStore,
3303
+ limit: input2.limit
3304
+ });
3305
+ return {
3306
+ status: "completed",
3307
+ chunks: stats.chunks,
3308
+ vectors: stats.vectors,
3309
+ startedAt,
3310
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
3311
+ };
3312
+ }
3313
+
3314
+ // src/multimodal/tasks.ts
3315
+ import crypto5 from "crypto";
3316
+ function nowIso4() {
3317
+ return (/* @__PURE__ */ new Date()).toISOString();
3318
+ }
3319
+ function stableId3(sourceMessageId, imageKey) {
3320
+ return crypto5.createHash("sha256").update(`${sourceMessageId}${imageKey}`).digest("hex").slice(0, 32);
3321
+ }
3322
+ function mapRow(row) {
3323
+ if (!row) {
3324
+ return void 0;
3325
+ }
3326
+ return {
3327
+ id: row.id,
3328
+ sourceMessageId: row.source_message_id,
3329
+ platformMessageId: row.platform_message_id,
3330
+ imageKey: row.image_key,
3331
+ storedPath: row.stored_path,
3332
+ mimeType: row.mime_type,
3333
+ status: row.status,
3334
+ attempts: row.attempts,
3335
+ ...row.last_error ? { lastError: row.last_error } : {},
3336
+ ...row.derived_message_id ? { derivedMessageId: row.derived_message_id } : {},
3337
+ createdAt: row.created_at,
3338
+ updatedAt: row.updated_at
3339
+ };
2849
3340
  }
2850
3341
  var ImageMultimodalTaskRepository = class {
2851
3342
  constructor(database) {
@@ -3082,231 +3573,79 @@ var ImageMultimodalWorker = class {
3082
3573
  }
3083
3574
  };
3084
3575
 
3085
- // src/rag/citations.ts
3086
- function isOpaqueId(value) {
3087
- return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
3088
- }
3089
- function formatTime(value) {
3090
- if (!value) {
3091
- return "\u672A\u77E5\u65F6\u95F4";
3092
- }
3093
- const date = new Date(value);
3094
- if (Number.isNaN(date.getTime())) {
3095
- return value;
3096
- }
3097
- const pad = (input2) => String(input2).padStart(2, "0");
3098
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
3099
- }
3100
- function formatSpeaker(source) {
3101
- if (source.type === "file") {
3102
- return isOpaqueId(source.label) ? "\u6587\u4EF6" : `\u6587\u4EF6 ${source.label}`;
3103
- }
3104
- if (source.sender && !isOpaqueId(source.sender)) {
3105
- return source.sender;
3106
- }
3107
- return "\u7FA4\u6210\u5458";
3108
- }
3109
- function clipText(value, maxLength) {
3110
- const normalized = value.replace(/\s+/g, " ").trim();
3111
- return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
3112
- }
3113
- function formatCitation(citation, options = {}) {
3114
- const maxTextLength = options.maxTextLength ?? 120;
3115
- const speaker = formatSpeaker(citation.source);
3116
- const time = formatTime(citation.source.timestamp);
3117
- const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
3118
- return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
3119
- }
3120
- function formatCitations(citations, options = {}) {
3121
- return citations.map((citation) => formatCitation(citation, options)).join("\n");
3122
- }
3123
-
3124
- // src/rag/answer.ts
3125
- var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
3126
- var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
3127
- var SCORE_TIE_THRESHOLD = 0.15;
3128
- function parseTimestamp(value) {
3129
- if (!value) {
3130
- return 0;
3576
+ // src/cron/tools.ts
3577
+ function readString(input2, key) {
3578
+ const value = typeof input2 === "object" && input2 !== null && key in input2 ? input2[key] : void 0;
3579
+ if (typeof value !== "string" || !value.trim()) {
3580
+ throw new Error(`${key} \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
3131
3581
  }
3132
- const time = Date.parse(value);
3133
- return Number.isFinite(time) ? time : 0;
3582
+ return value.trim();
3134
3583
  }
3135
- function rankEvidenceForPrompt(evidence) {
3136
- return [...evidence].sort((left, right) => {
3137
- const scoreDiff = right.score - left.score;
3138
- if (Math.abs(scoreDiff) > SCORE_TIE_THRESHOLD) {
3139
- return scoreDiff;
3140
- }
3141
- const timeDiff = parseTimestamp(right.source.timestamp) - parseTimestamp(left.source.timestamp);
3142
- if (timeDiff !== 0) {
3143
- return timeDiff;
3144
- }
3145
- return scoreDiff;
3146
- });
3147
- }
3148
- function buildEvidencePrompt(question, evidence, options = {}) {
3149
- if (evidence.length === 0) {
3150
- throw new Error("RAG evidence is required before answer generation.");
3151
- }
3152
- const maxEvidenceBlocks = options.maxEvidenceBlocks ?? DEFAULT_MAX_EVIDENCE_BLOCKS;
3153
- const maxCharsPerBlock = options.maxCharsPerBlock ?? DEFAULT_MAX_CHARS_PER_BLOCK;
3154
- const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
3155
- const citations = selected.map((item, index2) => ({
3156
- marker: `S${index2 + 1}`,
3157
- evidenceId: item.id,
3158
- source: item.source,
3159
- text: item.text
3160
- }));
3161
- const evidenceText = selected.map((item, index2) => {
3162
- const marker = citations[index2]?.marker;
3163
- const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
3164
- const sourceParts = [
3165
- item.source.label,
3166
- item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
3167
- item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
3168
- item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
3169
- ].filter(Boolean);
3170
- return `[${marker}]
3171
- \u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
3172
- \u5185\u5BB9\uFF1A${clippedText}`;
3173
- }).join("\n\n");
3174
- return {
3175
- citations,
3176
- messages: [
3177
- {
3178
- role: "system",
3179
- content: "\u4F60\u662F ChatterCatcher \u7684\u95EE\u7B54\u6A21\u5757\u3002\u53EA\u80FD\u6839\u636E\u63D0\u4F9B\u7684\u68C0\u7D22\u8BC1\u636E\u56DE\u7B54\uFF0C\u5FC5\u987B\u7B80\u77ED\u76F4\u63A5\u3002\u4E8B\u5B9E\u6027\u7ED3\u8BBA\u5FC5\u987B\u5F15\u7528 [S1] \u8FD9\u6837\u7684\u6765\u6E90\u6807\u8BB0\u3002\u8BC1\u636E\u4E0D\u8DB3\u65F6\u8BF4\u4E0D\u77E5\u9053\uFF0C\u4E0D\u8981\u731C\u3002\u82E5\u8BC1\u636E\u4E92\u76F8\u77DB\u76FE\uFF0C\u4F18\u5148\u91C7\u7528\u65F6\u95F4\u66F4\u65B0\u4E14\u8868\u8FF0\u660E\u786E\u7684\u8BC1\u636E\uFF1B\u5982\u679C\u8F83\u65B0\u7684\u8BC1\u636E\u53EA\u662F\u8BA8\u8BBA\u3001\u731C\u6D4B\u6216\u4E0D\u786E\u5B9A\u8868\u8FBE\uFF0C\u4E0D\u8981\u628A\u5B83\u5F53\u4F5C\u786E\u5B9A\u66F4\u65B0\u3002"
3584
+ function createCronJobTools(input2) {
3585
+ return [
3586
+ {
3587
+ name: "create_cron_job",
3588
+ description: "Create a scheduled AI message for the current Feishu chat only. The schedule must be a five-field cron string.",
3589
+ inputSchema: {
3590
+ type: "object",
3591
+ properties: {
3592
+ schedule: {
3593
+ type: "string",
3594
+ description: "Five-field cron schedule, for example 0 9 * * *."
3595
+ },
3596
+ prompt: {
3597
+ type: "string",
3598
+ description: "Prompt used later to generate the scheduled message."
3599
+ }
3600
+ },
3601
+ required: ["schedule", "prompt"],
3602
+ additionalProperties: false
3180
3603
  },
3181
- {
3182
- role: "user",
3183
- content: `\u95EE\u9898\uFF1A${question}
3184
-
3185
- \u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
3186
- 1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
3187
- 2. \u540C\u4E00\u4E8B\u9879\u51FA\u73B0\u591A\u4E2A\u7248\u672C\u65F6\uFF0C\u9ED8\u8BA4\u8F83\u65B0\u7684\u660E\u786E\u6D88\u606F\u4F18\u5148\u3002
3188
- 3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
3189
-
3190
- \u68C0\u7D22\u8BC1\u636E\uFF1A
3191
- ${evidenceText}`
3192
- }
3193
- ]
3194
- };
3195
- }
3196
- async function generateGroundedAnswer(input2) {
3197
- const prompt = buildEvidencePrompt(input2.question, input2.evidence);
3198
- const answer = await input2.model.complete(prompt.messages);
3199
- return {
3200
- answer,
3201
- citations: prompt.citations
3202
- };
3203
- }
3204
-
3205
- // src/rag/agentic-qa-service.ts
3206
- var DEFAULT_MAX_MODEL_TURNS = 4;
3207
- var DEFAULT_MAX_TOOL_CALLS = 8;
3208
- var DEFAULT_MAX_EVIDENCE = 12;
3209
- var NO_EVIDENCE_ANSWER = "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002";
3210
- var AGENTIC_SYSTEM_PROMPT = "\u4F60\u662F\u672C\u5730\u77E5\u8BC6\u4FE1\u606F\u6536\u96C6\u4EE3\u7406\u3002\u4F60\u7684\u804C\u8D23\u662F\u56F4\u7ED5\u7528\u6237\u95EE\u9898\u51B3\u5B9A\u662F\u5426\u8C03\u7528\u641C\u7D22\u5DE5\u5177\u3001\u9009\u62E9\u5408\u9002\u7684\u5DE5\u5177\u548C\u67E5\u8BE2\u8BCD\uFF0C\u5E76\u6839\u636E\u5F53\u524D\u7ED3\u679C\u51B3\u5B9A\u662F\u5426\u7EE7\u7EED\u641C\u7D22\u3002\u4E0D\u8981\u7F16\u9020\u4EFB\u4F55\u8BC1\u636E\u6216\u58F0\u79F0\u770B\u8FC7\u672A\u68C0\u7D22\u5230\u7684\u5185\u5BB9\u3002\u4F60\u7684\u8F93\u51FA\u53EA\u7528\u4E8E\u6536\u96C6\u8BC1\u636E\uFF0C\u6700\u7EC8\u7B54\u6848\u4F1A\u7531\u53E6\u4E00\u4E2A\u57FA\u4E8E\u8BC1\u636E\u7684\u6B65\u9AA4\u751F\u6210\u3002";
3211
- function toToolResultContent(results) {
3212
- return JSON.stringify(
3213
- results.map((item) => ({
3214
- id: item.id,
3215
- text: item.text,
3216
- score: item.score,
3217
- source: item.source
3218
- }))
3219
- );
3220
- }
3221
- function toToolErrorContent(message) {
3222
- return JSON.stringify({ error: message });
3223
- }
3224
- function dedupeEvidence(evidence, maxEvidence) {
3225
- const deduped = [];
3226
- const seen = /* @__PURE__ */ new Set();
3227
- for (const item of evidence) {
3228
- if (seen.has(item.id)) {
3229
- continue;
3230
- }
3231
- seen.add(item.id);
3232
- deduped.push(item);
3233
- if (deduped.length >= maxEvidence) {
3234
- break;
3235
- }
3236
- }
3237
- return deduped;
3238
- }
3239
- async function askWithAgenticRag(input2) {
3240
- if (!input2.model.completeWithTools) {
3241
- throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
3242
- }
3243
- const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
3244
- const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
3245
- const maxEvidence = input2.maxEvidence ?? DEFAULT_MAX_EVIDENCE;
3246
- const messages = [
3247
- { role: "system", content: AGENTIC_SYSTEM_PROMPT },
3248
- { role: "user", content: input2.question }
3249
- ];
3250
- const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
3251
- let evidence = [];
3252
- let toolCallsUsed = 0;
3253
- for (let turn = 0; turn < maxModelTurns; turn += 1) {
3254
- const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
3255
- messages.push({
3256
- role: "assistant",
3257
- content: assistantResult.content,
3258
- toolCalls: assistantResult.toolCalls
3259
- });
3260
- if (assistantResult.toolCalls.length === 0) {
3261
- break;
3262
- }
3263
- for (const toolCall of assistantResult.toolCalls) {
3264
- if (toolCallsUsed >= maxToolCalls) {
3265
- break;
3266
- }
3267
- toolCallsUsed += 1;
3268
- const tool = toolsByName.get(toolCall.name);
3269
- if (!tool) {
3270
- messages.push({
3271
- role: "tool",
3272
- toolCallId: toolCall.id,
3273
- content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
3604
+ execute: async (rawInput) => {
3605
+ const job = input2.repository.create({
3606
+ chatId: input2.chatId,
3607
+ createdByOpenId: input2.createdByOpenId,
3608
+ schedule: readString(rawInput, "schedule"),
3609
+ prompt: readString(rawInput, "prompt")
3274
3610
  });
3275
- continue;
3611
+ return JSON.stringify({ ok: true, job });
3276
3612
  }
3277
- try {
3278
- const results = await tool.execute(toolCall.input);
3279
- evidence = dedupeEvidence([...evidence, ...results], maxEvidence);
3280
- messages.push({
3281
- role: "tool",
3282
- toolCallId: toolCall.id,
3283
- content: toToolResultContent(results)
3284
- });
3285
- } catch (error) {
3286
- const message = error instanceof Error ? error.message : String(error);
3287
- messages.push({
3288
- role: "tool",
3289
- toolCallId: toolCall.id,
3290
- content: toToolErrorContent(message)
3613
+ },
3614
+ {
3615
+ name: "list_cron_jobs",
3616
+ description: "List active scheduled AI messages for the current Feishu chat only.",
3617
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
3618
+ execute: async () => JSON.stringify({ ok: true, jobs: input2.repository.listByChat(input2.chatId) })
3619
+ },
3620
+ {
3621
+ name: "delete_cron_job",
3622
+ description: "Delete a scheduled AI message by ID, only if it belongs to the current Feishu chat.",
3623
+ inputSchema: {
3624
+ type: "object",
3625
+ properties: {
3626
+ id: {
3627
+ type: "string",
3628
+ description: "Cron job ID returned by create_cron_job or list_cron_jobs."
3629
+ }
3630
+ },
3631
+ required: ["id"],
3632
+ additionalProperties: false
3633
+ },
3634
+ execute: async (rawInput) => {
3635
+ const id = readString(rawInput, "id");
3636
+ const ok = input2.repository.deleteByChat(id, input2.chatId);
3637
+ return JSON.stringify({
3638
+ ok,
3639
+ id,
3640
+ message: ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664\u3002" : "\u6CA1\u6709\u627E\u5230\u5F53\u524D\u7FA4\u91CC\u7684\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\u3002"
3291
3641
  });
3292
3642
  }
3293
3643
  }
3294
- }
3295
- if (evidence.length === 0) {
3296
- return {
3297
- answer: NO_EVIDENCE_ANSWER,
3298
- citations: []
3299
- };
3300
- }
3301
- return generateGroundedAnswer({
3302
- question: input2.question,
3303
- evidence,
3304
- model: input2.model
3305
- });
3644
+ ];
3306
3645
  }
3307
3646
 
3308
3647
  // src/rag/qa-logs.ts
3309
- import crypto5 from "crypto";
3648
+ import crypto6 from "crypto";
3310
3649
  function clampLimit(limit) {
3311
3650
  return Math.max(1, Math.min(200, Math.trunc(limit)));
3312
3651
  }
@@ -3317,7 +3656,7 @@ var QaLogRepository = class {
3317
3656
  database;
3318
3657
  create(input2) {
3319
3658
  const record = {
3320
- id: `qa_${crypto5.randomUUID()}`,
3659
+ id: `qa_${crypto6.randomUUID()}`,
3321
3660
  chatId: input2.chatId ?? null,
3322
3661
  questionMessageId: input2.questionMessageId ?? null,
3323
3662
  question: input2.question,
@@ -3430,6 +3769,76 @@ function stripMentions(text, mentions) {
3430
3769
  }
3431
3770
  return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
3432
3771
  }
3772
+ 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\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";
3773
+ var DEFAULT_MAX_MODEL_TURNS = 4;
3774
+ var DEFAULT_MAX_TOOL_CALLS = 8;
3775
+ 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";
3776
+ 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";
3777
+ function toToolResultContent(value) {
3778
+ return typeof value === "string" ? value : JSON.stringify(value);
3779
+ }
3780
+ function toToolErrorContent(message) {
3781
+ return JSON.stringify({ ok: false, error: message });
3782
+ }
3783
+ async function executeFeishuTool(tool, input2) {
3784
+ const result = await tool.execute(input2);
3785
+ return toToolResultContent(result);
3786
+ }
3787
+ async function runFeishuToolLoop(input2) {
3788
+ if (!input2.model.completeWithTools) {
3789
+ throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
3790
+ }
3791
+ const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
3792
+ const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
3793
+ const messages = [
3794
+ { role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
3795
+ { role: "user", content: input2.question }
3796
+ ];
3797
+ const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
3798
+ let toolCallsUsed = 0;
3799
+ for (let turn = 0; turn < maxModelTurns; turn += 1) {
3800
+ const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
3801
+ messages.push({
3802
+ role: "assistant",
3803
+ content: assistantResult.content,
3804
+ toolCalls: assistantResult.toolCalls
3805
+ });
3806
+ if (assistantResult.toolCalls.length === 0) {
3807
+ return assistantResult.content || FEISHU_TOOL_LOOP_FALLBACK;
3808
+ }
3809
+ for (const toolCall of assistantResult.toolCalls) {
3810
+ if (toolCallsUsed >= maxToolCalls) {
3811
+ return FEISHU_TOOL_LOOP_LIMIT_REACHED;
3812
+ }
3813
+ toolCallsUsed += 1;
3814
+ const tool = toolsByName.get(toolCall.name);
3815
+ if (!tool) {
3816
+ messages.push({
3817
+ role: "tool",
3818
+ toolCallId: toolCall.id,
3819
+ content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
3820
+ });
3821
+ continue;
3822
+ }
3823
+ try {
3824
+ const result = await executeFeishuTool(tool, toolCall.input);
3825
+ messages.push({
3826
+ role: "tool",
3827
+ toolCallId: toolCall.id,
3828
+ content: result
3829
+ });
3830
+ } catch (error) {
3831
+ const message = error instanceof Error ? error.message : String(error);
3832
+ messages.push({
3833
+ role: "tool",
3834
+ toolCallId: toolCall.id,
3835
+ content: toToolErrorContent(message)
3836
+ });
3837
+ }
3838
+ }
3839
+ }
3840
+ return FEISHU_TOOL_LOOP_FALLBACK;
3841
+ }
3433
3842
  function isMentionForBot(mention, config) {
3434
3843
  if (!config.feishu.botOpenId) {
3435
3844
  return false;
@@ -3524,27 +3933,28 @@ var FeishuQuestionHandler = class {
3524
3933
  });
3525
3934
  try {
3526
3935
  try {
3527
- const result = await askWithAgenticRag({
3936
+ const cronTools = createCronJobTools({
3937
+ repository: new CronJobRepository(this.options.database),
3938
+ chatId: decision.chatId,
3939
+ createdByOpenId: payload.event?.sender?.sender_id?.open_id
3940
+ });
3941
+ const allTools = [...tools, ...cronTools];
3942
+ const answer = await runFeishuToolLoop({
3528
3943
  question: decision.question,
3529
- tools,
3944
+ tools: allTools,
3530
3945
  model: this.options.model
3531
3946
  });
3532
3947
  qaLogs.create({
3533
3948
  chatId: decision.chatId,
3534
3949
  questionMessageId,
3535
3950
  question: decision.question,
3536
- answer: result.answer,
3537
- citations: result.citations,
3538
- retrievalDebug: { evidenceCount: result.citations.length },
3951
+ answer,
3952
+ citations: [],
3953
+ retrievalDebug: {},
3539
3954
  status: "answered",
3540
3955
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3541
3956
  });
3542
- const citations = formatCitations(result.citations);
3543
- const text = citations ? `${result.answer}
3544
-
3545
- \u5F15\u7528\uFF1A
3546
- ${citations}` : result.answer;
3547
- await this.sendResponse(decision.chatId, questionMessageId, text);
3957
+ await this.sendResponse(decision.chatId, questionMessageId, answer);
3548
3958
  } catch (error) {
3549
3959
  const message = error instanceof Error ? error.message : String(error);
3550
3960
  qaLogs.create({
@@ -3790,18 +4200,39 @@ function createFeishuGateway(options) {
3790
4200
  });
3791
4201
  }
3792
4202
  }) : void 0);
4203
+ const cronJobScheduler = options.cronJobScheduler ?? (options.cronJobProcessor ? createCronJobScheduler({
4204
+ repository: new CronJobRepository(options.cronJobProcessor.database),
4205
+ sendTextToChat: (chatId, text) => options.cronJobProcessor.sender.sendTextToChat(chatId, text),
4206
+ generateMessage: async (job, now) => {
4207
+ const { tools, close } = await createAgenticRagSearchTools({
4208
+ config: options.config,
4209
+ secrets: options.secrets,
4210
+ database: options.cronJobProcessor.database,
4211
+ messages: new MessageRepository(options.cronJobProcessor.database),
4212
+ scope: { platform: "feishu", platformChatId: job.chatId }
4213
+ });
4214
+ try {
4215
+ return await generateCronJobMessage({ prompt: job.prompt, model: options.cronJobProcessor.model, tools, now });
4216
+ } finally {
4217
+ close();
4218
+ }
4219
+ }
4220
+ }) : void 0);
3793
4221
  return {
3794
4222
  async start() {
3795
4223
  try {
3796
4224
  await wsClient.start({ eventDispatcher });
3797
4225
  indexingScheduler?.start();
4226
+ cronJobScheduler?.start();
3798
4227
  } catch (error) {
3799
4228
  indexingScheduler?.stop();
4229
+ cronJobScheduler?.stop();
3800
4230
  throw formatGatewayStartError(error);
3801
4231
  }
3802
4232
  },
3803
4233
  stop() {
3804
4234
  indexingScheduler?.stop();
4235
+ cronJobScheduler?.stop();
3805
4236
  wsClient.close({ force: true });
3806
4237
  }
3807
4238
  };
@@ -3873,7 +4304,7 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
3873
4304
  };
3874
4305
 
3875
4306
  // src/files/ingest.ts
3876
- import crypto6 from "crypto";
4307
+ import crypto7 from "crypto";
3877
4308
  import fs11 from "fs/promises";
3878
4309
  import path13 from "path";
3879
4310
 
@@ -3937,7 +4368,7 @@ function ensureSupportedTextFile(filePath) {
3937
4368
  }
3938
4369
  }
3939
4370
  function stableStoredName(sourcePath, fileName) {
3940
- const digest = crypto6.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
4371
+ const digest = crypto7.createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
3941
4372
  return `${digest}-${fileName}`;
3942
4373
  }
3943
4374
  async function ingestLocalFile(input2) {
@@ -4459,6 +4890,87 @@ function createMultimodalModel(config, secrets) {
4459
4890
  });
4460
4891
  }
4461
4892
 
4893
+ // src/rag/answer.ts
4894
+ var DEFAULT_MAX_EVIDENCE_BLOCKS = 8;
4895
+ var DEFAULT_MAX_CHARS_PER_BLOCK = 1200;
4896
+ var SCORE_TIE_THRESHOLD = 0.15;
4897
+ function parseTimestamp(value) {
4898
+ if (!value) {
4899
+ return 0;
4900
+ }
4901
+ const time = Date.parse(value);
4902
+ return Number.isFinite(time) ? time : 0;
4903
+ }
4904
+ function rankEvidenceForPrompt(evidence) {
4905
+ return [...evidence].sort((left, right) => {
4906
+ const scoreDiff = right.score - left.score;
4907
+ if (Math.abs(scoreDiff) > SCORE_TIE_THRESHOLD) {
4908
+ return scoreDiff;
4909
+ }
4910
+ const timeDiff = parseTimestamp(right.source.timestamp) - parseTimestamp(left.source.timestamp);
4911
+ if (timeDiff !== 0) {
4912
+ return timeDiff;
4913
+ }
4914
+ return scoreDiff;
4915
+ });
4916
+ }
4917
+ function buildEvidencePrompt(question, evidence, options = {}) {
4918
+ if (evidence.length === 0) {
4919
+ throw new Error("RAG evidence is required before answer generation.");
4920
+ }
4921
+ const maxEvidenceBlocks = options.maxEvidenceBlocks ?? DEFAULT_MAX_EVIDENCE_BLOCKS;
4922
+ const maxCharsPerBlock = options.maxCharsPerBlock ?? DEFAULT_MAX_CHARS_PER_BLOCK;
4923
+ const selected = rankEvidenceForPrompt(evidence).slice(0, maxEvidenceBlocks);
4924
+ const citations = selected.map((item, index2) => ({
4925
+ marker: `S${index2 + 1}`,
4926
+ evidenceId: item.id,
4927
+ source: item.source,
4928
+ text: item.text
4929
+ }));
4930
+ const evidenceText = selected.map((item, index2) => {
4931
+ const marker = citations[index2]?.marker;
4932
+ const clippedText = item.text.length > maxCharsPerBlock ? `${item.text.slice(0, maxCharsPerBlock)}...` : item.text;
4933
+ const sourceParts = [
4934
+ item.source.label,
4935
+ item.source.sender ? `\u53D1\u9001\u4EBA\uFF1A${item.source.sender}` : void 0,
4936
+ item.source.timestamp ? `\u65F6\u95F4\uFF1A${item.source.timestamp}` : void 0,
4937
+ item.source.location ? `\u4F4D\u7F6E\uFF1A${item.source.location}` : void 0
4938
+ ].filter(Boolean);
4939
+ return `[${marker}]
4940
+ \u6765\u6E90\uFF1A${sourceParts.join("\uFF1B")}
4941
+ \u5185\u5BB9\uFF1A${clippedText}`;
4942
+ }).join("\n\n");
4943
+ return {
4944
+ citations,
4945
+ messages: [
4946
+ {
4947
+ role: "system",
4948
+ content: "\u4F60\u662F ChatterCatcher \u7684\u95EE\u7B54\u6A21\u5757\u3002\u53EA\u80FD\u6839\u636E\u63D0\u4F9B\u7684\u68C0\u7D22\u8BC1\u636E\u56DE\u7B54\uFF0C\u5FC5\u987B\u7B80\u77ED\u76F4\u63A5\u3002\u4E8B\u5B9E\u6027\u7ED3\u8BBA\u5FC5\u987B\u5F15\u7528 [S1] \u8FD9\u6837\u7684\u6765\u6E90\u6807\u8BB0\u3002\u8BC1\u636E\u4E0D\u8DB3\u65F6\u8BF4\u4E0D\u77E5\u9053\uFF0C\u4E0D\u8981\u731C\u3002\u82E5\u8BC1\u636E\u4E92\u76F8\u77DB\u76FE\uFF0C\u4F18\u5148\u91C7\u7528\u65F6\u95F4\u66F4\u65B0\u4E14\u8868\u8FF0\u660E\u786E\u7684\u8BC1\u636E\uFF1B\u5982\u679C\u8F83\u65B0\u7684\u8BC1\u636E\u53EA\u662F\u8BA8\u8BBA\u3001\u731C\u6D4B\u6216\u4E0D\u786E\u5B9A\u8868\u8FBE\uFF0C\u4E0D\u8981\u628A\u5B83\u5F53\u4F5C\u786E\u5B9A\u66F4\u65B0\u3002"
4949
+ },
4950
+ {
4951
+ role: "user",
4952
+ content: `\u95EE\u9898\uFF1A${question}
4953
+
4954
+ \u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
4955
+ 1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
4956
+ 2. \u540C\u4E00\u4E8B\u9879\u51FA\u73B0\u591A\u4E2A\u7248\u672C\u65F6\uFF0C\u9ED8\u8BA4\u8F83\u65B0\u7684\u660E\u786E\u6D88\u606F\u4F18\u5148\u3002
4957
+ 3. \u56DE\u7B54\u53EA\u5F15\u7528\u5B9E\u9645\u652F\u6491\u7ED3\u8BBA\u7684\u8BC1\u636E\u3002
4958
+
4959
+ \u68C0\u7D22\u8BC1\u636E\uFF1A
4960
+ ${evidenceText}`
4961
+ }
4962
+ ]
4963
+ };
4964
+ }
4965
+ async function generateGroundedAnswer(input2) {
4966
+ const prompt = buildEvidencePrompt(input2.question, input2.evidence);
4967
+ const answer = await input2.model.complete(prompt.messages);
4968
+ return {
4969
+ answer,
4970
+ citations: prompt.citations
4971
+ };
4972
+ }
4973
+
4462
4974
  // src/rag/qa-service.ts
4463
4975
  async function askWithRag(input2) {
4464
4976
  const evidence = await input2.retriever.retrieve(input2.question);
@@ -4475,6 +4987,42 @@ async function askWithRag(input2) {
4475
4987
  });
4476
4988
  }
4477
4989
 
4990
+ // src/rag/citations.ts
4991
+ function isOpaqueId(value) {
4992
+ return Boolean(value && /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(value));
4993
+ }
4994
+ function formatTime(value) {
4995
+ if (!value) {
4996
+ return "\u672A\u77E5\u65F6\u95F4";
4997
+ }
4998
+ const date = new Date(value);
4999
+ if (Number.isNaN(date.getTime())) {
5000
+ return value;
5001
+ }
5002
+ const pad = (input2) => String(input2).padStart(2, "0");
5003
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
5004
+ }
5005
+ function formatSpeaker(source) {
5006
+ if (source.type === "file") {
5007
+ return isOpaqueId(source.label) ? "\u6587\u4EF6" : `\u6587\u4EF6 ${source.label}`;
5008
+ }
5009
+ if (source.sender && !isOpaqueId(source.sender)) {
5010
+ return source.sender;
5011
+ }
5012
+ return "\u7FA4\u6210\u5458";
5013
+ }
5014
+ function clipText(value, maxLength) {
5015
+ const normalized = value.replace(/\s+/g, " ").trim();
5016
+ return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
5017
+ }
5018
+ function formatCitation(citation, options = {}) {
5019
+ const maxTextLength = options.maxTextLength ?? 120;
5020
+ const speaker = formatSpeaker(citation.source);
5021
+ const time = formatTime(citation.source.timestamp);
5022
+ const verb = citation.source.type === "file" ? "\u8BB0\u5F55" : "\u8BF4";
5023
+ return `[${citation.marker}] ${speaker}\u5728 ${time} ${verb}\uFF1A\u201C${clipText(citation.text, maxTextLength)}\u201D`;
5024
+ }
5025
+
4478
5026
  // src/update/npm-updater.ts
4479
5027
  import { execFile } from "child_process";
4480
5028
  import { promisify } from "util";
@@ -4578,6 +5126,7 @@ async function updateChatterCatcher(options) {
4578
5126
  }
4579
5127
 
4580
5128
  // src/web/server.ts
5129
+ import crypto8 from "crypto";
4581
5130
  import Fastify from "fastify";
4582
5131
  function buildHtml() {
4583
5132
  return `<!doctype html>
@@ -4725,6 +5274,10 @@ function buildHtml() {
4725
5274
  <h2>\u89E3\u6790\u4EFB\u52A1</h2>
4726
5275
  <div id="file-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
4727
5276
  </section>
5277
+ <section>
5278
+ <h2>\u5B9A\u65F6\u4EFB\u52A1</h2>
5279
+ <div id="cron-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
5280
+ </section>
4728
5281
  <section>
4729
5282
  <h2>\u672C\u5730\u64CD\u4F5C</h2>
4730
5283
  <p><code>chattercatcher settings</code> \u4FEE\u6539\u914D\u7F6E\u3002</p>
@@ -4741,10 +5294,13 @@ function buildHtml() {
4741
5294
  const chats = document.querySelector("#chats");
4742
5295
  const files = document.querySelector("#files");
4743
5296
  const fileJobs = document.querySelector("#file-jobs");
5297
+ const cronJobs = document.querySelector("#cron-jobs");
4744
5298
  const qaLogs = document.querySelector("#qa-logs");
4745
5299
  const processMessages = document.querySelector("#process-messages");
4746
5300
  const actionStatus = document.querySelector("#action-status");
4747
5301
 
5302
+ let webActionToken = "__WEB_ACTION_TOKEN__";
5303
+
4748
5304
  function fmt(value) {
4749
5305
  return value == null || value === "" ? "-" : String(value);
4750
5306
  }
@@ -4933,6 +5489,36 @@ function buildHtml() {
4933
5489
  \`;
4934
5490
  }
4935
5491
 
5492
+ function renderCronJobs(items) {
5493
+ if (items.length === 0) {
5494
+ cronJobs.className = "empty";
5495
+ cronJobs.textContent = "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1\u3002\u53EF\u5728\u98DE\u4E66\u7FA4\u91CC @ \u673A\u5668\u4EBA\u521B\u5EFA\u3002";
5496
+ return;
5497
+ }
5498
+ cronJobs.className = "";
5499
+ cronJobs.innerHTML = \`
5500
+ <table>
5501
+ <thead><tr><th>\u4EFB\u52A1</th><th>\u72B6\u6001</th></tr></thead>
5502
+ <tbody>
5503
+ \${items.map((item) => \`
5504
+ <tr>
5505
+ <td>
5506
+ <div>\${escapeHtml(item.schedule)}</div>
5507
+ <div class="message" title="\${escapeHtml(item.prompt)}">\${escapeHtml(item.prompt)}</div>
5508
+ <div class="path" title="\${escapeHtml(item.id)}">ID: \${escapeHtml(item.id)}</div>
5509
+ <div class="path" title="\${escapeHtml(item.chatId)}">\u7FA4: \${escapeHtml(item.chatId)}</div>
5510
+ <div class="path">\u4E0B\u6B21: \${escapeHtml(formatDateTime(item.nextRunAt))}</div>
5511
+ <div class="path" title="\${escapeHtml(item.lastError || "")}">\${escapeHtml(item.lastError || "")}</div>
5512
+ \${item.status === "active" ? \`<button type="button" data-delete-cron-job="\${escapeHtml(item.id)}">\u5220\u9664</button>\` : ""}
5513
+ </td>
5514
+ <td>\${escapeHtml(item.status)}</td>
5515
+ </tr>
5516
+ \`).join("")}
5517
+ </tbody>
5518
+ </table>
5519
+ \`;
5520
+ }
5521
+
4936
5522
  function renderQaLogs(items) {
4937
5523
  if (items.length === 0) {
4938
5524
  qaLogs.className = "empty";
@@ -4964,7 +5550,7 @@ function buildHtml() {
4964
5550
  }
4965
5551
 
4966
5552
  async function load() {
4967
- const [status, recent, episodeList, chatList, fileList, jobList, qaLogList] = await Promise.all([
5553
+ const [status, recent, episodeList, chatList, fileList, jobList, qaLogList, cronJobList] = await Promise.all([
4968
5554
  fetch("/api/status").then((response) => response.json()),
4969
5555
  fetch("/api/messages/recent?limit=20").then((response) => response.json()),
4970
5556
  fetch("/api/episodes?limit=10").then((response) => response.json()),
@@ -4972,6 +5558,7 @@ function buildHtml() {
4972
5558
  fetch("/api/files").then((response) => response.json()),
4973
5559
  fetch("/api/file-jobs").then((response) => response.json()),
4974
5560
  fetch("/api/qa-logs?limit=10").then((response) => response.json()),
5561
+ fetch("/api/cron-jobs").then((response) => response.json()),
4975
5562
  ]);
4976
5563
  renderMetrics(status);
4977
5564
  renderMessages(recent.items);
@@ -4980,13 +5567,17 @@ function buildHtml() {
4980
5567
  renderFiles(fileList.items);
4981
5568
  renderFileJobs(jobList.items);
4982
5569
  renderQaLogs(qaLogList.items);
5570
+ renderCronJobs(cronJobList.items);
4983
5571
  }
4984
5572
 
4985
5573
  async function processNow() {
4986
5574
  processMessages.disabled = true;
4987
5575
  actionStatus.textContent = "\u6B63\u5728\u5904\u7406\u6D88\u606F\u7D22\u5F15...";
4988
5576
  try {
4989
- const response = await fetch("/api/process/messages", { method: "POST" });
5577
+ const response = await fetch("/api/process/messages", {
5578
+ method: "POST",
5579
+ headers: { "x-chattercatcher-web-token": webActionToken },
5580
+ });
4990
5581
  const result = await response.json();
4991
5582
  if (!response.ok) {
4992
5583
  actionStatus.textContent = result.message || "\u5904\u7406\u5931\u8D25\u3002";
@@ -5006,6 +5597,28 @@ function buildHtml() {
5006
5597
  }
5007
5598
  }
5008
5599
 
5600
+ document.addEventListener("click", async (event) => {
5601
+ const target = event.target;
5602
+ if (!(target instanceof HTMLElement)) return;
5603
+ const id = target.dataset.deleteCronJob;
5604
+ if (!id) return;
5605
+ target.setAttribute("disabled", "disabled");
5606
+ actionStatus.textContent = "\u6B63\u5728\u5220\u9664\u5B9A\u65F6\u4EFB\u52A1...";
5607
+ try {
5608
+ const response = await fetch(\`/api/cron-jobs/\${encodeURIComponent(id)}\`, {
5609
+ method: "DELETE",
5610
+ headers: { "x-chattercatcher-web-token": webActionToken },
5611
+ });
5612
+ const result = await response.json();
5613
+ actionStatus.textContent = result.ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664\u3002" : result.message || "\u5220\u9664\u5931\u8D25\u3002";
5614
+ await load();
5615
+ } catch (error) {
5616
+ actionStatus.textContent = error instanceof Error ? error.message : String(error);
5617
+ } finally {
5618
+ target.removeAttribute("disabled");
5619
+ }
5620
+ });
5621
+
5009
5622
  processMessages.addEventListener("click", () => void processNow());
5010
5623
  void load();
5011
5624
  setInterval(() => {
@@ -5014,6 +5627,7 @@ function buildHtml() {
5014
5627
  }
5015
5628
  }, 5000);
5016
5629
  </script>
5630
+ <script src="/app.js"></script>
5017
5631
  </body>
5018
5632
  </html>`;
5019
5633
  }
@@ -5021,6 +5635,20 @@ function parseLimit(value, fallback, max) {
5021
5635
  const rawLimit = Number(value ?? fallback);
5022
5636
  return Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), max) : fallback;
5023
5637
  }
5638
+ function getWebActionToken(secrets) {
5639
+ return secrets.web.actionToken;
5640
+ }
5641
+ function readHeader(value) {
5642
+ return Array.isArray(value) ? value[0] : value;
5643
+ }
5644
+ function isAuthorizedWebAction(request, token) {
5645
+ const provided = readHeader(request.headers["x-chattercatcher-web-token"]);
5646
+ return provided === token;
5647
+ }
5648
+ function extractInlineScript(html) {
5649
+ const match = /<script>([\s\S]*)<\/script>/.exec(html);
5650
+ return match?.[1] ?? "";
5651
+ }
5024
5652
  function createWebApp(config) {
5025
5653
  const app = Fastify({ logger: false });
5026
5654
  const database = openDatabase(config);
@@ -5028,30 +5656,44 @@ function createWebApp(config) {
5028
5656
  const episodes = new EpisodeRepository(database);
5029
5657
  const fileJobs = new FileJobRepository(database);
5030
5658
  const qaLogs = new QaLogRepository(database);
5659
+ const cronJobs = new CronJobRepository(database);
5660
+ let webActionToken = "";
5661
+ const tokenReady = (async () => {
5662
+ const secrets = await loadSecrets();
5663
+ if (!secrets.web.actionToken) {
5664
+ secrets.web.actionToken = crypto8.randomBytes(32).toString("hex");
5665
+ await saveSecrets(secrets);
5666
+ }
5667
+ webActionToken = getWebActionToken(secrets);
5668
+ })();
5031
5669
  app.addHook("onClose", async () => {
5032
5670
  database.close();
5033
5671
  });
5034
- app.get("/api/status", async () => ({
5035
- app: "ChatterCatcher",
5036
- gateway: getGatewayStatus(config),
5037
- data: {
5038
- chats: messages.getChatCount(),
5039
- messages: messages.getMessageCount(),
5040
- episodes: episodes.getEpisodeCount(),
5041
- files: messages.listFiles(1e3).length,
5042
- qaLogs: qaLogs.getCount()
5043
- },
5044
- rag: {
5045
- mode: "required",
5046
- note: "\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22\u8BC1\u636E\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0\u3002",
5047
- retrieval: {
5048
- keyword: "SQLite FTS5",
5049
- vector: "SQLite embedding",
5050
- hybrid: true
5051
- }
5052
- },
5053
- web: config.web
5054
- }));
5672
+ app.get("/api/status", async () => {
5673
+ await tokenReady;
5674
+ return {
5675
+ app: "ChatterCatcher",
5676
+ gateway: getGatewayStatus(config),
5677
+ data: {
5678
+ chats: messages.getChatCount(),
5679
+ messages: messages.getMessageCount(),
5680
+ episodes: episodes.getEpisodeCount(),
5681
+ files: messages.listFiles(1e3).length,
5682
+ qaLogs: qaLogs.getCount(),
5683
+ cronJobs: cronJobs.list(1e3).length
5684
+ },
5685
+ rag: {
5686
+ mode: "required",
5687
+ note: "\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22\u8BC1\u636E\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0\u3002",
5688
+ retrieval: {
5689
+ keyword: "SQLite FTS5",
5690
+ vector: "SQLite embedding",
5691
+ hybrid: true
5692
+ }
5693
+ },
5694
+ web: config.web
5695
+ };
5696
+ });
5055
5697
  app.get("/api/chats", async () => ({
5056
5698
  items: messages.listChats()
5057
5699
  }));
@@ -5086,7 +5728,33 @@ function createWebApp(config) {
5086
5728
  items: qaLogs.listRecent(limit)
5087
5729
  };
5088
5730
  });
5089
- app.post("/api/process/messages", async (_request, reply) => {
5731
+ app.get("/api/cron-jobs", async (request) => {
5732
+ const limit = parseLimit(request.query.limit, 50, 200);
5733
+ return {
5734
+ items: cronJobs.list(limit)
5735
+ };
5736
+ });
5737
+ app.delete("/api/cron-jobs/:id", async (request, reply) => {
5738
+ await tokenReady;
5739
+ if (!isAuthorizedWebAction(request, webActionToken)) {
5740
+ reply.code(403);
5741
+ return { ok: false, message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
5742
+ }
5743
+ const id = request.params.id;
5744
+ const job = cronJobs.get(id);
5745
+ if (!job) {
5746
+ reply.code(404);
5747
+ return { ok: false, message: "\u6CA1\u6709\u627E\u5230\u5B9A\u65F6\u4EFB\u52A1\u3002" };
5748
+ }
5749
+ const ok = cronJobs.deleteByChat(id, job.chatId);
5750
+ return { ok };
5751
+ });
5752
+ app.post("/api/process/messages", async (request, reply) => {
5753
+ await tokenReady;
5754
+ if (!isAuthorizedWebAction(request, webActionToken)) {
5755
+ reply.code(403);
5756
+ return { status: "failed", message: "Web \u64CD\u4F5C\u672A\u6388\u6743\u3002" };
5757
+ }
5090
5758
  try {
5091
5759
  return await processMessagesNow({
5092
5760
  config,
@@ -5102,6 +5770,11 @@ function createWebApp(config) {
5102
5770
  };
5103
5771
  }
5104
5772
  });
5773
+ app.get("/app.js", async (_request, reply) => {
5774
+ await tokenReady;
5775
+ reply.type("application/javascript; charset=utf-8");
5776
+ return extractInlineScript(buildHtml()).replaceAll("__WEB_ACTION_TOKEN__", webActionToken);
5777
+ });
5105
5778
  app.get("/", async (_request, reply) => {
5106
5779
  reply.type("text/html; charset=utf-8");
5107
5780
  return buildHtml();
@@ -5301,6 +5974,8 @@ async function startGatewayForegroundCommand() {
5301
5974
  mode: "gateway"
5302
5975
  });
5303
5976
  const database = openDatabase(config);
5977
+ const chatModel = createChatModel(config, secrets);
5978
+ const sender = FeishuMessageSender.fromConfig(config, secrets);
5304
5979
  const vectorStore = hasEmbeddingConfig(config, secrets) ? new SqliteVectorStore(database, { model: config.embedding.model }) : null;
5305
5980
  const gatewayRuntime = createFeishuGateway({
5306
5981
  config,
@@ -5315,7 +5990,7 @@ async function startGatewayForegroundCommand() {
5315
5990
  }) : void 0,
5316
5991
  episodeProcessor: {
5317
5992
  database,
5318
- model: createChatModel(config, secrets)
5993
+ model: chatModel
5319
5994
  },
5320
5995
  imageMultimodalProcessor: config.multimodal.baseUrl && config.multimodal.model && secrets.multimodal.apiKey ? {
5321
5996
  database,
@@ -5324,12 +5999,17 @@ async function startGatewayForegroundCommand() {
5324
5999
  indexingProcessor: {
5325
6000
  database
5326
6001
  },
6002
+ cronJobProcessor: {
6003
+ database,
6004
+ model: chatModel,
6005
+ sender
6006
+ },
5327
6007
  questionHandler: new FeishuQuestionHandler({
5328
6008
  config,
5329
6009
  secrets,
5330
6010
  database,
5331
- sender: FeishuMessageSender.fromConfig(config, secrets),
5332
- model: createChatModel(config, secrets)
6011
+ sender,
6012
+ model: chatModel
5333
6013
  })
5334
6014
  });
5335
6015
  const cleanup = () => {