openclaw-lark-multi-agent 1.0.4 → 1.0.5
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 +3 -2
- package/dist/feishu-bot.d.ts +1 -0
- package/dist/feishu-bot.js +20 -1
- package/dist/index.js +22 -1
- package/dist/openclaw-client.d.ts +15 -1
- package/dist/openclaw-client.js +136 -17
- package/package.json +1 -1
|
@@ -133,10 +133,11 @@ export class DiscussionManager {
|
|
|
133
133
|
buildPrompt(session) {
|
|
134
134
|
const previous = session.completedRounds.length === 0
|
|
135
135
|
? "(暂无,当前是第一轮)"
|
|
136
|
-
:
|
|
136
|
+
: (() => {
|
|
137
|
+
const round = session.completedRounds[session.completedRounds.length - 1];
|
|
137
138
|
const lines = Object.entries(round.replies).map(([bot, text]) => `- ${bot}: ${text || "NO_REPLY"}`);
|
|
138
139
|
return `Round ${round.round}:\n${lines.join("\n")}`;
|
|
139
|
-
})
|
|
140
|
+
})();
|
|
140
141
|
return [
|
|
141
142
|
"这是一个多智能体结构化讨论。",
|
|
142
143
|
"",
|
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -146,6 +146,7 @@ export declare class FeishuBot {
|
|
|
146
146
|
/**
|
|
147
147
|
* Send a model-drift notification to the affected chat.
|
|
148
148
|
*/
|
|
149
|
+
private hydrateInlineImageKeys;
|
|
149
150
|
/**
|
|
150
151
|
* Download a resource (image/file/audio) from a Feishu message.
|
|
151
152
|
* Returns the local file path.
|
package/dist/feishu-bot.js
CHANGED
|
@@ -360,7 +360,7 @@ export class FeishuBot {
|
|
|
360
360
|
}
|
|
361
361
|
else if (messageType === "post") {
|
|
362
362
|
// Rich text post - extract all text content
|
|
363
|
-
cleanText = this.extractPostText(content);
|
|
363
|
+
cleanText = await this.hydrateInlineImageKeys(this.extractPostText(content), messageId);
|
|
364
364
|
}
|
|
365
365
|
else if (messageType === "sticker") {
|
|
366
366
|
cleanText = `[Sticker: ${content.file_key || "unknown"}]`;
|
|
@@ -1107,6 +1107,7 @@ export class FeishuBot {
|
|
|
1107
1107
|
currentSenderName: "Discussion Scheduler",
|
|
1108
1108
|
deliver: false,
|
|
1109
1109
|
timeoutMs: 1_800_000,
|
|
1110
|
+
emptyFinalAsNoReply: true,
|
|
1110
1111
|
});
|
|
1111
1112
|
}
|
|
1112
1113
|
finally {
|
|
@@ -1588,6 +1589,24 @@ export class FeishuBot {
|
|
|
1588
1589
|
/**
|
|
1589
1590
|
* Send a model-drift notification to the affected chat.
|
|
1590
1591
|
*/
|
|
1592
|
+
async hydrateInlineImageKeys(text, messageId) {
|
|
1593
|
+
const imageKeyPattern = /\[Image: (img_[^\]\n]+)\]/g;
|
|
1594
|
+
const replacements = [];
|
|
1595
|
+
for (const match of text.matchAll(imageKeyPattern)) {
|
|
1596
|
+
const imageKey = match[1];
|
|
1597
|
+
try {
|
|
1598
|
+
const imgPath = await this.downloadResource(messageId, imageKey, "image");
|
|
1599
|
+
replacements.push({ from: match[0], to: `[Image: ${imgPath}]` });
|
|
1600
|
+
}
|
|
1601
|
+
catch (err) {
|
|
1602
|
+
replacements.push({ from: match[0], to: `[Image: download failed - ${err.message}]` });
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
let out = text;
|
|
1606
|
+
for (const r of replacements)
|
|
1607
|
+
out = out.replace(r.from, r.to);
|
|
1608
|
+
return out;
|
|
1609
|
+
}
|
|
1591
1610
|
/**
|
|
1592
1611
|
* Download a resource (image/file/audio) from a Feishu message.
|
|
1593
1612
|
* Returns the local file path.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { loadConfig } from "./config.js";
|
|
|
2
2
|
import { OpenClawClient } from "./openclaw-client.js";
|
|
3
3
|
import { MessageStore } from "./message-store.js";
|
|
4
4
|
import { FeishuBot } from "./feishu-bot.js";
|
|
5
|
-
import { mkdirSync } from "fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
6
|
import { dirname, resolve } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
export async function startApp(configPath) {
|
|
@@ -19,6 +19,22 @@ export async function startApp(configPath) {
|
|
|
19
19
|
: resolve(dirname(resolvedConfigPath), "data");
|
|
20
20
|
mkdirSync(dataDir, { recursive: true });
|
|
21
21
|
console.log(`Data dir: ${dataDir}`);
|
|
22
|
+
const lockPath = resolve(dataDir, "lma.pid");
|
|
23
|
+
if (existsSync(lockPath)) {
|
|
24
|
+
const oldPid = Number(readFileSync(lockPath, "utf8").trim());
|
|
25
|
+
if (Number.isFinite(oldPid) && oldPid > 0) {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(oldPid, 0);
|
|
28
|
+
throw new Error(`Another openclaw-lark-multi-agent instance is already running (pid ${oldPid}). Stop it before starting a new one.`);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const code = err.code;
|
|
32
|
+
if (code !== "ESRCH")
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
writeFileSync(lockPath, `${process.pid}\n`);
|
|
22
38
|
const store = new MessageStore(resolve(dataDir, "messages.db"));
|
|
23
39
|
// Connect to OpenClaw Gateway via WebSocket
|
|
24
40
|
const openclawClient = new OpenClawClient(config.openclaw);
|
|
@@ -39,6 +55,11 @@ export async function startApp(configPath) {
|
|
|
39
55
|
console.log("\nShutting down...");
|
|
40
56
|
openclawClient.disconnect();
|
|
41
57
|
store.close();
|
|
58
|
+
try {
|
|
59
|
+
if (readFileSync(lockPath, "utf8").trim() === String(process.pid))
|
|
60
|
+
rmSync(lockPath, { force: true });
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
42
63
|
process.exit(0);
|
|
43
64
|
};
|
|
44
65
|
process.on("SIGINT", shutdown);
|
|
@@ -6,6 +6,8 @@ export type ChatAttachment = {
|
|
|
6
6
|
fileName?: string;
|
|
7
7
|
content: string;
|
|
8
8
|
};
|
|
9
|
+
export declare const GATEWAY_PROTOCOL_MIN = 3;
|
|
10
|
+
export declare const GATEWAY_PROTOCOL_MAX = 4;
|
|
9
11
|
/**
|
|
10
12
|
* OpenClaw Gateway WebSocket client.
|
|
11
13
|
* Full agent pipeline — tools, memory, skills, context management by OpenClaw.
|
|
@@ -25,9 +27,12 @@ export declare class OpenClawClient {
|
|
|
25
27
|
private sessionMessageCallbacks;
|
|
26
28
|
/** Session keys that should be re-subscribed on reconnect */
|
|
27
29
|
private subscribedKeys;
|
|
28
|
-
/** Session keys
|
|
30
|
+
/** Session keys whose transcript/session.message updates are currently suppressed. */
|
|
29
31
|
private suppressedSessions;
|
|
30
32
|
private suppressedSessionTimers;
|
|
33
|
+
/** Session keys whose delivery is owned by this bridge's chatSend final path. */
|
|
34
|
+
private ownedDeliverySessions;
|
|
35
|
+
private ownedDeliverySessionTimers;
|
|
31
36
|
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
32
37
|
private mutedProactiveSessions;
|
|
33
38
|
private mutedProactiveSessionCounts;
|
|
@@ -73,8 +78,15 @@ export declare class OpenClawClient {
|
|
|
73
78
|
* deliver=false prevents OpenClaw from auto-posting to channels.
|
|
74
79
|
*/
|
|
75
80
|
abortChat(sessionKey: string, runId: string): Promise<any>;
|
|
81
|
+
private sessionKeyVariants;
|
|
82
|
+
private trackChatEventSession;
|
|
83
|
+
private extractTextFromChatMessage;
|
|
84
|
+
private emitProactiveForSession;
|
|
76
85
|
private suppressSessionKeys;
|
|
77
86
|
private releaseSuppressedSessionKeysAfter;
|
|
87
|
+
private ownDeliverySessionKeys;
|
|
88
|
+
private releaseOwnedDeliverySessionKeysAfter;
|
|
89
|
+
private isOwnedDeliverySession;
|
|
78
90
|
private addMutedProactiveKey;
|
|
79
91
|
private releaseMutedProactiveKey;
|
|
80
92
|
muteProactiveDelivery(sessionKey: string): (delayMs?: number) => void;
|
|
@@ -84,6 +96,7 @@ export declare class OpenClawClient {
|
|
|
84
96
|
attachments?: ChatAttachment[];
|
|
85
97
|
deliver?: boolean;
|
|
86
98
|
timeoutMs?: number;
|
|
99
|
+
emptyFinalAsNoReply?: boolean;
|
|
87
100
|
}): Promise<string>;
|
|
88
101
|
private shouldInjectBridgeAttachmentHint;
|
|
89
102
|
private bridgeAttachmentHint;
|
|
@@ -107,6 +120,7 @@ export declare class OpenClawClient {
|
|
|
107
120
|
currentSenderName: string;
|
|
108
121
|
deliver?: boolean;
|
|
109
122
|
timeoutMs?: number;
|
|
123
|
+
emptyFinalAsNoReply?: boolean;
|
|
110
124
|
}): Promise<string>;
|
|
111
125
|
private extractImageAttachments;
|
|
112
126
|
disconnect(): Promise<void>;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -3,6 +3,8 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { basename, extname } from "path";
|
|
5
5
|
import { getBridgeAttachmentsDir } from "./paths.js";
|
|
6
|
+
export const GATEWAY_PROTOCOL_MIN = 3;
|
|
7
|
+
export const GATEWAY_PROTOCOL_MAX = 4;
|
|
6
8
|
const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
|
|
7
9
|
/**
|
|
8
10
|
* OpenClaw Gateway WebSocket client.
|
|
@@ -23,9 +25,12 @@ export class OpenClawClient {
|
|
|
23
25
|
sessionMessageCallbacks = new Map();
|
|
24
26
|
/** Session keys that should be re-subscribed on reconnect */
|
|
25
27
|
subscribedKeys = new Set();
|
|
26
|
-
/** Session keys
|
|
28
|
+
/** Session keys whose transcript/session.message updates are currently suppressed. */
|
|
27
29
|
suppressedSessions = new Set();
|
|
28
30
|
suppressedSessionTimers = new Map();
|
|
31
|
+
/** Session keys whose delivery is owned by this bridge's chatSend final path. */
|
|
32
|
+
ownedDeliverySessions = new Set();
|
|
33
|
+
ownedDeliverySessionTimers = new Map();
|
|
29
34
|
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
30
35
|
mutedProactiveSessions = new Set();
|
|
31
36
|
mutedProactiveSessionCounts = new Map();
|
|
@@ -60,8 +65,8 @@ export class OpenClawClient {
|
|
|
60
65
|
id: "connect-1",
|
|
61
66
|
method: "connect",
|
|
62
67
|
params: {
|
|
63
|
-
minProtocol:
|
|
64
|
-
maxProtocol:
|
|
68
|
+
minProtocol: GATEWAY_PROTOCOL_MIN,
|
|
69
|
+
maxProtocol: GATEWAY_PROTOCOL_MAX,
|
|
65
70
|
client: {
|
|
66
71
|
id: "gateway-client",
|
|
67
72
|
version: "1.0.0",
|
|
@@ -111,8 +116,24 @@ export class OpenClawClient {
|
|
|
111
116
|
// Normalize chat events to look like agent events for collectReply
|
|
112
117
|
if (frame.event === "chat") {
|
|
113
118
|
const state = frame.payload?.state;
|
|
119
|
+
this.trackChatEventSession(sk, state, frame.payload);
|
|
114
120
|
const msg = frame.payload?.message;
|
|
115
|
-
if (state === "
|
|
121
|
+
if (state === "delta") {
|
|
122
|
+
// v4: chat delta events carry deltaText (incremental text chunk)
|
|
123
|
+
const deltaText = frame.payload?.deltaText;
|
|
124
|
+
if (deltaText) {
|
|
125
|
+
this.agentEvents.get(sk).push({
|
|
126
|
+
...frame.payload,
|
|
127
|
+
stream: "chatDelta",
|
|
128
|
+
data: {
|
|
129
|
+
deltaText,
|
|
130
|
+
delta: deltaText, // v3 compat
|
|
131
|
+
replace: frame.payload?.replace || false,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (state === "final") {
|
|
116
137
|
// Store chat final text as fallback (only used if agent stream had no text)
|
|
117
138
|
const textParts = [];
|
|
118
139
|
if (msg?.content) {
|
|
@@ -215,7 +236,8 @@ export class OpenClawClient {
|
|
|
215
236
|
return false;
|
|
216
237
|
}
|
|
217
238
|
if (this.suppressedSessions.has(rawKey) || this.suppressedSessions.has(shortKey)) {
|
|
218
|
-
console.log(`[OpenClaw]
|
|
239
|
+
console.log(`[OpenClaw] Dropping proactive transcript msg for ${shortKey}; waiting for final delivery path`);
|
|
240
|
+
return false;
|
|
219
241
|
}
|
|
220
242
|
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
221
243
|
if (cb)
|
|
@@ -266,9 +288,10 @@ export class OpenClawClient {
|
|
|
266
288
|
* No aggressive timeout — waits for lifecycle end as the source of truth.
|
|
267
289
|
* 30-minute safety net only for catastrophic WS disconnection.
|
|
268
290
|
*/
|
|
269
|
-
collectReply(runId, timeoutMs = 1800000, targetSessionKey) {
|
|
291
|
+
collectReply(runId, timeoutMs = 1800000, targetSessionKey, options) {
|
|
270
292
|
return new Promise((resolve, reject) => {
|
|
271
293
|
let text = "";
|
|
294
|
+
let chatDeltaText = "";
|
|
272
295
|
let chatFinalText = "";
|
|
273
296
|
let sessionKey = targetSessionKey ? `agent:main:${targetSessionKey}` : "";
|
|
274
297
|
let chatFinalTimer = null;
|
|
@@ -292,7 +315,7 @@ export class OpenClawClient {
|
|
|
292
315
|
this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
|
|
293
316
|
console.warn(`[OpenClaw] abort after collectReply idle timeout failed:`, err.message);
|
|
294
317
|
});
|
|
295
|
-
resolve(text || chatFinalText || "(timeout: no reply received)");
|
|
318
|
+
resolve(text || chatDeltaText || chatFinalText || "(timeout: no reply received)");
|
|
296
319
|
}, timeoutMs);
|
|
297
320
|
};
|
|
298
321
|
resetIdleTimer();
|
|
@@ -348,14 +371,23 @@ export class OpenClawClient {
|
|
|
348
371
|
lifecycleStartedLogged = true;
|
|
349
372
|
console.log(`[OpenClaw] lifecycle start for runId=${runId} after ${Date.now() - collectStartedAt}ms`);
|
|
350
373
|
}
|
|
351
|
-
if (ev.stream === "assistant" && (ev.data?.deltaText || ev.data?.delta)) {
|
|
374
|
+
if ((ev.stream === "assistant" || ev.stream === "chatDelta") && (ev.data?.deltaText || ev.data?.delta)) {
|
|
352
375
|
const chunk = ev.data.deltaText || ev.data.delta;
|
|
353
|
-
if (ev.
|
|
354
|
-
|
|
355
|
-
|
|
376
|
+
if (ev.stream === "assistant") {
|
|
377
|
+
if (ev.data?.replace) {
|
|
378
|
+
text = chunk;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
text += chunk;
|
|
382
|
+
}
|
|
356
383
|
}
|
|
357
384
|
else {
|
|
358
|
-
|
|
385
|
+
if (ev.data?.replace) {
|
|
386
|
+
chatDeltaText = chunk;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
chatDeltaText += chunk;
|
|
390
|
+
}
|
|
359
391
|
}
|
|
360
392
|
}
|
|
361
393
|
if (ev.stream === "chatFinal") {
|
|
@@ -369,10 +401,13 @@ export class OpenClawClient {
|
|
|
369
401
|
});
|
|
370
402
|
// Prefer final chat message over accumulated deltas: some providers may
|
|
371
403
|
// emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
|
|
372
|
-
const latestFinalText = chatFinalText || text;
|
|
404
|
+
const latestFinalText = chatFinalText || text || chatDeltaText;
|
|
373
405
|
if (latestFinalText) {
|
|
374
406
|
finish(latestFinalText);
|
|
375
407
|
}
|
|
408
|
+
else if (options?.emptyFinalAsNoReply) {
|
|
409
|
+
finish("NO_REPLY");
|
|
410
|
+
}
|
|
376
411
|
else {
|
|
377
412
|
console.warn(`[OpenClaw] collectReply: empty chatFinal fallback ignored; waiting for real text or idle timeout`);
|
|
378
413
|
chatFinalTimer = null;
|
|
@@ -383,9 +418,9 @@ export class OpenClawClient {
|
|
|
383
418
|
if (ev.stream === "lifecycle" && ev.data?.phase === "end") {
|
|
384
419
|
// Prefer final chat message over accumulated deltas: some providers may
|
|
385
420
|
// emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
|
|
386
|
-
const finalText = chatFinalText || text;
|
|
421
|
+
const finalText = chatFinalText || text || chatDeltaText;
|
|
387
422
|
const finishFromLifecycle = () => {
|
|
388
|
-
const latestFinalText = chatFinalText || text;
|
|
423
|
+
const latestFinalText = chatFinalText || text || chatDeltaText;
|
|
389
424
|
if (!chatFinalText && latestFinalText.trim() === "N") {
|
|
390
425
|
// Some providers stream the first character of NO_REPLY ("N") but
|
|
391
426
|
// never deliver a final chat message in time. Never surface a lone
|
|
@@ -393,6 +428,10 @@ export class OpenClawClient {
|
|
|
393
428
|
finish("NO_REPLY");
|
|
394
429
|
return;
|
|
395
430
|
}
|
|
431
|
+
if (!latestFinalText && options?.emptyFinalAsNoReply) {
|
|
432
|
+
finish("NO_REPLY");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
396
435
|
if (!latestFinalText) {
|
|
397
436
|
const state = ev.data?.livenessState || "unknown";
|
|
398
437
|
const reason = ev.data?.stopReason || "";
|
|
@@ -414,7 +453,7 @@ export class OpenClawClient {
|
|
|
414
453
|
};
|
|
415
454
|
// If lifecycle end beats chat final, a short delta like "N" can be a truncated
|
|
416
455
|
// final reply. Wait for chatFinal before resolving; otherwise suppress lone "N".
|
|
417
|
-
if (!chatFinalText && text.length <= 1) {
|
|
456
|
+
if (!options?.emptyFinalAsNoReply && !chatFinalText && text.length <= 1) {
|
|
418
457
|
lifecycleEndTimer = setTimeout(finishFromLifecycle, 5000);
|
|
419
458
|
}
|
|
420
459
|
else {
|
|
@@ -490,6 +529,53 @@ export class OpenClawClient {
|
|
|
490
529
|
const key = sessionKey.startsWith("agent:main:") ? sessionKey.slice("agent:main:".length) : sessionKey;
|
|
491
530
|
return this.rpc("chat.abort", { sessionKey: key, runId }, 5000).catch(() => { });
|
|
492
531
|
}
|
|
532
|
+
sessionKeyVariants(key) {
|
|
533
|
+
const shortKey = key.startsWith("agent:main:") ? key.slice("agent:main:".length) : key;
|
|
534
|
+
return [shortKey, `agent:main:${shortKey}`];
|
|
535
|
+
}
|
|
536
|
+
trackChatEventSession(sessionKey, state, payload) {
|
|
537
|
+
if (!sessionKey || sessionKey === "__default__")
|
|
538
|
+
return;
|
|
539
|
+
const keys = this.sessionKeyVariants(sessionKey);
|
|
540
|
+
if (this.isOwnedDeliverySession(sessionKey)) {
|
|
541
|
+
// This chat event belongs to a bridge-owned chat.send run. The final answer
|
|
542
|
+
// is delivered by collectReply/processQueue, so transcript session.message
|
|
543
|
+
// mirrors must stay suppressed briefly.
|
|
544
|
+
this.suppressSessionKeys(keys);
|
|
545
|
+
if (state === "final" || state === "error" || state === "aborted")
|
|
546
|
+
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// External WebChat/Control UI chat against an LMA session: do not forward
|
|
550
|
+
// streaming transcript updates, but allow the final chat message to be
|
|
551
|
+
// delivered through the proactive callback after it is committed.
|
|
552
|
+
if (state === "delta") {
|
|
553
|
+
this.suppressSessionKeys(keys);
|
|
554
|
+
}
|
|
555
|
+
else if (state === "final") {
|
|
556
|
+
const text = this.extractTextFromChatMessage(payload?.message);
|
|
557
|
+
if (text)
|
|
558
|
+
this.emitProactiveForSession(sessionKey, text);
|
|
559
|
+
// Keep transcript mirrors suppressed briefly; chat final already emitted
|
|
560
|
+
// the user-visible result for external WebChat/Control UI turns.
|
|
561
|
+
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
562
|
+
}
|
|
563
|
+
else if (state === "error" || state === "aborted") {
|
|
564
|
+
this.releaseSuppressedSessionKeysAfter(keys, 0);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
extractTextFromChatMessage(message) {
|
|
568
|
+
const parts = Array.isArray(message?.content) ? message.content : [];
|
|
569
|
+
return parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
|
|
570
|
+
}
|
|
571
|
+
emitProactiveForSession(sessionKey, text) {
|
|
572
|
+
const [shortKey, fullKey] = this.sessionKeyVariants(sessionKey);
|
|
573
|
+
const cb = this.sessionMessageCallbacks.get(fullKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
574
|
+
if (!cb)
|
|
575
|
+
return false;
|
|
576
|
+
cb(text);
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
493
579
|
suppressSessionKeys(keys) {
|
|
494
580
|
for (const key of keys) {
|
|
495
581
|
const timer = this.suppressedSessionTimers.get(key);
|
|
@@ -511,6 +597,31 @@ export class OpenClawClient {
|
|
|
511
597
|
this.suppressedSessionTimers.set(key, timer);
|
|
512
598
|
}
|
|
513
599
|
}
|
|
600
|
+
ownDeliverySessionKeys(keys) {
|
|
601
|
+
for (const key of keys) {
|
|
602
|
+
const timer = this.ownedDeliverySessionTimers.get(key);
|
|
603
|
+
if (timer)
|
|
604
|
+
clearTimeout(timer);
|
|
605
|
+
this.ownedDeliverySessionTimers.delete(key);
|
|
606
|
+
this.ownedDeliverySessions.add(key);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
releaseOwnedDeliverySessionKeysAfter(keys, delayMs) {
|
|
610
|
+
for (const key of keys) {
|
|
611
|
+
const oldTimer = this.ownedDeliverySessionTimers.get(key);
|
|
612
|
+
if (oldTimer)
|
|
613
|
+
clearTimeout(oldTimer);
|
|
614
|
+
const timer = setTimeout(() => {
|
|
615
|
+
this.ownedDeliverySessions.delete(key);
|
|
616
|
+
this.ownedDeliverySessionTimers.delete(key);
|
|
617
|
+
}, delayMs);
|
|
618
|
+
this.ownedDeliverySessionTimers.set(key, timer);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
isOwnedDeliverySession(sessionKey) {
|
|
622
|
+
const [shortKey, fullKey] = this.sessionKeyVariants(sessionKey);
|
|
623
|
+
return this.ownedDeliverySessions.has(shortKey) || this.ownedDeliverySessions.has(fullKey);
|
|
624
|
+
}
|
|
514
625
|
addMutedProactiveKey(key) {
|
|
515
626
|
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
516
627
|
this.mutedProactiveSessionCounts.set(key, count + 1);
|
|
@@ -552,6 +663,7 @@ export class OpenClawClient {
|
|
|
552
663
|
const fullSessionKey = `agent:main:${sk}`;
|
|
553
664
|
const suppressedKeys = [sk, fullSessionKey];
|
|
554
665
|
this.suppressSessionKeys(suppressedKeys);
|
|
666
|
+
this.ownDeliverySessionKeys(suppressedKeys);
|
|
555
667
|
try {
|
|
556
668
|
// Drop stale buffered events for this session before starting a new run.
|
|
557
669
|
// This prevents an old final text (e.g. previous "ok") from being consumed by
|
|
@@ -567,7 +679,7 @@ export class OpenClawClient {
|
|
|
567
679
|
idempotencyKey: randomUUID(),
|
|
568
680
|
});
|
|
569
681
|
console.log(`[OpenClaw] chat.send runId: ${result.runId} (rpc=${Date.now() - sendStartedAt}ms, attachments=${params.attachments?.length || 0})`);
|
|
570
|
-
return await this.collectReply(result.runId, params.timeoutMs || 1800000, sk);
|
|
682
|
+
return await this.collectReply(result.runId, params.timeoutMs || 1800000, sk, { emptyFinalAsNoReply: params.emptyFinalAsNoReply });
|
|
571
683
|
}
|
|
572
684
|
finally {
|
|
573
685
|
// OpenClaw can emit the final assistant session.message a moment after
|
|
@@ -575,6 +687,7 @@ export class OpenClawClient {
|
|
|
575
687
|
// are not delivered twice via the proactive-message path. Cron/LMA runs
|
|
576
688
|
// are unaffected because they do not go through chatSend.
|
|
577
689
|
this.releaseSuppressedSessionKeysAfter(suppressedKeys, 30000);
|
|
690
|
+
this.releaseOwnedDeliverySessionKeysAfter(suppressedKeys, 30000);
|
|
578
691
|
}
|
|
579
692
|
}
|
|
580
693
|
shouldInjectBridgeAttachmentHint(text) {
|
|
@@ -617,6 +730,7 @@ export class OpenClawClient {
|
|
|
617
730
|
attachments,
|
|
618
731
|
deliver: params.deliver,
|
|
619
732
|
timeoutMs: params.timeoutMs,
|
|
733
|
+
emptyFinalAsNoReply: params.emptyFinalAsNoReply,
|
|
620
734
|
});
|
|
621
735
|
}
|
|
622
736
|
// Build context block + actual message in one chat.send
|
|
@@ -639,6 +753,7 @@ export class OpenClawClient {
|
|
|
639
753
|
attachments,
|
|
640
754
|
deliver: params.deliver,
|
|
641
755
|
timeoutMs: params.timeoutMs,
|
|
756
|
+
emptyFinalAsNoReply: params.emptyFinalAsNoReply,
|
|
642
757
|
});
|
|
643
758
|
}
|
|
644
759
|
extractImageAttachments(contents) {
|
|
@@ -676,6 +791,10 @@ export class OpenClawClient {
|
|
|
676
791
|
this.suppressedSessions.clear();
|
|
677
792
|
this.mutedProactiveSessions.clear();
|
|
678
793
|
this.mutedProactiveSessionCounts.clear();
|
|
794
|
+
for (const timer of this.ownedDeliverySessionTimers.values())
|
|
795
|
+
clearTimeout(timer);
|
|
796
|
+
this.ownedDeliverySessionTimers.clear();
|
|
797
|
+
this.ownedDeliverySessions.clear();
|
|
679
798
|
if (this.ws) {
|
|
680
799
|
this.ws.close();
|
|
681
800
|
this.ws = null;
|