@clawling/clawchat-plugin-openclaw 2026.5.12-31 → 2026.5.12-38
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 +4 -6
- package/dist/src/config.js +36 -0
- package/dist/src/login.runtime.js +2 -0
- package/dist/src/reply-dispatcher.js +335 -15
- 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 +0 -1
- package/src/api-client.ts +13 -7
- package/src/client.ts +10 -2
- package/src/commands.ts +4 -6
- package/src/config.ts +44 -0
- package/src/login.runtime.ts +2 -0
- package/src/reply-dispatcher.ts +350 -17
- package/src/runtime.ts +54 -1
- package/src/tools-schema.ts +0 -8
- package/src/tools.ts +1 -46
package/dist/src/api-client.js
CHANGED
|
@@ -249,7 +249,7 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
249
249
|
headers: { "content-type": "application/json" },
|
|
250
250
|
});
|
|
251
251
|
},
|
|
252
|
-
async agentsConnect({ code: inviteCode, platform, type }) {
|
|
252
|
+
async agentsConnect({ code: inviteCode, platform, type, user_id: userId }) {
|
|
253
253
|
if (!inviteCode?.trim()) {
|
|
254
254
|
throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
|
|
255
255
|
}
|
|
@@ -259,14 +259,18 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
259
259
|
if (!type?.trim()) {
|
|
260
260
|
throw new ClawlingApiError("validation", "agentsConnect: type is required");
|
|
261
261
|
}
|
|
262
|
+
const body = {
|
|
263
|
+
code: inviteCode.trim(),
|
|
264
|
+
platform: platform.trim(),
|
|
265
|
+
type: type.trim(),
|
|
266
|
+
};
|
|
267
|
+
if (userId?.trim()) {
|
|
268
|
+
body.user_id = userId.trim();
|
|
269
|
+
}
|
|
262
270
|
return await call("POST", "/v1/agents/connect", {
|
|
263
271
|
// `X-Device-Id` is added globally via `authHeaders` on every request.
|
|
264
272
|
headers: { "content-type": "application/json" },
|
|
265
|
-
body: JSON.stringify(
|
|
266
|
-
code: inviteCode.trim(),
|
|
267
|
-
platform: platform.trim(),
|
|
268
|
-
type: type.trim(),
|
|
269
|
-
}),
|
|
273
|
+
body: JSON.stringify(body),
|
|
270
274
|
});
|
|
271
275
|
},
|
|
272
276
|
async uploadMedia(params) {
|
package/dist/src/client.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
1
3
|
import { createClawChatClient } from "./ws-client.js";
|
|
4
|
+
import { CHANNEL_ID } from "./config.js";
|
|
5
|
+
export function resolveOpenclawClawlingDeviceId(account) {
|
|
6
|
+
const material = [CHANNEL_ID, account.accountId, account.userId, os.hostname()].join("\0");
|
|
7
|
+
const digest = createHash("sha256").update(material).digest("hex").slice(0, 24);
|
|
8
|
+
return `${CHANNEL_ID}-${digest}`;
|
|
9
|
+
}
|
|
2
10
|
export function createOpenclawClawlingClient(account, overrides = {}) {
|
|
3
11
|
const client = createClawChatClient({
|
|
4
12
|
url: account.websocketUrl,
|
|
5
13
|
token: account.token,
|
|
6
|
-
deviceId: account
|
|
14
|
+
deviceId: resolveOpenclawClawlingDeviceId(account),
|
|
7
15
|
...(overrides.transport ? { transport: overrides.transport } : {}),
|
|
8
16
|
reconnect: {
|
|
9
17
|
enabled: true,
|
package/dist/src/commands.js
CHANGED
|
@@ -64,18 +64,16 @@ function persistOutputVisibility(draft, chatId, outputVisibility) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
function formatOutputVisibilityResult(outputVisibility) {
|
|
67
|
-
const runtimeStatus = outputVisibility === "full" ? "on" : "off";
|
|
68
67
|
const detailLevel = {
|
|
69
|
-
minimal: "
|
|
70
|
-
normal: "
|
|
71
|
-
full: "
|
|
68
|
+
minimal: "final only",
|
|
69
|
+
normal: "final plus block output",
|
|
70
|
+
full: "final plus buffered reasoning, tool/progress, and block output",
|
|
72
71
|
};
|
|
73
72
|
return [
|
|
74
73
|
"**ClawChat output updated**",
|
|
75
74
|
"",
|
|
76
75
|
`- visibility: \`${outputVisibility}\``,
|
|
77
|
-
`-
|
|
78
|
-
`- detail level: \`${detailLevel[outputVisibility]}\``,
|
|
76
|
+
`- output: \`${detailLevel[outputVisibility]}\``,
|
|
79
77
|
"",
|
|
80
78
|
"Applies to new ClawChat messages.",
|
|
81
79
|
].join("\n");
|
package/dist/src/config.js
CHANGED
|
@@ -191,6 +191,29 @@ function readGroupMode(value) {
|
|
|
191
191
|
function readGroupCommandMode(value) {
|
|
192
192
|
return value === "all" || value === "off" ? value : "owner";
|
|
193
193
|
}
|
|
194
|
+
function readOutputVisibility(value) {
|
|
195
|
+
return value === "minimal" || value === "full" ? value : "normal";
|
|
196
|
+
}
|
|
197
|
+
function readOptionalOutputVisibility(value) {
|
|
198
|
+
return value === "minimal" || value === "normal" || value === "full" ? value : undefined;
|
|
199
|
+
}
|
|
200
|
+
function readChats(value) {
|
|
201
|
+
const rawChats = value && typeof value === "object" && !Array.isArray(value)
|
|
202
|
+
? value
|
|
203
|
+
: {};
|
|
204
|
+
const chats = {};
|
|
205
|
+
for (const [chatId, rawChat] of Object.entries(rawChats)) {
|
|
206
|
+
if (!chatId)
|
|
207
|
+
continue;
|
|
208
|
+
const chat = rawChat && typeof rawChat === "object" && !Array.isArray(rawChat)
|
|
209
|
+
? rawChat
|
|
210
|
+
: {};
|
|
211
|
+
const outputVisibility = readOptionalOutputVisibility(chat.outputVisibility);
|
|
212
|
+
if (outputVisibility)
|
|
213
|
+
chats[chatId] = { outputVisibility };
|
|
214
|
+
}
|
|
215
|
+
return chats;
|
|
216
|
+
}
|
|
194
217
|
function readGroups(value) {
|
|
195
218
|
const rawGroups = value && typeof value === "object" && !Array.isArray(value)
|
|
196
219
|
? value
|
|
@@ -202,9 +225,11 @@ function readGroups(value) {
|
|
|
202
225
|
const group = rawGroup && typeof rawGroup === "object" && !Array.isArray(rawGroup)
|
|
203
226
|
? rawGroup
|
|
204
227
|
: {};
|
|
228
|
+
const outputVisibility = readOptionalOutputVisibility(group.outputVisibility);
|
|
205
229
|
groups[chatId] = {
|
|
206
230
|
groupMode: readGroupMode(group.groupMode),
|
|
207
231
|
groupCommandMode: readGroupCommandMode(group.groupCommandMode),
|
|
232
|
+
...(outputVisibility ? { outputVisibility } : {}),
|
|
208
233
|
};
|
|
209
234
|
}
|
|
210
235
|
return groups;
|
|
@@ -219,6 +244,13 @@ export function effectiveGroupCommandMode(account, chatId) {
|
|
|
219
244
|
?? account.groups["*"]?.groupCommandMode
|
|
220
245
|
?? account.groupCommandMode;
|
|
221
246
|
}
|
|
247
|
+
export function effectiveOutputVisibility(account, chatId, chatType) {
|
|
248
|
+
return account.chats?.[chatId]?.outputVisibility
|
|
249
|
+
?? (chatType === "group" ? account.groups?.[chatId]?.outputVisibility : undefined)
|
|
250
|
+
?? (chatType === "group" ? account.groups?.["*"]?.outputVisibility : undefined)
|
|
251
|
+
?? account.outputVisibility
|
|
252
|
+
?? "normal";
|
|
253
|
+
}
|
|
222
254
|
function readReconnect(raw) {
|
|
223
255
|
const s = raw && typeof raw === "object" ? raw : {};
|
|
224
256
|
return {
|
|
@@ -261,6 +293,8 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
261
293
|
const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
|
|
262
294
|
const ownerUserId = readOptionalString(channel.ownerUserId) || readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV);
|
|
263
295
|
const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
296
|
+
const outputVisibility = readOutputVisibility(channel.outputVisibility);
|
|
297
|
+
const chats = readChats(channel.chats);
|
|
264
298
|
const groupMode = readGroupMode(channel.groupMode);
|
|
265
299
|
const groupCommandMode = readGroupCommandMode(channel.groupCommandMode);
|
|
266
300
|
const groups = readGroups(channel.groups);
|
|
@@ -284,6 +318,8 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
284
318
|
agentId,
|
|
285
319
|
userId,
|
|
286
320
|
ownerUserId,
|
|
321
|
+
outputVisibility,
|
|
322
|
+
chats,
|
|
287
323
|
groupMode,
|
|
288
324
|
groupCommandMode,
|
|
289
325
|
groups,
|
|
@@ -139,10 +139,12 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
139
139
|
runtime.log("Verifying invite code …");
|
|
140
140
|
let result;
|
|
141
141
|
try {
|
|
142
|
+
const existingUserId = account.userId.trim();
|
|
142
143
|
result = await apiClient.agentsConnect({
|
|
143
144
|
code: inviteCode,
|
|
144
145
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
145
146
|
type: AGENTS_CONNECT_TYPE,
|
|
147
|
+
...(existingUserId ? { user_id: existingUserId } : {}),
|
|
146
148
|
});
|
|
147
149
|
}
|
|
148
150
|
catch (err) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
|
|
2
2
|
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
3
3
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
4
|
+
import { effectiveOutputVisibility, } from "./config.js";
|
|
4
5
|
import { uploadOutboundMedia } from "./media-runtime.js";
|
|
5
6
|
import { sendOpenclawClawlingText, } from "./outbound.js";
|
|
6
7
|
import { isClawChatNoopResponseText } from "./profile-prompt.js";
|
|
@@ -103,18 +104,162 @@ function resolvePayloadText(payload) {
|
|
|
103
104
|
return payload.text ?? "";
|
|
104
105
|
return renderMessagePresentationFallbackText({ presentation, text: payload.text ?? null });
|
|
105
106
|
}
|
|
107
|
+
const FULL_OUTPUT_SUMMARY_MAX = 600;
|
|
108
|
+
function truncateSummary(text) {
|
|
109
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
110
|
+
if (compact.length <= FULL_OUTPUT_SUMMARY_MAX)
|
|
111
|
+
return compact;
|
|
112
|
+
return `${compact.slice(0, FULL_OUTPUT_SUMMARY_MAX - 1).trimEnd()}…`;
|
|
113
|
+
}
|
|
114
|
+
function summarizeValue(value) {
|
|
115
|
+
if (value == null)
|
|
116
|
+
return "";
|
|
117
|
+
if (typeof value === "string") {
|
|
118
|
+
const trimmed = value.trim();
|
|
119
|
+
if (!trimmed)
|
|
120
|
+
return "";
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(trimmed);
|
|
123
|
+
if (parsed && typeof parsed === "object")
|
|
124
|
+
return summarizeValue(parsed);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Plain text is already the best summary.
|
|
128
|
+
}
|
|
129
|
+
return truncateSummary(trimmed);
|
|
130
|
+
}
|
|
131
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
132
|
+
return String(value);
|
|
133
|
+
if (Array.isArray(value))
|
|
134
|
+
return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
135
|
+
if (typeof value === "object") {
|
|
136
|
+
const keys = Object.keys(value).slice(0, 6);
|
|
137
|
+
return keys.length ? `object with ${keys.join(", ")}` : "object";
|
|
138
|
+
}
|
|
139
|
+
return truncateSummary(String(value));
|
|
140
|
+
}
|
|
141
|
+
function readStringField(payload, field) {
|
|
142
|
+
const value = payload[field];
|
|
143
|
+
return typeof value === "string" ? value.trim() : "";
|
|
144
|
+
}
|
|
145
|
+
function readPayloadCommand(payload) {
|
|
146
|
+
const args = payload.args;
|
|
147
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
148
|
+
return "";
|
|
149
|
+
const command = args.command;
|
|
150
|
+
return typeof command === "string" ? command.trim() : "";
|
|
151
|
+
}
|
|
152
|
+
function normalizeCommandLabel(value) {
|
|
153
|
+
const trimmed = value.trim();
|
|
154
|
+
return trimmed.replace(/^command\s+/i, "").trim() || trimmed;
|
|
155
|
+
}
|
|
156
|
+
function isTerminalCommandOutput(payload) {
|
|
157
|
+
const phase = readStringField(payload, "phase").toLowerCase();
|
|
158
|
+
const status = readStringField(payload, "status").toLowerCase();
|
|
159
|
+
return (phase === "end" ||
|
|
160
|
+
phase === "error" ||
|
|
161
|
+
typeof payload.exitCode === "number" ||
|
|
162
|
+
status === "completed" ||
|
|
163
|
+
status === "ok" ||
|
|
164
|
+
status === "success" ||
|
|
165
|
+
status === "failed" ||
|
|
166
|
+
status === "error");
|
|
167
|
+
}
|
|
168
|
+
function isToolProgressItem(payload) {
|
|
169
|
+
const kind = readStringField(payload, "kind").toLowerCase();
|
|
170
|
+
const title = readStringField(payload, "title").toLowerCase();
|
|
171
|
+
const name = readStringField(payload, "name").toLowerCase();
|
|
172
|
+
const progressText = readStringField(payload, "progressText").toLowerCase();
|
|
173
|
+
return (kind === "tool" ||
|
|
174
|
+
kind === "command" ||
|
|
175
|
+
title.startsWith("exec ") ||
|
|
176
|
+
title.startsWith("command ") ||
|
|
177
|
+
name.startsWith("exec ") ||
|
|
178
|
+
name.startsWith("command ") ||
|
|
179
|
+
progressText.startsWith("exec ") ||
|
|
180
|
+
progressText.startsWith("command "));
|
|
181
|
+
}
|
|
182
|
+
function isDefaultToolResultText(text) {
|
|
183
|
+
return /^[🛠🔧]/u.test(text.trim());
|
|
184
|
+
}
|
|
185
|
+
function summarizeProgressPayload(payload) {
|
|
186
|
+
return (readStringField(payload, "progressText") ||
|
|
187
|
+
readStringField(payload, "summary") ||
|
|
188
|
+
readStringField(payload, "message") ||
|
|
189
|
+
readStringField(payload, "title") ||
|
|
190
|
+
readStringField(payload, "name") ||
|
|
191
|
+
readStringField(payload, "status") ||
|
|
192
|
+
summarizeValue(payload));
|
|
193
|
+
}
|
|
194
|
+
function formatToolStartSummary(payload) {
|
|
195
|
+
const name = payload.name?.trim() || "tool";
|
|
196
|
+
const phase = payload.phase?.trim();
|
|
197
|
+
if (phase && phase !== "start")
|
|
198
|
+
return "";
|
|
199
|
+
const command = readPayloadCommand(payload);
|
|
200
|
+
if ((name === "exec" || name === "command") && !command)
|
|
201
|
+
return "";
|
|
202
|
+
return `[tool] ${[name, command].filter(Boolean).join(" ")} started`;
|
|
203
|
+
}
|
|
204
|
+
function formatCommandOutputSummary(payload) {
|
|
205
|
+
if (!isTerminalCommandOutput(payload))
|
|
206
|
+
return "";
|
|
207
|
+
const name = normalizeCommandLabel(readStringField(payload, "title") || readStringField(payload, "name") || "command");
|
|
208
|
+
const status = readStringField(payload, "status").toLowerCase();
|
|
209
|
+
const visibleStatus = status && status !== "ok" && status !== "completed" && status !== "success" ? status : "";
|
|
210
|
+
const exitCode = typeof payload.exitCode === "number" ? ` exit ${payload.exitCode}` : "";
|
|
211
|
+
const output = summarizeValue(payload.output);
|
|
212
|
+
const prefix = `[command] ${[name, visibleStatus].filter(Boolean).join(" ")}${exitCode}`;
|
|
213
|
+
return truncateSummary(output ? `${prefix}: ${output}` : prefix);
|
|
214
|
+
}
|
|
215
|
+
function formatPatchSummary(payload) {
|
|
216
|
+
const summary = readStringField(payload, "summary");
|
|
217
|
+
if (summary)
|
|
218
|
+
return `[patch] ${truncateSummary(summary)}`;
|
|
219
|
+
const parts = [];
|
|
220
|
+
for (const key of ["added", "modified", "deleted"]) {
|
|
221
|
+
const value = payload[key];
|
|
222
|
+
if (Array.isArray(value) && value.length > 0)
|
|
223
|
+
parts.push(`${key}: ${value.map(String).join(", ")}`);
|
|
224
|
+
else if (typeof value === "number" && value > 0)
|
|
225
|
+
parts.push(`${key}: ${value}`);
|
|
226
|
+
}
|
|
227
|
+
return `[patch] ${truncateSummary(parts.join("; ") || "updated")}`;
|
|
228
|
+
}
|
|
229
|
+
function formatItemEventSummary(payload) {
|
|
230
|
+
const kind = readStringField(payload, "kind") || "progress";
|
|
231
|
+
const title = readStringField(payload, "title") || readStringField(payload, "name");
|
|
232
|
+
const status = readStringField(payload, "status");
|
|
233
|
+
const phase = readStringField(payload, "phase");
|
|
234
|
+
const summary = summarizeProgressPayload(payload);
|
|
235
|
+
const label = [title, status || phase].filter(Boolean).join(" ");
|
|
236
|
+
return `[${kind}] ${truncateSummary(label || summary || "activity")}`;
|
|
237
|
+
}
|
|
238
|
+
function formatPlanSummary(payload) {
|
|
239
|
+
const title = readStringField(payload, "title") || "plan";
|
|
240
|
+
const explanation = readStringField(payload, "explanation");
|
|
241
|
+
const steps = Array.isArray(payload.steps) ? payload.steps.map(String).filter(Boolean) : [];
|
|
242
|
+
return `[plan] ${truncateSummary([title, explanation, ...steps].filter(Boolean).join(": "))}`;
|
|
243
|
+
}
|
|
244
|
+
function formatApprovalSummary(payload) {
|
|
245
|
+
const title = readStringField(payload, "title") || readStringField(payload, "kind") || "approval";
|
|
246
|
+
const status = readStringField(payload, "status") || readStringField(payload, "phase");
|
|
247
|
+
const message = readStringField(payload, "message") || readStringField(payload, "reason");
|
|
248
|
+
return `[approval] ${truncateSummary([title, status, message].filter(Boolean).join(" "))}`;
|
|
249
|
+
}
|
|
106
250
|
/**
|
|
107
251
|
* Reply dispatcher for clawchat-plugin-openclaw.
|
|
108
252
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* `message.send` / `message.reply` frames for the final reply.
|
|
253
|
+
* ClawChat emits only materialized `message.send` / `message.reply` frames for
|
|
254
|
+
* complete OpenClaw output units. `disableBlockStreaming` prevents token/block
|
|
255
|
+
* streaming; full visibility still forwards complete tool/progress/output units
|
|
256
|
+
* as separate ClawChat messages.
|
|
114
257
|
*/
|
|
115
258
|
export function createOpenclawClawlingReplyDispatcher(options) {
|
|
116
259
|
const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, log, } = options;
|
|
117
260
|
const isGroupTarget = target.chatType === "group";
|
|
261
|
+
const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
|
|
262
|
+
const splitFullOutput = outputVisibility === "full" && !isGroupTarget;
|
|
118
263
|
const ownerDirectTarget = () => {
|
|
119
264
|
const ownerUserId = account.ownerUserId?.trim();
|
|
120
265
|
return ownerUserId ? { chatId: ownerUserId, chatType: "direct" } : null;
|
|
@@ -142,9 +287,14 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
142
287
|
}
|
|
143
288
|
// ----- Reply state ------------------------------------------------------
|
|
144
289
|
let reasoningText = "";
|
|
290
|
+
let bufferedOutputText = "";
|
|
291
|
+
const bufferedOutputLineSet = new Set();
|
|
292
|
+
const emittedFullSegmentSet = new Set();
|
|
293
|
+
const bufferedOutputUrls = [];
|
|
145
294
|
let runDone = false;
|
|
146
295
|
let typingActive = false;
|
|
147
296
|
let terminalReplySuppressed = false;
|
|
297
|
+
let finalDeliverySeen = false;
|
|
148
298
|
const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
|
|
149
299
|
const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
|
|
150
300
|
const terminalSendScopeId = options.terminalSendScopeId ?? null;
|
|
@@ -229,6 +379,42 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
229
379
|
recordOutbound("thinking", messageId, thinkingText);
|
|
230
380
|
reasoningText = "";
|
|
231
381
|
};
|
|
382
|
+
const appendBufferedText = (value) => {
|
|
383
|
+
const trimmed = value.trim();
|
|
384
|
+
if (!trimmed)
|
|
385
|
+
return;
|
|
386
|
+
if (!trimmed.includes("\n")) {
|
|
387
|
+
if (bufferedOutputLineSet.has(trimmed))
|
|
388
|
+
return;
|
|
389
|
+
bufferedOutputLineSet.add(trimmed);
|
|
390
|
+
}
|
|
391
|
+
bufferedOutputText = bufferedOutputText ? `${bufferedOutputText}\n${trimmed}` : trimmed;
|
|
392
|
+
};
|
|
393
|
+
const appendBufferedUrls = (urls) => {
|
|
394
|
+
for (const url of urls) {
|
|
395
|
+
if (url && !bufferedOutputUrls.includes(url))
|
|
396
|
+
bufferedOutputUrls.push(url);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const fullSegmentKey = (text, urls) => JSON.stringify({
|
|
400
|
+
text: text.replace(/\s+/g, " ").trim(),
|
|
401
|
+
urls: urls.filter(Boolean),
|
|
402
|
+
});
|
|
403
|
+
const mergeFinalText = (text) => {
|
|
404
|
+
if (outputVisibility === "minimal" || outputVisibility === "full")
|
|
405
|
+
return text;
|
|
406
|
+
return [bufferedOutputText.trim(), text.trim()].filter(Boolean).join("\n");
|
|
407
|
+
};
|
|
408
|
+
const mergeFinalUrls = (urls) => {
|
|
409
|
+
if (outputVisibility === "minimal" || outputVisibility === "full")
|
|
410
|
+
return urls;
|
|
411
|
+
const merged = bufferedOutputUrls.slice();
|
|
412
|
+
for (const url of urls) {
|
|
413
|
+
if (url && !merged.includes(url))
|
|
414
|
+
merged.push(url);
|
|
415
|
+
}
|
|
416
|
+
return merged;
|
|
417
|
+
};
|
|
232
418
|
const mintStaticMessageId = () => `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
233
419
|
const emitTyping = (isTyping) => {
|
|
234
420
|
if (!isTyping && !typingActive)
|
|
@@ -336,13 +522,38 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
336
522
|
});
|
|
337
523
|
return result;
|
|
338
524
|
};
|
|
525
|
+
const emitFullSegment = async (text, urls = []) => {
|
|
526
|
+
if (outputVisibility !== "full") {
|
|
527
|
+
appendBufferedText(text);
|
|
528
|
+
appendBufferedUrls(urls);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!splitFullOutput) {
|
|
532
|
+
appendBufferedText(text);
|
|
533
|
+
appendBufferedUrls(urls);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const trimmed = text.trim();
|
|
537
|
+
if (!trimmed && urls.length === 0)
|
|
538
|
+
return;
|
|
539
|
+
const segmentKey = fullSegmentKey(trimmed, urls);
|
|
540
|
+
if (emittedFullSegmentSet.has(segmentKey))
|
|
541
|
+
return;
|
|
542
|
+
emittedFullSegmentSet.add(segmentKey);
|
|
543
|
+
const mediaFragments = await uploadMediaUrls(urls);
|
|
544
|
+
await sendStatic(trimmed, mediaFragments, [], { recordMessage: true });
|
|
545
|
+
};
|
|
546
|
+
const emitFullRuntimeText = async (label, text, urls = []) => {
|
|
547
|
+
const summary = summarizeValue(text);
|
|
548
|
+
if (!summary && urls.length === 0)
|
|
549
|
+
return;
|
|
550
|
+
await emitFullSegment(summary ? `[${label}] ${summary}` : "", urls);
|
|
551
|
+
};
|
|
339
552
|
// ----- Dispatcher -------------------------------------------------------
|
|
340
553
|
const base = runtime.channel.reply.createReplyDispatcherWithTyping({
|
|
341
554
|
humanDelay,
|
|
342
555
|
onReplyStart: async () => {
|
|
343
556
|
emitTyping(true);
|
|
344
|
-
reasoningText = "";
|
|
345
|
-
runDone = false;
|
|
346
557
|
},
|
|
347
558
|
deliver: async (payload, info) => {
|
|
348
559
|
if (consumeTerminalSend(info?.kind ?? "unknown"))
|
|
@@ -352,24 +563,47 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
352
563
|
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
353
564
|
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`);
|
|
354
565
|
if (isGroupTarget && richFragment) {
|
|
566
|
+
if (info?.kind === "final")
|
|
567
|
+
finalDeliverySeen = true;
|
|
355
568
|
if (info?.kind !== "final")
|
|
356
569
|
return;
|
|
357
570
|
await sendOwnerAttention(resolvePayloadText(payload), richFragment);
|
|
358
571
|
return;
|
|
359
572
|
}
|
|
360
573
|
if (isGroupTarget && info?.kind === "final" && looksLikeApprovalFallbackText(text)) {
|
|
574
|
+
finalDeliverySeen = true;
|
|
361
575
|
await sendOwnerAttention(text);
|
|
362
576
|
return;
|
|
363
577
|
}
|
|
364
578
|
if (payload.isReasoning) {
|
|
365
|
-
if (
|
|
579
|
+
if (outputVisibility !== "full")
|
|
366
580
|
return;
|
|
367
|
-
|
|
581
|
+
await emitFullSegment(text, urls);
|
|
582
|
+
const trimmed = text.trim();
|
|
583
|
+
if (trimmed)
|
|
584
|
+
reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
368
585
|
return;
|
|
369
586
|
}
|
|
370
|
-
if (info?.kind === "tool")
|
|
587
|
+
if (info?.kind === "tool") {
|
|
588
|
+
if (isDefaultToolResultText(text))
|
|
589
|
+
return;
|
|
590
|
+
if (outputVisibility === "full") {
|
|
591
|
+
await emitFullRuntimeText("tool result", text, urls);
|
|
592
|
+
}
|
|
371
593
|
return;
|
|
594
|
+
}
|
|
595
|
+
if (info?.kind === "block") {
|
|
596
|
+
if (outputVisibility === "full") {
|
|
597
|
+
await emitFullSegment(text, urls);
|
|
598
|
+
}
|
|
599
|
+
else if (outputVisibility === "minimal" || outputVisibility === "normal") {
|
|
600
|
+
appendBufferedText(text);
|
|
601
|
+
appendBufferedUrls(urls);
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
372
605
|
if (info?.kind === "final") {
|
|
606
|
+
finalDeliverySeen = true;
|
|
373
607
|
if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
|
|
374
608
|
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
|
|
375
609
|
openclawLlmContextDebug.writeSnapshot({
|
|
@@ -391,19 +625,21 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
391
625
|
});
|
|
392
626
|
return;
|
|
393
627
|
}
|
|
394
|
-
const
|
|
395
|
-
const
|
|
628
|
+
const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
|
|
629
|
+
const finalUrls = mergeFinalUrls(urls);
|
|
630
|
+
const mediaFragments = await uploadMediaUrls(finalUrls);
|
|
631
|
+
const result = await sendStatic(finalText, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
|
|
396
632
|
if (result?.messageId)
|
|
397
633
|
recordThinkingIfLinked(result.messageId);
|
|
398
634
|
return;
|
|
399
635
|
}
|
|
400
|
-
//
|
|
401
|
-
// the model is producing output. ClawChat gets only the final materialized
|
|
402
|
-
// reply.
|
|
636
|
+
// Unknown delivery kind: keep ClawChat output tied to OpenClaw final.
|
|
403
637
|
},
|
|
404
638
|
onError: (error, info) => {
|
|
405
639
|
const errorText = normalizeReplyErrorText(error);
|
|
406
640
|
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`);
|
|
641
|
+
if (!isGroupTarget && outputVisibility === "full")
|
|
642
|
+
void emitFullRuntimeText("error", errorText);
|
|
407
643
|
if (isGroupTarget) {
|
|
408
644
|
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`);
|
|
409
645
|
return;
|
|
@@ -414,6 +650,16 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
414
650
|
if (runDone)
|
|
415
651
|
return;
|
|
416
652
|
runDone = true;
|
|
653
|
+
if (finalDeliverySeen)
|
|
654
|
+
return;
|
|
655
|
+
const fallbackText = bufferedOutputText.trim();
|
|
656
|
+
const fallbackUrls = bufferedOutputUrls.slice();
|
|
657
|
+
if (!fallbackText && fallbackUrls.length === 0)
|
|
658
|
+
return;
|
|
659
|
+
const mediaFragments = await uploadMediaUrls(fallbackUrls);
|
|
660
|
+
const result = await sendStatic(fallbackText, mediaFragments, [], { recordMessage: true });
|
|
661
|
+
if (result?.messageId)
|
|
662
|
+
recordThinkingIfLinked(result.messageId);
|
|
417
663
|
},
|
|
418
664
|
onCleanup: () => {
|
|
419
665
|
emitTyping(false);
|
|
@@ -425,6 +671,80 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
425
671
|
...base.replyOptions,
|
|
426
672
|
sourceReplyDeliveryMode: "automatic",
|
|
427
673
|
disableBlockStreaming: true,
|
|
674
|
+
suppressDefaultToolProgressMessages: true,
|
|
675
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: outputVisibility === "full" ? true : undefined,
|
|
676
|
+
onReasoningStream: outputVisibility === "full"
|
|
677
|
+
? async (payload) => {
|
|
678
|
+
if (consumeTerminalSend("reasoning"))
|
|
679
|
+
return;
|
|
680
|
+
const text = resolvePayloadText(payload);
|
|
681
|
+
await emitFullRuntimeText("reasoning", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
682
|
+
const trimmed = text.trim();
|
|
683
|
+
if (trimmed)
|
|
684
|
+
reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
685
|
+
}
|
|
686
|
+
: undefined,
|
|
687
|
+
onToolStart: outputVisibility === "full"
|
|
688
|
+
? async (payload) => {
|
|
689
|
+
if (consumeTerminalSend("tool-start"))
|
|
690
|
+
return;
|
|
691
|
+
await emitFullSegment(formatToolStartSummary(payload));
|
|
692
|
+
}
|
|
693
|
+
: undefined,
|
|
694
|
+
onToolResult: outputVisibility === "full"
|
|
695
|
+
? async (payload) => {
|
|
696
|
+
if (consumeTerminalSend("tool-result"))
|
|
697
|
+
return;
|
|
698
|
+
const text = resolvePayloadText(payload);
|
|
699
|
+
if (isDefaultToolResultText(text))
|
|
700
|
+
return;
|
|
701
|
+
await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
702
|
+
}
|
|
703
|
+
: undefined,
|
|
704
|
+
onItemEvent: outputVisibility === "full"
|
|
705
|
+
? async (payload) => {
|
|
706
|
+
if (consumeTerminalSend("item-event"))
|
|
707
|
+
return;
|
|
708
|
+
if (isToolProgressItem(payload))
|
|
709
|
+
return;
|
|
710
|
+
await emitFullRuntimeText("progress", summarizeProgressPayload(payload));
|
|
711
|
+
}
|
|
712
|
+
: undefined,
|
|
713
|
+
onPlanUpdate: outputVisibility === "full"
|
|
714
|
+
? async (payload) => {
|
|
715
|
+
if (consumeTerminalSend("plan-update"))
|
|
716
|
+
return;
|
|
717
|
+
await emitFullRuntimeText("plan", summarizeProgressPayload(payload));
|
|
718
|
+
}
|
|
719
|
+
: undefined,
|
|
720
|
+
onCommandOutput: outputVisibility === "full"
|
|
721
|
+
? async (payload) => {
|
|
722
|
+
if (consumeTerminalSend("command-output"))
|
|
723
|
+
return;
|
|
724
|
+
await emitFullSegment(formatCommandOutputSummary(payload));
|
|
725
|
+
}
|
|
726
|
+
: undefined,
|
|
727
|
+
onPatchSummary: outputVisibility === "full"
|
|
728
|
+
? async (payload) => {
|
|
729
|
+
if (consumeTerminalSend("patch-summary"))
|
|
730
|
+
return;
|
|
731
|
+
await emitFullSegment(formatPatchSummary(payload));
|
|
732
|
+
}
|
|
733
|
+
: undefined,
|
|
734
|
+
onCompactionStart: outputVisibility === "full"
|
|
735
|
+
? async () => {
|
|
736
|
+
if (consumeTerminalSend("compaction-start"))
|
|
737
|
+
return;
|
|
738
|
+
await emitFullSegment("[compaction] started");
|
|
739
|
+
}
|
|
740
|
+
: undefined,
|
|
741
|
+
onCompactionEnd: outputVisibility === "full"
|
|
742
|
+
? async () => {
|
|
743
|
+
if (consumeTerminalSend("compaction-end"))
|
|
744
|
+
return;
|
|
745
|
+
await emitFullSegment("[compaction] finished");
|
|
746
|
+
}
|
|
747
|
+
: undefined,
|
|
428
748
|
},
|
|
429
749
|
markDispatchIdle: base.markDispatchIdle,
|
|
430
750
|
};
|