openclaw-lark-multi-agent 1.0.11 → 1.0.13

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.
@@ -45,6 +45,8 @@ export declare class FeishuBot {
45
45
  * Format: lma-<botname>-<chatId>
46
46
  */
47
47
  getSessionKey(chatId: string): string;
48
+ private lmaBridgePolicy;
49
+ private injectBridgePolicy;
48
50
  /**
49
51
  * Ensure the session for a given chatId exists with the correct model.
50
52
  * Lazy: only creates on first message in that chat.
@@ -100,6 +100,23 @@ export class FeishuBot {
100
100
  getSessionKey(chatId) {
101
101
  return `lma-${this.config.name.toLowerCase()}-${chatId}`;
102
102
  }
103
+ lmaBridgePolicy() {
104
+ return [
105
+ "[LMA bridge policy]",
106
+ "你正在 OpenClaw Lark Multi-Agent bridge 会话中。",
107
+ "不要调用 message、sessions_send、feishu_im_user_message 或任何主动向飞书/外部聊天发送消息的工具。",
108
+ "直接在当前回复中作答;LMA bridge 会负责把最终回复投递回原始飞书群。",
109
+ ].join("\n");
110
+ }
111
+ async injectBridgePolicy(sessionKey) {
112
+ await this.openclawClient.injectAssistantMessage({
113
+ sessionKey,
114
+ message: this.lmaBridgePolicy(),
115
+ label: "LMA bridge policy",
116
+ }).catch((err) => {
117
+ console.warn(`[${this.config.name}] bridge policy inject failed:`, err.message);
118
+ });
119
+ }
103
120
  /**
104
121
  * Ensure the session for a given chatId exists with the correct model.
105
122
  * Lazy: only creates on first message in that chat.
@@ -136,6 +153,7 @@ export class FeishuBot {
136
153
  });
137
154
  // Always patch model after create to ensure it takes effect
138
155
  await this.openclawClient.patchSession({ key: sessionKey, model: this.config.model });
156
+ await this.injectBridgePolicy(sessionKey);
139
157
  console.log(`[${this.config.name}] Session created: ${sessionKey} (model: ${this.config.model})`);
140
158
  }
141
159
  }
@@ -43,6 +43,10 @@ export declare class OpenClawClient {
43
43
  private chatSendConcurrency;
44
44
  private activeChatSends;
45
45
  private chatSendWaiters;
46
+ /** Maintenance RPCs like sessions.compact are heavy and should not fan out. */
47
+ private maintenanceConcurrency;
48
+ private activeMaintenanceRpcs;
49
+ private maintenanceWaiters;
46
50
  constructor(config: OpenClawConfig);
47
51
  connect(): Promise<void>;
48
52
  private _doConnect;
@@ -59,6 +63,7 @@ export declare class OpenClawClient {
59
63
  * 30-minute safety net only for catastrophic WS disconnection.
60
64
  */
61
65
  private collectReply;
66
+ private pickBestCollectedText;
62
67
  createSession(params: {
63
68
  key: string;
64
69
  model: string;
@@ -70,6 +75,11 @@ export declare class OpenClawClient {
70
75
  label?: string;
71
76
  }): Promise<any>;
72
77
  getSessionStatus(key: string): Promise<any>;
78
+ injectAssistantMessage(params: {
79
+ sessionKey: string;
80
+ message: string;
81
+ label?: string;
82
+ }): Promise<any>;
73
83
  /**
74
84
  * Get session info (model, tokens, etc.) for status display.
75
85
  */
@@ -101,6 +111,8 @@ export declare class OpenClawClient {
101
111
  muteProactiveDelivery(sessionKey: string): (delayMs?: number) => void;
102
112
  private acquireChatSendSlot;
103
113
  private releaseChatSendSlot;
114
+ private acquireMaintenanceSlot;
115
+ private releaseMaintenanceSlot;
104
116
  chatSend(params: {
105
117
  sessionKey: string;
106
118
  message: string;
@@ -44,6 +44,10 @@ export class OpenClawClient {
44
44
  chatSendConcurrency = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_CHAT_SEND_CONCURRENCY || 3);
45
45
  activeChatSends = 0;
46
46
  chatSendWaiters = [];
47
+ /** Maintenance RPCs like sessions.compact are heavy and should not fan out. */
48
+ maintenanceConcurrency = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_MAINTENANCE_CONCURRENCY || 1);
49
+ activeMaintenanceRpcs = 0;
50
+ maintenanceWaiters = [];
47
51
  constructor(config) {
48
52
  this.config = config;
49
53
  }
@@ -192,7 +196,7 @@ export class OpenClawClient {
192
196
  this.agentEvents.get(rawKey).push({
193
197
  runId: frame.payload.runId,
194
198
  sessionKey: rawKey,
195
- stream: "assistant",
199
+ stream: "transcriptAssistant",
196
200
  data: { deltaText: visibleAssistantText, delta: visibleAssistantText, replace: true },
197
201
  });
198
202
  }
@@ -336,6 +340,7 @@ export class OpenClawClient {
336
340
  let text = "";
337
341
  let chatDeltaText = "";
338
342
  let chatFinalText = "";
343
+ let transcriptAssistantText = "";
339
344
  let sessionKey = targetSessionKey ? `agent:main:${targetSessionKey.replace(/^agent:[^:]+:/, "")}` : "";
340
345
  let shortSessionKey = targetSessionKey ? targetSessionKey.replace(/^agent:[^:]+:/, "") : "";
341
346
  let chatFinalTimer = null;
@@ -374,7 +379,7 @@ export class OpenClawClient {
374
379
  this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
375
380
  console.warn(`[OpenClaw] abort after collectReply idle timeout failed:`, err.message);
376
381
  });
377
- const visibleText = text || chatDeltaText || chatFinalText;
382
+ const visibleText = this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
378
383
  if (visibleText) {
379
384
  resolve(visibleText);
380
385
  }
@@ -399,7 +404,7 @@ export class OpenClawClient {
399
404
  }
400
405
  return `运行事件 item${data.kind ? `/${data.kind}` : ""}`;
401
406
  }
402
- if (ev.stream === "assistant" || ev.stream === "chatDelta") {
407
+ if (ev.stream === "assistant" || ev.stream === "chatDelta" || ev.stream === "transcriptAssistant") {
403
408
  const chunk = String(ev.data?.deltaText || ev.data?.delta || "").trim();
404
409
  return chunk ? `模型输出片段: ${chunk.slice(0, 300)}` : "模型输出片段";
405
410
  }
@@ -550,7 +555,7 @@ export class OpenClawClient {
550
555
  lifecycleStartedLogged = true;
551
556
  console.log(`[OpenClaw] lifecycle start for runId=${runId} after ${Date.now() - collectStartedAt}ms`);
552
557
  }
553
- if ((ev.stream === "assistant" || ev.stream === "chatDelta") && (ev.data?.deltaText || ev.data?.delta)) {
558
+ if ((ev.stream === "assistant" || ev.stream === "chatDelta" || ev.stream === "transcriptAssistant") && (ev.data?.deltaText || ev.data?.delta)) {
554
559
  const chunk = ev.data.deltaText || ev.data.delta;
555
560
  if (ev.stream === "assistant") {
556
561
  if (ev.data?.replace) {
@@ -560,7 +565,7 @@ export class OpenClawClient {
560
565
  text += chunk;
561
566
  }
562
567
  }
563
- else {
568
+ else if (ev.stream === "chatDelta") {
564
569
  if (ev.data?.replace) {
565
570
  chatDeltaText = chunk;
566
571
  }
@@ -568,6 +573,14 @@ export class OpenClawClient {
568
573
  chatDeltaText += chunk;
569
574
  }
570
575
  }
576
+ else {
577
+ if (ev.data?.replace) {
578
+ transcriptAssistantText = chunk;
579
+ }
580
+ else {
581
+ transcriptAssistantText += chunk;
582
+ }
583
+ }
571
584
  }
572
585
  if (ev.stream === "chatFinal") {
573
586
  chatFinalText = ev.data?.text || "";
@@ -580,7 +593,7 @@ export class OpenClawClient {
580
593
  });
581
594
  // Prefer final chat message over accumulated deltas: some providers may
582
595
  // emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
583
- const latestFinalText = chatFinalText || text || chatDeltaText;
596
+ const latestFinalText = this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
584
597
  if (latestFinalText) {
585
598
  finish(latestFinalText);
586
599
  }
@@ -597,9 +610,9 @@ export class OpenClawClient {
597
610
  if (ev.stream === "lifecycle" && ev.data?.phase === "end") {
598
611
  // Prefer final chat message over accumulated deltas: some providers may
599
612
  // emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
600
- const finalText = chatFinalText || text || chatDeltaText;
613
+ const finalText = this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
601
614
  const finishFromLifecycle = () => {
602
- const latestFinalText = chatFinalText || text || chatDeltaText;
615
+ const latestFinalText = this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
603
616
  if (!chatFinalText && latestFinalText.trim() === "N") {
604
617
  // Some providers stream the first character of NO_REPLY ("N") but
605
618
  // never deliver a final chat message in time. Never surface a lone
@@ -662,6 +675,17 @@ export class OpenClawClient {
662
675
  }, 50);
663
676
  });
664
677
  }
678
+ pickBestCollectedText(chatFinalText, assistantText, chatDeltaText, transcriptAssistantText) {
679
+ if (chatFinalText)
680
+ return chatFinalText;
681
+ const candidates = [assistantText, chatDeltaText, transcriptAssistantText].filter((value) => value && value.trim());
682
+ if (candidates.length === 0)
683
+ return "";
684
+ // transcriptAssistant is a full transcript mirror. Keep it separate from
685
+ // streaming deltas to avoid concatenating the same response twice; choose
686
+ // the richest available non-final text as fallback.
687
+ return candidates.sort((a, b) => b.length - a.length)[0];
688
+ }
665
689
  // --- Session management ---
666
690
  async createSession(params) {
667
691
  return this.rpc("sessions.create", params);
@@ -672,6 +696,9 @@ export class OpenClawClient {
672
696
  async getSessionStatus(key) {
673
697
  return this.rpc("sessions.describe", { key });
674
698
  }
699
+ async injectAssistantMessage(params) {
700
+ return this.rpc("chat.inject", params, 10000);
701
+ }
675
702
  /**
676
703
  * Get session info (model, tokens, etc.) for status display.
677
704
  */
@@ -704,7 +731,15 @@ export class OpenClawClient {
704
731
  return this.rpc("sessions.reset", { key }, 5000).catch(() => { });
705
732
  }
706
733
  async compactSession(key) {
707
- return this.rpc("sessions.compact", { key });
734
+ const release = await this.acquireMaintenanceSlot("sessions.compact");
735
+ const startedAt = Date.now();
736
+ try {
737
+ return await this.rpc("sessions.compact", { key }, 10 * 60 * 1000);
738
+ }
739
+ finally {
740
+ console.log(`[OpenClaw] sessions.compact finished for ${key} in ${Date.now() - startedAt}ms`);
741
+ release();
742
+ }
708
743
  }
709
744
  // --- Chat ---
710
745
  /**
@@ -862,6 +897,24 @@ export class OpenClawClient {
862
897
  if (next)
863
898
  next();
864
899
  }
900
+ async acquireMaintenanceSlot(kind) {
901
+ const limit = Math.max(1, this.maintenanceConcurrency || 1);
902
+ if (this.activeMaintenanceRpcs < limit) {
903
+ this.activeMaintenanceRpcs++;
904
+ return () => this.releaseMaintenanceSlot();
905
+ }
906
+ const startedWaitingAt = Date.now();
907
+ await new Promise((resolve) => this.maintenanceWaiters.push(resolve));
908
+ this.activeMaintenanceRpcs++;
909
+ console.log(`[OpenClaw] ${kind} waited ${Date.now() - startedWaitingAt}ms for maintenance slot (active=${this.activeMaintenanceRpcs}/${limit})`);
910
+ return () => this.releaseMaintenanceSlot();
911
+ }
912
+ releaseMaintenanceSlot() {
913
+ this.activeMaintenanceRpcs = Math.max(0, this.activeMaintenanceRpcs - 1);
914
+ const next = this.maintenanceWaiters.shift();
915
+ if (next)
916
+ next();
917
+ }
865
918
  async chatSend(params) {
866
919
  const sk = params.sessionKey;
867
920
  const fullSessionKey = `agent:main:${sk}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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": {