chattercatcher 0.1.23 → 0.1.25

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/README.md CHANGED
@@ -59,8 +59,11 @@ ChatterCatcher 是一个早期 MVP。它已经具备飞书长连接接入、本
59
59
 
60
60
  - **无 native 向量库依赖**:语义向量写入 SQLite,避免 LanceDB 平台包在不同 macOS/CPU 架构上安装失败。
61
61
  - **SQLite FTS + embedding 混合 RAG**:关键词和语义检索并行召回,回答前必须先找到本地证据。
62
+ - **相对时间归一化**:自动将”今天””明天””今晚”等相对时间表述转换为具体日期。所有 LLM 路径(会话摘要、RAG 问答、Agent 工具循环)统一注入当前时间并强调时间推导规则,让回答中的日期始终准确。
63
+ - **群内定时任务**:在群里用自然语言创建 cron 定时任务,支持”每天 9 点总结昨天群聊”等日常需求。创建、查看、删除都在群里一句话搞定,任务限定当前群聊不能跨群。
64
+ - **工具循环智能兜底**:当 Agent 多轮工具调用达到上限时,自动用对话历史生成最终答案,不再返回含混的”操作已提交”提示。
62
65
  - **自动识别飞书机器人身份**:可通过 App ID / App Secret 自动获取 `botOpenId`,减少手动配置错误。
63
- - **会话记忆块**:把 10 分钟窗口、静默 2 分钟后的碎片聊天整理成 episode summary,让“我要发一个 API key”与后续短消息保持上下文关联。
66
+ - **会话记忆块**:把 10 分钟窗口、静默 2 分钟后的碎片聊天整理成 episode summary,让”我要发一个 API key”与后续短消息保持上下文关联。
64
67
  - **敏感摘要保护**:会话摘要会脱敏疑似 token/API key;原始消息仍保留在本地,方便必要时追溯。
65
68
 
66
69
  当前核心方向是:
@@ -121,7 +124,9 @@ ChatterCatcher 是一个早期 MVP。它已经具备飞书长连接接入、本
121
124
  | 消息入库 | 普通文本消息写入 SQLite;`@` 提问直接回答并跳过入库 |
122
125
  | 会话记忆块 | 默认 10 分钟窗口 + 2 分钟静默期,把碎片聊天整理成可检索 episode summary,并关联原始消息 |
123
126
  | RAG 检索 | SQLite FTS 关键词检索、SQLite embedding 向量检索、episode summary 检索、混合重排、证据来源保留 |
124
- | 问答 | OpenAI-compatible chat completions、证据不足时说不知道、回答带引用 |
127
+ | 时间归一化 | 自动将相对时间(今天/明天/今晚)推导为具体日期,覆盖会话摘要、RAG 问答、Agent 工具循环全部 LLM 路径 |
128
+ | 问答 | OpenAI-compatible chat completions、Agent 多轮工具调用、证据不足时说不知道、回答带引用、工具循环耗尽智能兜底 |
129
+ | 定时任务 | 群内自然语言创建 cron 定时任务(如"每天 9 点总结昨天群聊"),限定当前群聊,支持创建/查看/删除 |
125
130
  | 引用格式 | 展示“谁在什么时候说了什么”,避免暴露 `ou_` / `oc_` 等 opaque id |
126
131
  | 文件知识源 | 支持 txt、md、json、csv、tsv、log、docx、pdf 导入和解析 |
127
132
  | CLI | setup、settings、doctor、gateway、process、index、files、export、restore |
@@ -250,12 +255,14 @@ http://127.0.0.1:3878
250
255
  期望回答类似:
251
256
 
252
257
  ```text
253
- 后天 13:40 [S1]。
258
+ 最近一次编程课是 2026-04-28 13:40 [S1]。
254
259
 
255
260
  引用:
256
- [S1] 群成员在 2026-04-26 13:36 说:“编程课的时间改成了后天13:40”
261
+ [S1] 群成员在 2026-04-26 13:36 说:”编程课的时间改成了后天13:40”
257
262
  ```
258
263
 
264
+ 注意:原文说的是”后天”,但 ChatterCatcher 已根据消息时间戳自动推导为具体日期。
265
+
259
266
  ---
260
267
 
261
268
  ## 常用命令
@@ -275,6 +282,8 @@ http://127.0.0.1:3878
275
282
  | `chattercatcher files jobs` | 查看文件解析任务 |
276
283
  | `chattercatcher export --out <file>` | 导出本地知识库数据,不含密钥 |
277
284
  | `chattercatcher restore <file>` | 从导出文件恢复 |
285
+ | `chattercatcher cron list` | 列出所有定时任务 |
286
+ | `chattercatcher cron run` | 手动触发到期定时任务 |
278
287
 
279
288
  ---
280
289
 
@@ -301,6 +310,28 @@ chattercatcher process episodes
301
310
 
302
311
  ---
303
312
 
313
+ ## 群内定时任务
314
+
315
+ 在群里用自然语言即可创建定时任务。ChatterCatcher 会把自然语言时间描述自动转换为 cron 表达式:
316
+
317
+ ```text
318
+ @小陈 每天 9 点总结昨天群聊
319
+ @小陈 每周一上午 10 点提醒大家报备周末计划
320
+ @小陈 查看定时任务
321
+ @小陈 删除 job-abc123
322
+ ```
323
+
324
+ 定时任务限定在当前群聊内,不能跨群查看或操作其他群的任务。
325
+
326
+ CLI 管理:
327
+
328
+ ```bash
329
+ chattercatcher cron list # 查看所有定时任务
330
+ chattercatcher cron run # 手动触发到期任务
331
+ ```
332
+
333
+ ---
334
+
304
335
  ## 本地数据目录
305
336
 
306
337
  默认数据目录:
@@ -391,6 +422,14 @@ npm install -g chattercatcher@latest
391
422
 
392
423
  摘要层会脱敏疑似 API key、token、cookie、私钥和 URL 凭据;原始消息仍然只保存在本地数据库,用于必要时追溯证据。
393
424
 
425
+ ### ChatterCatcher 能理解"明天""今晚"这种相对时间吗?
426
+
427
+ 可以。ChatterCatcher 会自动识别消息的时间戳,将"今天""明天""今晚""下周三"等相对表述推导为具体日期写入摘要和回答。你不需要在每次提问时手工推算日期。
428
+
429
+ ### 定时任务怎么用?会不会跨群操作?
430
+
431
+ 在群里 @ 机器人说"每天 9 点总结昨天群聊"即可创建定时任务。用"查看定时任务"列出当前群的任务,"删除 xxx"删掉指定任务。定时任务限定在当前群聊,不会跨群查看或操作。
432
+
394
433
  ### Web UI 可以暴露到公网吗?
395
434
 
396
435
  默认不建议。ChatterCatcher 面向家庭隐私数据,默认只监听 `127.0.0.1`。
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.23",
11
+ version: "0.1.25",
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",
@@ -1623,7 +1623,7 @@ var EpisodeRepository = class {
1623
1623
  endedAt: last.sentAt,
1624
1624
  messages: windowMessages
1625
1625
  };
1626
- const summary = await input2.summarize(window);
1626
+ const summary = await input2.summarize(window, input2.now);
1627
1627
  created.push(this.insertEpisode(window, summary));
1628
1628
  }
1629
1629
  }
@@ -2613,16 +2613,17 @@ async function restoreLocalData(input2) {
2613
2613
  }
2614
2614
 
2615
2615
  // src/episodes/summarizer.ts
2616
- async function summarizeEpisodeWindow(window, model) {
2616
+ async function summarizeEpisodeWindow(window, model, now) {
2617
2617
  const transcript = window.messages.map((message) => `[${message.sentAt}] ${message.senderName}\uFF1A${message.text}`).join("\n");
2618
2618
  const summary = await model.complete([
2619
2619
  {
2620
2620
  role: "system",
2621
- content: "\u4F60\u662F ChatterCatcher \u7684\u4F1A\u8BDD\u8BB0\u5FC6\u6574\u7406\u6A21\u5757\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u628A\u788E\u7247\u5316\u95F2\u804A\u6574\u7406\u6210\u53EF\u68C0\u7D22\u4E8B\u5B9E\uFF0C\u8865\u5168\u77ED\u6D88\u606F\u3001\u4EE3\u8BCD\u3001\u7F29\u5199\u4E0E\u4E0A\u4E0B\u6587\u4E4B\u95F4\u7684\u5173\u7CFB\u3002\u53EA\u603B\u7ED3\u660E\u786E\u4E8B\u5B9E\uFF0C\u4E0D\u8981\u7F16\u9020\u3002\u4FDD\u7559\u91CD\u8981\u6570\u5B57\u3001\u65E5\u671F\u3001\u94FE\u63A5\u548C\u4EE3\u7801\uFF1B\u5982\u679C\u5185\u5BB9\u50CF\u5BC6\u7801\u3001API key\u3001token \u6216\u5BC6\u94A5\uFF0C\u53EA\u63CF\u8FF0\u5176\u4E0A\u4E0B\u6587\u5173\u7CFB\uFF0C\u4E0D\u8981\u5728\u6458\u8981\u4E2D\u590D\u5199\u539F\u6587\u3002"
2621
+ content: "\u4F60\u662F ChatterCatcher \u7684\u4F1A\u8BDD\u8BB0\u5FC6\u6574\u7406\u6A21\u5757\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u628A\u788E\u7247\u5316\u95F2\u804A\u6574\u7406\u6210\u53EF\u68C0\u7D22\u4E8B\u5B9E\uFF0C\u8865\u5168\u77ED\u6D88\u606F\u3001\u4EE3\u8BCD\u3001\u7F29\u5199\u4E0E\u4E0A\u4E0B\u6587\u4E4B\u95F4\u7684\u5173\u7CFB\u3002\u53EA\u603B\u7ED3\u660E\u786E\u4E8B\u5B9E\uFF0C\u4E0D\u8981\u7F16\u9020\u3002\u4FDD\u7559\u91CD\u8981\u6570\u5B57\u3001\u65E5\u671F\u3001\u94FE\u63A5\u548C\u4EE3\u7801\uFF1B\u5982\u679C\u5185\u5BB9\u50CF\u5BC6\u7801\u3001API key\u3001token \u6216\u5BC6\u94A5\uFF0C\u53EA\u63CF\u8FF0\u5176\u4E0A\u4E0B\u6587\u5173\u7CFB\uFF0C\u4E0D\u8981\u5728\u6458\u8981\u4E2D\u590D\u5199\u539F\u6587\u3002\u6D88\u606F\u91CC\u7684\u201C\u4ECA\u5929\u201D\u201C\u660E\u5929\u201D\u201C\u6628\u665A\u201D\u201C\u4E0B\u5468\u4E09\u201D\u7B49\u76F8\u5BF9\u65F6\u95F4\u8868\u8FF0\uFF0C\u8BF7\u57FA\u4E8E\u6BCF\u6761\u6D88\u606F\u524D\u7684\u53D1\u9001\u65F6\u95F4\u6233\u63A8\u5BFC\u4E3A\u5177\u4F53\u65E5\u671F\u5199\u5165\u6458\u8981\u3002\u4F8B\u5982 [2026-05-05T20:00:00.000Z] \u5988\u5988\u8BF4\u201C\u660E\u5929\u8981\u7528\u4E1D\u4E1D\u9732\u201D\uFF0C\u6458\u8981\u5E94\u5199\u4E3A\u201C2026-05-06 \u8981\u7528\u4E1D\u4E1D\u9732\u201D\u3002"
2622
2622
  },
2623
2623
  {
2624
2624
  role: "user",
2625
- content: `\u7FA4\u804A\uFF1A${window.chatName}
2625
+ content: `\u5F53\u524D\u65F6\u95F4\uFF1A${now.toISOString()}
2626
+ \u7FA4\u804A\uFF1A${window.chatName}
2626
2627
  \u65F6\u95F4\uFF1A${window.startedAt} - ${window.endedAt}
2627
2628
 
2628
2629
  \u804A\u5929\u8BB0\u5F55\uFF1A
@@ -2641,7 +2642,7 @@ async function processEpisodesNow(input2) {
2641
2642
  now: input2.now ?? /* @__PURE__ */ new Date(),
2642
2643
  quietMs: input2.config.episodes.quietMinutes * 60 * 1e3,
2643
2644
  windowMs: input2.config.episodes.windowMinutes * 60 * 1e3,
2644
- summarize: (window) => summarizeEpisodeWindow(window, input2.model)
2645
+ summarize: (window, now) => summarizeEpisodeWindow(window, input2.model, now)
2645
2646
  });
2646
2647
  return { created: created.length };
2647
2648
  }
@@ -3772,7 +3773,7 @@ function stripMentions(text, mentions) {
3772
3773
  }
3773
3774
  return result.replace(/@/g, " ").replace(/\s+/g, " ").trim();
3774
3775
  }
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 FEISHU_TOOL_SYSTEM_PROMPT = "\u4F60\u662F\u98DE\u4E66\u7FA4\u804A\u52A9\u624B\u3002\u4F60\u53EF\u4EE5\u5148\u641C\u7D22\u672C\u5730\u77E5\u8BC6\u6765\u56DE\u7B54\u95EE\u9898\uFF1B\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u521B\u5EFA\u3001\u67E5\u770B\u6216\u5220\u9664\u7FA4\u6D88\u606F\u5B9A\u65F6\u4EFB\u52A1\u65F6\uFF0C\u4E5F\u53EF\u4EE5\u8C03\u7528\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u3002\u5B9A\u65F6\u4EFB\u52A1\u5DE5\u5177\u53EA\u7BA1\u7406\u5F53\u524D\u7FA4\u804A\uFF0C\u4E0D\u80FD\u8DE8\u7FA4\u64CD\u4F5C\u3002\u82E5\u7528\u6237\u7528\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u65F6\u95F4\uFF0C\u4F60\u9700\u8981\u5148\u5C06\u5176\u8F6C\u6362\u4E3A\u4E94\u5B57\u6BB5 cron \u8868\u8FBE\u5F0F\uFF08\u5206 \u65F6 \u65E5 \u6708 \u5468\uFF09\uFF0C\u518D\u8C03\u7528\u5DE5\u5177\u3002\u5F53\u524D\u65F6\u95F4\u4F1A\u63D0\u4F9B\u7ED9\u4F60\u3002\u68C0\u7D22\u8BC1\u636E\u4E2D\u7684\u65F6\u95F4\u6233\u662F\u6D88\u606F\u88AB\u53D1\u9001\u65F6\u7684\u771F\u5B9E\u65F6\u95F4\u3002\u56DE\u7B54\u65F6\u82E5\u6D89\u53CA\u76F8\u5BF9\u65F6\u95F4\u8868\u8FF0\uFF08\u5982\u6D88\u606F\u4E2D\u8BF4\u201C\u660E\u5929\u201D\u201C\u4ECA\u665A\u201D\uFF09\uFF0C\u5FC5\u987B\u57FA\u4E8E\u8BC1\u636E\u4E2D\u6BCF\u6761\u6D88\u606F\u7684\u65F6\u95F4\u6233\u63A8\u5BFC\u4E3A\u5177\u4F53\u65E5\u671F\uFF0C\u4E0D\u8981\u7167\u642C\u539F\u6587\u7684\u76F8\u5BF9\u8868\u8FF0\u3002\u5BF9\u4E8E\u4E00\u822C\u95EE\u7B54\uFF0C\u5148\u6309\u9700\u8C03\u7528\u641C\u7D22\u5DE5\u5177\uFF0C\u518D\u57FA\u4E8E\u5DE5\u5177\u8FD4\u56DE\u7684\u8BC1\u636E\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\uFF1B\u82E5\u5F15\u7528\u4E86\u68C0\u7D22\u7ED3\u679C\uFF0C\u8981\u5728\u7B54\u6848\u91CC\u76F4\u63A5\u5199\u51FA\u5F15\u7528\u5185\u5BB9\u3002\u4E0D\u8981\u58F0\u79F0\u5B8C\u6210\u4E86\u672A\u5B9E\u9645\u8C03\u7528\u7684\u64CD\u4F5C\u3002";
3776
3777
  var DEFAULT_MAX_MODEL_TURNS = 4;
3777
3778
  var DEFAULT_MAX_TOOL_CALLS = 8;
3778
3779
  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";
@@ -3795,7 +3796,8 @@ async function runFeishuToolLoop(input2) {
3795
3796
  const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
3796
3797
  const messages = [
3797
3798
  { role: "system", content: FEISHU_TOOL_SYSTEM_PROMPT },
3798
- { role: "user", content: input2.question }
3799
+ { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
3800
+ \u95EE\u9898\uFF1A${input2.question}` }
3799
3801
  ];
3800
3802
  const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
3801
3803
  let toolCallsUsed = 0;
@@ -3841,7 +3843,15 @@ async function runFeishuToolLoop(input2) {
3841
3843
  }
3842
3844
  }
3843
3845
  }
3844
- return FEISHU_TOOL_LOOP_FALLBACK;
3846
+ try {
3847
+ const salvageAnswer = await input2.model.complete([
3848
+ ...messages,
3849
+ { role: "system", content: "\u8BF7\u57FA\u4E8E\u4EE5\u4E0A\u6240\u6709\u5DE5\u5177\u8FD4\u56DE\u7684\u4FE1\u606F\uFF0C\u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u7B54\u6848\u3002\u4E0D\u8981\u518D\u8C03\u7528\u5DE5\u5177\u3002" }
3850
+ ]);
3851
+ return salvageAnswer || "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
3852
+ } catch {
3853
+ return "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
3854
+ }
3845
3855
  }
3846
3856
  function isMentionForBot(mention, config) {
3847
3857
  if (!config.feishu.botOpenId) {
@@ -3926,6 +3936,7 @@ var FeishuQuestionHandler = class {
3926
3936
  return decision;
3927
3937
  }
3928
3938
  const questionMessageId = payload.event?.message?.message_id;
3939
+ const now = /* @__PURE__ */ new Date();
3929
3940
  const qaLogs = new QaLogRepository(this.options.database);
3930
3941
  await this.acknowledgeQuestion(decision.chatId, questionMessageId);
3931
3942
  const { tools, close } = await createAgenticRagSearchTools({
@@ -3945,6 +3956,7 @@ var FeishuQuestionHandler = class {
3945
3956
  const allTools = [...tools, ...cronTools];
3946
3957
  const answer = await runFeishuToolLoop({
3947
3958
  question: decision.question,
3959
+ now,
3948
3960
  tools: allTools,
3949
3961
  model: this.options.model
3950
3962
  });
@@ -4918,7 +4930,8 @@ function rankEvidenceForPrompt(evidence) {
4918
4930
  return scoreDiff;
4919
4931
  });
4920
4932
  }
4921
- function buildEvidencePrompt(question, evidence, options = {}) {
4933
+ function buildEvidencePrompt(input2, options = {}) {
4934
+ const { question, evidence, now } = input2;
4922
4935
  if (evidence.length === 0) {
4923
4936
  throw new Error("RAG evidence is required before answer generation.");
4924
4937
  }
@@ -4949,11 +4962,12 @@ function buildEvidencePrompt(question, evidence, options = {}) {
4949
4962
  messages: [
4950
4963
  {
4951
4964
  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"
4965
+ 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\u68C0\u7D22\u8BC1\u636E\u4E2D\u7684\u65F6\u95F4\u6233\u662F\u6D88\u606F\u88AB\u53D1\u9001\u65F6\u7684\u771F\u5B9E\u65F6\u95F4\u3002\u56DE\u7B54\u65F6\u82E5\u6D89\u53CA\u76F8\u5BF9\u65F6\u95F4\u8868\u8FF0\uFF08\u5982\u6D88\u606F\u4E2D\u8BF4\u201C\u660E\u5929\u201D\u201C\u4ECA\u665A\u201D\uFF09\uFF0C\u5FC5\u987B\u57FA\u4E8E\u8BC1\u636E\u4E2D\u6BCF\u6761\u6D88\u606F\u7684\u65F6\u95F4\u6233\u63A8\u5BFC\u4E3A\u5177\u4F53\u65E5\u671F\uFF08\u5982\u201C2026-05-06\u201D\uFF09\uFF0C\u4E0D\u8981\u7167\u642C\u539F\u6587\u7684\u76F8\u5BF9\u8868\u8FF0\u3002\u8BC1\u636E\u4E2D\u6BCF\u6761\u6D88\u606F\u6807\u6CE8\u4E86\u53D1\u9001\u65F6\u95F4\u3002\u56DE\u7B54\u65F6\u4F18\u5148\u8F93\u51FA\u7EDD\u5BF9\u65E5\u671F\uFF0C\u4E0D\u786E\u5B9A\u65F6\u5F15\u7528\u539F\u6587\u65F6\u95F4\u6233\uFF0C\u4E0D\u8981\u4F7F\u7528\u201C\u4ECA\u5929\u201D\u201C\u660E\u5929\u201D\u7B49\u4F9D\u8D56\u5F53\u524D\u4E0A\u4E0B\u6587\u7684\u6A21\u7CCA\u8868\u8FF0\u3002"
4953
4966
  },
4954
4967
  {
4955
4968
  role: "user",
4956
- content: `\u95EE\u9898\uFF1A${question}
4969
+ content: `\u5F53\u524D\u65F6\u95F4\uFF1A${now.toISOString()}
4970
+ \u95EE\u9898\uFF1A${question}
4957
4971
 
4958
4972
  \u8BC1\u636E\u5904\u7406\u89C4\u5219\uFF1A
4959
4973
  1. \u5148\u5224\u65AD\u8BC1\u636E\u662F\u5426\u8DB3\u4EE5\u56DE\u7B54\u95EE\u9898\u3002
@@ -4967,7 +4981,7 @@ ${evidenceText}`
4967
4981
  };
4968
4982
  }
4969
4983
  async function generateGroundedAnswer(input2) {
4970
- const prompt = buildEvidencePrompt(input2.question, input2.evidence);
4984
+ const prompt = buildEvidencePrompt({ question: input2.question, evidence: input2.evidence, now: input2.now });
4971
4985
  const answer = await input2.model.complete(prompt.messages);
4972
4986
  return {
4973
4987
  answer,
@@ -4977,6 +4991,7 @@ async function generateGroundedAnswer(input2) {
4977
4991
 
4978
4992
  // src/rag/qa-service.ts
4979
4993
  async function askWithRag(input2) {
4994
+ const now = input2.now ?? /* @__PURE__ */ new Date();
4980
4995
  const evidence = await input2.retriever.retrieve(input2.question);
4981
4996
  if (evidence.length === 0) {
4982
4997
  return {
@@ -4987,7 +5002,8 @@ async function askWithRag(input2) {
4987
5002
  return generateGroundedAnswer({
4988
5003
  question: input2.question,
4989
5004
  evidence,
4990
- model: input2.model
5005
+ model: input2.model,
5006
+ now
4991
5007
  });
4992
5008
  }
4993
5009