@clawling/clawchat-plugin-openclaw 2026.5.12-28
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/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import type { Fragment } from "./protocol-types.ts";
|
|
2
|
+
import type { ClawlingChatClient } from "./ws-client.ts";
|
|
3
|
+
import {
|
|
4
|
+
interactiveReplyToPresentation,
|
|
5
|
+
renderMessagePresentationFallbackText,
|
|
6
|
+
type MessagePresentation,
|
|
7
|
+
type MessagePresentationBlock,
|
|
8
|
+
type MessagePresentationButtonStyle,
|
|
9
|
+
} from "openclaw/plugin-sdk/interactive-runtime";
|
|
10
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
11
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
12
|
+
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
13
|
+
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
|
14
|
+
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
15
|
+
import type { ChatType } from "./client.ts";
|
|
16
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
17
|
+
import { uploadOutboundMedia, type ClawlingMediaFragment } from "./media-runtime.ts";
|
|
18
|
+
import {
|
|
19
|
+
sendOpenclawClawlingText,
|
|
20
|
+
type OutboundReplyCtx,
|
|
21
|
+
type SendResult,
|
|
22
|
+
} from "./outbound.ts";
|
|
23
|
+
import { isClawChatNoopResponseText } from "./profile-prompt.ts";
|
|
24
|
+
import type { ClawChatStore } from "./storage.ts";
|
|
25
|
+
import { consumeTerminalClawChatSend } from "./terminal-send.ts";
|
|
26
|
+
import { openclawLlmContextDebug } from "./llm-context-debug.ts";
|
|
27
|
+
|
|
28
|
+
export interface ReplyDispatcherOptions {
|
|
29
|
+
cfg: OpenClawConfig;
|
|
30
|
+
runtime: PluginRuntime;
|
|
31
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
32
|
+
client: ClawlingChatClient;
|
|
33
|
+
/**
|
|
34
|
+
* New-protocol routing: `chatId` is the subject id, `chatType` is `"direct"`
|
|
35
|
+
* or `"group"`. Every outbound frame carries these as `chat_id` + `chat_type`.
|
|
36
|
+
*/
|
|
37
|
+
target: { chatId: string; chatType: ChatType };
|
|
38
|
+
replyCtx?: OutboundReplyCtx;
|
|
39
|
+
/**
|
|
40
|
+
* The `message_id` from the inbound `message.send` envelope that triggered
|
|
41
|
+
* this reply run. Preserved for reply context compatibility.
|
|
42
|
+
*/
|
|
43
|
+
inboundMessageId?: string;
|
|
44
|
+
terminalSendScopeId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Describes the inbound user message for reply preview compatibility.
|
|
47
|
+
*/
|
|
48
|
+
inboundForFinalReply?: {
|
|
49
|
+
chatId?: string;
|
|
50
|
+
senderId: string;
|
|
51
|
+
senderNickName: string;
|
|
52
|
+
bodyText: string;
|
|
53
|
+
};
|
|
54
|
+
store?: Pick<
|
|
55
|
+
ClawChatStore,
|
|
56
|
+
"insertMessage" | "claimMessageOnce" | "markMessageAcknowledged" | "updateMessageByIdentity"
|
|
57
|
+
> | null;
|
|
58
|
+
log?: { info?: (m: string) => void; error?: (m: string) => void };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type TypedReplyDispatcherResult = ReturnType<
|
|
62
|
+
PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"]
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
type ClawChatReplyOptions = TypedReplyDispatcherResult["replyOptions"] &
|
|
66
|
+
{
|
|
67
|
+
sourceReplyDeliveryMode: "automatic";
|
|
68
|
+
disableBlockStreaming: true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type RichAction = {
|
|
72
|
+
id: string;
|
|
73
|
+
label: string;
|
|
74
|
+
style?: MessagePresentationButtonStyle;
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
payload?: Record<string, unknown>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type RichInteractionFragment = {
|
|
80
|
+
kind: "approval_request" | "action_card";
|
|
81
|
+
title?: string;
|
|
82
|
+
fallback_text: string;
|
|
83
|
+
state: "pending";
|
|
84
|
+
actions: RichAction[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const GROUP_OWNER_ATTENTION_TITLE = "requires owner attention";
|
|
88
|
+
|
|
89
|
+
function ownerAttentionText(groupId: string, fallbackText: string): string {
|
|
90
|
+
const body = fallbackText.trim();
|
|
91
|
+
return `ClawChat group ${groupId} ${GROUP_OWNER_ATTENTION_TITLE}.${body ? `\n\n${body}` : ""}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeReplyErrorText(error: unknown): string {
|
|
95
|
+
const raw = String(error);
|
|
96
|
+
const retryWrapped = raw.match(/^Error: Retry failed for delivery [^:]+:\s*(.+)$/s);
|
|
97
|
+
if (retryWrapped?.[1]?.trim()) return retryWrapped[1].trim();
|
|
98
|
+
const retryWrappedBare = raw.match(/^Retry failed for delivery [^:]+:\s*(.+)$/s);
|
|
99
|
+
if (retryWrappedBare?.[1]?.trim()) return retryWrappedBare[1].trim();
|
|
100
|
+
return raw;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isMessagePresentation(value: unknown): value is MessagePresentation {
|
|
104
|
+
return Boolean(
|
|
105
|
+
value &&
|
|
106
|
+
typeof value === "object" &&
|
|
107
|
+
Array.isArray((value as { blocks?: unknown }).blocks),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolvePresentation(payload: ReplyPayload): MessagePresentation | undefined {
|
|
112
|
+
if (isMessagePresentation(payload.presentation)) return payload.presentation;
|
|
113
|
+
if (payload.interactive) return interactiveReplyToPresentation(payload.interactive);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeActionId(value: string | undefined, label: string, index: number): string {
|
|
118
|
+
const raw = value?.trim() || label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
119
|
+
return raw.replace(/^-+|-+$/g, "") || `action-${index + 1}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectPresentationActions(blocks: MessagePresentationBlock[]): RichAction[] {
|
|
123
|
+
const actions: RichAction[] = [];
|
|
124
|
+
for (const block of blocks) {
|
|
125
|
+
if (block.type === "buttons") {
|
|
126
|
+
for (const button of block.buttons) {
|
|
127
|
+
const value = button.value?.trim();
|
|
128
|
+
const url = button.url?.trim();
|
|
129
|
+
const action: RichAction = {
|
|
130
|
+
id: normalizeActionId(value ?? url, button.label, actions.length),
|
|
131
|
+
label: button.label,
|
|
132
|
+
...(button.style ? { style: button.style } : {}),
|
|
133
|
+
...(value || url ? { payload: { ...(value ? { value } : {}), ...(url ? { url } : {}) } } : {}),
|
|
134
|
+
};
|
|
135
|
+
actions.push(action);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (block.type === "select") {
|
|
139
|
+
for (const option of block.options) {
|
|
140
|
+
actions.push({
|
|
141
|
+
id: normalizeActionId(option.value, option.label, actions.length),
|
|
142
|
+
label: option.label,
|
|
143
|
+
style: "secondary",
|
|
144
|
+
payload: { value: option.value },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return actions;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function looksLikeApproval(actions: RichAction[], presentation: MessagePresentation): boolean {
|
|
153
|
+
if (presentation.tone === "warning" || presentation.tone === "danger") return true;
|
|
154
|
+
const ids = new Set(actions.map((action) => action.id.toLowerCase()));
|
|
155
|
+
return ids.has("approve") || ids.has("deny") || ids.has("reject");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildRichInteractionFragment(payload: ReplyPayload): RichInteractionFragment | null {
|
|
159
|
+
const presentation = resolvePresentation(payload);
|
|
160
|
+
if (!presentation) return null;
|
|
161
|
+
const actions = collectPresentationActions(presentation.blocks);
|
|
162
|
+
if (actions.length === 0) return null;
|
|
163
|
+
const fallbackText = renderMessagePresentationFallbackText({
|
|
164
|
+
presentation,
|
|
165
|
+
text: payload.text ?? null,
|
|
166
|
+
}).trim();
|
|
167
|
+
if (!fallbackText) return null;
|
|
168
|
+
return {
|
|
169
|
+
kind: looksLikeApproval(actions, presentation) ? "approval_request" : "action_card",
|
|
170
|
+
...(presentation.title?.trim() ? { title: presentation.title.trim() } : {}),
|
|
171
|
+
fallback_text: fallbackText,
|
|
172
|
+
state: "pending",
|
|
173
|
+
actions,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolvePayloadText(payload: ReplyPayload): string {
|
|
178
|
+
const presentation = resolvePresentation(payload);
|
|
179
|
+
if (!presentation) return payload.text ?? "";
|
|
180
|
+
return renderMessagePresentationFallbackText({ presentation, text: payload.text ?? null });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reply dispatcher for clawchat-plugin-openclaw.
|
|
185
|
+
*
|
|
186
|
+
* The plugin intentionally forces complete-message delivery. It sets
|
|
187
|
+
* `disableBlockStreaming: true` in reply options so OpenClaw does not split
|
|
188
|
+
* deliver blocks for this channel. If the host still delivers non-final
|
|
189
|
+
* blocks, the dispatcher buffers or ignores them and only emits materialized
|
|
190
|
+
* `message.send` / `message.reply` frames for the final reply.
|
|
191
|
+
*/
|
|
192
|
+
export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOptions): {
|
|
193
|
+
dispatcher: TypedReplyDispatcherResult["dispatcher"];
|
|
194
|
+
replyOptions: ClawChatReplyOptions;
|
|
195
|
+
markDispatchIdle: TypedReplyDispatcherResult["markDispatchIdle"];
|
|
196
|
+
} {
|
|
197
|
+
const {
|
|
198
|
+
cfg,
|
|
199
|
+
runtime,
|
|
200
|
+
account,
|
|
201
|
+
client,
|
|
202
|
+
target,
|
|
203
|
+
replyCtx,
|
|
204
|
+
inboundMessageId,
|
|
205
|
+
store,
|
|
206
|
+
log,
|
|
207
|
+
} = options;
|
|
208
|
+
const isGroupTarget = target.chatType === "group";
|
|
209
|
+
const ownerDirectTarget = () => {
|
|
210
|
+
const ownerUserId = account.ownerUserId?.trim();
|
|
211
|
+
return ownerUserId ? { chatId: ownerUserId, chatType: "direct" as const } : null;
|
|
212
|
+
};
|
|
213
|
+
const humanDelay = runtime.channel.reply.resolveHumanDelayConfig(cfg, account.userId);
|
|
214
|
+
|
|
215
|
+
const buildApiClient = () => {
|
|
216
|
+
if (!account.baseUrl || !account.token) return null;
|
|
217
|
+
return createOpenclawClawlingApiClient({
|
|
218
|
+
baseUrl: account.baseUrl,
|
|
219
|
+
token: account.token,
|
|
220
|
+
userId: account.userId,
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
async function uploadMediaUrls(urls: string[]): Promise<ClawlingMediaFragment[]> {
|
|
225
|
+
if (urls.length === 0) return [];
|
|
226
|
+
const apiClient = buildApiClient();
|
|
227
|
+
if (!apiClient) {
|
|
228
|
+
log?.info?.(
|
|
229
|
+
`[${account.accountId}] clawchat-plugin-openclaw outbound media skipped: baseUrl not configured`,
|
|
230
|
+
);
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
return await uploadOutboundMedia(urls, { apiClient, runtime, log });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ----- Reply state ------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
let reasoningText = "";
|
|
239
|
+
let runDone = false;
|
|
240
|
+
let typingActive = false;
|
|
241
|
+
let terminalReplySuppressed = false;
|
|
242
|
+
|
|
243
|
+
const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
|
|
244
|
+
const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
|
|
245
|
+
const terminalSendScopeId = options.terminalSendScopeId ?? null;
|
|
246
|
+
const consumeTerminalSend = (reason: string): boolean => {
|
|
247
|
+
if (terminalReplySuppressed) return true;
|
|
248
|
+
if (!terminalSendScopeId) return false;
|
|
249
|
+
const terminal = consumeTerminalClawChatSend({
|
|
250
|
+
accountId: account.accountId,
|
|
251
|
+
chatId: target.chatId,
|
|
252
|
+
scopeId: terminalSendScopeId,
|
|
253
|
+
});
|
|
254
|
+
if (!terminal) return false;
|
|
255
|
+
terminalReplySuppressed = true;
|
|
256
|
+
log?.info?.(
|
|
257
|
+
`[${account.accountId}] clawchat-plugin-openclaw suppressing ${reason} reply after terminal tool send msg=${terminal.messageId} to=${target.chatId}`,
|
|
258
|
+
);
|
|
259
|
+
return true;
|
|
260
|
+
};
|
|
261
|
+
const claimOutbound = (
|
|
262
|
+
eventType: "message.send" | "message.reply",
|
|
263
|
+
messageId: string,
|
|
264
|
+
text: string,
|
|
265
|
+
raw: unknown,
|
|
266
|
+
): true | false | null => {
|
|
267
|
+
if (!store || !messageId) return null;
|
|
268
|
+
try {
|
|
269
|
+
return store.claimMessageOnce({
|
|
270
|
+
platform: "openclaw",
|
|
271
|
+
accountId: account.accountId,
|
|
272
|
+
kind: "message",
|
|
273
|
+
direction: "outbound",
|
|
274
|
+
eventType,
|
|
275
|
+
chatId: target.chatId,
|
|
276
|
+
messageId,
|
|
277
|
+
text,
|
|
278
|
+
raw,
|
|
279
|
+
});
|
|
280
|
+
} catch {
|
|
281
|
+
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw sqlite outbound claim failed`);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const recordOutbound = (kind: "message" | "thinking", messageId: string, text: string) => {
|
|
286
|
+
if (!store || !messageId) return;
|
|
287
|
+
try {
|
|
288
|
+
store.insertMessage({
|
|
289
|
+
platform: "openclaw",
|
|
290
|
+
accountId: account.accountId,
|
|
291
|
+
kind,
|
|
292
|
+
direction: "outbound",
|
|
293
|
+
eventType: outboundEventType(),
|
|
294
|
+
chatId: target.chatId,
|
|
295
|
+
messageId,
|
|
296
|
+
text,
|
|
297
|
+
raw: outboundRaw(),
|
|
298
|
+
});
|
|
299
|
+
} catch {
|
|
300
|
+
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw sqlite outbound insert failed; continuing`);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const recordOutboundAck = (kind: "message", messageId: string, result: SendResult | null) => {
|
|
304
|
+
if (!store || !messageId || !result?.messageId) return;
|
|
305
|
+
try {
|
|
306
|
+
store.markMessageAcknowledged?.({
|
|
307
|
+
accountId: account.accountId,
|
|
308
|
+
kind,
|
|
309
|
+
direction: "outbound",
|
|
310
|
+
messageId,
|
|
311
|
+
protocolMessageId: result.messageId,
|
|
312
|
+
ackedAt: result.acceptedAt ?? Date.now(),
|
|
313
|
+
});
|
|
314
|
+
} catch {
|
|
315
|
+
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw sqlite outbound ack update failed`);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const recordThinkingIfLinked = (messageId: string) => {
|
|
319
|
+
const thinkingText = reasoningText.trim();
|
|
320
|
+
if (!thinkingText) return;
|
|
321
|
+
recordOutbound("thinking", messageId, thinkingText);
|
|
322
|
+
reasoningText = "";
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const mintStaticMessageId = () =>
|
|
326
|
+
`${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
327
|
+
|
|
328
|
+
const emitTyping = (isTyping: boolean) => {
|
|
329
|
+
if (!isTyping && !typingActive) return;
|
|
330
|
+
try {
|
|
331
|
+
client.typing(target.chatId, isTyping);
|
|
332
|
+
typingActive = isTyping;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
log?.error?.(
|
|
335
|
+
`[${account.accountId}] clawchat-plugin-openclaw typing update failed: ${String(error)}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const sendOwnerAttention = async (
|
|
341
|
+
fallbackText: string,
|
|
342
|
+
richFragment: Fragment | null = null,
|
|
343
|
+
): Promise<void> => {
|
|
344
|
+
const ownerTarget = ownerDirectTarget();
|
|
345
|
+
if (!ownerTarget) {
|
|
346
|
+
log?.error?.(
|
|
347
|
+
`[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason=missing_owner_user_id group=${target.chatId}`,
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
await sendOpenclawClawlingText({
|
|
352
|
+
client,
|
|
353
|
+
account,
|
|
354
|
+
to: ownerTarget,
|
|
355
|
+
text: ownerAttentionText(target.chatId, fallbackText),
|
|
356
|
+
messageId: mintStaticMessageId(),
|
|
357
|
+
...(richFragment ? { richFragments: [richFragment] } : {}),
|
|
358
|
+
log,
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// ----- Static send ------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
const sendStatic = async (
|
|
365
|
+
text: string,
|
|
366
|
+
mediaFragments: ClawlingMediaFragment[] = [],
|
|
367
|
+
richFragments: Fragment[] = [],
|
|
368
|
+
options: { recordMessage?: boolean } = {},
|
|
369
|
+
): Promise<SendResult | null> => {
|
|
370
|
+
if (
|
|
371
|
+
isClawChatNoopResponseText(text) &&
|
|
372
|
+
mediaFragments.length === 0 &&
|
|
373
|
+
richFragments.length === 0
|
|
374
|
+
) {
|
|
375
|
+
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw outbound suppressed: no-reply token`);
|
|
376
|
+
openclawLlmContextDebug.writeSnapshot({
|
|
377
|
+
visibility: "host_event",
|
|
378
|
+
trace: {
|
|
379
|
+
messageId: inboundMessageId ?? "unknown",
|
|
380
|
+
chatId: target.chatId,
|
|
381
|
+
chatType: target.chatType,
|
|
382
|
+
},
|
|
383
|
+
input: { injectedPrompt: "", eventText: "" },
|
|
384
|
+
output: {
|
|
385
|
+
rawModelOutput: text,
|
|
386
|
+
finalAssistantText: text,
|
|
387
|
+
adapterFilteredText: "",
|
|
388
|
+
outboundClawChatMessage: null,
|
|
389
|
+
suppressed: true,
|
|
390
|
+
suppressionReason: "no-reply token",
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
if (!text.trim() && mediaFragments.length === 0 && richFragments.length === 0) return null;
|
|
396
|
+
if (consumeTerminalSend("static")) return null;
|
|
397
|
+
log?.info?.(
|
|
398
|
+
`[${account.accountId}] clawchat-plugin-openclaw sending static text_len=${text.length} media=${mediaFragments.length} rich=${richFragments.length} to=${target.chatId}`,
|
|
399
|
+
);
|
|
400
|
+
const messageId = mintStaticMessageId();
|
|
401
|
+
const raw = { target, replyCtx: replyCtx ?? null, mode: "static" };
|
|
402
|
+
const claimed = options.recordMessage
|
|
403
|
+
? claimOutbound(outboundEventType(), messageId, text, raw)
|
|
404
|
+
: true;
|
|
405
|
+
if (claimed === false) {
|
|
406
|
+
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw outbound duplicate skipped msg=${messageId}`);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
if (claimed === null) {
|
|
410
|
+
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw outbound skipped msg=${messageId} reason=claim_unavailable`);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const result = await sendOpenclawClawlingText({
|
|
414
|
+
client,
|
|
415
|
+
account,
|
|
416
|
+
to: target,
|
|
417
|
+
text,
|
|
418
|
+
messageId,
|
|
419
|
+
...(replyCtx ? { replyCtx } : {}),
|
|
420
|
+
...(richFragments.length > 0 ? { richFragments } : {}),
|
|
421
|
+
...(mediaFragments.length > 0 ? { mediaFragments } : {}),
|
|
422
|
+
log,
|
|
423
|
+
});
|
|
424
|
+
recordOutboundAck("message", messageId, result);
|
|
425
|
+
log?.info?.(
|
|
426
|
+
`[${account.accountId}] clawchat-plugin-openclaw send complete to=${target.chatId}`,
|
|
427
|
+
);
|
|
428
|
+
openclawLlmContextDebug.writeSnapshot({
|
|
429
|
+
visibility: "host_event",
|
|
430
|
+
trace: {
|
|
431
|
+
messageId,
|
|
432
|
+
chatId: target.chatId,
|
|
433
|
+
chatType: target.chatType,
|
|
434
|
+
},
|
|
435
|
+
input: { injectedPrompt: "", eventText: "" },
|
|
436
|
+
output: {
|
|
437
|
+
rawModelOutput: text,
|
|
438
|
+
finalAssistantText: text,
|
|
439
|
+
adapterFilteredText: text,
|
|
440
|
+
outboundClawChatMessage: {
|
|
441
|
+
messageId: result?.messageId ?? messageId,
|
|
442
|
+
chatId: target.chatId,
|
|
443
|
+
chatType: target.chatType,
|
|
444
|
+
},
|
|
445
|
+
suppressed: false,
|
|
446
|
+
suppressionReason: null,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
return result;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// ----- Dispatcher -------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
const base = runtime.channel.reply.createReplyDispatcherWithTyping({
|
|
455
|
+
humanDelay,
|
|
456
|
+
onReplyStart: async () => {
|
|
457
|
+
emitTyping(true);
|
|
458
|
+
reasoningText = "";
|
|
459
|
+
runDone = false;
|
|
460
|
+
},
|
|
461
|
+
deliver: async (payload: ReplyPayload, info?: { kind: "tool" | "block" | "final" }) => {
|
|
462
|
+
if (consumeTerminalSend(info?.kind ?? "unknown")) return;
|
|
463
|
+
const richFragment = buildRichInteractionFragment(payload);
|
|
464
|
+
const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
|
|
465
|
+
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
466
|
+
log?.info?.(
|
|
467
|
+
`[${account.accountId}] clawchat-plugin-openclaw deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (isGroupTarget && richFragment) {
|
|
471
|
+
if (info?.kind !== "final") return;
|
|
472
|
+
await sendOwnerAttention(
|
|
473
|
+
resolvePayloadText(payload),
|
|
474
|
+
richFragment as unknown as Fragment,
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (payload.isReasoning) {
|
|
480
|
+
if (isGroupTarget || !account.forwardThinking) return;
|
|
481
|
+
reasoningText = text;
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (info?.kind === "tool") return;
|
|
486
|
+
|
|
487
|
+
if (info?.kind === "final") {
|
|
488
|
+
if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
|
|
489
|
+
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
|
|
490
|
+
openclawLlmContextDebug.writeSnapshot({
|
|
491
|
+
visibility: "host_event",
|
|
492
|
+
trace: {
|
|
493
|
+
messageId: inboundMessageId ?? "unknown",
|
|
494
|
+
chatId: target.chatId,
|
|
495
|
+
chatType: target.chatType,
|
|
496
|
+
},
|
|
497
|
+
input: { injectedPrompt: "", eventText: "" },
|
|
498
|
+
output: {
|
|
499
|
+
rawModelOutput: text,
|
|
500
|
+
finalAssistantText: text,
|
|
501
|
+
adapterFilteredText: "",
|
|
502
|
+
outboundClawChatMessage: null,
|
|
503
|
+
suppressed: true,
|
|
504
|
+
suppressionReason: "no-reply token",
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const mediaFragments = await uploadMediaUrls(urls);
|
|
510
|
+
const result = await sendStatic(
|
|
511
|
+
text,
|
|
512
|
+
mediaFragments,
|
|
513
|
+
richFragment && account.richInteractions ? ([richFragment] as unknown as Fragment[]) : [],
|
|
514
|
+
{ recordMessage: true },
|
|
515
|
+
);
|
|
516
|
+
if (result?.messageId) recordThinkingIfLinked(result.messageId);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// kind === "block" or unknown: OpenClaw may still call this path while
|
|
521
|
+
// the model is producing output. ClawChat gets only the final materialized
|
|
522
|
+
// reply.
|
|
523
|
+
},
|
|
524
|
+
onError: (error: unknown, info: { kind: string }) => {
|
|
525
|
+
const errorText = normalizeReplyErrorText(error);
|
|
526
|
+
log?.error?.(
|
|
527
|
+
`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`,
|
|
528
|
+
);
|
|
529
|
+
if (isGroupTarget) {
|
|
530
|
+
log?.error?.(
|
|
531
|
+
`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`,
|
|
532
|
+
);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
onIdle: async () => {
|
|
537
|
+
emitTyping(false);
|
|
538
|
+
if (runDone) return;
|
|
539
|
+
runDone = true;
|
|
540
|
+
},
|
|
541
|
+
onCleanup: () => {
|
|
542
|
+
emitTyping(false);
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
dispatcher: base.dispatcher,
|
|
548
|
+
replyOptions: {
|
|
549
|
+
...base.replyOptions,
|
|
550
|
+
sourceReplyDeliveryMode: "automatic",
|
|
551
|
+
disableBlockStreaming: true,
|
|
552
|
+
},
|
|
553
|
+
markDispatchIdle: base.markDispatchIdle,
|
|
554
|
+
};
|
|
555
|
+
}
|