openclaw-lark-multi-agent 0.1.14 → 0.1.15
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/discussion-manager.js +17 -0
- package/dist/feishu-bot.js +39 -18
- package/dist/message-store.d.ts +1 -0
- package/dist/message-store.js +23 -0
- package/dist/openclaw-client.d.ts +8 -1
- package/dist/openclaw-client.js +90 -40
- package/package.json +1 -1
|
@@ -92,10 +92,27 @@ export class DiscussionManager {
|
|
|
92
92
|
replies[value.botName] = value.error ? `[ERROR] ${value.error}` : value.text.trim();
|
|
93
93
|
}
|
|
94
94
|
current.completedRounds.push({ round: current.currentRound, replies });
|
|
95
|
+
const noReplyNames = participants
|
|
96
|
+
.map((participant) => participant.name)
|
|
97
|
+
.filter((name) => {
|
|
98
|
+
const text = (replies[name] || "").trim();
|
|
99
|
+
return !text || text.toUpperCase() === "NO_REPLY";
|
|
100
|
+
});
|
|
101
|
+
const errorNames = participants
|
|
102
|
+
.map((participant) => participant.name)
|
|
103
|
+
.filter((name) => (replies[name] || "").trim().startsWith("[ERROR]"));
|
|
95
104
|
const allNoReply = participants.every((participant) => {
|
|
96
105
|
const text = (replies[participant.name] || "").trim();
|
|
97
106
|
return !text || text.toUpperCase() === "NO_REPLY" || text.startsWith("[ERROR]");
|
|
98
107
|
});
|
|
108
|
+
if (sendSystemMessage && !allNoReply && (noReplyNames.length > 0 || errorNames.length > 0)) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (noReplyNames.length > 0)
|
|
111
|
+
parts.push(`${noReplyNames.join("、")} 无新增回复`);
|
|
112
|
+
if (errorNames.length > 0)
|
|
113
|
+
parts.push(`${errorNames.join("、")} 出错`);
|
|
114
|
+
await sendSystemMessage(`💬 第 ${current.currentRound}/${current.maxRounds} 轮:${parts.join(";")}`).catch(() => { });
|
|
115
|
+
}
|
|
99
116
|
if (allNoReply) {
|
|
100
117
|
current.status = "completed";
|
|
101
118
|
this.sessions.delete(current.chatId);
|
package/dist/feishu-bot.js
CHANGED
|
@@ -503,6 +503,8 @@ export class FeishuBot {
|
|
|
503
503
|
}
|
|
504
504
|
// --- Discuss mode: group-level multi-bot round scheduler. It takes over
|
|
505
505
|
// plain human messages so normal Free mode does not duplicate Round 1.
|
|
506
|
+
// Targeted mentions must fall through to normal routing so @GPT still
|
|
507
|
+
// works while discuss mode is enabled.
|
|
506
508
|
if (chatType !== "p2p" && !isBot && this.store.getChatInfo(chatId)?.discuss) {
|
|
507
509
|
const mentions = message.mentions || [];
|
|
508
510
|
const hasTargetedMention = mentions.some((m) => !this.isAllMentionItem(m));
|
|
@@ -924,11 +926,16 @@ export class FeishuBot {
|
|
|
924
926
|
async enqueueAndDispatchDelivery(chatId, sourceType, sourceId, text, attachments = [], replyToMessageId, deliveryKey) {
|
|
925
927
|
if (!text.trim() && attachments.length === 0)
|
|
926
928
|
return;
|
|
927
|
-
const
|
|
929
|
+
const attachmentsJson = JSON.stringify(attachments);
|
|
930
|
+
const normalizedPayload = `${text.trim()}|${attachmentsJson}`;
|
|
928
931
|
const contentHash = this.stableHash(normalizedPayload);
|
|
929
932
|
const finalDeliveryKey = deliveryKey || sourceId;
|
|
930
|
-
if (!deliveryKey
|
|
931
|
-
|
|
933
|
+
if (!deliveryKey) {
|
|
934
|
+
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000))
|
|
935
|
+
return;
|
|
936
|
+
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8))
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
932
939
|
const deliveryId = this.store.enqueueDelivery({
|
|
933
940
|
sessionKey: this.deliverySessionKey(chatId),
|
|
934
941
|
chatId,
|
|
@@ -938,7 +945,7 @@ export class FeishuBot {
|
|
|
938
945
|
deliveryKey: finalDeliveryKey,
|
|
939
946
|
contentHash,
|
|
940
947
|
content: text,
|
|
941
|
-
attachmentsJson
|
|
948
|
+
attachmentsJson,
|
|
942
949
|
replyToMessageId: replyToMessageId || "",
|
|
943
950
|
});
|
|
944
951
|
if (deliveryId === null)
|
|
@@ -998,22 +1005,36 @@ export class FeishuBot {
|
|
|
998
1005
|
}
|
|
999
1006
|
async runDiscussionTurn(chatId, prompt, meta) {
|
|
1000
1007
|
const sessionKey = await this.ensureSession(chatId);
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1008
|
+
const releaseProactiveMute = this.openclawClient.muteProactiveDelivery(sessionKey);
|
|
1009
|
+
let reply;
|
|
1010
|
+
try {
|
|
1011
|
+
reply = await this.openclawClient.chatSendWithContext({
|
|
1012
|
+
sessionKey,
|
|
1013
|
+
unsyncedMessages: [],
|
|
1014
|
+
currentMessage: prompt,
|
|
1015
|
+
currentSenderName: "Discussion Scheduler",
|
|
1016
|
+
deliver: false,
|
|
1017
|
+
timeoutMs: 1_800_000,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
finally {
|
|
1021
|
+
// OpenClaw can emit the final assistant session.message shortly after
|
|
1022
|
+
// chatSend/collectReply returns. Keep discussion proactive muted briefly;
|
|
1023
|
+
// the discussion coordinator already owns user-visible delivery.
|
|
1024
|
+
releaseProactiveMute(120_000);
|
|
1025
|
+
}
|
|
1009
1026
|
const parsedReply = this.extractBridgeAttachments(reply);
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1027
|
+
const rawVisibleReply = parsedReply.text.trim();
|
|
1028
|
+
const discussionMarkerPattern = /\n*—— 第 \d+\/\d+ 轮 · .+$/;
|
|
1029
|
+
const cleanVisibleReply = rawVisibleReply.replace(discussionMarkerPattern, "").trim();
|
|
1030
|
+
let displayReply = cleanVisibleReply;
|
|
1031
|
+
const isVisible = cleanVisibleReply.length > 0 && cleanVisibleReply.toUpperCase() !== "NO_REPLY";
|
|
1012
1032
|
if (isVisible && meta) {
|
|
1013
|
-
|
|
1033
|
+
const roundMarker = `—— 第 ${meta.round}/${meta.maxRounds} 轮 · ${this.config.name}`;
|
|
1034
|
+
displayReply = `${displayReply}\n\n${roundMarker}`;
|
|
1014
1035
|
}
|
|
1015
1036
|
if (isVisible || parsedReply.attachments.length > 0) {
|
|
1016
|
-
const storedContent = [
|
|
1037
|
+
const storedContent = [displayReply, ...parsedReply.attachments.map((a) => `[Attachment: ${a.type || "file"} ${a.path}]`)]
|
|
1017
1038
|
.filter(Boolean)
|
|
1018
1039
|
.join("\n");
|
|
1019
1040
|
this.store.insert({
|
|
@@ -1024,9 +1045,9 @@ export class FeishuBot {
|
|
|
1024
1045
|
content: storedContent,
|
|
1025
1046
|
timestamp: Date.now(),
|
|
1026
1047
|
});
|
|
1027
|
-
await this.enqueueAndDispatchDelivery(chatId, "discussion", `discussion:${Date.now()}:${Math.random().toString(36).slice(2)}`, isVisible ?
|
|
1048
|
+
await this.enqueueAndDispatchDelivery(chatId, "discussion", `discussion:${Date.now()}:${Math.random().toString(36).slice(2)}`, isVisible ? displayReply : "", parsedReply.attachments);
|
|
1028
1049
|
}
|
|
1029
|
-
return { botName: this.config.name, text:
|
|
1050
|
+
return { botName: this.config.name, text: cleanVisibleReply, visible: isVisible };
|
|
1030
1051
|
}
|
|
1031
1052
|
async replyMessage(messageId, text) {
|
|
1032
1053
|
// Use Feishu CardKit v2 markdown component for full Markdown rendering.
|
package/dist/message-store.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export declare class MessageStore {
|
|
|
61
61
|
getDeliveryBySource(sessionKey: string, sourceType: string, sourceId: string): DeliveryOutboxItem | null;
|
|
62
62
|
getPendingDeliveries(chatId?: string, botName?: string, maxCount?: number): DeliveryOutboxItem[];
|
|
63
63
|
hasRecentSimilarDelivery(botName: string, chatId: string, contentHash: string, windowMs: number): boolean;
|
|
64
|
+
hasRecentOverlappingDelivery(botName: string, chatId: string, content: string, attachmentsJson: string, windowMs: number, minShortLength?: number): boolean;
|
|
64
65
|
claimDelivery(id: number): boolean;
|
|
65
66
|
markDeliveryDelivered(id: number): void;
|
|
66
67
|
markDeliveryFailed(id: number): void;
|
package/dist/message-store.js
CHANGED
|
@@ -304,6 +304,29 @@ export class MessageStore {
|
|
|
304
304
|
`).get(botName, chatId, contentHash, Date.now() - windowMs);
|
|
305
305
|
return !!row;
|
|
306
306
|
}
|
|
307
|
+
hasRecentOverlappingDelivery(botName, chatId, content, attachmentsJson, windowMs, minShortLength = 8) {
|
|
308
|
+
const normalized = content.trim();
|
|
309
|
+
if (normalized.length < minShortLength)
|
|
310
|
+
return false;
|
|
311
|
+
const rows = this.db.prepare(`
|
|
312
|
+
SELECT content, attachments_json FROM delivery_outbox
|
|
313
|
+
WHERE bot_name = ? AND chat_id = ? AND created_at >= ? AND status IN ('pending', 'delivering', 'delivered')
|
|
314
|
+
ORDER BY created_at DESC
|
|
315
|
+
LIMIT 20
|
|
316
|
+
`).all(botName, chatId, Date.now() - windowMs);
|
|
317
|
+
for (const row of rows) {
|
|
318
|
+
if ((row.attachments_json || '[]') !== attachmentsJson)
|
|
319
|
+
continue;
|
|
320
|
+
const existing = String(row.content || '').trim();
|
|
321
|
+
if (existing.length < minShortLength)
|
|
322
|
+
continue;
|
|
323
|
+
const shorter = existing.length <= normalized.length ? existing : normalized;
|
|
324
|
+
const longer = existing.length <= normalized.length ? normalized : existing;
|
|
325
|
+
if (shorter.length >= minShortLength && longer.includes(shorter))
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
307
330
|
claimDelivery(id) {
|
|
308
331
|
const result = this.db.prepare(`
|
|
309
332
|
UPDATE delivery_outbox SET status = 'delivering', attempts = attempts + 1, updated_at = ? WHERE id = ? AND status = 'pending'
|
|
@@ -25,12 +25,16 @@ export declare class OpenClawClient {
|
|
|
25
25
|
private sessionMessageCallbacks;
|
|
26
26
|
/** Session keys that should be re-subscribed on reconnect */
|
|
27
27
|
private subscribedKeys;
|
|
28
|
-
/** Session keys with active/recent chatSend —
|
|
28
|
+
/** Session keys with active/recent chatSend — proactive is forwarded and deduped by outbox. */
|
|
29
29
|
private suppressedSessions;
|
|
30
30
|
private suppressedSessionTimers;
|
|
31
|
+
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
32
|
+
private mutedProactiveSessions;
|
|
33
|
+
private mutedProactiveSessionCounts;
|
|
31
34
|
constructor(config: OpenClawConfig);
|
|
32
35
|
connect(): Promise<void>;
|
|
33
36
|
private _doConnect;
|
|
37
|
+
private handleProactiveSessionMessage;
|
|
34
38
|
private scheduleReconnect;
|
|
35
39
|
private rpc;
|
|
36
40
|
/**
|
|
@@ -71,6 +75,9 @@ export declare class OpenClawClient {
|
|
|
71
75
|
abortChat(sessionKey: string, runId: string): Promise<any>;
|
|
72
76
|
private suppressSessionKeys;
|
|
73
77
|
private releaseSuppressedSessionKeysAfter;
|
|
78
|
+
private addMutedProactiveKey;
|
|
79
|
+
private releaseMutedProactiveKey;
|
|
80
|
+
muteProactiveDelivery(sessionKey: string): (delayMs?: number) => void;
|
|
74
81
|
chatSend(params: {
|
|
75
82
|
sessionKey: string;
|
|
76
83
|
message: string;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -23,9 +23,12 @@ export class OpenClawClient {
|
|
|
23
23
|
sessionMessageCallbacks = new Map();
|
|
24
24
|
/** Session keys that should be re-subscribed on reconnect */
|
|
25
25
|
subscribedKeys = new Set();
|
|
26
|
-
/** Session keys with active/recent chatSend —
|
|
26
|
+
/** Session keys with active/recent chatSend — proactive is forwarded and deduped by outbox. */
|
|
27
27
|
suppressedSessions = new Set();
|
|
28
28
|
suppressedSessionTimers = new Map();
|
|
29
|
+
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
30
|
+
mutedProactiveSessions = new Set();
|
|
31
|
+
mutedProactiveSessionCounts = new Map();
|
|
29
32
|
constructor(config) {
|
|
30
33
|
this.config = config;
|
|
31
34
|
}
|
|
@@ -140,45 +143,7 @@ export class OpenClawClient {
|
|
|
140
143
|
// Try both raw key and without agent:main: prefix
|
|
141
144
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
142
145
|
const msg = frame.payload.message || frame.payload;
|
|
143
|
-
|
|
144
|
-
const content = msg.content;
|
|
145
|
-
// Proactive assistant text messages (suppress during active chatSend).
|
|
146
|
-
// Cron/session-targeted runs often emit structured content arrays rather
|
|
147
|
-
// than a plain string. Extract only visible text parts and ignore thinking
|
|
148
|
-
// / tool blocks so the bridge can deliver final cron results via the bot.
|
|
149
|
-
let proactiveText = "";
|
|
150
|
-
if (role === "assistant") {
|
|
151
|
-
if (typeof content === "string") {
|
|
152
|
-
proactiveText = content;
|
|
153
|
-
}
|
|
154
|
-
else if (Array.isArray(content)) {
|
|
155
|
-
const hasToolBlock = content.some((part) => {
|
|
156
|
-
const type = String(part?.type || "").toLowerCase();
|
|
157
|
-
return type === "toolcall" || type === "tool_call" || type === "tooluse" || type === "tool_use" || type === "toolresult" || type === "tool_result";
|
|
158
|
-
});
|
|
159
|
-
// Do not deliver mixed text+toolCall assistant messages through
|
|
160
|
-
// the proactive final-text path; those are usually intermediate
|
|
161
|
-
// reasoning/status during a tool loop. Tool calls are still
|
|
162
|
-
// delivered via the verbose channel from agent item events when
|
|
163
|
-
// /verbose is enabled. Cron final messages arrive as text-only
|
|
164
|
-
// (optionally with thinking).
|
|
165
|
-
if (!hasToolBlock) {
|
|
166
|
-
proactiveText = content
|
|
167
|
-
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
168
|
-
.map((part) => part.text)
|
|
169
|
-
.join("\n")
|
|
170
|
-
.trim();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (proactiveText) {
|
|
175
|
-
if (this.suppressedSessions.has(rawKey) || this.suppressedSessions.has(shortKey)) {
|
|
176
|
-
console.log(`[OpenClaw] Forwarding proactive msg for ${shortKey} during active chatSend; delivery outbox will dedupe`);
|
|
177
|
-
}
|
|
178
|
-
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
179
|
-
if (cb)
|
|
180
|
-
cb(proactiveText);
|
|
181
|
-
}
|
|
146
|
+
this.handleProactiveSessionMessage(rawKey, msg);
|
|
182
147
|
// Tool calls in assistant messages — skip, using agent item events instead
|
|
183
148
|
// (session.message toolCall events are batched, not real-time)
|
|
184
149
|
}
|
|
@@ -210,6 +175,53 @@ export class OpenClawClient {
|
|
|
210
175
|
});
|
|
211
176
|
});
|
|
212
177
|
}
|
|
178
|
+
handleProactiveSessionMessage(rawKey, msg) {
|
|
179
|
+
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
180
|
+
const role = msg.role;
|
|
181
|
+
const content = msg.content;
|
|
182
|
+
// Proactive assistant text messages. Cron/session-targeted runs often emit
|
|
183
|
+
// structured content arrays rather than a plain string. Extract only visible
|
|
184
|
+
// text parts and ignore thinking/tool blocks so the bridge can deliver final
|
|
185
|
+
// cron results via the bot.
|
|
186
|
+
let proactiveText = "";
|
|
187
|
+
if (role === "assistant") {
|
|
188
|
+
if (typeof content === "string") {
|
|
189
|
+
proactiveText = content;
|
|
190
|
+
}
|
|
191
|
+
else if (Array.isArray(content)) {
|
|
192
|
+
const hasToolBlock = content.some((part) => {
|
|
193
|
+
const type = String(part?.type || "").toLowerCase();
|
|
194
|
+
return type === "toolcall" || type === "tool_call" || type === "tooluse" || type === "tool_use" || type === "toolresult" || type === "tool_result";
|
|
195
|
+
});
|
|
196
|
+
// Do not deliver mixed text+toolCall assistant messages through
|
|
197
|
+
// the proactive final-text path; those are usually intermediate
|
|
198
|
+
// reasoning/status during a tool loop. Tool calls are still
|
|
199
|
+
// delivered via the verbose channel from agent item events when
|
|
200
|
+
// /verbose is enabled. Cron final messages arrive as text-only
|
|
201
|
+
// (optionally with thinking).
|
|
202
|
+
if (!hasToolBlock) {
|
|
203
|
+
proactiveText = content
|
|
204
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
205
|
+
.map((part) => part.text)
|
|
206
|
+
.join("\n")
|
|
207
|
+
.trim();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!proactiveText)
|
|
212
|
+
return false;
|
|
213
|
+
if (this.mutedProactiveSessions.has(rawKey) || this.mutedProactiveSessions.has(shortKey)) {
|
|
214
|
+
console.log(`[OpenClaw] Dropping proactive msg for ${shortKey}; delivery is owned by the caller`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (this.suppressedSessions.has(rawKey) || this.suppressedSessions.has(shortKey)) {
|
|
218
|
+
console.log(`[OpenClaw] Forwarding proactive msg for ${shortKey} during active chatSend; delivery outbox will dedupe`);
|
|
219
|
+
}
|
|
220
|
+
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
221
|
+
if (cb)
|
|
222
|
+
cb(proactiveText);
|
|
223
|
+
return Boolean(cb);
|
|
224
|
+
}
|
|
213
225
|
scheduleReconnect() {
|
|
214
226
|
const delay = this.reconnectDelay;
|
|
215
227
|
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
@@ -492,6 +504,42 @@ export class OpenClawClient {
|
|
|
492
504
|
this.suppressedSessionTimers.set(key, timer);
|
|
493
505
|
}
|
|
494
506
|
}
|
|
507
|
+
addMutedProactiveKey(key) {
|
|
508
|
+
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
509
|
+
this.mutedProactiveSessionCounts.set(key, count + 1);
|
|
510
|
+
this.mutedProactiveSessions.add(key);
|
|
511
|
+
}
|
|
512
|
+
releaseMutedProactiveKey(key) {
|
|
513
|
+
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
514
|
+
if (count <= 1) {
|
|
515
|
+
this.mutedProactiveSessionCounts.delete(key);
|
|
516
|
+
this.mutedProactiveSessions.delete(key);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
this.mutedProactiveSessionCounts.set(key, count - 1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
muteProactiveDelivery(sessionKey) {
|
|
523
|
+
const shortKey = sessionKey.startsWith("agent:main:") ? sessionKey.slice("agent:main:".length) : sessionKey;
|
|
524
|
+
const fullKey = `agent:main:${shortKey}`;
|
|
525
|
+
const keys = [shortKey, fullKey];
|
|
526
|
+
for (const key of keys)
|
|
527
|
+
this.addMutedProactiveKey(key);
|
|
528
|
+
let released = false;
|
|
529
|
+
return (delayMs = 0) => {
|
|
530
|
+
if (released)
|
|
531
|
+
return;
|
|
532
|
+
released = true;
|
|
533
|
+
const release = () => {
|
|
534
|
+
for (const key of keys)
|
|
535
|
+
this.releaseMutedProactiveKey(key);
|
|
536
|
+
};
|
|
537
|
+
if (delayMs > 0)
|
|
538
|
+
setTimeout(release, delayMs);
|
|
539
|
+
else
|
|
540
|
+
release();
|
|
541
|
+
};
|
|
542
|
+
}
|
|
495
543
|
async chatSend(params) {
|
|
496
544
|
const sk = params.sessionKey;
|
|
497
545
|
const fullSessionKey = `agent:main:${sk}`;
|
|
@@ -619,6 +667,8 @@ export class OpenClawClient {
|
|
|
619
667
|
clearTimeout(timer);
|
|
620
668
|
this.suppressedSessionTimers.clear();
|
|
621
669
|
this.suppressedSessions.clear();
|
|
670
|
+
this.mutedProactiveSessions.clear();
|
|
671
|
+
this.mutedProactiveSessionCounts.clear();
|
|
622
672
|
if (this.ws) {
|
|
623
673
|
this.ws.close();
|
|
624
674
|
this.ws = null;
|