openclaw-lark-multi-agent 1.0.1 β†’ 1.0.3

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.
@@ -33,6 +33,8 @@ export declare class FeishuBot {
33
33
  private delayedFailureTimers;
34
34
  /** Last time a real assistant-visible reply was successfully handed to the delivery pipeline. */
35
35
  private lastRealDeliveryAt;
36
+ /** Active chatSend trigger target so final replies and proactive session.message share one delivery key. */
37
+ private activeDeliveryTargets;
36
38
  private adminOpenId;
37
39
  private static allBots;
38
40
  constructor(config: BotConfig, openclawClient: OpenClawClient, store: MessageStore, adminOpenId?: string);
@@ -84,6 +86,7 @@ export declare class FeishuBot {
84
86
  private formatUserVisibleError;
85
87
  private isRuntimeFailureText;
86
88
  private cancelDelayedFailure;
89
+ private setActiveDeliveryTarget;
87
90
  private scheduleDelayedFailure;
88
91
  private deliverySessionKey;
89
92
  private stableHash;
@@ -1,9 +1,12 @@
1
1
  import * as lark from "@larksuiteoapi/node-sdk";
2
+ import { createRequire } from "module";
2
3
  import { existsSync, readFileSync, statSync } from "fs";
3
4
  import { basename, extname, resolve } from "path";
4
5
  import { getBridgeAttachmentAllowedRoots, getBridgeAttachmentsDir } from "./paths.js";
5
6
  import { buildFeishuCardElements } from "./markdown.js";
6
7
  import { discussionManager } from "./discussion-manager.js";
8
+ const require = createRequire(import.meta.url);
9
+ const LMA_VERSION = require("../package.json").version;
7
10
  const MAX_BOT_STREAK = 10;
8
11
  const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
9
12
  const BRIDGE_ATTACHMENT_ALLOWED_ROOTS = getBridgeAttachmentAllowedRoots();
@@ -39,6 +42,8 @@ export class FeishuBot {
39
42
  delayedFailureTimers = new Map();
40
43
  /** Last time a real assistant-visible reply was successfully handed to the delivery pipeline. */
41
44
  lastRealDeliveryAt = new Map();
45
+ /** Active chatSend trigger target so final replies and proactive session.message share one delivery key. */
46
+ activeDeliveryTargets = new Map();
42
47
  adminOpenId;
43
48
  static allBots = new Map();
44
49
  constructor(config, openclawClient, store, adminOpenId) {
@@ -146,7 +151,8 @@ export class FeishuBot {
146
151
  const parsed = this.extractBridgeAttachments(text);
147
152
  if (parsed.text.trim() || parsed.attachments.length > 0)
148
153
  this.cancelDelayedFailure(chatId);
149
- await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("proactive", `${Date.now()}:${Math.random()}:${parsed.text.trim()}|${JSON.stringify(parsed.attachments)}`), parsed.text.trim(), parsed.attachments);
154
+ const activeTarget = this.activeDeliveryTargets.get(chatId);
155
+ await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("proactive", `${Date.now()}:${Math.random()}:${parsed.text.trim()}|${JSON.stringify(parsed.attachments)}`), parsed.text.trim(), parsed.attachments, activeTarget?.messageId, activeTarget ? `trigger:${activeTarget.triggerId}` : undefined);
150
156
  }
151
157
  catch (err) {
152
158
  console.error(`[${this.config.name}] Failed to deliver proactive msg:`, err.message);
@@ -279,8 +285,11 @@ export class FeishuBot {
279
285
  }
280
286
  if (messageType !== "text" && messageType !== "image" && messageType !== "file" && messageType !== "audio" && messageType !== "sticker" && messageType !== "post")
281
287
  return;
282
- // --- Dedup: skip if this bot already processed this message ---
283
- if (this.store.hasBotProcessed(this.config.name, messageId))
288
+ // --- Dedup: atomically claim this message for this bot before any await.
289
+ // Feishu/WebSocket can deliver the same event more than once; a separate
290
+ // has-then-mark sequence races and can send the same user message into
291
+ // OpenClaw twice.
292
+ if (!this.store.tryMarkBotProcessed(this.config.name, messageId))
284
293
  return;
285
294
  // --- Cache chat info (lazy, at most once per hour) ---
286
295
  await this.fetchAndCacheChatInfo(chatId, chatType);
@@ -385,8 +394,6 @@ export class FeishuBot {
385
394
  });
386
395
  if (insertedId < 0)
387
396
  insertedId = this.store.getMessageId(messageId) || -1;
388
- // Mark as processed only after successful parse + insert
389
- this.store.markBotProcessed(this.config.name, messageId);
390
397
  // --- Commands: in p2p always respond; in group, check shouldRespond first ---
391
398
  // Single slash commands are handled by the bridge. Double slash commands were
392
399
  // already unescaped above and should pass through to OpenClaw instead.
@@ -668,16 +675,26 @@ export class FeishuBot {
668
675
  }
669
676
  }
670
677
  try {
671
- const reply = await this.openclawClient.chatSendWithContext({
672
- sessionKey,
673
- unsyncedMessages: contextMsgs,
674
- currentMessage: lastHuman.content,
675
- currentSenderName: lastHuman.senderName,
676
- deliver: false,
677
- // Keep bridge UX responsive; long agent/tool loops should surface a clear failure
678
- // instead of leaving reactions stuck forever.
679
- timeoutMs: 1_800_000,
680
- });
678
+ const releaseActiveDeliveryTarget = lastHuman.messageId ? this.setActiveDeliveryTarget(chatId, triggerId, lastHuman.messageId) : () => { };
679
+ let reply;
680
+ try {
681
+ reply = await this.openclawClient.chatSendWithContext({
682
+ sessionKey,
683
+ unsyncedMessages: contextMsgs,
684
+ currentMessage: lastHuman.content,
685
+ currentSenderName: lastHuman.senderName,
686
+ deliver: false,
687
+ // Keep bridge UX responsive; long agent/tool loops should surface a clear failure
688
+ // instead of leaving reactions stuck forever.
689
+ timeoutMs: 1_800_000,
690
+ });
691
+ }
692
+ finally {
693
+ // OpenClaw may emit the final assistant session.message just after
694
+ // collectReply returns. Keep the trigger mapping briefly so proactive
695
+ // and chat-final paths share the same delivery key.
696
+ releaseActiveDeliveryTarget();
697
+ }
681
698
  console.log(`[${this.config.name}] OpenClaw reply collected for ${chatId.slice(-8)} in ${Date.now() - queueStartedAt}ms`);
682
699
  const parsedReply = this.extractBridgeAttachments(reply);
683
700
  const visibleReply = parsedReply.text;
@@ -933,6 +950,23 @@ export class FeishuBot {
933
950
  clearTimeout(timer);
934
951
  this.delayedFailureTimers.delete(chatId);
935
952
  }
953
+ setActiveDeliveryTarget(chatId, triggerId, messageId) {
954
+ const existing = this.activeDeliveryTargets.get(chatId);
955
+ if (existing?.timer)
956
+ clearTimeout(existing.timer);
957
+ const token = Symbol(`${chatId}:${triggerId}`);
958
+ this.activeDeliveryTargets.set(chatId, { triggerId, messageId, token });
959
+ return () => {
960
+ const timer = setTimeout(() => {
961
+ const current = this.activeDeliveryTargets.get(chatId);
962
+ if (current?.token === token)
963
+ this.activeDeliveryTargets.delete(chatId);
964
+ }, 60_000);
965
+ const current = this.activeDeliveryTargets.get(chatId);
966
+ if (current?.token === token)
967
+ current.timer = timer;
968
+ };
969
+ }
936
970
  scheduleDelayedFailure(chatId, replyToMessageId, text, triggerId) {
937
971
  const lastRealDelivery = this.lastRealDeliveryAt.get(chatId) || 0;
938
972
  if (Date.now() - lastRealDelivery < 90_000) {
@@ -1434,6 +1468,7 @@ export class FeishuBot {
1434
1468
  `πŸ“Š ${this.config.name} Bot Status`,
1435
1469
  `━━━━━━━━━━━━━━━━━━`,
1436
1470
  `πŸ€– Bot: ${this.config.name}`,
1471
+ `🧩 LMA: ${LMA_VERSION}`,
1437
1472
  `🧠 ζ¨‘εž‹: ${model}`,
1438
1473
  `πŸ’¬ 会话: ${chatLabel} (${chatType === "p2p" ? "私聊" : "羀聊"})`,
1439
1474
  `πŸ“‹ Session: ${sessionExists} | ${status}`,
@@ -119,5 +119,6 @@ export declare class MessageStore {
119
119
  * Mark a message as processed by a specific bot.
120
120
  */
121
121
  markBotProcessed(botName: string, messageId: string): void;
122
+ tryMarkBotProcessed(botName: string, messageId: string): boolean;
122
123
  close(): void;
123
124
  }
@@ -625,6 +625,10 @@ export class MessageStore {
625
625
  markBotProcessed(botName, messageId) {
626
626
  this.db.prepare(`INSERT OR IGNORE INTO processed_events (bot_name, message_id) VALUES (?, ?)`).run(botName, messageId);
627
627
  }
628
+ tryMarkBotProcessed(botName, messageId) {
629
+ const result = this.db.prepare(`INSERT OR IGNORE INTO processed_events (bot_name, message_id) VALUES (?, ?)`).run(botName, messageId);
630
+ return result.changes === 1;
631
+ }
628
632
  close() {
629
633
  this.db.close();
630
634
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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": {