openclaw-lark-multi-agent 0.1.6 → 0.1.8

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.
@@ -73,6 +73,7 @@ export declare class FeishuBot {
73
73
  private resolveHumanName;
74
74
  private cleanMentions;
75
75
  private stripLeadingCommandMentions;
76
+ private buildMarkdownCard;
76
77
  private replyMessage;
77
78
  private extractBridgeAttachments;
78
79
  private validateBridgeAttachmentPath;
@@ -344,7 +344,7 @@ export class FeishuBot {
344
344
  // Single slash commands are handled by the bridge. Double slash commands were
345
345
  // already unescaped above and should pass through to OpenClaw instead.
346
346
  const isBridgeCommand = !commandText.startsWith("//");
347
- const isCommand = isBridgeCommand && /^\/(help|status|compact|reset|verbose|free)/.test(cleanText.trim());
347
+ const isCommand = isBridgeCommand && /^\/(help|status|compact|reset|verbose|free|mute|mode)/.test(cleanText.trim());
348
348
  if (isCommand) {
349
349
  // In group chats, bridge commands must be explicitly routed to this bot
350
350
  // or @all. Do not let Free Discussion make a bot execute commands meant
@@ -366,7 +366,9 @@ export class FeishuBot {
366
366
  `🧹 /compact — 压缩当前 bot 的 OpenClaw session`,
367
367
  `🔄 /reset — 重置当前 bot 的 OpenClaw session`,
368
368
  `🔊 /verbose — 开关当前聊天里的 Tool Call 显示`,
369
- `🔓 /free [on|off|status] 开关当前 bot 在当前群聊的 Free Discussion`,
369
+ `🔓 /free 切换当前 bot free 模式(不 @ 也可回复)`,
370
+ `🤐 /mute — 切换当前 bot 的 mute 模式(禁言,不转发 OpenClaw)`,
371
+ `🎛️ /mode — 查看当前 bot 在当前群聊的模式`,
370
372
  `❓ /help — 显示此帮助信息`,
371
373
  ``,
372
374
  `OpenClaw 原生命令(双斜杠,会转成单斜杠发给 OpenClaw)`,
@@ -425,31 +427,63 @@ export class FeishuBot {
425
427
  }
426
428
  if (cleanText.trim().startsWith("/free")) {
427
429
  if (chatType === "p2p") {
428
- await this.replyMessage(messageId, "❌ Free Discussion 只在群聊中可用");
430
+ await this.replyMessage(messageId, "❌ Free 模式只在群聊中可用");
429
431
  markCommandSynced();
430
432
  return;
431
433
  }
432
- const parts = cleanText.trim().split(/\s+/);
433
- const arg = (parts[1] || "").toLowerCase();
434
- const isOn = this.store.getBotFreeDiscussion(this.config.name, chatId);
435
- const next = arg === "on" || arg === "true" || arg === "1"
436
- ? true
437
- : arg === "off" || arg === "false" || arg === "0"
438
- ? false
439
- : arg === "status"
440
- ? isOn
441
- : !isOn;
442
- if (arg !== "status")
443
- this.store.setBotFreeDiscussion(this.config.name, chatId, next);
444
- if (next) {
445
- await this.replyMessage(messageId, `🔓 ${this.config.name} Free Discussion 已开启\n只影响当前 Bot 在当前群聊的自由发言(连续 Bot 回复超过 ${MAX_BOT_STREAK} 轮将暂停,等待人类发言)`);
434
+ const current = this.store.getBotMode(this.config.name, chatId);
435
+ const next = current === "free" ? "normal" : "free";
436
+ this.store.setBotMode(this.config.name, chatId, next);
437
+ if (next === "free") {
438
+ await this.replyMessage(messageId, `🔓 ${this.config.name} 已切换到 free 模式\n不需要 @ 也可以参与回复(连续 Bot 回复超过 ${MAX_BOT_STREAK} 轮将暂停,等待人类发言)`);
446
439
  }
447
440
  else {
448
- await this.replyMessage(messageId, `🔒 ${this.config.name} Free Discussion 已关闭\n只影响当前 Bot;群聊中需要 @ 指定 Bot 才会回复`);
441
+ await this.replyMessage(messageId, `🔒 ${this.config.name} 已切换到 normal 模式\n只有明确 @ 我才会回复`);
449
442
  }
450
443
  markCommandSynced();
451
444
  return;
452
445
  }
446
+ if (cleanText.trim().startsWith("/mute")) {
447
+ if (chatType === "p2p") {
448
+ await this.replyMessage(messageId, "❌ Mute 模式只在群聊中可用");
449
+ markCommandSynced();
450
+ return;
451
+ }
452
+ const current = this.store.getBotMode(this.config.name, chatId);
453
+ const next = current === "mute" ? "normal" : "mute";
454
+ this.store.setBotMode(this.config.name, chatId, next);
455
+ if (next === "mute") {
456
+ await this.replyMessage(messageId, `🤐 ${this.config.name} 已切换到 mute 模式\n普通消息、@所有人 都不会回复;明确 @ 我时只提示禁言中`);
457
+ }
458
+ else {
459
+ await this.replyMessage(messageId, `🔒 ${this.config.name} 已解除 mute,回到 normal 模式\n只有明确 @ 我才会回复`);
460
+ }
461
+ markCommandSynced();
462
+ return;
463
+ }
464
+ if (cleanText.trim().startsWith("/mode")) {
465
+ if (chatType === "p2p") {
466
+ await this.replyMessage(messageId, `🎛️ ${this.config.name} 当前模式:normal(私聊总是响应)`);
467
+ }
468
+ else {
469
+ const mode = this.store.getBotMode(this.config.name, chatId);
470
+ const desc = mode === "free" ? "不需要 @ 也可以参与回复" : mode === "mute" ? "禁言中;明确 @ 我时只提示禁言中" : "只有明确 @ 我才会回复";
471
+ await this.replyMessage(messageId, `🎛️ ${this.config.name} 当前模式:${mode}\n${desc}`);
472
+ }
473
+ markCommandSynced();
474
+ return;
475
+ }
476
+ }
477
+ // --- Mute mode: do not forward anything to OpenClaw. Only direct mentions get a local notice. ---
478
+ if (chatType !== "p2p" && !isBot && this.store.getBotMode(this.config.name, chatId) === "mute") {
479
+ if (this.isMentioned(message.mentions || [])) {
480
+ await this.replyMessage(messageId, `🤐 ${this.config.name} 当前处于 mute 模式,发送 /mute 可解除`);
481
+ if (insertedId > 0) {
482
+ this.store.markSynced(this.config.name, chatId, insertedId);
483
+ this.store.clearPendingTriggers(this.config.name, chatId, insertedId);
484
+ }
485
+ }
486
+ return;
453
487
  }
454
488
  // --- Should this bot respond? ---
455
489
  if (!this.shouldRespond(chatType, message, isBot, chatId, message.content))
@@ -663,9 +697,9 @@ export class FeishuBot {
663
697
  const anyBotMentioned = mentions.some((m) => this.mentionedBotName(m) !== null);
664
698
  if (anyBotMentioned && !this.isMentioned(mentions))
665
699
  return false;
666
- // No bot mentioned: check free discussion mode
700
+ // No bot mentioned: check current per-bot mode
667
701
  if (chatId) {
668
- if (this.store.getBotFreeDiscussion(this.config.name, chatId))
702
+ if (this.store.getBotMode(this.config.name, chatId) === "free")
669
703
  return true;
670
704
  }
671
705
  // Default: don't respond without @
@@ -724,16 +758,23 @@ export class FeishuBot {
724
758
  }
725
759
  return s;
726
760
  }
727
- async replyMessage(messageId, text) {
728
- // Use interactive card for markdown rendering
729
- const card = {
730
- elements: [
731
- {
732
- tag: "markdown",
733
- content: text,
734
- },
735
- ],
761
+ buildMarkdownCard(text) {
762
+ return {
763
+ schema: "2.0",
764
+ config: { wide_screen_mode: true },
765
+ body: {
766
+ elements: [
767
+ {
768
+ tag: "markdown",
769
+ content: text,
770
+ },
771
+ ],
772
+ },
736
773
  };
774
+ }
775
+ async replyMessage(messageId, text) {
776
+ // Use Feishu CardKit v2 markdown component for full Markdown rendering.
777
+ const card = this.buildMarkdownCard(text);
737
778
  try {
738
779
  await this.client.im.message.reply({
739
780
  path: { message_id: messageId },
@@ -889,14 +930,7 @@ ${doc.url}`);
889
930
  * Send a proactive message to a chat (not a reply).
890
931
  */
891
932
  async sendMessage(chatId, text) {
892
- const card = {
893
- elements: [
894
- {
895
- tag: "markdown",
896
- content: text,
897
- },
898
- ],
899
- };
933
+ const card = this.buildMarkdownCard(text);
900
934
  try {
901
935
  await this.client.im.message.create({
902
936
  params: { receive_id_type: "chat_id" },
@@ -1000,7 +1034,7 @@ ${doc.url}`);
1000
1034
  const sessionExists = session ? "✅ 活跃" : "⏳ 未初始化";
1001
1035
  const status = session?.status || "unknown";
1002
1036
  const verboseStatus = this.store.getBotVerbose(this.config.name, chatId) ? "🔊 开启" : "🔇 关闭";
1003
- const freeStatus = this.store.getBotFreeDiscussion(this.config.name, chatId) ? "🔓 开启" : "🔒 关闭";
1037
+ const mode = chatType === "p2p" ? "normal" : this.store.getBotMode(this.config.name, chatId);
1004
1038
  const statusText = [
1005
1039
  `📊 ${this.config.name} Bot Status`,
1006
1040
  `━━━━━━━━━━━━━━━━━━`,
@@ -1013,7 +1047,7 @@ ${doc.url}`);
1013
1047
  `🧮 上下文: ${fmtK(totalTokens)} / ${fmtK(contextTokens)} (${usedPct}%)${tokenNote}`,
1014
1048
  `📥 输入: ${fmtK(inputTokens)} | 📤 输出: ${fmtK(outputTokens)}`,
1015
1049
  `🔧 Verbose: ${verboseStatus}`,
1016
- `💬 Free Discussion: ${freeStatus}`,
1050
+ `🎛️ Mode: ${mode}`,
1017
1051
  ].join("\n");
1018
1052
  await this.replyMessage(messageId, statusText);
1019
1053
  }
@@ -1,3 +1,4 @@
1
+ export type BotChatMode = "normal" | "free" | "mute";
1
2
  export interface ChatInfo {
2
3
  chatId: string;
3
4
  chatType: "p2p" | "group";
@@ -8,7 +9,7 @@ export interface ChatInfo {
8
9
  memberNames: string;
9
10
  /** Which bot owns this chat (for p2p isolation) */
10
11
  ownerBot: string;
11
- /** Free discussion mode (group chat: all bots respond without @) */
12
+ /** Legacy chat-level free discussion flag; per-bot mode is authoritative. */
12
13
  freeDiscussion: boolean;
13
14
  verbose: boolean;
14
15
  updatedAt: number;
@@ -58,6 +59,8 @@ export declare class MessageStore {
58
59
  setVerbose(chatId: string, verbose: boolean): void;
59
60
  setBotVerbose(botName: string, chatId: string, verbose: boolean): void;
60
61
  getBotVerbose(botName: string, chatId: string): boolean;
62
+ setBotMode(botName: string, chatId: string, mode: BotChatMode): void;
63
+ getBotMode(botName: string, chatId: string): BotChatMode;
61
64
  setBotFreeDiscussion(botName: string, chatId: string, on: boolean): void;
62
65
  getBotFreeDiscussion(botName: string, chatId: string): boolean;
63
66
  getChatInfo(chatId: string): ChatInfo | null;
@@ -76,6 +76,7 @@ export class MessageStore {
76
76
  chat_id TEXT NOT NULL,
77
77
  verbose INTEGER NOT NULL DEFAULT 0,
78
78
  free_discussion INTEGER NOT NULL DEFAULT 0,
79
+ mode TEXT NOT NULL DEFAULT 'normal',
79
80
  updated_at INTEGER NOT NULL DEFAULT 0,
80
81
  PRIMARY KEY (bot_name, chat_id)
81
82
  );
@@ -108,6 +109,18 @@ export class MessageStore {
108
109
  catch {
109
110
  // Column already exists
110
111
  }
112
+ // Migration: replace independent free/mute booleans with one mutually exclusive mode.
113
+ try {
114
+ this.db.exec(`ALTER TABLE bot_chat_settings ADD COLUMN mode TEXT NOT NULL DEFAULT 'normal'`);
115
+ }
116
+ catch {
117
+ // Column already exists
118
+ }
119
+ this.db.exec(`
120
+ UPDATE bot_chat_settings
121
+ SET mode = 'free'
122
+ WHERE free_discussion = 1 AND (mode IS NULL OR mode = '' OR mode = 'normal')
123
+ `);
111
124
  }
112
125
  /**
113
126
  * Insert a message. Returns the auto-increment id, or -1 if duplicate.
@@ -278,21 +291,31 @@ export class MessageStore {
278
291
  `).get(botName, chatId);
279
292
  return !!row?.verbose;
280
293
  }
281
- setBotFreeDiscussion(botName, chatId, on) {
294
+ setBotMode(botName, chatId, mode) {
295
+ const freeDiscussion = mode === "free" ? 1 : 0;
282
296
  this.db.prepare(`
283
- INSERT INTO bot_chat_settings (bot_name, chat_id, free_discussion, updated_at)
284
- VALUES (?, ?, ?, ?)
297
+ INSERT INTO bot_chat_settings (bot_name, chat_id, mode, free_discussion, updated_at)
298
+ VALUES (?, ?, ?, ?, ?)
285
299
  ON CONFLICT (bot_name, chat_id) DO UPDATE SET
300
+ mode = excluded.mode,
286
301
  free_discussion = excluded.free_discussion,
287
302
  updated_at = excluded.updated_at
288
- `).run(botName, chatId, on ? 1 : 0, Date.now());
303
+ `).run(botName, chatId, mode, freeDiscussion, Date.now());
289
304
  }
290
- getBotFreeDiscussion(botName, chatId) {
305
+ getBotMode(botName, chatId) {
291
306
  const row = this.db.prepare(`
292
- SELECT free_discussion FROM bot_chat_settings
307
+ SELECT mode, free_discussion FROM bot_chat_settings
293
308
  WHERE bot_name = ? AND chat_id = ?
294
309
  `).get(botName, chatId);
295
- return !!row?.free_discussion;
310
+ if (row?.mode === "free" || row?.mode === "mute" || row?.mode === "normal")
311
+ return row.mode;
312
+ return row?.free_discussion ? "free" : "normal";
313
+ }
314
+ setBotFreeDiscussion(botName, chatId, on) {
315
+ this.setBotMode(botName, chatId, on ? "free" : "normal");
316
+ }
317
+ getBotFreeDiscussion(botName, chatId) {
318
+ return this.getBotMode(botName, chatId) === "free";
296
319
  }
297
320
  getChatInfo(chatId) {
298
321
  const row = this.db.prepare(`SELECT * FROM chat_info WHERE chat_id = ?`).get(chatId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
5
5
  "type": "module",
6
6
  "scripts": {