@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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/README.md +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -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 +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -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.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
package/dist/src/outbound.js
CHANGED
|
@@ -1,19 +1,55 @@
|
|
|
1
|
+
import { MessageSendError } from "./protocol-types.js";
|
|
1
2
|
import { createAttachedChannelResultAdapter, } from "openclaw/plugin-sdk/channel-send-result";
|
|
2
3
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
3
4
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
4
5
|
import { CHANNEL_ID, resolveOpenclawClawlingAccount } from "./config.js";
|
|
5
6
|
import { textToFragments } from "./message-mapper.js";
|
|
6
7
|
import { uploadOutboundMedia } from "./media-runtime.js";
|
|
8
|
+
import { isClawChatNoopResponseText } from "./profile-prompt.js";
|
|
7
9
|
import { getOpenclawClawlingClient, getOpenclawClawlingRuntime, waitForOpenclawClawlingClient, } from "./runtime.js";
|
|
10
|
+
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
8
11
|
import { createAlignedWsQueue } from "./ws-alignment.js";
|
|
9
12
|
import { formatWsLog } from "./ws-log.js";
|
|
10
13
|
const alignedOutboundQueues = new WeakMap();
|
|
11
14
|
const alignedOutboundContexts = new WeakMap();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const alignedOutboundMessageErrorTrackers = new WeakMap();
|
|
16
|
+
const alignedOutboundCloseHandlers = new WeakMap();
|
|
17
|
+
const alignedOutboundStateHandlers = new WeakMap();
|
|
18
|
+
function addAlignedOutboundCloseHandler(client, handler) {
|
|
19
|
+
const key = client;
|
|
20
|
+
let entry = alignedOutboundCloseHandlers.get(key);
|
|
21
|
+
if (!entry) {
|
|
22
|
+
const handlers = new Set();
|
|
23
|
+
const listener = (close) => {
|
|
24
|
+
for (const current of [...handlers])
|
|
25
|
+
current(close);
|
|
26
|
+
};
|
|
27
|
+
entry = { handlers, listener };
|
|
28
|
+
alignedOutboundCloseHandlers.set(key, entry);
|
|
29
|
+
client.on("close", listener);
|
|
30
|
+
}
|
|
31
|
+
entry.handlers.add(handler);
|
|
32
|
+
return () => {
|
|
33
|
+
entry?.handlers.delete(handler);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function addAlignedOutboundStateHandler(client, handler) {
|
|
37
|
+
const key = client;
|
|
38
|
+
let entry = alignedOutboundStateHandlers.get(key);
|
|
39
|
+
if (!entry) {
|
|
40
|
+
const handlers = new Set();
|
|
41
|
+
const listener = (state) => {
|
|
42
|
+
for (const current of [...handlers])
|
|
43
|
+
current(state);
|
|
44
|
+
};
|
|
45
|
+
entry = { handlers, listener };
|
|
46
|
+
alignedOutboundStateHandlers.set(key, entry);
|
|
47
|
+
client.on("state", listener);
|
|
48
|
+
}
|
|
49
|
+
entry.handlers.add(handler);
|
|
50
|
+
return () => {
|
|
51
|
+
entry?.handlers.delete(handler);
|
|
52
|
+
};
|
|
17
53
|
}
|
|
18
54
|
function getAlignedOutboundQueue(client, account, log) {
|
|
19
55
|
const existing = alignedOutboundQueues.get(client);
|
|
@@ -32,25 +68,60 @@ function getAlignedOutboundQueue(client, account, log) {
|
|
|
32
68
|
alignedOutboundQueues.set(client, queue);
|
|
33
69
|
return queue;
|
|
34
70
|
}
|
|
71
|
+
function getAlignedMessageErrorTracker(client, account, log) {
|
|
72
|
+
const key = client;
|
|
73
|
+
const existing = alignedOutboundMessageErrorTrackers.get(key);
|
|
74
|
+
if (existing)
|
|
75
|
+
return existing;
|
|
76
|
+
const pending = new Set();
|
|
77
|
+
const listener = (env) => {
|
|
78
|
+
if (env.event !== "message.error")
|
|
79
|
+
return;
|
|
80
|
+
if (pending.has(env.trace_id)) {
|
|
81
|
+
client.markMessageErrorHandled?.(env.trace_id);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (client.hasPendingAckTrace?.(env.trace_id)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const context = alignedOutboundContexts.get(key)?.() ?? {
|
|
88
|
+
attempt: 1,
|
|
89
|
+
reconnectCount: 0,
|
|
90
|
+
state: client.transportState === "open" ? "ready" : "reconnecting",
|
|
91
|
+
};
|
|
92
|
+
if (log?.info) {
|
|
93
|
+
log.info(formatWsLog({
|
|
94
|
+
event: "ack_unmatched",
|
|
95
|
+
accountId: account.accountId,
|
|
96
|
+
attempt: context.attempt,
|
|
97
|
+
reconnectCount: context.reconnectCount,
|
|
98
|
+
state: context.state,
|
|
99
|
+
action: "ignore",
|
|
100
|
+
fields: [
|
|
101
|
+
["trace_id", env.trace_id],
|
|
102
|
+
["chat_id", env.chat_id],
|
|
103
|
+
],
|
|
104
|
+
}));
|
|
105
|
+
client.markMessageErrorHandled?.(env.trace_id);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
client.on("raw", listener);
|
|
109
|
+
const tracker = { pending, listener };
|
|
110
|
+
alignedOutboundMessageErrorTrackers.set(key, tracker);
|
|
111
|
+
return tracker;
|
|
112
|
+
}
|
|
35
113
|
export function setAlignedOutboundLogContext(client, context) {
|
|
36
114
|
alignedOutboundContexts.set(client, context);
|
|
37
115
|
}
|
|
38
116
|
export function flushAlignedOutboundQueue(client) {
|
|
39
|
-
const raw = getRawClientInternals(client);
|
|
40
|
-
if (!raw?.opts?.transport)
|
|
41
|
-
return;
|
|
42
117
|
const queue = alignedOutboundQueues.get(client);
|
|
43
|
-
queue?.flush((wire) =>
|
|
118
|
+
queue?.flush((wire) => client.sendWire(wire));
|
|
44
119
|
}
|
|
45
120
|
export function getAlignedOutboundQueueSize(client) {
|
|
46
121
|
return alignedOutboundQueues.get(client)?.snapshot().length ?? 0;
|
|
47
122
|
}
|
|
48
123
|
async function sendAlignedAckableEnvelope(params) {
|
|
49
|
-
const
|
|
50
|
-
if (!raw?.opts?.transport || !raw.opts.traceIdFactory)
|
|
51
|
-
return null;
|
|
52
|
-
const transport = raw.opts.transport;
|
|
53
|
-
const traceId = raw.opts.traceIdFactory();
|
|
124
|
+
const traceId = params.client.nextTraceId();
|
|
54
125
|
const env = {
|
|
55
126
|
version: "2",
|
|
56
127
|
event: params.eventName,
|
|
@@ -61,15 +132,45 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
61
132
|
};
|
|
62
133
|
const wire = JSON.stringify(env);
|
|
63
134
|
const queue = getAlignedOutboundQueue(params.client, params.account, params.log);
|
|
135
|
+
const messageErrorTracker = getAlignedMessageErrorTracker(params.client, params.account, params.log);
|
|
136
|
+
const isReady = () => {
|
|
137
|
+
const state = params.client.state;
|
|
138
|
+
return params.client.transportState === "open" && (!state || state === "connected");
|
|
139
|
+
};
|
|
140
|
+
const isDisconnected = () => params.client.state === "disconnected";
|
|
64
141
|
return await new Promise((resolve, reject) => {
|
|
142
|
+
let state = "queued";
|
|
65
143
|
let timer;
|
|
144
|
+
let rawListenerRegistered = false;
|
|
145
|
+
let removeCloseListener;
|
|
146
|
+
let removeStateListener;
|
|
147
|
+
const clearAckTimer = () => {
|
|
148
|
+
if (!timer)
|
|
149
|
+
return;
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
timer = undefined;
|
|
152
|
+
};
|
|
153
|
+
const removeRawListener = () => {
|
|
154
|
+
if (!rawListenerRegistered)
|
|
155
|
+
return;
|
|
156
|
+
params.client.off("raw", onRaw);
|
|
157
|
+
rawListenerRegistered = false;
|
|
158
|
+
};
|
|
66
159
|
const cleanup = () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
160
|
+
messageErrorTracker.pending.delete(traceId);
|
|
161
|
+
clearAckTimer();
|
|
162
|
+
removeRawListener();
|
|
163
|
+
removeCloseListener?.();
|
|
164
|
+
removeCloseListener = undefined;
|
|
165
|
+
removeStateListener?.();
|
|
166
|
+
removeStateListener = undefined;
|
|
167
|
+
};
|
|
168
|
+
const fail = (err) => {
|
|
169
|
+
if (state === "acked" || state === "failed")
|
|
170
|
+
return;
|
|
171
|
+
state = "failed";
|
|
172
|
+
cleanup();
|
|
173
|
+
reject(err);
|
|
73
174
|
};
|
|
74
175
|
const logAck = (event, action, fields) => {
|
|
75
176
|
params.log?.info?.(formatWsLog({
|
|
@@ -78,7 +179,7 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
78
179
|
...(alignedOutboundContexts.get(params.client)?.() ?? {
|
|
79
180
|
attempt: 1,
|
|
80
181
|
reconnectCount: 0,
|
|
81
|
-
state:
|
|
182
|
+
state: isReady() ? "ready" : "reconnecting",
|
|
82
183
|
}),
|
|
83
184
|
action,
|
|
84
185
|
fields: [
|
|
@@ -89,20 +190,40 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
89
190
|
],
|
|
90
191
|
}));
|
|
91
192
|
};
|
|
92
|
-
|
|
93
|
-
if (ack.
|
|
193
|
+
function onRaw(ack) {
|
|
194
|
+
if (ack.trace_id !== traceId)
|
|
195
|
+
return;
|
|
196
|
+
if (ack.event === "message.error") {
|
|
197
|
+
if (state === "acked" || state === "failed")
|
|
198
|
+
return;
|
|
199
|
+
const payload = ack.payload;
|
|
200
|
+
const code = typeof payload.code === "string" && payload.code ? payload.code : "unknown";
|
|
201
|
+
const message = typeof payload.message === "string" && payload.message ? payload.message : "message send failed";
|
|
202
|
+
fail(new MessageSendError(traceId, code, message, ack.chat_id));
|
|
94
203
|
return;
|
|
204
|
+
}
|
|
205
|
+
if (ack.event !== "message.ack")
|
|
206
|
+
return;
|
|
207
|
+
if (state === "acked" || state === "failed")
|
|
208
|
+
return;
|
|
209
|
+
state = "acked";
|
|
95
210
|
cleanup();
|
|
96
211
|
const payload = ack.payload;
|
|
97
212
|
logAck("ack_received", "resolve", [["message_id", payload.message_id]]);
|
|
98
213
|
resolve(ack);
|
|
99
|
-
}
|
|
214
|
+
}
|
|
100
215
|
const startAckTimer = () => {
|
|
101
|
-
|
|
216
|
+
if (state === "acked" || state === "failed")
|
|
217
|
+
return;
|
|
218
|
+
state = "written_waiting_ack";
|
|
219
|
+
messageErrorTracker.pending.add(traceId);
|
|
220
|
+
clearAckTimer();
|
|
221
|
+
removeRawListener();
|
|
222
|
+
params.client.on("raw", onRaw);
|
|
223
|
+
rawListenerRegistered = true;
|
|
102
224
|
timer = setTimeout(() => {
|
|
103
|
-
cleanup();
|
|
104
225
|
logAck("ack_timeout", "reject_no_reconnect", [["timeout_ms", params.account.ack.timeout]]);
|
|
105
|
-
|
|
226
|
+
fail(new Error(`ack timeout after ${params.account.ack.timeout}ms for trace_id=${traceId}`));
|
|
106
227
|
}, params.account.ack.timeout);
|
|
107
228
|
};
|
|
108
229
|
const item = {
|
|
@@ -111,18 +232,52 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
111
232
|
chatId: params.chatId,
|
|
112
233
|
wire,
|
|
113
234
|
onWrite: startAckTimer,
|
|
235
|
+
onDrop: () => {
|
|
236
|
+
fail(new Error(`send queue full; dropped ${params.eventName} before write for trace_id=${traceId}`));
|
|
237
|
+
},
|
|
114
238
|
};
|
|
115
|
-
|
|
239
|
+
function isTerminalClose(close) {
|
|
240
|
+
return close?.code === 1000 || close?.reason === "client close" || close?.reason === "auth failed";
|
|
241
|
+
}
|
|
242
|
+
function onClose(close) {
|
|
243
|
+
if (state === "acked" || state === "failed")
|
|
244
|
+
return;
|
|
245
|
+
if (isTerminalClose(close)) {
|
|
246
|
+
const reason = typeof close?.reason === "string" && close.reason ? close.reason : "websocket closed";
|
|
247
|
+
queue.remove(item);
|
|
248
|
+
fail(new Error(`send cancelled because ${reason}`));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (state !== "written_waiting_ack")
|
|
252
|
+
return;
|
|
253
|
+
clearAckTimer();
|
|
254
|
+
removeRawListener();
|
|
255
|
+
state = "queued";
|
|
256
|
+
queue.enqueue(item);
|
|
257
|
+
}
|
|
258
|
+
function onState(next) {
|
|
259
|
+
if (state === "acked" || state === "failed" || next?.to !== "disconnected")
|
|
260
|
+
return;
|
|
261
|
+
queue.remove(item);
|
|
262
|
+
fail(new Error("send cancelled because client disconnected"));
|
|
263
|
+
}
|
|
264
|
+
removeCloseListener = addAlignedOutboundCloseHandler(params.client, onClose);
|
|
265
|
+
removeStateListener = addAlignedOutboundStateHandler(params.client, onState);
|
|
266
|
+
if (!isReady()) {
|
|
267
|
+
if (isDisconnected()) {
|
|
268
|
+
fail(new Error("send cancelled because client disconnected"));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
116
271
|
queue.enqueue(item);
|
|
117
272
|
return;
|
|
118
273
|
}
|
|
119
274
|
try {
|
|
120
275
|
queue.enqueue(item);
|
|
121
|
-
queue.flush((queuedWire) =>
|
|
276
|
+
queue.flush((queuedWire) => params.client.sendWire(queuedWire));
|
|
122
277
|
}
|
|
123
|
-
catch
|
|
124
|
-
|
|
125
|
-
|
|
278
|
+
catch {
|
|
279
|
+
// The queue keeps the failed frame at the head for reconnect retry, so
|
|
280
|
+
// keep this promise pending until the frame is written+acked, dropped, or timed out.
|
|
126
281
|
}
|
|
127
282
|
});
|
|
128
283
|
}
|
|
@@ -179,23 +334,31 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
179
334
|
const text = (params.text ?? "").trim();
|
|
180
335
|
const richFragments = params.richFragments ?? [];
|
|
181
336
|
const mediaFragments = params.mediaFragments ?? [];
|
|
337
|
+
if (isClawChatNoopResponseText(text) &&
|
|
338
|
+
richFragments.length === 0 &&
|
|
339
|
+
mediaFragments.length === 0) {
|
|
340
|
+
params.log?.info?.(`[${params.account.accountId}] openclaw-clawchat outbound suppressed: silent response`);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
182
343
|
if (!text && richFragments.length === 0 && mediaFragments.length === 0) {
|
|
183
344
|
params.log?.info?.(`[${params.account.accountId}] openclaw-clawchat outbound suppressed: empty text and no media`);
|
|
184
345
|
return null;
|
|
185
346
|
}
|
|
186
347
|
const mentions = params.mentions ?? [];
|
|
187
348
|
const textFragments = text ? textToFragments(text) : [];
|
|
188
|
-
//
|
|
189
|
-
// with one of the
|
|
349
|
+
// Each MediaItem object is structurally compatible
|
|
350
|
+
// with one of the local narrow Fragment members (ImageFragment / FileFragment /
|
|
190
351
|
// AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
|
|
191
352
|
// shape lets us build a single uniform array without a per-kind switch.
|
|
192
353
|
const fragments = [...textFragments, ...richFragments, ...mediaFragments];
|
|
193
354
|
const useReply = Boolean(params.replyCtx);
|
|
355
|
+
const messageId = params.messageId;
|
|
194
356
|
let ack;
|
|
195
357
|
let mode;
|
|
196
358
|
if (useReply && params.replyCtx) {
|
|
197
359
|
mode = "reply";
|
|
198
360
|
const payload = {
|
|
361
|
+
...(messageId ? { message_id: messageId } : {}),
|
|
199
362
|
message_mode: "normal",
|
|
200
363
|
message: {
|
|
201
364
|
body: { fragments },
|
|
@@ -212,49 +375,37 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
212
375
|
},
|
|
213
376
|
},
|
|
214
377
|
};
|
|
215
|
-
ack =
|
|
378
|
+
ack = await sendAlignedAckableEnvelope({
|
|
216
379
|
client: params.client,
|
|
217
380
|
account: params.account,
|
|
218
381
|
eventName: "message.reply",
|
|
219
382
|
chatId: params.to.chatId,
|
|
220
383
|
payload,
|
|
221
384
|
...(params.log ? { log: params.log } : {}),
|
|
222
|
-
})) ?? await params.client.replyMessage({
|
|
223
|
-
chat_id: params.to.chatId,
|
|
224
|
-
mode: "normal",
|
|
225
|
-
replyTo: {
|
|
226
|
-
msgId: params.replyCtx.replyToMessageId,
|
|
227
|
-
senderId: params.replyCtx.replyPreviewSenderId,
|
|
228
|
-
nickName: params.replyCtx.replyPreviewNickName,
|
|
229
|
-
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
230
|
-
},
|
|
231
|
-
body: { fragments },
|
|
232
|
-
context: { mentions },
|
|
233
385
|
});
|
|
234
386
|
}
|
|
235
387
|
else {
|
|
236
388
|
mode = "send";
|
|
237
389
|
const payload = {
|
|
390
|
+
...(messageId ? { message_id: messageId } : {}),
|
|
238
391
|
message_mode: "normal",
|
|
239
392
|
message: {
|
|
240
393
|
body: { fragments },
|
|
241
394
|
context: { mentions, reply: null },
|
|
242
395
|
},
|
|
243
396
|
};
|
|
244
|
-
ack =
|
|
397
|
+
ack = await sendAlignedAckableEnvelope({
|
|
245
398
|
client: params.client,
|
|
246
399
|
account: params.account,
|
|
247
400
|
eventName: "message.send",
|
|
248
401
|
chatId: params.to.chatId,
|
|
249
402
|
payload,
|
|
250
403
|
...(params.log ? { log: params.log } : {}),
|
|
251
|
-
})) ?? await params.client.sendMessage({
|
|
252
|
-
chat_id: params.to.chatId,
|
|
253
|
-
mode: "normal",
|
|
254
|
-
body: { fragments },
|
|
255
|
-
context: { mentions, reply: null },
|
|
256
404
|
});
|
|
257
405
|
}
|
|
406
|
+
if (messageId && ack.payload.message_id !== messageId) {
|
|
407
|
+
throw new Error(`ack message_id mismatch: expected ${messageId} got ${ack.payload.message_id}`);
|
|
408
|
+
}
|
|
258
409
|
params.log?.info?.(`[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`);
|
|
259
410
|
return {
|
|
260
411
|
messageId: ack.payload.message_id,
|
|
@@ -281,11 +432,48 @@ export async function sendOpenclawClawlingMedia(params) {
|
|
|
281
432
|
to: params.to,
|
|
282
433
|
text: params.text ?? "",
|
|
283
434
|
mediaFragments: params.mediaFragments,
|
|
435
|
+
...(params.messageId ? { messageId: params.messageId } : {}),
|
|
284
436
|
...(params.replyCtx ? { replyCtx: params.replyCtx } : {}),
|
|
285
437
|
...(params.mentions ? { mentions: params.mentions } : {}),
|
|
286
438
|
...(params.log ? { log: params.log } : {}),
|
|
287
439
|
});
|
|
288
440
|
}
|
|
441
|
+
function mintOutboundMessageId(account) {
|
|
442
|
+
return `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
443
|
+
}
|
|
444
|
+
function resolveChannelOutboundStore() {
|
|
445
|
+
try {
|
|
446
|
+
const runtime = getOpenclawClawlingRuntime();
|
|
447
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
448
|
+
return getClawChatStore({
|
|
449
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function claimChannelOutbound(params) {
|
|
457
|
+
const store = resolveChannelOutboundStore();
|
|
458
|
+
if (!store)
|
|
459
|
+
return null;
|
|
460
|
+
try {
|
|
461
|
+
return store.claimMessageOnce({
|
|
462
|
+
platform: "openclaw",
|
|
463
|
+
accountId: params.account.accountId,
|
|
464
|
+
kind: "message",
|
|
465
|
+
direction: "outbound",
|
|
466
|
+
eventType: "message.send",
|
|
467
|
+
chatId: params.target.chatId,
|
|
468
|
+
messageId: params.messageId,
|
|
469
|
+
text: params.text,
|
|
470
|
+
raw: params.raw,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
289
477
|
export const openclawClawlingOutbound = {
|
|
290
478
|
deliveryMode: "direct",
|
|
291
479
|
chunker: (text, limit) => chunkMarkdownText(text, limit),
|
|
@@ -297,15 +485,35 @@ export const openclawClawlingOutbound = {
|
|
|
297
485
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
298
486
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
299
487
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
488
|
+
const target = parseOpenclawRecipient(to);
|
|
489
|
+
const messageId = mintOutboundMessageId(account);
|
|
490
|
+
const trimmedText = text.trim();
|
|
491
|
+
if (!trimmedText) {
|
|
492
|
+
throw new Error("openclaw-clawchat sendText requires non-empty text");
|
|
493
|
+
}
|
|
494
|
+
const claimed = claimChannelOutbound({
|
|
495
|
+
account,
|
|
496
|
+
target,
|
|
497
|
+
messageId,
|
|
498
|
+
text: trimmedText,
|
|
499
|
+
raw: { target, mode: "channel-sendText" },
|
|
500
|
+
});
|
|
501
|
+
if (claimed === false) {
|
|
502
|
+
throw new Error("openclaw-clawchat outbound duplicate claim; message not sent");
|
|
503
|
+
}
|
|
504
|
+
if (claimed === null) {
|
|
505
|
+
throw new Error("openclaw-clawchat outbound message claim failed");
|
|
506
|
+
}
|
|
300
507
|
const result = await sendOpenclawClawlingText({
|
|
301
508
|
client,
|
|
302
509
|
account,
|
|
303
|
-
to:
|
|
510
|
+
to: target,
|
|
304
511
|
text,
|
|
512
|
+
messageId,
|
|
305
513
|
});
|
|
306
514
|
return {
|
|
307
515
|
to,
|
|
308
|
-
messageId: result?.messageId ??
|
|
516
|
+
messageId: result?.messageId ?? messageId,
|
|
309
517
|
};
|
|
310
518
|
},
|
|
311
519
|
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
@@ -331,16 +539,33 @@ export const openclawClawlingOutbound = {
|
|
|
331
539
|
if (mediaFragments.length === 0) {
|
|
332
540
|
throw new Error(`openclaw-clawchat failed to upload media: ${mediaUrl}`);
|
|
333
541
|
}
|
|
542
|
+
const target = parseOpenclawRecipient(to);
|
|
543
|
+
const messageId = mintOutboundMessageId(account);
|
|
544
|
+
const claimText = (text ?? "").trim();
|
|
545
|
+
const claimed = claimChannelOutbound({
|
|
546
|
+
account,
|
|
547
|
+
target,
|
|
548
|
+
messageId,
|
|
549
|
+
text: claimText,
|
|
550
|
+
raw: { target, mode: "channel-sendMedia", mediaCount: mediaFragments.length },
|
|
551
|
+
});
|
|
552
|
+
if (claimed === false) {
|
|
553
|
+
throw new Error("openclaw-clawchat outbound duplicate claim; message not sent");
|
|
554
|
+
}
|
|
555
|
+
if (claimed === null) {
|
|
556
|
+
throw new Error("openclaw-clawchat outbound message claim failed");
|
|
557
|
+
}
|
|
334
558
|
const result = await sendOpenclawClawlingMedia({
|
|
335
559
|
client,
|
|
336
560
|
account,
|
|
337
|
-
to:
|
|
561
|
+
to: target,
|
|
338
562
|
text,
|
|
339
563
|
mediaFragments,
|
|
564
|
+
messageId,
|
|
340
565
|
});
|
|
341
566
|
return {
|
|
342
567
|
to,
|
|
343
|
-
messageId: result?.messageId ??
|
|
568
|
+
messageId: result?.messageId ?? messageId,
|
|
344
569
|
};
|
|
345
570
|
},
|
|
346
571
|
}),
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const REQUIRED_PROMPT_NAMES = ["platform"];
|
|
5
|
+
const OPTIONAL_PROMPT_NAMES = ["user", "group"];
|
|
6
|
+
function findDefaultPluginRoot() {
|
|
7
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const root = path.parse(current).root;
|
|
9
|
+
while (true) {
|
|
10
|
+
if (fs.existsSync(path.join(current, "prompts")) ||
|
|
11
|
+
fs.existsSync(path.join(current, "openclaw.plugin.json"))) {
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
if (current === root) {
|
|
15
|
+
return process.cwd();
|
|
16
|
+
}
|
|
17
|
+
current = path.dirname(current);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function readRequiredPrompt(root, name) {
|
|
21
|
+
const promptPath = path.join(root, "prompts", `${name}.md`);
|
|
22
|
+
let prompt = "";
|
|
23
|
+
try {
|
|
24
|
+
prompt = fs.readFileSync(promptPath, "utf8").trim();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(`missing or empty ClawChat prompt: ${name} at ${promptPath}`, {
|
|
28
|
+
cause: error,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (prompt.length === 0) {
|
|
32
|
+
throw new Error(`missing or empty ClawChat prompt: ${name} at ${promptPath}`);
|
|
33
|
+
}
|
|
34
|
+
return prompt;
|
|
35
|
+
}
|
|
36
|
+
function readOptionalPrompt(root, name) {
|
|
37
|
+
const promptPath = path.join(root, "prompts", `${name}.md`);
|
|
38
|
+
if (!fs.existsSync(promptPath)) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
const prompt = fs.readFileSync(promptPath, "utf8").trim();
|
|
42
|
+
if (prompt.length === 0) {
|
|
43
|
+
throw new Error(`missing or empty ClawChat prompt: ${name} at ${promptPath}`);
|
|
44
|
+
}
|
|
45
|
+
return prompt;
|
|
46
|
+
}
|
|
47
|
+
export function loadClawChatPromptsFromRoot(root) {
|
|
48
|
+
const prompts = {
|
|
49
|
+
platform: "",
|
|
50
|
+
user: "",
|
|
51
|
+
group: "",
|
|
52
|
+
};
|
|
53
|
+
for (const name of REQUIRED_PROMPT_NAMES) {
|
|
54
|
+
prompts[name] = readRequiredPrompt(root, name);
|
|
55
|
+
}
|
|
56
|
+
for (const name of OPTIONAL_PROMPT_NAMES) {
|
|
57
|
+
prompts[name] = readOptionalPrompt(root, name);
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze(prompts);
|
|
60
|
+
}
|
|
61
|
+
const CLAWCHAT_PROMPTS = loadClawChatPromptsFromRoot(findDefaultPluginRoot());
|
|
62
|
+
export function getClawChatPlatformPrompt() {
|
|
63
|
+
return CLAWCHAT_PROMPTS.platform;
|
|
64
|
+
}
|
|
65
|
+
export function getClawChatUserPrompt() {
|
|
66
|
+
return CLAWCHAT_PROMPTS.user;
|
|
67
|
+
}
|
|
68
|
+
export function getClawChatGroupPrompt() {
|
|
69
|
+
return CLAWCHAT_PROMPTS.group;
|
|
70
|
+
}
|
|
71
|
+
export function getClawChatModePrompt(mode) {
|
|
72
|
+
if (mode === "group") {
|
|
73
|
+
return getClawChatGroupPrompt();
|
|
74
|
+
}
|
|
75
|
+
return getClawChatUserPrompt();
|
|
76
|
+
}
|