@clawling/clawchat-plugin-openclaw 2026.5.12-32 → 2026.5.12-39
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/src/api-client.js +10 -6
- package/dist/src/client.js +9 -1
- package/dist/src/commands.js +1 -1
- package/dist/src/login.runtime.js +2 -0
- package/dist/src/reply-dispatcher.js +299 -24
- package/dist/src/runtime.js +41 -2
- package/dist/src/tools-schema.js +0 -5
- package/dist/src/tools.js +2 -36
- package/openclaw.plugin.json +0 -1
- package/package.json +1 -1
- package/skills/clawchat/SKILL.md +13 -1
- package/src/api-client.ts +13 -7
- package/src/client.ts +10 -2
- package/src/commands.ts +1 -1
- package/src/login.runtime.ts +2 -0
- package/src/reply-dispatcher.ts +315 -25
- package/src/runtime.ts +54 -1
- package/src/tools-schema.ts +0 -8
- package/src/tools.ts +1 -46
package/package.json
CHANGED
package/skills/clawchat/SKILL.md
CHANGED
|
@@ -13,6 +13,7 @@ This skill guides agent behavior for ClawChat-aware tasks. Use the registered Cl
|
|
|
13
13
|
|
|
14
14
|
- Use registered ClawChat plugin tools for account/profile, friends, users, moments, comments, reactions, avatar, media, and read-only conversation lookup.
|
|
15
15
|
- If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin.
|
|
16
|
+
- Use the `/clawchat-output` slash command when the user asks to change how much ClawChat runtime output is shown in the current conversation.
|
|
16
17
|
|
|
17
18
|
## OpenClaw CLI
|
|
18
19
|
|
|
@@ -30,6 +31,18 @@ Use `update --force` only when local ClawChat plugin or skill files look corrupt
|
|
|
30
31
|
|
|
31
32
|
If `channels add` reports `Unknown channel: clawchat-plugin-openclaw`, use the runtime slash command `/clawchat-activate CODE` after the operator ensures the plugin is loaded.
|
|
32
33
|
|
|
34
|
+
## Output Visibility
|
|
35
|
+
|
|
36
|
+
When the user asks to change ClawChat output verbosity, use the runtime slash command for the current conversation. Treat natural-language wording as aliases for the three supported modes:
|
|
37
|
+
|
|
38
|
+
| User wording | Command |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| quiet mode, silent mode, minimal output, final-only output, `minimal` | `/clawchat-output minimal` |
|
|
41
|
+
| conversation mode, normal mode, regular mode, default output, `normal` | `/clawchat-output normal` |
|
|
42
|
+
| dev mode, developer mode, verbose mode, full output, `full` | `/clawchat-output full` |
|
|
43
|
+
|
|
44
|
+
Do not edit config files directly for this request. If the slash command returns an error, report that error instead of claiming the mode changed.
|
|
45
|
+
|
|
33
46
|
## Plugin Tool Routing
|
|
34
47
|
|
|
35
48
|
Tool descriptions are authoritative. These routing hints resolve common ambiguity:
|
|
@@ -57,7 +70,6 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
|
|
|
57
70
|
| Reply to an existing comment | `clawchat_reply_moment_comment` with `replyToCommentId` |
|
|
58
71
|
| Delete a comment/reply | `clawchat_delete_moment_comment` with exact `momentId` and `commentId` |
|
|
59
72
|
| Nickname or bio update | `clawchat_update_account_profile` |
|
|
60
|
-
| Standalone shareable media URL | `clawchat_upload_media_file` |
|
|
61
73
|
|
|
62
74
|
## Profile And Identity Sync
|
|
63
75
|
|
package/src/api-client.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface OpenclawClawlingApiClient {
|
|
|
69
69
|
uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
|
|
70
70
|
/**
|
|
71
71
|
* Exchange an invite code for an agent token.
|
|
72
|
-
* Request body shape: `{ code, platform, type }`.
|
|
72
|
+
* Request body shape: `{ code, platform, type, user_id? }`.
|
|
73
73
|
*/
|
|
74
74
|
agentsConnect(params: {
|
|
75
75
|
/** The invite code entered by the operator. */
|
|
@@ -78,6 +78,8 @@ export interface OpenclawClawlingApiClient {
|
|
|
78
78
|
platform: string;
|
|
79
79
|
/** Agent type tag (e.g. "bot"). */
|
|
80
80
|
type: string;
|
|
81
|
+
/** Existing configured ClawChat user id, when re-activating an account. */
|
|
82
|
+
user_id?: string;
|
|
81
83
|
}): Promise<AgentConnectResult>;
|
|
82
84
|
/**
|
|
83
85
|
* Upload an avatar image via `POST /v1/files/upload-url`. The resulting
|
|
@@ -423,7 +425,7 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
423
425
|
},
|
|
424
426
|
);
|
|
425
427
|
},
|
|
426
|
-
async agentsConnect({ code: inviteCode, platform, type }): Promise<AgentConnectResult> {
|
|
428
|
+
async agentsConnect({ code: inviteCode, platform, type, user_id: userId }): Promise<AgentConnectResult> {
|
|
427
429
|
if (!inviteCode?.trim()) {
|
|
428
430
|
throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
|
|
429
431
|
}
|
|
@@ -433,14 +435,18 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
433
435
|
if (!type?.trim()) {
|
|
434
436
|
throw new ClawlingApiError("validation", "agentsConnect: type is required");
|
|
435
437
|
}
|
|
438
|
+
const body: Record<string, string> = {
|
|
439
|
+
code: inviteCode.trim(),
|
|
440
|
+
platform: platform.trim(),
|
|
441
|
+
type: type.trim(),
|
|
442
|
+
};
|
|
443
|
+
if (userId?.trim()) {
|
|
444
|
+
body.user_id = userId.trim();
|
|
445
|
+
}
|
|
436
446
|
return await call<AgentConnectResult>("POST", "/v1/agents/connect", {
|
|
437
447
|
// `X-Device-Id` is added globally via `authHeaders` on every request.
|
|
438
448
|
headers: { "content-type": "application/json" },
|
|
439
|
-
body: JSON.stringify(
|
|
440
|
-
code: inviteCode.trim(),
|
|
441
|
-
platform: platform.trim(),
|
|
442
|
-
type: type.trim(),
|
|
443
|
-
}),
|
|
449
|
+
body: JSON.stringify(body),
|
|
444
450
|
});
|
|
445
451
|
},
|
|
446
452
|
async uploadMedia(params): Promise<UploadResult> {
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
1
3
|
import type { Envelope, Transport } from "./protocol-types.ts";
|
|
2
4
|
import { createClawChatClient, type ClawlingChatClient } from "./ws-client.ts";
|
|
3
|
-
import type
|
|
5
|
+
import { CHANNEL_ID, type ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
4
6
|
|
|
5
7
|
export type { ChatType } from "./protocol-types.ts";
|
|
6
8
|
|
|
@@ -15,6 +17,12 @@ export interface CreateClientOverrides {
|
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
export function resolveOpenclawClawlingDeviceId(account: ResolvedOpenclawClawlingAccount): string {
|
|
21
|
+
const material = [CHANNEL_ID, account.accountId, account.userId, os.hostname()].join("\0");
|
|
22
|
+
const digest = createHash("sha256").update(material).digest("hex").slice(0, 24);
|
|
23
|
+
return `${CHANNEL_ID}-${digest}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
export function createOpenclawClawlingClient(
|
|
19
27
|
account: ResolvedOpenclawClawlingAccount,
|
|
20
28
|
overrides: CreateClientOverrides = {},
|
|
@@ -22,7 +30,7 @@ export function createOpenclawClawlingClient(
|
|
|
22
30
|
const client = createClawChatClient({
|
|
23
31
|
url: account.websocketUrl,
|
|
24
32
|
token: account.token,
|
|
25
|
-
deviceId: account
|
|
33
|
+
deviceId: resolveOpenclawClawlingDeviceId(account),
|
|
26
34
|
...(overrides.transport ? { transport: overrides.transport } : {}),
|
|
27
35
|
reconnect: {
|
|
28
36
|
enabled: true,
|
package/src/commands.ts
CHANGED
|
@@ -78,7 +78,7 @@ function persistOutputVisibility(
|
|
|
78
78
|
function formatOutputVisibilityResult(outputVisibility: OutputVisibility): string {
|
|
79
79
|
const detailLevel: Record<OutputVisibility, string> = {
|
|
80
80
|
minimal: "final only",
|
|
81
|
-
normal: "final plus block
|
|
81
|
+
normal: "final plus block output",
|
|
82
82
|
full: "final plus buffered reasoning, tool/progress, and block output",
|
|
83
83
|
};
|
|
84
84
|
return [
|
package/src/login.runtime.ts
CHANGED
|
@@ -202,10 +202,12 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
202
202
|
runtime.log("Verifying invite code …");
|
|
203
203
|
let result;
|
|
204
204
|
try {
|
|
205
|
+
const existingUserId = account.userId.trim();
|
|
205
206
|
result = await apiClient.agentsConnect({
|
|
206
207
|
code: inviteCode,
|
|
207
208
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
208
209
|
type: AGENTS_CONNECT_TYPE,
|
|
210
|
+
...(existingUserId ? { user_id: existingUserId } : {}),
|
|
209
211
|
});
|
|
210
212
|
} catch (err) {
|
|
211
213
|
if (err instanceof ClawlingApiError) {
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -69,6 +69,22 @@ type ClawChatReplyOptions = TypedReplyDispatcherResult["replyOptions"] &
|
|
|
69
69
|
{
|
|
70
70
|
sourceReplyDeliveryMode: "automatic";
|
|
71
71
|
disableBlockStreaming: boolean;
|
|
72
|
+
suppressDefaultToolProgressMessages: boolean;
|
|
73
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed?: boolean;
|
|
74
|
+
onReasoningStream?: (payload: ReplyPayload) => void | Promise<void>;
|
|
75
|
+
onToolStart?: (payload: {
|
|
76
|
+
name?: string;
|
|
77
|
+
phase?: string;
|
|
78
|
+
args?: Record<string, unknown>;
|
|
79
|
+
detailMode?: "explain" | "raw";
|
|
80
|
+
}) => void | Promise<void>;
|
|
81
|
+
onToolResult?: (payload: ReplyPayload) => void | Promise<void>;
|
|
82
|
+
onItemEvent?: (payload: Record<string, unknown>) => void | Promise<void>;
|
|
83
|
+
onPlanUpdate?: (payload: Record<string, unknown>) => void | Promise<void>;
|
|
84
|
+
onCommandOutput?: (payload: Record<string, unknown>) => void | Promise<void>;
|
|
85
|
+
onPatchSummary?: (payload: Record<string, unknown>) => void | Promise<void>;
|
|
86
|
+
onCompactionStart?: () => void | Promise<void>;
|
|
87
|
+
onCompactionEnd?: () => void | Promise<void>;
|
|
72
88
|
};
|
|
73
89
|
|
|
74
90
|
type RichAction = {
|
|
@@ -188,12 +204,171 @@ function resolvePayloadText(payload: ReplyPayload): string {
|
|
|
188
204
|
return renderMessagePresentationFallbackText({ presentation, text: payload.text ?? null });
|
|
189
205
|
}
|
|
190
206
|
|
|
207
|
+
const FULL_OUTPUT_SUMMARY_MAX = 600;
|
|
208
|
+
|
|
209
|
+
function truncateSummary(text: string): string {
|
|
210
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
211
|
+
if (compact.length <= FULL_OUTPUT_SUMMARY_MAX) return compact;
|
|
212
|
+
return `${compact.slice(0, FULL_OUTPUT_SUMMARY_MAX - 1).trimEnd()}…`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function summarizeValue(value: unknown): string {
|
|
216
|
+
if (value == null) return "";
|
|
217
|
+
if (typeof value === "string") {
|
|
218
|
+
const trimmed = value.trim();
|
|
219
|
+
if (!trimmed) return "";
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
222
|
+
if (parsed && typeof parsed === "object") return summarizeValue(parsed);
|
|
223
|
+
} catch {
|
|
224
|
+
// Plain text is already the best summary.
|
|
225
|
+
}
|
|
226
|
+
return truncateSummary(trimmed);
|
|
227
|
+
}
|
|
228
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
229
|
+
if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
230
|
+
if (typeof value === "object") {
|
|
231
|
+
const keys = Object.keys(value as Record<string, unknown>).slice(0, 6);
|
|
232
|
+
return keys.length ? `object with ${keys.join(", ")}` : "object";
|
|
233
|
+
}
|
|
234
|
+
return truncateSummary(String(value));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readStringField(payload: Record<string, unknown>, field: string): string {
|
|
238
|
+
const value = payload[field];
|
|
239
|
+
return typeof value === "string" ? value.trim() : "";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function readPayloadCommand(payload: { args?: unknown }): string {
|
|
243
|
+
const args = payload.args;
|
|
244
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return "";
|
|
245
|
+
const command = (args as Record<string, unknown>).command;
|
|
246
|
+
return typeof command === "string" ? command.trim() : "";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeCommandLabel(value: string): string {
|
|
250
|
+
const trimmed = value.trim();
|
|
251
|
+
return trimmed.replace(/^command\s+/i, "").trim() || trimmed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isTerminalCommandOutput(payload: Record<string, unknown>): boolean {
|
|
255
|
+
const phase = readStringField(payload, "phase").toLowerCase();
|
|
256
|
+
const status = readStringField(payload, "status").toLowerCase();
|
|
257
|
+
return (
|
|
258
|
+
phase === "end" ||
|
|
259
|
+
phase === "error" ||
|
|
260
|
+
typeof payload.exitCode === "number" ||
|
|
261
|
+
status === "completed" ||
|
|
262
|
+
status === "ok" ||
|
|
263
|
+
status === "success" ||
|
|
264
|
+
status === "failed" ||
|
|
265
|
+
status === "error"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isToolProgressItem(payload: Record<string, unknown>): boolean {
|
|
270
|
+
const kind = readStringField(payload, "kind").toLowerCase();
|
|
271
|
+
const title = readStringField(payload, "title").toLowerCase();
|
|
272
|
+
const name = readStringField(payload, "name").toLowerCase();
|
|
273
|
+
const progressText = readStringField(payload, "progressText").toLowerCase();
|
|
274
|
+
return (
|
|
275
|
+
kind === "tool" ||
|
|
276
|
+
kind === "command" ||
|
|
277
|
+
title.startsWith("exec ") ||
|
|
278
|
+
title.startsWith("command ") ||
|
|
279
|
+
name.startsWith("exec ") ||
|
|
280
|
+
name.startsWith("command ") ||
|
|
281
|
+
progressText.startsWith("exec ") ||
|
|
282
|
+
progressText.startsWith("command ")
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isDefaultToolResultText(text: string): boolean {
|
|
287
|
+
return /^[🛠🔧]/u.test(text.trim());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function summarizeProgressPayload(payload: Record<string, unknown>): string {
|
|
291
|
+
return (
|
|
292
|
+
readStringField(payload, "progressText") ||
|
|
293
|
+
readStringField(payload, "summary") ||
|
|
294
|
+
readStringField(payload, "message") ||
|
|
295
|
+
readStringField(payload, "title") ||
|
|
296
|
+
readStringField(payload, "name") ||
|
|
297
|
+
readStringField(payload, "status") ||
|
|
298
|
+
summarizeValue(payload)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function formatToolStartSummary(payload: {
|
|
303
|
+
name?: string;
|
|
304
|
+
phase?: string;
|
|
305
|
+
args?: unknown;
|
|
306
|
+
}): string {
|
|
307
|
+
const name = payload.name?.trim() || "tool";
|
|
308
|
+
const phase = payload.phase?.trim();
|
|
309
|
+
if (phase && phase !== "start") return "";
|
|
310
|
+
const command = readPayloadCommand(payload);
|
|
311
|
+
if ((name === "exec" || name === "command") && !command) return "";
|
|
312
|
+
return `[tool] ${[name, command].filter(Boolean).join(" ")} started`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function formatCommandOutputSummary(payload: Record<string, unknown>): string {
|
|
316
|
+
if (!isTerminalCommandOutput(payload)) return "";
|
|
317
|
+
const name = normalizeCommandLabel(
|
|
318
|
+
readStringField(payload, "title") || readStringField(payload, "name") || "command",
|
|
319
|
+
);
|
|
320
|
+
const status = readStringField(payload, "status").toLowerCase();
|
|
321
|
+
const visibleStatus =
|
|
322
|
+
status && status !== "ok" && status !== "completed" && status !== "success" ? status : "";
|
|
323
|
+
const exitCode = typeof payload.exitCode === "number" ? ` exit ${payload.exitCode}` : "";
|
|
324
|
+
const output = summarizeValue(payload.output);
|
|
325
|
+
const prefix = `[command] ${[name, visibleStatus].filter(Boolean).join(" ")}${exitCode}`;
|
|
326
|
+
return truncateSummary(output ? `${prefix}: ${output}` : prefix);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function formatPatchSummary(payload: Record<string, unknown>): string {
|
|
330
|
+
const summary = readStringField(payload, "summary");
|
|
331
|
+
if (summary) return `[patch] ${truncateSummary(summary)}`;
|
|
332
|
+
const parts: string[] = [];
|
|
333
|
+
for (const key of ["added", "modified", "deleted"]) {
|
|
334
|
+
const value = payload[key];
|
|
335
|
+
if (Array.isArray(value) && value.length > 0) parts.push(`${key}: ${value.map(String).join(", ")}`);
|
|
336
|
+
else if (typeof value === "number" && value > 0) parts.push(`${key}: ${value}`);
|
|
337
|
+
}
|
|
338
|
+
return `[patch] ${truncateSummary(parts.join("; ") || "updated")}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function formatItemEventSummary(payload: Record<string, unknown>): string {
|
|
342
|
+
const kind = readStringField(payload, "kind") || "progress";
|
|
343
|
+
const title = readStringField(payload, "title") || readStringField(payload, "name");
|
|
344
|
+
const status = readStringField(payload, "status");
|
|
345
|
+
const phase = readStringField(payload, "phase");
|
|
346
|
+
const summary = summarizeProgressPayload(payload);
|
|
347
|
+
const label = [title, status || phase].filter(Boolean).join(" ");
|
|
348
|
+
return `[${kind}] ${truncateSummary(label || summary || "activity")}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function formatPlanSummary(payload: Record<string, unknown>): string {
|
|
352
|
+
const title = readStringField(payload, "title") || "plan";
|
|
353
|
+
const explanation = readStringField(payload, "explanation");
|
|
354
|
+
const steps = Array.isArray(payload.steps) ? payload.steps.map(String).filter(Boolean) : [];
|
|
355
|
+
return `[plan] ${truncateSummary([title, explanation, ...steps].filter(Boolean).join(": "))}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function formatApprovalSummary(payload: Record<string, unknown>): string {
|
|
359
|
+
const title = readStringField(payload, "title") || readStringField(payload, "kind") || "approval";
|
|
360
|
+
const status = readStringField(payload, "status") || readStringField(payload, "phase");
|
|
361
|
+
const message = readStringField(payload, "message") || readStringField(payload, "reason");
|
|
362
|
+
return `[approval] ${truncateSummary([title, status, message].filter(Boolean).join(" "))}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
191
365
|
/**
|
|
192
366
|
* Reply dispatcher for clawchat-plugin-openclaw.
|
|
193
367
|
*
|
|
194
368
|
* ClawChat emits only materialized `message.send` / `message.reply` frames for
|
|
195
|
-
*
|
|
196
|
-
*
|
|
369
|
+
* complete OpenClaw output units. `disableBlockStreaming` prevents token/block
|
|
370
|
+
* streaming; full visibility still forwards complete tool/progress/output units
|
|
371
|
+
* as separate ClawChat messages.
|
|
197
372
|
*/
|
|
198
373
|
export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOptions): {
|
|
199
374
|
dispatcher: TypedReplyDispatcherResult["dispatcher"];
|
|
@@ -213,6 +388,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
213
388
|
} = options;
|
|
214
389
|
const isGroupTarget = target.chatType === "group";
|
|
215
390
|
const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
|
|
391
|
+
const splitFullOutput = outputVisibility === "full";
|
|
392
|
+
const splitNormalBlockOutput = outputVisibility === "normal";
|
|
216
393
|
const ownerDirectTarget = () => {
|
|
217
394
|
const ownerUserId = account.ownerUserId?.trim();
|
|
218
395
|
return ownerUserId ? { chatId: ownerUserId, chatType: "direct" as const } : null;
|
|
@@ -245,10 +422,13 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
245
422
|
|
|
246
423
|
let reasoningText = "";
|
|
247
424
|
let bufferedOutputText = "";
|
|
425
|
+
const bufferedOutputLineSet = new Set<string>();
|
|
426
|
+
const emittedFullSegmentSet = new Set<string>();
|
|
248
427
|
const bufferedOutputUrls: string[] = [];
|
|
249
428
|
let runDone = false;
|
|
250
429
|
let typingActive = false;
|
|
251
430
|
let terminalReplySuppressed = false;
|
|
431
|
+
let finalDeliverySeen = false;
|
|
252
432
|
|
|
253
433
|
const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
|
|
254
434
|
const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
|
|
@@ -331,13 +511,13 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
331
511
|
recordOutbound("thinking", messageId, thinkingText);
|
|
332
512
|
reasoningText = "";
|
|
333
513
|
};
|
|
334
|
-
const resetBufferedOutput = () => {
|
|
335
|
-
bufferedOutputText = "";
|
|
336
|
-
bufferedOutputUrls.length = 0;
|
|
337
|
-
};
|
|
338
514
|
const appendBufferedText = (value: string) => {
|
|
339
515
|
const trimmed = value.trim();
|
|
340
516
|
if (!trimmed) return;
|
|
517
|
+
if (!trimmed.includes("\n")) {
|
|
518
|
+
if (bufferedOutputLineSet.has(trimmed)) return;
|
|
519
|
+
bufferedOutputLineSet.add(trimmed);
|
|
520
|
+
}
|
|
341
521
|
bufferedOutputText = bufferedOutputText ? `${bufferedOutputText}\n${trimmed}` : trimmed;
|
|
342
522
|
};
|
|
343
523
|
const appendBufferedUrls = (urls: string[]) => {
|
|
@@ -345,12 +525,17 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
345
525
|
if (url && !bufferedOutputUrls.includes(url)) bufferedOutputUrls.push(url);
|
|
346
526
|
}
|
|
347
527
|
};
|
|
528
|
+
const fullSegmentKey = (text: string, urls: string[]): string =>
|
|
529
|
+
JSON.stringify({
|
|
530
|
+
text: text.replace(/\s+/g, " ").trim(),
|
|
531
|
+
urls: urls.filter(Boolean),
|
|
532
|
+
});
|
|
348
533
|
const mergeFinalText = (text: string): string => {
|
|
349
|
-
if (outputVisibility
|
|
534
|
+
if (outputVisibility === "minimal" || outputVisibility === "full") return text;
|
|
350
535
|
return [bufferedOutputText.trim(), text.trim()].filter(Boolean).join("\n");
|
|
351
536
|
};
|
|
352
537
|
const mergeFinalUrls = (urls: string[]): string[] => {
|
|
353
|
-
if (outputVisibility === "minimal") return urls;
|
|
538
|
+
if (outputVisibility === "minimal" || outputVisibility === "full") return urls;
|
|
354
539
|
const merged = bufferedOutputUrls.slice();
|
|
355
540
|
for (const url of urls) {
|
|
356
541
|
if (url && !merged.includes(url)) merged.push(url);
|
|
@@ -485,15 +670,42 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
485
670
|
return result;
|
|
486
671
|
};
|
|
487
672
|
|
|
673
|
+
const emitFullSegment = async (text: string, urls: string[] = []): Promise<void> => {
|
|
674
|
+
if (outputVisibility !== "full" && !splitNormalBlockOutput) {
|
|
675
|
+
appendBufferedText(text);
|
|
676
|
+
appendBufferedUrls(urls);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (!splitFullOutput && !splitNormalBlockOutput) {
|
|
680
|
+
appendBufferedText(text);
|
|
681
|
+
appendBufferedUrls(urls);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const trimmed = text.trim();
|
|
685
|
+
if (!trimmed && urls.length === 0) return;
|
|
686
|
+
const segmentKey = fullSegmentKey(trimmed, urls);
|
|
687
|
+
if (emittedFullSegmentSet.has(segmentKey)) return;
|
|
688
|
+
emittedFullSegmentSet.add(segmentKey);
|
|
689
|
+
const mediaFragments = await uploadMediaUrls(urls);
|
|
690
|
+
await sendStatic(trimmed, mediaFragments, [], { recordMessage: true });
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const emitFullRuntimeText = async (
|
|
694
|
+
label: string,
|
|
695
|
+
text: string,
|
|
696
|
+
urls: string[] = [],
|
|
697
|
+
): Promise<void> => {
|
|
698
|
+
const summary = summarizeValue(text);
|
|
699
|
+
if (!summary && urls.length === 0) return;
|
|
700
|
+
await emitFullSegment(summary ? `[${label}] ${summary}` : "", urls);
|
|
701
|
+
};
|
|
702
|
+
|
|
488
703
|
// ----- Dispatcher -------------------------------------------------------
|
|
489
704
|
|
|
490
705
|
const base = runtime.channel.reply.createReplyDispatcherWithTyping({
|
|
491
706
|
humanDelay,
|
|
492
707
|
onReplyStart: async () => {
|
|
493
708
|
emitTyping(true);
|
|
494
|
-
reasoningText = "";
|
|
495
|
-
resetBufferedOutput();
|
|
496
|
-
runDone = false;
|
|
497
709
|
},
|
|
498
710
|
deliver: async (payload: ReplyPayload, info?: { kind: "tool" | "block" | "final" }) => {
|
|
499
711
|
if (consumeTerminalSend(info?.kind ?? "unknown")) return;
|
|
@@ -505,6 +717,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
505
717
|
);
|
|
506
718
|
|
|
507
719
|
if (isGroupTarget && richFragment) {
|
|
720
|
+
if (info?.kind === "final") finalDeliverySeen = true;
|
|
508
721
|
if (info?.kind !== "final") return;
|
|
509
722
|
await sendOwnerAttention(
|
|
510
723
|
resolvePayloadText(payload),
|
|
@@ -514,29 +727,33 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
514
727
|
}
|
|
515
728
|
|
|
516
729
|
if (isGroupTarget && info?.kind === "final" && looksLikeApprovalFallbackText(text)) {
|
|
730
|
+
finalDeliverySeen = true;
|
|
517
731
|
await sendOwnerAttention(text);
|
|
518
732
|
return;
|
|
519
733
|
}
|
|
520
734
|
|
|
521
735
|
if (payload.isReasoning) {
|
|
522
|
-
if (
|
|
523
|
-
|
|
736
|
+
if (outputVisibility !== "full") return;
|
|
737
|
+
await emitFullSegment(text, urls);
|
|
524
738
|
const trimmed = text.trim();
|
|
525
739
|
if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
526
740
|
return;
|
|
527
741
|
}
|
|
528
742
|
|
|
529
743
|
if (info?.kind === "tool") {
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
744
|
+
if (isDefaultToolResultText(text)) return;
|
|
745
|
+
if (outputVisibility === "full") {
|
|
746
|
+
await emitFullRuntimeText("tool result", text, urls);
|
|
533
747
|
}
|
|
534
748
|
return;
|
|
535
749
|
}
|
|
536
750
|
|
|
537
751
|
if (info?.kind === "block") {
|
|
538
|
-
if (
|
|
539
|
-
|
|
752
|
+
if (outputVisibility === "full") {
|
|
753
|
+
await emitFullSegment(text, urls);
|
|
754
|
+
} else if (splitNormalBlockOutput) {
|
|
755
|
+
await emitFullSegment(text, urls);
|
|
756
|
+
} else if (outputVisibility === "minimal" || outputVisibility === "normal") {
|
|
540
757
|
appendBufferedText(text);
|
|
541
758
|
appendBufferedUrls(urls);
|
|
542
759
|
}
|
|
@@ -544,6 +761,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
544
761
|
}
|
|
545
762
|
|
|
546
763
|
if (info?.kind === "final") {
|
|
764
|
+
finalDeliverySeen = true;
|
|
547
765
|
if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
|
|
548
766
|
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
|
|
549
767
|
openclawLlmContextDebug.writeSnapshot({
|
|
@@ -567,6 +785,14 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
567
785
|
}
|
|
568
786
|
const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
|
|
569
787
|
const finalUrls = mergeFinalUrls(urls);
|
|
788
|
+
if (
|
|
789
|
+
isClawChatNoopResponseText(finalText) &&
|
|
790
|
+
!richFragment &&
|
|
791
|
+
finalUrls.length === 0
|
|
792
|
+
) {
|
|
793
|
+
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
570
796
|
const mediaFragments = await uploadMediaUrls(finalUrls);
|
|
571
797
|
const result = await sendStatic(
|
|
572
798
|
finalText,
|
|
@@ -585,17 +811,19 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
585
811
|
log?.error?.(
|
|
586
812
|
`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`,
|
|
587
813
|
);
|
|
588
|
-
if (
|
|
589
|
-
log?.error?.(
|
|
590
|
-
`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`,
|
|
591
|
-
);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
814
|
+
if (outputVisibility === "full") void emitFullRuntimeText("error", errorText);
|
|
594
815
|
},
|
|
595
816
|
onIdle: async () => {
|
|
596
817
|
emitTyping(false);
|
|
597
818
|
if (runDone) return;
|
|
598
819
|
runDone = true;
|
|
820
|
+
if (finalDeliverySeen) return;
|
|
821
|
+
const fallbackText = bufferedOutputText.trim();
|
|
822
|
+
const fallbackUrls = bufferedOutputUrls.slice();
|
|
823
|
+
if (!fallbackText && fallbackUrls.length === 0) return;
|
|
824
|
+
const mediaFragments = await uploadMediaUrls(fallbackUrls);
|
|
825
|
+
const result = await sendStatic(fallbackText, mediaFragments, [], { recordMessage: true });
|
|
826
|
+
if (result?.messageId) recordThinkingIfLinked(result.messageId);
|
|
599
827
|
},
|
|
600
828
|
onCleanup: () => {
|
|
601
829
|
emitTyping(false);
|
|
@@ -607,7 +835,69 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
607
835
|
replyOptions: {
|
|
608
836
|
...base.replyOptions,
|
|
609
837
|
sourceReplyDeliveryMode: "automatic",
|
|
610
|
-
disableBlockStreaming:
|
|
838
|
+
disableBlockStreaming: !splitNormalBlockOutput,
|
|
839
|
+
suppressDefaultToolProgressMessages: true,
|
|
840
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: splitFullOutput ? true : undefined,
|
|
841
|
+
onReasoningStream: splitFullOutput
|
|
842
|
+
? async (payload: ReplyPayload) => {
|
|
843
|
+
if (consumeTerminalSend("reasoning")) return;
|
|
844
|
+
const text = resolvePayloadText(payload);
|
|
845
|
+
await emitFullRuntimeText("reasoning", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
846
|
+
const trimmed = text.trim();
|
|
847
|
+
if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
848
|
+
}
|
|
849
|
+
: undefined,
|
|
850
|
+
onToolStart: splitFullOutput
|
|
851
|
+
? async (payload) => {
|
|
852
|
+
if (consumeTerminalSend("tool-start")) return;
|
|
853
|
+
await emitFullSegment(formatToolStartSummary(payload));
|
|
854
|
+
}
|
|
855
|
+
: undefined,
|
|
856
|
+
onToolResult: splitFullOutput
|
|
857
|
+
? async (payload: ReplyPayload) => {
|
|
858
|
+
if (consumeTerminalSend("tool-result")) return;
|
|
859
|
+
const text = resolvePayloadText(payload);
|
|
860
|
+
if (isDefaultToolResultText(text)) return;
|
|
861
|
+
await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
862
|
+
}
|
|
863
|
+
: undefined,
|
|
864
|
+
onItemEvent: splitFullOutput
|
|
865
|
+
? async (payload: Record<string, unknown>) => {
|
|
866
|
+
if (consumeTerminalSend("item-event")) return;
|
|
867
|
+
if (isToolProgressItem(payload)) return;
|
|
868
|
+
await emitFullRuntimeText("progress", summarizeProgressPayload(payload));
|
|
869
|
+
}
|
|
870
|
+
: undefined,
|
|
871
|
+
onPlanUpdate: splitFullOutput
|
|
872
|
+
? async (payload: Record<string, unknown>) => {
|
|
873
|
+
if (consumeTerminalSend("plan-update")) return;
|
|
874
|
+
await emitFullRuntimeText("plan", summarizeProgressPayload(payload));
|
|
875
|
+
}
|
|
876
|
+
: undefined,
|
|
877
|
+
onCommandOutput: splitFullOutput
|
|
878
|
+
? async (payload: Record<string, unknown>) => {
|
|
879
|
+
if (consumeTerminalSend("command-output")) return;
|
|
880
|
+
await emitFullSegment(formatCommandOutputSummary(payload));
|
|
881
|
+
}
|
|
882
|
+
: undefined,
|
|
883
|
+
onPatchSummary: splitFullOutput
|
|
884
|
+
? async (payload: Record<string, unknown>) => {
|
|
885
|
+
if (consumeTerminalSend("patch-summary")) return;
|
|
886
|
+
await emitFullSegment(formatPatchSummary(payload));
|
|
887
|
+
}
|
|
888
|
+
: undefined,
|
|
889
|
+
onCompactionStart: splitFullOutput
|
|
890
|
+
? async () => {
|
|
891
|
+
if (consumeTerminalSend("compaction-start")) return;
|
|
892
|
+
await emitFullSegment("[compaction] started");
|
|
893
|
+
}
|
|
894
|
+
: undefined,
|
|
895
|
+
onCompactionEnd: splitFullOutput
|
|
896
|
+
? async () => {
|
|
897
|
+
if (consumeTerminalSend("compaction-end")) return;
|
|
898
|
+
await emitFullSegment("[compaction] finished");
|
|
899
|
+
}
|
|
900
|
+
: undefined,
|
|
611
901
|
},
|
|
612
902
|
markDispatchIdle: base.markDispatchIdle,
|
|
613
903
|
};
|