openclaw-lark-multi-agent 1.0.21 → 1.0.23
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 +29 -1
- package/dist/feishu-bot.js +297 -52
- package/dist/live-status.d.ts +56 -0
- package/dist/live-status.js +184 -0
- package/dist/message-store.d.ts +8 -0
- package/dist/message-store.js +81 -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
|
@@ -70,6 +70,12 @@ export declare class MessageStore {
|
|
|
70
70
|
enqueueDelivery(item: Omit<DeliveryOutboxItem, "id" | "status" | "attempts" | "createdAt" | "updatedAt">): number | null;
|
|
71
71
|
getDeliveryBySource(sessionKey: string, sourceType: string, sourceId: string): DeliveryOutboxItem | null;
|
|
72
72
|
getPendingDeliveries(chatId?: string, botName?: string, maxCount?: number): DeliveryOutboxItem[];
|
|
73
|
+
markBotSeenInChat(botName: string, chatId: string): void;
|
|
74
|
+
hasBotSeenInChat(botName: string, chatId: string): boolean;
|
|
75
|
+
markBotUnavailableInChat(botName: string, chatId: string, reason?: string): void;
|
|
76
|
+
clearBotUnavailableInChat(botName: string, chatId: string): void;
|
|
77
|
+
isBotUnavailableInChat(botName: string, chatId: string): boolean;
|
|
78
|
+
hasAnyDeliveredToChat(botName: string, chatId: string): boolean;
|
|
73
79
|
hasRecentSimilarDelivery(botName: string, chatId: string, contentHash: string, windowMs: number, sourceTypes?: string[]): boolean;
|
|
74
80
|
hasRecentOverlappingDelivery(botName: string, chatId: string, content: string, attachmentsJson: string, windowMs: number, minShortLength?: number, sourceTypes?: string[]): boolean;
|
|
75
81
|
resetStaleDeliveries(botName: string, staleMs: number, maxAttempts: number): {
|
|
@@ -119,6 +125,8 @@ export declare class MessageStore {
|
|
|
119
125
|
getChairmanBot(chatId: string): string;
|
|
120
126
|
setBotVerbose(botName: string, chatId: string, verbose: boolean): void;
|
|
121
127
|
getBotVerbose(botName: string, chatId: string): boolean;
|
|
128
|
+
setBotLiveStatus(botName: string, chatId: string, on: boolean): void;
|
|
129
|
+
getBotLiveStatus(botName: string, chatId: string): boolean;
|
|
122
130
|
setBotMode(botName: string, chatId: string, mode: BotChatMode): void;
|
|
123
131
|
getBotMode(botName: string, chatId: string): BotChatMode;
|
|
124
132
|
setBotFreeDiscussion(botName: string, chatId: string, on: boolean): void;
|
package/dist/message-store.js
CHANGED
|
@@ -104,12 +104,34 @@ 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,
|
|
110
111
|
PRIMARY KEY (bot_name, chat_id)
|
|
111
112
|
);
|
|
112
113
|
|
|
114
|
+
-- Durable positive membership signal. Feishu does not reliably expose bot
|
|
115
|
+
-- members via chat_members, so receiving an event in a chat is the strongest
|
|
116
|
+
-- proof that this bot app is actually installed in that group.
|
|
117
|
+
CREATE TABLE IF NOT EXISTS bot_chat_seen (
|
|
118
|
+
bot_name TEXT NOT NULL,
|
|
119
|
+
chat_id TEXT NOT NULL,
|
|
120
|
+
updated_at INTEGER NOT NULL,
|
|
121
|
+
PRIMARY KEY (bot_name, chat_id)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
-- Durable negative membership cache. Feishu message create returns 230002
|
|
125
|
+
-- when a bot app is not in a chat; cache that so discussion scheduling and
|
|
126
|
+
-- later proactive deliveries do not keep generating ghost participants.
|
|
127
|
+
CREATE TABLE IF NOT EXISTS bot_chat_unavailable (
|
|
128
|
+
bot_name TEXT NOT NULL,
|
|
129
|
+
chat_id TEXT NOT NULL,
|
|
130
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
131
|
+
updated_at INTEGER NOT NULL,
|
|
132
|
+
PRIMARY KEY (bot_name, chat_id)
|
|
133
|
+
);
|
|
134
|
+
|
|
113
135
|
-- Durable delivery ledger for user-visible assistant outputs. All final
|
|
114
136
|
-- text/attachment outputs should be inserted here first and dispatched
|
|
115
137
|
-- once, regardless of whether they came from chat final, proactive
|
|
@@ -135,6 +157,13 @@ export class MessageStore {
|
|
|
135
157
|
);
|
|
136
158
|
CREATE INDEX IF NOT EXISTS idx_delivery_outbox_pending ON delivery_outbox(status, created_at);
|
|
137
159
|
`);
|
|
160
|
+
// Migration: add per-bot live status toggle if missing
|
|
161
|
+
try {
|
|
162
|
+
this.db.exec(`ALTER TABLE bot_chat_settings ADD COLUMN live_status INTEGER NOT NULL DEFAULT 1`);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Column already exists
|
|
166
|
+
}
|
|
138
167
|
// Migration: add delivery_outbox delivery key columns if missing
|
|
139
168
|
try {
|
|
140
169
|
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN delivery_key TEXT NOT NULL DEFAULT ''`);
|
|
@@ -401,6 +430,42 @@ export class MessageStore {
|
|
|
401
430
|
`).all(maxCount);
|
|
402
431
|
return rows.map((r) => this.mapDelivery(r));
|
|
403
432
|
}
|
|
433
|
+
markBotSeenInChat(botName, chatId) {
|
|
434
|
+
this.db.prepare(`
|
|
435
|
+
INSERT INTO bot_chat_seen(bot_name, chat_id, updated_at)
|
|
436
|
+
VALUES (?, ?, ?)
|
|
437
|
+
ON CONFLICT(bot_name, chat_id) DO UPDATE SET updated_at = excluded.updated_at
|
|
438
|
+
`).run(botName, chatId, Date.now());
|
|
439
|
+
// If the bot is receiving events from this chat again, it is no longer out
|
|
440
|
+
// of the chat. This recovers automatically when a bot is re-added to a group.
|
|
441
|
+
this.clearBotUnavailableInChat(botName, chatId);
|
|
442
|
+
}
|
|
443
|
+
hasBotSeenInChat(botName, chatId) {
|
|
444
|
+
const row = this.db.prepare(`SELECT 1 FROM bot_chat_seen WHERE bot_name = ? AND chat_id = ? LIMIT 1`).get(botName, chatId);
|
|
445
|
+
return !!row;
|
|
446
|
+
}
|
|
447
|
+
markBotUnavailableInChat(botName, chatId, reason = '') {
|
|
448
|
+
this.db.prepare(`
|
|
449
|
+
INSERT INTO bot_chat_unavailable(bot_name, chat_id, reason, updated_at)
|
|
450
|
+
VALUES (?, ?, ?, ?)
|
|
451
|
+
ON CONFLICT(bot_name, chat_id) DO UPDATE SET reason = excluded.reason, updated_at = excluded.updated_at
|
|
452
|
+
`).run(botName, chatId, reason, Date.now());
|
|
453
|
+
}
|
|
454
|
+
clearBotUnavailableInChat(botName, chatId) {
|
|
455
|
+
this.db.prepare(`DELETE FROM bot_chat_unavailable WHERE bot_name = ? AND chat_id = ?`).run(botName, chatId);
|
|
456
|
+
}
|
|
457
|
+
isBotUnavailableInChat(botName, chatId) {
|
|
458
|
+
const row = this.db.prepare(`SELECT 1 FROM bot_chat_unavailable WHERE bot_name = ? AND chat_id = ? LIMIT 1`).get(botName, chatId);
|
|
459
|
+
return !!row;
|
|
460
|
+
}
|
|
461
|
+
hasAnyDeliveredToChat(botName, chatId) {
|
|
462
|
+
const row = this.db.prepare(`
|
|
463
|
+
SELECT 1 FROM delivery_outbox
|
|
464
|
+
WHERE bot_name = ? AND chat_id = ? AND status = 'delivered'
|
|
465
|
+
LIMIT 1
|
|
466
|
+
`).get(botName, chatId);
|
|
467
|
+
return !!row;
|
|
468
|
+
}
|
|
404
469
|
hasRecentSimilarDelivery(botName, chatId, contentHash, windowMs, sourceTypes) {
|
|
405
470
|
if (!contentHash)
|
|
406
471
|
return false;
|
|
@@ -694,6 +759,22 @@ export class MessageStore {
|
|
|
694
759
|
`).get(botName, chatId);
|
|
695
760
|
return !!row?.verbose;
|
|
696
761
|
}
|
|
762
|
+
setBotLiveStatus(botName, chatId, on) {
|
|
763
|
+
this.db.prepare(`
|
|
764
|
+
INSERT INTO bot_chat_settings (bot_name, chat_id, live_status, updated_at)
|
|
765
|
+
VALUES (?, ?, ?, ?)
|
|
766
|
+
ON CONFLICT (bot_name, chat_id) DO UPDATE SET
|
|
767
|
+
live_status = excluded.live_status,
|
|
768
|
+
updated_at = excluded.updated_at
|
|
769
|
+
`).run(botName, chatId, on ? 1 : 0, Date.now());
|
|
770
|
+
}
|
|
771
|
+
getBotLiveStatus(botName, chatId) {
|
|
772
|
+
const row = this.db.prepare(`
|
|
773
|
+
SELECT live_status FROM bot_chat_settings
|
|
774
|
+
WHERE bot_name = ? AND chat_id = ?
|
|
775
|
+
`).get(botName, chatId);
|
|
776
|
+
return row ? row.live_status !== 0 : true;
|
|
777
|
+
}
|
|
697
778
|
setBotMode(botName, chatId, mode) {
|
|
698
779
|
const freeDiscussion = mode === "free" ? 1 : 0;
|
|
699
780
|
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) {
|