chattercatcher 0.1.29 → 0.1.31

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 fs15 from "fs/promises";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "chattercatcher",
11
- version: "0.1.29",
11
+ version: "0.1.31",
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",
@@ -4019,6 +4019,20 @@ import crypto6 from "crypto";
4019
4019
  function clampLimit(limit) {
4020
4020
  return Math.max(1, Math.min(200, Math.trunc(limit)));
4021
4021
  }
4022
+ function mapQaLogRow(row) {
4023
+ return {
4024
+ id: row.id,
4025
+ chatId: row.chat_id,
4026
+ questionMessageId: row.question_message_id,
4027
+ question: row.question,
4028
+ answer: row.answer,
4029
+ citations: JSON.parse(row.citations_json),
4030
+ retrievalDebug: JSON.parse(row.retrieval_debug_json),
4031
+ status: row.status,
4032
+ error: row.error,
4033
+ createdAt: row.created_at
4034
+ };
4035
+ }
4022
4036
  var QaLogRepository = class {
4023
4037
  constructor(database) {
4024
4038
  this.database = database;
@@ -4097,18 +4111,29 @@ var QaLogRepository = class {
4097
4111
  LIMIT ?
4098
4112
  `
4099
4113
  ).all(clampLimit(limit));
4100
- return rows.map((row) => ({
4101
- id: row.id,
4102
- chatId: row.chat_id,
4103
- questionMessageId: row.question_message_id,
4104
- question: row.question,
4105
- answer: row.answer,
4106
- citations: JSON.parse(row.citations_json),
4107
- retrievalDebug: JSON.parse(row.retrieval_debug_json),
4108
- status: row.status,
4109
- error: row.error,
4110
- createdAt: row.created_at
4111
- }));
4114
+ return rows.map(mapQaLogRow);
4115
+ }
4116
+ listRecentByChat(chatId, limit) {
4117
+ const rows = this.database.prepare(
4118
+ `
4119
+ SELECT
4120
+ id,
4121
+ chat_id,
4122
+ question_message_id,
4123
+ question,
4124
+ answer,
4125
+ citations_json,
4126
+ retrieval_debug_json,
4127
+ status,
4128
+ error,
4129
+ created_at
4130
+ FROM qa_logs
4131
+ WHERE chat_id = ? AND status = 'answered'
4132
+ ORDER BY created_at DESC
4133
+ LIMIT ?
4134
+ `
4135
+ ).all(chatId, clampLimit(limit));
4136
+ return rows.map(mapQaLogRow);
4112
4137
  }
4113
4138
  getCount() {
4114
4139
  const row = this.database.prepare("SELECT COUNT(*) AS count FROM qa_logs").get();
@@ -4180,10 +4205,16 @@ async function runFeishuToolLoop(input2) {
4180
4205
  }
4181
4206
  const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
4182
4207
  const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
4183
- const systemPrompt = input2.memberPrompt ? `${FEISHU_TOOL_SYSTEM_PROMPT}
4184
-
4185
- ${input2.memberPrompt}
4186
- \u56DE\u7B54\u4E2D\u9047\u5230\u4E0A\u8FF0 ID \u65F6\u4F18\u5148\u4F7F\u7528\u5BF9\u5E94\u7FA4\u6635\u79F0\uFF1B\u6CA1\u6709\u6620\u5C04\u65F6\u4FDD\u7559\u539F ID\uFF0C\u4E0D\u8981\u7F16\u9020\u6635\u79F0\u3002` : FEISHU_TOOL_SYSTEM_PROMPT;
4208
+ const systemPromptParts = [FEISHU_TOOL_SYSTEM_PROMPT];
4209
+ if (input2.memberPrompt) {
4210
+ systemPromptParts.push(`${input2.memberPrompt}
4211
+ \u56DE\u7B54\u4E2D\u9047\u5230\u4E0A\u8FF0 ID \u65F6\u4F18\u5148\u4F7F\u7528\u5BF9\u5E94\u7FA4\u6635\u79F0\uFF1B\u6CA1\u6709\u6620\u5C04\u65F6\u4FDD\u7559\u539F ID\uFF0C\u4E0D\u8981\u7F16\u9020\u6635\u79F0\u3002`);
4212
+ }
4213
+ if (input2.conversationContext) {
4214
+ systemPromptParts.push(`${input2.conversationContext}
4215
+ \u8FD9\u4E9B\u662F\u5F53\u524D\u7FA4\u804A\u91CC\u6700\u8FD1\u51E0\u8F6E\u4F60\u548C\u7528\u6237\u7684\u95EE\u7B54\uFF0C\u53EA\u4F5C\u4E3A\u7406\u89E3\u7701\u7565\u6307\u4EE3\u548C\u8FDE\u7EED\u8FFD\u95EE\u7684\u4E0A\u4E0B\u6587\uFF1B\u5982\u679C\u4E0E\u68C0\u7D22\u8BC1\u636E\u51B2\u7A81\uFF0C\u4EE5\u68C0\u7D22\u8BC1\u636E\u4E3A\u51C6\u3002`);
4216
+ }
4217
+ const systemPrompt = systemPromptParts.join("\n\n");
4187
4218
  const messages = [
4188
4219
  { role: "system", content: systemPrompt },
4189
4220
  { role: "user", content: `\u5F53\u524D\u65F6\u95F4\uFF1A${input2.now.toISOString()}
@@ -4247,6 +4278,13 @@ ${input2.memberPrompt}
4247
4278
  return "\u62B1\u6B49\uFF0C\u56DE\u7B54\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
4248
4279
  }
4249
4280
  }
4281
+ function formatConversationContext(records) {
4282
+ const lines = records.slice().reverse().map((record, index2) => `\u7B2C ${index2 + 1} \u8F6E
4283
+ \u7528\u6237\uFF1A${record.question}
4284
+ \u52A9\u624B\uFF1A${record.answer}`);
4285
+ return lines.length ? `\u8FD1\u671F\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1A
4286
+ ${lines.join("\n\n")}` : "";
4287
+ }
4250
4288
  function isMentionForBot(mention, config) {
4251
4289
  if (!config.feishu.botOpenId) {
4252
4290
  return false;
@@ -4356,12 +4394,14 @@ var FeishuQuestionHandler = class {
4356
4394
  const allTools = [...tools, ...cronTools];
4357
4395
  const memberRepository = this.options.memberRepository ?? new FeishuMemberRepository(this.options.database);
4358
4396
  const memberPrompt = formatFeishuMemberPrompt(memberRepository.listByChat(decision.chatId));
4397
+ const conversationContext = formatConversationContext(qaLogs.listRecentByChat(decision.chatId, 6));
4359
4398
  const answer = await runFeishuToolLoop({
4360
4399
  question: decision.question,
4361
4400
  now,
4362
4401
  tools: allTools,
4363
4402
  model: this.options.model,
4364
- memberPrompt
4403
+ memberPrompt,
4404
+ conversationContext
4365
4405
  });
4366
4406
  qaLogs.create({
4367
4407
  chatId: decision.chatId,
@@ -4399,6 +4439,166 @@ var FeishuQuestionHandler = class {
4399
4439
  // src/feishu/sender.ts
4400
4440
  import * as lark from "@larksuiteoapi/node-sdk";
4401
4441
  import fs9 from "fs/promises";
4442
+
4443
+ // src/feishu/markdown-post.ts
4444
+ function escapeAtText(value) {
4445
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4446
+ }
4447
+ function formatTextWithMentions(text, options) {
4448
+ const mentions = options?.mentions ?? [];
4449
+ if (mentions.length === 0) return text;
4450
+ const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
4451
+ return `${prefix} ${text}`.trim();
4452
+ }
4453
+ function findMarkdownLinkEnd(text, start) {
4454
+ let depth = 0;
4455
+ for (let index2 = start; index2 < text.length; index2 += 1) {
4456
+ const char = text[index2];
4457
+ if (char === "(") {
4458
+ depth += 1;
4459
+ } else if (char === ")") {
4460
+ if (depth === 0) return index2;
4461
+ depth -= 1;
4462
+ }
4463
+ }
4464
+ return -1;
4465
+ }
4466
+ function parseInline(text) {
4467
+ const elements = [];
4468
+ let index2 = 0;
4469
+ while (index2 < text.length) {
4470
+ const linkStart = text.indexOf("[", index2);
4471
+ const boldStarStart = text.indexOf("**", index2);
4472
+ const boldUnderscoreStart = text.indexOf("__", index2);
4473
+ const candidates = [linkStart, boldStarStart, boldUnderscoreStart].filter((value) => value >= 0);
4474
+ const next = candidates.length ? Math.min(...candidates) : -1;
4475
+ if (next < 0) {
4476
+ elements.push({ tag: "text", text: text.slice(index2) });
4477
+ break;
4478
+ }
4479
+ if (next > index2) {
4480
+ elements.push({ tag: "text", text: text.slice(index2, next) });
4481
+ }
4482
+ if (next === linkStart) {
4483
+ const labelEnd = text.indexOf("](", next);
4484
+ if (labelEnd > next) {
4485
+ const hrefStart = labelEnd + 2;
4486
+ const hrefEnd = findMarkdownLinkEnd(text, hrefStart);
4487
+ const href = hrefEnd >= 0 ? text.slice(hrefStart, hrefEnd) : "";
4488
+ if (hrefEnd >= 0 && /^https?:\/\/\S+$/.test(href)) {
4489
+ elements.push({ tag: "a", text: text.slice(next + 1, labelEnd), href });
4490
+ index2 = hrefEnd + 1;
4491
+ continue;
4492
+ }
4493
+ }
4494
+ elements.push({ tag: "text", text: text[next] });
4495
+ index2 = next + 1;
4496
+ continue;
4497
+ }
4498
+ const marker = next === boldStarStart ? "**" : "__";
4499
+ const close = text.indexOf(marker, next + marker.length);
4500
+ if (close > next + marker.length) {
4501
+ elements.push({ tag: "text", text: text.slice(next + marker.length, close), style: ["bold"] });
4502
+ index2 = close + marker.length;
4503
+ continue;
4504
+ }
4505
+ elements.push({ tag: "text", text: marker });
4506
+ index2 = next + marker.length;
4507
+ }
4508
+ const compacted = elements.filter((element) => element.tag !== "text" || element.text.length > 0);
4509
+ return compacted.length ? compacted : [{ tag: "text", text: " " }];
4510
+ }
4511
+ function pushParagraph(content, lines) {
4512
+ if (lines.length === 0) return;
4513
+ content.push(parseInline(lines.join("\n")));
4514
+ lines.length = 0;
4515
+ }
4516
+ function parseMarkdownBlocks(markdown) {
4517
+ if (!markdown.trim()) {
4518
+ return [[{ tag: "text", text: " " }]];
4519
+ }
4520
+ const content = [];
4521
+ const paragraph = [];
4522
+ const code = [];
4523
+ let inCodeBlock = false;
4524
+ for (const rawLine of markdown.replace(/\r\n/g, "\n").split("\n")) {
4525
+ const line = rawLine.trimEnd();
4526
+ if (line.startsWith("```")) {
4527
+ if (inCodeBlock) {
4528
+ content.push([{ tag: "text", text: `\`\`\`
4529
+ ${code.join("\n")}
4530
+ \`\`\`` }]);
4531
+ code.length = 0;
4532
+ inCodeBlock = false;
4533
+ } else {
4534
+ pushParagraph(content, paragraph);
4535
+ inCodeBlock = true;
4536
+ }
4537
+ continue;
4538
+ }
4539
+ if (inCodeBlock) {
4540
+ code.push(rawLine);
4541
+ continue;
4542
+ }
4543
+ if (!line.trim()) {
4544
+ pushParagraph(content, paragraph);
4545
+ continue;
4546
+ }
4547
+ const heading = line.match(/^#{1,6}\s+(.+)$/);
4548
+ if (heading) {
4549
+ pushParagraph(content, paragraph);
4550
+ content.push([{ tag: "text", text: heading[1], style: ["bold"] }]);
4551
+ continue;
4552
+ }
4553
+ const unordered = line.match(/^[-*]\s+(.+)$/);
4554
+ if (unordered) {
4555
+ pushParagraph(content, paragraph);
4556
+ content.push(parseInline(`\u2022 ${unordered[1]}`));
4557
+ continue;
4558
+ }
4559
+ const ordered = line.match(/^(\d+)\.\s+(.+)$/);
4560
+ if (ordered) {
4561
+ pushParagraph(content, paragraph);
4562
+ content.push(parseInline(`${ordered[1]}. ${ordered[2]}`));
4563
+ continue;
4564
+ }
4565
+ paragraph.push(line);
4566
+ }
4567
+ if (inCodeBlock) {
4568
+ content.push([{ tag: "text", text: `\`\`\`
4569
+ ${code.join("\n")}` }]);
4570
+ }
4571
+ pushParagraph(content, paragraph);
4572
+ return content.length ? content : [[{ tag: "text", text: markdown }]];
4573
+ }
4574
+ function buildFeishuPostContent(markdown, options) {
4575
+ const content = parseMarkdownBlocks(markdown);
4576
+ const mentions = options?.mentions ?? [];
4577
+ if (mentions.length) {
4578
+ const mentionElements = mentions.map((mention) => ({
4579
+ tag: "at",
4580
+ user_id: mention.openId,
4581
+ user_name: mention.name
4582
+ }));
4583
+ const firstLine = content[0] ?? [];
4584
+ const firstText = firstLine[0];
4585
+ if (firstText?.tag === "text") {
4586
+ content[0] = [...mentionElements, { ...firstText, text: ` ${firstText.text}` }, ...firstLine.slice(1)];
4587
+ } else {
4588
+ content[0] = [...mentionElements, { tag: "text", text: " " }, ...firstLine];
4589
+ }
4590
+ }
4591
+ return {
4592
+ post: {
4593
+ zh_cn: {
4594
+ title: "",
4595
+ content
4596
+ }
4597
+ }
4598
+ };
4599
+ }
4600
+
4601
+ // src/feishu/sender.ts
4402
4602
  function mapDomain(domain) {
4403
4603
  return domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu;
4404
4604
  }
@@ -4414,14 +4614,21 @@ function extractImageKey(response) {
4414
4614
  }
4415
4615
  throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
4416
4616
  }
4417
- function escapeAtText(value) {
4418
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4617
+ function isRichTextCompatibilityError(error) {
4618
+ const value = error && typeof error === "object" ? error : {};
4619
+ const code = value.code ?? value.errorCode;
4620
+ const message = error instanceof Error ? error.message : String(error);
4621
+ return code === 230001 || /post|msg_type|content|unsupported|invalid/i.test(message);
4419
4622
  }
4420
- function formatTextWithMentions(text, options) {
4421
- const mentions = options?.mentions ?? [];
4422
- if (mentions.length === 0) return text;
4423
- const prefix = mentions.map((mention) => `<at user_id="${escapeAtText(mention.openId)}">${escapeAtText(mention.name)}</at>`).join(" ");
4424
- return `${prefix} ${text}`.trim();
4623
+ async function sendWithTextFallback(input2) {
4624
+ try {
4625
+ await input2.sendPost();
4626
+ } catch (error) {
4627
+ if (!isRichTextCompatibilityError(error)) {
4628
+ throw error;
4629
+ }
4630
+ await input2.sendText();
4631
+ }
4425
4632
  }
4426
4633
  var FeishuMessageSender = class _FeishuMessageSender {
4427
4634
  constructor(client) {
@@ -4437,7 +4644,17 @@ var FeishuMessageSender = class _FeishuMessageSender {
4437
4644
  return new _FeishuMessageSender(client);
4438
4645
  }
4439
4646
  async sendTextToChat(chatId, text, options) {
4440
- const payload = {
4647
+ const postPayload = {
4648
+ data: {
4649
+ receive_id: chatId,
4650
+ msg_type: "post",
4651
+ content: JSON.stringify(buildFeishuPostContent(text, options))
4652
+ },
4653
+ params: {
4654
+ receive_id_type: "chat_id"
4655
+ }
4656
+ };
4657
+ const textPayload = {
4441
4658
  data: {
4442
4659
  receive_id: chatId,
4443
4660
  msg_type: "text",
@@ -4448,16 +4665,20 @@ var FeishuMessageSender = class _FeishuMessageSender {
4448
4665
  }
4449
4666
  };
4450
4667
  if (this.client.im.v1?.message.create) {
4451
- await this.client.im.v1.message.create(payload);
4668
+ await sendWithTextFallback({
4669
+ sendPost: () => this.client.im.v1.message.create(postPayload),
4670
+ sendText: () => this.client.im.v1.message.create(textPayload)
4671
+ });
4452
4672
  return;
4453
4673
  }
4454
4674
  if (this.client.im.message?.create) {
4455
- await this.client.im.message.create(payload);
4675
+ await sendWithTextFallback({
4676
+ sendPost: () => this.client.im.message.create(postPayload),
4677
+ sendText: () => this.client.im.message.create(textPayload)
4678
+ });
4456
4679
  return;
4457
4680
  }
4458
- {
4459
- throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
4460
- }
4681
+ throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
4461
4682
  }
4462
4683
  async sendImageToChat(chatId, imagePath) {
4463
4684
  const imageCreate = this.client.im.v1?.image?.create;
@@ -4493,7 +4714,16 @@ var FeishuMessageSender = class _FeishuMessageSender {
4493
4714
  throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u53D1\u9001\u63A5\u53E3\u3002");
4494
4715
  }
4495
4716
  async replyTextToMessage(messageId, text) {
4496
- const payload = {
4717
+ const postPayload = {
4718
+ path: {
4719
+ message_id: messageId
4720
+ },
4721
+ data: {
4722
+ msg_type: "post",
4723
+ content: JSON.stringify(buildFeishuPostContent(text))
4724
+ }
4725
+ };
4726
+ const textPayload = {
4497
4727
  path: {
4498
4728
  message_id: messageId
4499
4729
  },
@@ -4503,11 +4733,17 @@ var FeishuMessageSender = class _FeishuMessageSender {
4503
4733
  }
4504
4734
  };
4505
4735
  if (this.client.im.v1?.message.reply) {
4506
- await this.client.im.v1.message.reply(payload);
4736
+ await sendWithTextFallback({
4737
+ sendPost: () => this.client.im.v1.message.reply(postPayload),
4738
+ sendText: () => this.client.im.v1.message.reply(textPayload)
4739
+ });
4507
4740
  return;
4508
4741
  }
4509
4742
  if (this.client.im.message?.reply) {
4510
- await this.client.im.message.reply(payload);
4743
+ await sendWithTextFallback({
4744
+ sendPost: () => this.client.im.message.reply(postPayload),
4745
+ sendText: () => this.client.im.message.reply(textPayload)
4746
+ });
4511
4747
  return;
4512
4748
  }
4513
4749
  throw new Error("\u5F53\u524D\u98DE\u4E66 SDK \u4E0D\u652F\u6301\u6D88\u606F\u56DE\u590D\u63A5\u53E3\u3002");