openclaw-lark-multi-agent 1.0.20 → 1.0.22
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/feishu-bot.d.ts +19 -0
- package/dist/feishu-bot.js +363 -53
- package/dist/live-status.d.ts +56 -0
- package/dist/live-status.js +184 -0
- package/dist/message-store.d.ts +6 -0
- package/dist/message-store.js +38 -0
- package/dist/openclaw-client.d.ts +15 -0
- package/dist/openclaw-client.js +31 -8
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ProgressEvent } from "./openclaw-client";
|
|
2
|
+
export type LiveStatusCallbacks = {
|
|
3
|
+
create: (text: string) => Promise<string | undefined>;
|
|
4
|
+
edit: (messageId: string, text: string) => Promise<void>;
|
|
5
|
+
warn?: (message: string, err?: unknown) => void;
|
|
6
|
+
};
|
|
7
|
+
export type LiveStatusOptions = {
|
|
8
|
+
botName: string;
|
|
9
|
+
locale?: "zh" | "en";
|
|
10
|
+
delayMs?: number;
|
|
11
|
+
maxChars?: number;
|
|
12
|
+
maxEdits?: number;
|
|
13
|
+
disableHint?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare class LiveStatusController {
|
|
16
|
+
private readonly callbacks;
|
|
17
|
+
private readonly opts;
|
|
18
|
+
private messageId?;
|
|
19
|
+
private lastSentText;
|
|
20
|
+
private detail;
|
|
21
|
+
/** Number of edits already issued against the live message (excludes create). */
|
|
22
|
+
private editCount;
|
|
23
|
+
private createTimer?;
|
|
24
|
+
private createPromise?;
|
|
25
|
+
private finalized;
|
|
26
|
+
private disabled;
|
|
27
|
+
/** True after the running-state budget is spent; future progress is ignored. */
|
|
28
|
+
private runningLimitReached;
|
|
29
|
+
constructor(callbacks: LiveStatusCallbacks, opts: LiveStatusOptions);
|
|
30
|
+
get id(): string | undefined;
|
|
31
|
+
start(initialDetail?: string): void;
|
|
32
|
+
progress(event: ProgressEvent | string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Finish the live status when the real (final) reply is delivered separately.
|
|
35
|
+
* The final answer is sent by the normal interactive-card path, so the status
|
|
36
|
+
* message must NOT be overwritten with the answer (text type does not render
|
|
37
|
+
* Markdown). Feishu renders message deletion as a visible "recalled a message"
|
|
38
|
+
* tombstone, so keep the status message and mark it done instead.
|
|
39
|
+
*/
|
|
40
|
+
complete(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Finish the live status when the run failed and the error is delivered by the
|
|
43
|
+
* normal path. Use a neutral/negative marker so it does not contradict the
|
|
44
|
+
* separately delivered error message.
|
|
45
|
+
*/
|
|
46
|
+
fail(): Promise<void>;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
private stopTimers;
|
|
49
|
+
private ensureCreatedNow;
|
|
50
|
+
private ensureCreated;
|
|
51
|
+
private editRunningStatus;
|
|
52
|
+
private safeEdit;
|
|
53
|
+
private formatToolStart;
|
|
54
|
+
private formatStatus;
|
|
55
|
+
private formatLimitNotice;
|
|
56
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const DEFAULT_DELAY_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_DELAY_MS || 800);
|
|
2
|
+
const DEFAULT_MAX_CHARS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_MAX_CHARS || 120);
|
|
3
|
+
// Feishu caps a single message at 20 edits (code=230072 once exhausted). Keep the
|
|
4
|
+
// final edit for the completion marker; running-state updates may consume at most
|
|
5
|
+
// maxEdits - 1, with the last running edit reserved for an explicit limit notice.
|
|
6
|
+
const DEFAULT_MAX_EDITS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_MAX_EDITS || 20);
|
|
7
|
+
export class LiveStatusController {
|
|
8
|
+
callbacks;
|
|
9
|
+
opts;
|
|
10
|
+
messageId;
|
|
11
|
+
lastSentText = "";
|
|
12
|
+
detail = "";
|
|
13
|
+
/** Number of edits already issued against the live message (excludes create). */
|
|
14
|
+
editCount = 0;
|
|
15
|
+
createTimer;
|
|
16
|
+
createPromise;
|
|
17
|
+
finalized = false;
|
|
18
|
+
disabled = false;
|
|
19
|
+
/** True after the running-state budget is spent; future progress is ignored. */
|
|
20
|
+
runningLimitReached = false;
|
|
21
|
+
constructor(callbacks, opts) {
|
|
22
|
+
this.callbacks = callbacks;
|
|
23
|
+
this.opts = opts;
|
|
24
|
+
}
|
|
25
|
+
get id() { return this.messageId; }
|
|
26
|
+
start(initialDetail) {
|
|
27
|
+
if (this.disabled || this.finalized || this.createTimer || this.messageId)
|
|
28
|
+
return;
|
|
29
|
+
this.detail = initialDetail || "";
|
|
30
|
+
this.createTimer = setTimeout(() => {
|
|
31
|
+
this.createTimer = undefined;
|
|
32
|
+
this.createPromise = this.ensureCreated();
|
|
33
|
+
}, this.opts.delayMs ?? DEFAULT_DELAY_MS);
|
|
34
|
+
}
|
|
35
|
+
async progress(event) {
|
|
36
|
+
if (this.disabled || this.finalized || this.runningLimitReached)
|
|
37
|
+
return;
|
|
38
|
+
// Only tool-call start is meaningful enough to spend one of Feishu's limited
|
|
39
|
+
// edit slots. Ignore verbose assistant notes, lifecycle ticks, tool end, and
|
|
40
|
+
// tool error; the final/error answer is delivered by the normal message path.
|
|
41
|
+
if (typeof event !== "string") {
|
|
42
|
+
if (event.kind !== "tool" || event.phase !== "start")
|
|
43
|
+
return;
|
|
44
|
+
this.detail = this.formatToolStart(event);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// String progress is kept for tests/backwards compatibility, but in normal
|
|
48
|
+
// runtime OpenClaw progress is structured and only tool start passes above.
|
|
49
|
+
this.detail = event.trim();
|
|
50
|
+
}
|
|
51
|
+
if (!this.detail)
|
|
52
|
+
return;
|
|
53
|
+
await this.ensureCreatedNow();
|
|
54
|
+
await this.editRunningStatus();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Finish the live status when the real (final) reply is delivered separately.
|
|
58
|
+
* The final answer is sent by the normal interactive-card path, so the status
|
|
59
|
+
* message must NOT be overwritten with the answer (text type does not render
|
|
60
|
+
* Markdown). Feishu renders message deletion as a visible "recalled a message"
|
|
61
|
+
* tombstone, so keep the status message and mark it done instead.
|
|
62
|
+
*/
|
|
63
|
+
async complete() {
|
|
64
|
+
this.finalized = true;
|
|
65
|
+
this.stopTimers();
|
|
66
|
+
if (this.createPromise)
|
|
67
|
+
await this.createPromise.catch(() => { });
|
|
68
|
+
if (!this.messageId || this.disabled)
|
|
69
|
+
return;
|
|
70
|
+
const doneText = this.opts.locale === "en"
|
|
71
|
+
? `\u2705 ${this.opts.botName} done`
|
|
72
|
+
: `\u2705 ${this.opts.botName} \u5df2\u5b8c\u6210`;
|
|
73
|
+
await this.safeEdit(doneText, true);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Finish the live status when the run failed and the error is delivered by the
|
|
77
|
+
* normal path. Use a neutral/negative marker so it does not contradict the
|
|
78
|
+
* separately delivered error message.
|
|
79
|
+
*/
|
|
80
|
+
async fail() {
|
|
81
|
+
this.finalized = true;
|
|
82
|
+
this.stopTimers();
|
|
83
|
+
if (this.createPromise)
|
|
84
|
+
await this.createPromise.catch(() => { });
|
|
85
|
+
if (!this.messageId || this.disabled)
|
|
86
|
+
return;
|
|
87
|
+
const failedText = this.opts.locale === "en"
|
|
88
|
+
? `\u26A0\uFE0F ${this.opts.botName} stopped`
|
|
89
|
+
: `\u26A0\uFE0F ${this.opts.botName} \u6267\u884c\u4e2d\u65ad`;
|
|
90
|
+
await this.safeEdit(failedText, true);
|
|
91
|
+
}
|
|
92
|
+
dispose() {
|
|
93
|
+
this.finalized = true;
|
|
94
|
+
this.stopTimers();
|
|
95
|
+
}
|
|
96
|
+
stopTimers() {
|
|
97
|
+
if (this.createTimer) {
|
|
98
|
+
clearTimeout(this.createTimer);
|
|
99
|
+
this.createTimer = undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async ensureCreatedNow() {
|
|
103
|
+
if (this.createTimer) {
|
|
104
|
+
clearTimeout(this.createTimer);
|
|
105
|
+
this.createTimer = undefined;
|
|
106
|
+
this.createPromise = this.ensureCreated();
|
|
107
|
+
}
|
|
108
|
+
if (this.createPromise)
|
|
109
|
+
await this.createPromise.catch(() => { });
|
|
110
|
+
}
|
|
111
|
+
async ensureCreated() {
|
|
112
|
+
if (this.disabled || this.finalized || this.messageId)
|
|
113
|
+
return;
|
|
114
|
+
const text = this.formatStatus(true);
|
|
115
|
+
try {
|
|
116
|
+
const id = await this.callbacks.create(text);
|
|
117
|
+
if (!id) {
|
|
118
|
+
this.disabled = true;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.messageId = id;
|
|
122
|
+
this.lastSentText = text;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
this.disabled = true;
|
|
126
|
+
this.callbacks.warn?.("live status create failed", err);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async editRunningStatus() {
|
|
130
|
+
if (!this.messageId || this.disabled || this.finalized || this.runningLimitReached)
|
|
131
|
+
return;
|
|
132
|
+
const maxEdits = this.opts.maxEdits ?? DEFAULT_MAX_EDITS;
|
|
133
|
+
const runningBudget = Math.max(0, maxEdits - 1); // reserve final edit for complete()
|
|
134
|
+
if (this.editCount >= runningBudget)
|
|
135
|
+
return;
|
|
136
|
+
const isLimitNotice = this.editCount === runningBudget - 1;
|
|
137
|
+
const text = isLimitNotice ? this.formatLimitNotice() : this.formatStatus();
|
|
138
|
+
const ok = await this.safeEdit(text);
|
|
139
|
+
if (ok && isLimitNotice)
|
|
140
|
+
this.runningLimitReached = true;
|
|
141
|
+
}
|
|
142
|
+
async safeEdit(text, force = false) {
|
|
143
|
+
if (!this.messageId || this.disabled)
|
|
144
|
+
return false;
|
|
145
|
+
if (!force && this.finalized)
|
|
146
|
+
return false;
|
|
147
|
+
if (!force && text === this.lastSentText)
|
|
148
|
+
return true;
|
|
149
|
+
try {
|
|
150
|
+
await this.callbacks.edit(this.messageId, text);
|
|
151
|
+
this.lastSentText = text;
|
|
152
|
+
this.editCount++;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
this.disabled = true;
|
|
157
|
+
this.stopTimers();
|
|
158
|
+
this.callbacks.warn?.("live status edit failed", err);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
formatToolStart(event) {
|
|
163
|
+
const raw = event.text || event.name || "";
|
|
164
|
+
const prefix = `${event.name} start`;
|
|
165
|
+
let detail = raw.startsWith(prefix) ? raw.slice(prefix.length).trim() : raw.trim();
|
|
166
|
+
if (detail.startsWith(":"))
|
|
167
|
+
detail = detail.slice(1).trim();
|
|
168
|
+
return detail ? `${event.name}: ${detail}` : event.name;
|
|
169
|
+
}
|
|
170
|
+
formatStatus(includeDisableHint = false) {
|
|
171
|
+
const maxChars = this.opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
172
|
+
const clean = (this.detail || "").replace(/\s+/g, " ").trim();
|
|
173
|
+
const clipped = clean.length > maxChars ? `${clean.slice(0, Math.max(0, maxChars - 1))}\u2026` : clean;
|
|
174
|
+
const base = !clipped
|
|
175
|
+
? (this.opts.locale === "en" ? `${this.opts.botName} is working` : `${this.opts.botName} \u6b63\u5728\u6267\u884c`)
|
|
176
|
+
: (this.opts.locale === "en" ? `${this.opts.botName} is working: ${clipped}` : `${this.opts.botName} \u6b63\u5728\u6267\u884c\uff1a${clipped}`);
|
|
177
|
+
return includeDisableHint && this.opts.disableHint ? `${base}\n${this.opts.disableHint}` : base;
|
|
178
|
+
}
|
|
179
|
+
formatLimitNotice() {
|
|
180
|
+
return this.opts.locale === "en"
|
|
181
|
+
? `${this.opts.botName} reached the update limit and is still running`
|
|
182
|
+
: `${this.opts.botName} \u5df2\u8fbe\u66f4\u65b0\u9650\u5236\uff0c\u6b63\u5728\u6301\u7eed\u6267\u884c\u4e2d`;
|
|
183
|
+
}
|
|
184
|
+
}
|
package/dist/message-store.d.ts
CHANGED
|
@@ -72,6 +72,10 @@ export declare class MessageStore {
|
|
|
72
72
|
getPendingDeliveries(chatId?: string, botName?: string, maxCount?: number): DeliveryOutboxItem[];
|
|
73
73
|
hasRecentSimilarDelivery(botName: string, chatId: string, contentHash: string, windowMs: number, sourceTypes?: string[]): boolean;
|
|
74
74
|
hasRecentOverlappingDelivery(botName: string, chatId: string, content: string, attachmentsJson: string, windowMs: number, minShortLength?: number, sourceTypes?: string[]): boolean;
|
|
75
|
+
resetStaleDeliveries(botName: string, staleMs: number, maxAttempts: number): {
|
|
76
|
+
restored: number;
|
|
77
|
+
failed: number;
|
|
78
|
+
};
|
|
75
79
|
claimDelivery(id: number): boolean;
|
|
76
80
|
markDeliveryDelivered(id: number): void;
|
|
77
81
|
markDeliveryFailed(id: number): void;
|
|
@@ -115,6 +119,8 @@ export declare class MessageStore {
|
|
|
115
119
|
getChairmanBot(chatId: string): string;
|
|
116
120
|
setBotVerbose(botName: string, chatId: string, verbose: boolean): void;
|
|
117
121
|
getBotVerbose(botName: string, chatId: string): boolean;
|
|
122
|
+
setBotLiveStatus(botName: string, chatId: string, on: boolean): void;
|
|
123
|
+
getBotLiveStatus(botName: string, chatId: string): boolean;
|
|
118
124
|
setBotMode(botName: string, chatId: string, mode: BotChatMode): void;
|
|
119
125
|
getBotMode(botName: string, chatId: string): BotChatMode;
|
|
120
126
|
setBotFreeDiscussion(botName: string, chatId: string, on: boolean): void;
|
package/dist/message-store.js
CHANGED
|
@@ -104,6 +104,7 @@ export class MessageStore {
|
|
|
104
104
|
bot_name TEXT NOT NULL,
|
|
105
105
|
chat_id TEXT NOT NULL,
|
|
106
106
|
verbose INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
live_status INTEGER NOT NULL DEFAULT 1,
|
|
107
108
|
free_discussion INTEGER NOT NULL DEFAULT 0,
|
|
108
109
|
mode TEXT NOT NULL DEFAULT 'normal',
|
|
109
110
|
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
@@ -135,6 +136,13 @@ export class MessageStore {
|
|
|
135
136
|
);
|
|
136
137
|
CREATE INDEX IF NOT EXISTS idx_delivery_outbox_pending ON delivery_outbox(status, created_at);
|
|
137
138
|
`);
|
|
139
|
+
// Migration: add per-bot live status toggle if missing
|
|
140
|
+
try {
|
|
141
|
+
this.db.exec(`ALTER TABLE bot_chat_settings ADD COLUMN live_status INTEGER NOT NULL DEFAULT 1`);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Column already exists
|
|
145
|
+
}
|
|
138
146
|
// Migration: add delivery_outbox delivery key columns if missing
|
|
139
147
|
try {
|
|
140
148
|
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN delivery_key TEXT NOT NULL DEFAULT ''`);
|
|
@@ -436,6 +444,20 @@ export class MessageStore {
|
|
|
436
444
|
}
|
|
437
445
|
return false;
|
|
438
446
|
}
|
|
447
|
+
resetStaleDeliveries(botName, staleMs, maxAttempts) {
|
|
448
|
+
const cutoff = Date.now() - staleMs;
|
|
449
|
+
const fail = this.db.prepare(`
|
|
450
|
+
UPDATE delivery_outbox
|
|
451
|
+
SET status = 'failed', updated_at = ?
|
|
452
|
+
WHERE bot_name = ? AND status = 'delivering' AND updated_at < ? AND attempts >= ?
|
|
453
|
+
`).run(Date.now(), botName, cutoff, maxAttempts);
|
|
454
|
+
const restore = this.db.prepare(`
|
|
455
|
+
UPDATE delivery_outbox
|
|
456
|
+
SET status = 'pending', updated_at = ?
|
|
457
|
+
WHERE bot_name = ? AND status = 'delivering' AND updated_at < ? AND attempts < ?
|
|
458
|
+
`).run(Date.now(), botName, cutoff, maxAttempts);
|
|
459
|
+
return { restored: restore.changes, failed: fail.changes };
|
|
460
|
+
}
|
|
439
461
|
claimDelivery(id) {
|
|
440
462
|
const result = this.db.prepare(`
|
|
441
463
|
UPDATE delivery_outbox SET status = 'delivering', attempts = attempts + 1, updated_at = ? WHERE id = ? AND status = 'pending'
|
|
@@ -680,6 +702,22 @@ export class MessageStore {
|
|
|
680
702
|
`).get(botName, chatId);
|
|
681
703
|
return !!row?.verbose;
|
|
682
704
|
}
|
|
705
|
+
setBotLiveStatus(botName, chatId, on) {
|
|
706
|
+
this.db.prepare(`
|
|
707
|
+
INSERT INTO bot_chat_settings (bot_name, chat_id, live_status, updated_at)
|
|
708
|
+
VALUES (?, ?, ?, ?)
|
|
709
|
+
ON CONFLICT (bot_name, chat_id) DO UPDATE SET
|
|
710
|
+
live_status = excluded.live_status,
|
|
711
|
+
updated_at = excluded.updated_at
|
|
712
|
+
`).run(botName, chatId, on ? 1 : 0, Date.now());
|
|
713
|
+
}
|
|
714
|
+
getBotLiveStatus(botName, chatId) {
|
|
715
|
+
const row = this.db.prepare(`
|
|
716
|
+
SELECT live_status FROM bot_chat_settings
|
|
717
|
+
WHERE bot_name = ? AND chat_id = ?
|
|
718
|
+
`).get(botName, chatId);
|
|
719
|
+
return row ? row.live_status !== 0 : true;
|
|
720
|
+
}
|
|
683
721
|
setBotMode(botName, chatId, mode) {
|
|
684
722
|
const freeDiscussion = mode === "free" ? 1 : 0;
|
|
685
723
|
this.db.prepare(`
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { OpenClawConfig } from "./config.js";
|
|
2
2
|
import { ChatMessage } from "./message-store.js";
|
|
3
|
+
export type ProgressEvent = {
|
|
4
|
+
kind: "tool";
|
|
5
|
+
phase: "start" | "end" | "error";
|
|
6
|
+
name: string;
|
|
7
|
+
text: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "assistant_note";
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "lifecycle";
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
3
15
|
export type ChatAttachment = {
|
|
4
16
|
type?: string;
|
|
5
17
|
mimeType?: string;
|
|
@@ -26,6 +38,7 @@ export declare class OpenClawClient {
|
|
|
26
38
|
private toolEventCallbacks;
|
|
27
39
|
private lastToolNamesByItemId;
|
|
28
40
|
private sessionMessageCallbacks;
|
|
41
|
+
private progressCallbacks;
|
|
29
42
|
/** Sessions where verbose mode should deliver visible transcript text even when mixed with tool blocks. */
|
|
30
43
|
private verboseTranscriptSessions;
|
|
31
44
|
private verboseAssistantTimers;
|
|
@@ -138,6 +151,7 @@ export declare class OpenClawClient {
|
|
|
138
151
|
onSendAttempt?: () => void | Promise<void>;
|
|
139
152
|
/** Called after chat.send RPC succeeds and OpenClaw has accepted the user message. */
|
|
140
153
|
onSubmitted?: (runId: string) => void | Promise<void>;
|
|
154
|
+
onProgress?: (event: ProgressEvent) => void | Promise<void>;
|
|
141
155
|
}): Promise<string>;
|
|
142
156
|
private shouldInjectBridgeAttachmentHint;
|
|
143
157
|
private bridgeAttachmentHint;
|
|
@@ -170,6 +184,7 @@ export declare class OpenClawClient {
|
|
|
170
184
|
onSendAttempt?: () => void | Promise<void>;
|
|
171
185
|
/** Called after chat.send RPC succeeds and OpenClaw has accepted the combined message. */
|
|
172
186
|
onSubmitted?: (runId: string) => void | Promise<void>;
|
|
187
|
+
onProgress?: (event: ProgressEvent) => void | Promise<void>;
|
|
173
188
|
}): Promise<string>;
|
|
174
189
|
private formatContextLines;
|
|
175
190
|
private writeContextSyncFile;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -29,6 +29,7 @@ export class OpenClawClient {
|
|
|
29
29
|
toolEventCallbacks = new Map();
|
|
30
30
|
lastToolNamesByItemId = new Map();
|
|
31
31
|
sessionMessageCallbacks = new Map();
|
|
32
|
+
progressCallbacks = new Map();
|
|
32
33
|
/** Sessions where verbose mode should deliver visible transcript text even when mixed with tool blocks. */
|
|
33
34
|
verboseTranscriptSessions = new Set();
|
|
34
35
|
verboseAssistantTimers = new Map();
|
|
@@ -239,11 +240,17 @@ export class OpenClawClient {
|
|
|
239
240
|
this.flushVerboseAssistantState(rawKey);
|
|
240
241
|
this.clearVerboseAssistantState(rawKey);
|
|
241
242
|
}
|
|
242
|
-
if (
|
|
243
|
+
if (data.kind === "tool" && data.name && (data.phase === "start" || data.phase === "end" || data.phase === "error")) {
|
|
243
244
|
const phase = data.phase || "event";
|
|
244
245
|
const meta = data.meta || data.input || data.args || "";
|
|
245
246
|
const output = data.output || data.result || data.error || "";
|
|
246
|
-
|
|
247
|
+
if (toolCb)
|
|
248
|
+
toolCb(`${data.name} ${phase}`.trim(), String(meta || ""), String(output || ""));
|
|
249
|
+
const progressCb = this.progressCallbacks.get(rawKey) || this.progressCallbacks.get(shortKey);
|
|
250
|
+
if (progressCb) {
|
|
251
|
+
const detail = phase === "start" ? String(meta || "") : String(output || meta || "");
|
|
252
|
+
void progressCb({ kind: "tool", phase, name: String(data.name), text: `${data.name} ${phase}${detail ? `: ${detail}` : ""}` });
|
|
253
|
+
}
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
if (frame.event === "agent" && frame.payload?.stream === "command_output") {
|
|
@@ -348,11 +355,12 @@ export class OpenClawClient {
|
|
|
348
355
|
}
|
|
349
356
|
}
|
|
350
357
|
flushVerboseAssistantState(rawKey) {
|
|
351
|
-
if (!this.isVerboseTranscriptEnabled(rawKey))
|
|
352
|
-
return false;
|
|
353
358
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
354
359
|
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
355
|
-
|
|
360
|
+
const progressCb = this.progressCallbacks.get(rawKey) || this.progressCallbacks.get(shortKey);
|
|
361
|
+
if (!this.isVerboseTranscriptEnabled(rawKey) && !progressCb)
|
|
362
|
+
return false;
|
|
363
|
+
if (!cb && !progressCb)
|
|
356
364
|
return false;
|
|
357
365
|
const key = rawKey;
|
|
358
366
|
const latest = this.verboseAssistantLatest.get(key)?.trim() || "";
|
|
@@ -368,16 +376,22 @@ export class OpenClawClient {
|
|
|
368
376
|
if (!toSend)
|
|
369
377
|
return false;
|
|
370
378
|
this.verboseAssistantSent.set(key, latest);
|
|
371
|
-
|
|
379
|
+
if (this.isVerboseTranscriptEnabled(rawKey) && cb)
|
|
380
|
+
cb(toSend, { sourceType: "verbose_transcript" });
|
|
381
|
+
if (progressCb)
|
|
382
|
+
void progressCb({ kind: "assistant_note", text: toSend });
|
|
372
383
|
return true;
|
|
373
384
|
}
|
|
374
385
|
handleVerboseAssistantStream(payload) {
|
|
375
386
|
const rawKey = payload?.sessionKey || "";
|
|
376
|
-
if (!rawKey
|
|
387
|
+
if (!rawKey)
|
|
377
388
|
return;
|
|
378
389
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
379
390
|
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
380
|
-
|
|
391
|
+
const progressCb = this.progressCallbacks.get(rawKey) || this.progressCallbacks.get(shortKey);
|
|
392
|
+
if (!this.isVerboseTranscriptEnabled(rawKey) && !progressCb)
|
|
393
|
+
return;
|
|
394
|
+
if (!cb && !progressCb)
|
|
381
395
|
return;
|
|
382
396
|
const data = payload.data || {};
|
|
383
397
|
const fullText = typeof data.text === "string" ? data.text : typeof data.deltaText === "string" && data.replace ? data.deltaText : "";
|
|
@@ -1090,6 +1104,11 @@ export class OpenClawClient {
|
|
|
1090
1104
|
}
|
|
1091
1105
|
console.log(`[OpenClaw] chat.send runId: ${result.runId} (rpc=${Date.now() - sendStartedAt}ms, attachments=${params.attachments?.length || 0})`);
|
|
1092
1106
|
await params.onSubmitted?.(result.runId);
|
|
1107
|
+
if (params.onProgress) {
|
|
1108
|
+
this.progressCallbacks.set(sk, params.onProgress);
|
|
1109
|
+
this.progressCallbacks.set(fullSessionKey, params.onProgress);
|
|
1110
|
+
void params.onProgress({ kind: "lifecycle", text: "等待 OpenClaw 回复" });
|
|
1111
|
+
}
|
|
1093
1112
|
return await this.collectReply(result.runId, params.timeoutMs || 1800000, sk, { emptyFinalAsNoReply: params.emptyFinalAsNoReply, expectedUserText: params.message });
|
|
1094
1113
|
}
|
|
1095
1114
|
finally {
|
|
@@ -1097,6 +1116,8 @@ export class OpenClawClient {
|
|
|
1097
1116
|
// collectReply returns. Keep a short grace window so normal chat replies
|
|
1098
1117
|
// are not delivered twice via the proactive-message path. Cron/LMA runs
|
|
1099
1118
|
// are unaffected because they do not go through chatSend.
|
|
1119
|
+
this.progressCallbacks.delete(sk);
|
|
1120
|
+
this.progressCallbacks.delete(fullSessionKey);
|
|
1100
1121
|
this.releaseSuppressedSessionKeysAfter(suppressedKeys, 30000);
|
|
1101
1122
|
this.releaseOwnedDeliverySessionKeysAfter(suppressedKeys, 30000);
|
|
1102
1123
|
}
|
|
@@ -1149,6 +1170,7 @@ export class OpenClawClient {
|
|
|
1149
1170
|
emptyFinalAsNoReply: params.emptyFinalAsNoReply,
|
|
1150
1171
|
onSendAttempt: params.onSendAttempt,
|
|
1151
1172
|
onSubmitted: params.onSubmitted,
|
|
1173
|
+
onProgress: params.onProgress,
|
|
1152
1174
|
});
|
|
1153
1175
|
}
|
|
1154
1176
|
// Build context block + actual message in one chat.send, or write the
|
|
@@ -1184,6 +1206,7 @@ export class OpenClawClient {
|
|
|
1184
1206
|
emptyFinalAsNoReply: params.emptyFinalAsNoReply,
|
|
1185
1207
|
onSendAttempt: params.onSendAttempt,
|
|
1186
1208
|
onSubmitted: params.onSubmitted,
|
|
1209
|
+
onProgress: params.onProgress,
|
|
1187
1210
|
});
|
|
1188
1211
|
}
|
|
1189
1212
|
formatContextLines(messages) {
|