chattercatcher 0.1.19 → 0.1.21

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