@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13
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 +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- 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 +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- 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 +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -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 +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
package/dist/src/outbound.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -5,6 +6,280 @@ import { CHANNEL_ID, resolveOpenclawClawlingAccount } from "./config.js";
|
|
|
5
6
|
import { textToFragments } from "./message-mapper.js";
|
|
6
7
|
import { uploadOutboundMedia } from "./media-runtime.js";
|
|
7
8
|
import { getOpenclawClawlingClient, getOpenclawClawlingRuntime, waitForOpenclawClawlingClient, } from "./runtime.js";
|
|
9
|
+
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
10
|
+
import { createAlignedWsQueue } from "./ws-alignment.js";
|
|
11
|
+
import { formatWsLog } from "./ws-log.js";
|
|
12
|
+
const alignedOutboundQueues = new WeakMap();
|
|
13
|
+
const alignedOutboundContexts = new WeakMap();
|
|
14
|
+
const alignedOutboundMessageErrorTrackers = new WeakMap();
|
|
15
|
+
const alignedOutboundCloseHandlers = new WeakMap();
|
|
16
|
+
const alignedOutboundStateHandlers = new WeakMap();
|
|
17
|
+
function addAlignedOutboundCloseHandler(client, handler) {
|
|
18
|
+
const key = client;
|
|
19
|
+
let entry = alignedOutboundCloseHandlers.get(key);
|
|
20
|
+
if (!entry) {
|
|
21
|
+
const handlers = new Set();
|
|
22
|
+
const listener = (close) => {
|
|
23
|
+
for (const current of [...handlers])
|
|
24
|
+
current(close);
|
|
25
|
+
};
|
|
26
|
+
entry = { handlers, listener };
|
|
27
|
+
alignedOutboundCloseHandlers.set(key, entry);
|
|
28
|
+
client.on("close", listener);
|
|
29
|
+
}
|
|
30
|
+
entry.handlers.add(handler);
|
|
31
|
+
return () => {
|
|
32
|
+
entry?.handlers.delete(handler);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function addAlignedOutboundStateHandler(client, handler) {
|
|
36
|
+
const key = client;
|
|
37
|
+
let entry = alignedOutboundStateHandlers.get(key);
|
|
38
|
+
if (!entry) {
|
|
39
|
+
const handlers = new Set();
|
|
40
|
+
const listener = (state) => {
|
|
41
|
+
for (const current of [...handlers])
|
|
42
|
+
current(state);
|
|
43
|
+
};
|
|
44
|
+
entry = { handlers, listener };
|
|
45
|
+
alignedOutboundStateHandlers.set(key, entry);
|
|
46
|
+
client.on("state", listener);
|
|
47
|
+
}
|
|
48
|
+
entry.handlers.add(handler);
|
|
49
|
+
return () => {
|
|
50
|
+
entry?.handlers.delete(handler);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function getAlignedOutboundQueue(client, account, log) {
|
|
54
|
+
const existing = alignedOutboundQueues.get(client);
|
|
55
|
+
if (existing)
|
|
56
|
+
return existing;
|
|
57
|
+
const queue = createAlignedWsQueue({
|
|
58
|
+
accountId: account.accountId,
|
|
59
|
+
log: (msg) => log?.info?.(msg),
|
|
60
|
+
maxSize: 128,
|
|
61
|
+
context: () => alignedOutboundContexts.get(client)?.() ?? {
|
|
62
|
+
attempt: 1,
|
|
63
|
+
reconnectCount: 0,
|
|
64
|
+
state: "ready",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
alignedOutboundQueues.set(client, queue);
|
|
68
|
+
return queue;
|
|
69
|
+
}
|
|
70
|
+
function getAlignedMessageErrorTracker(client, account, log) {
|
|
71
|
+
const key = client;
|
|
72
|
+
const existing = alignedOutboundMessageErrorTrackers.get(key);
|
|
73
|
+
if (existing)
|
|
74
|
+
return existing;
|
|
75
|
+
const pending = new Set();
|
|
76
|
+
const listener = (env) => {
|
|
77
|
+
if (env.event !== "message.error")
|
|
78
|
+
return;
|
|
79
|
+
if (pending.has(env.trace_id)) {
|
|
80
|
+
client.markMessageErrorHandled?.(env.trace_id);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (client.hasPendingAckTrace?.(env.trace_id)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const context = alignedOutboundContexts.get(key)?.() ?? {
|
|
87
|
+
attempt: 1,
|
|
88
|
+
reconnectCount: 0,
|
|
89
|
+
state: client.transportState === "open" ? "ready" : "reconnecting",
|
|
90
|
+
};
|
|
91
|
+
if (log?.info) {
|
|
92
|
+
log.info(formatWsLog({
|
|
93
|
+
event: "ack_unmatched",
|
|
94
|
+
accountId: account.accountId,
|
|
95
|
+
attempt: context.attempt,
|
|
96
|
+
reconnectCount: context.reconnectCount,
|
|
97
|
+
state: context.state,
|
|
98
|
+
action: "ignore",
|
|
99
|
+
fields: [
|
|
100
|
+
["trace_id", env.trace_id],
|
|
101
|
+
["chat_id", env.chat_id],
|
|
102
|
+
],
|
|
103
|
+
}));
|
|
104
|
+
client.markMessageErrorHandled?.(env.trace_id);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
client.on("raw", listener);
|
|
108
|
+
const tracker = { pending, listener };
|
|
109
|
+
alignedOutboundMessageErrorTrackers.set(key, tracker);
|
|
110
|
+
return tracker;
|
|
111
|
+
}
|
|
112
|
+
export function setAlignedOutboundLogContext(client, context) {
|
|
113
|
+
alignedOutboundContexts.set(client, context);
|
|
114
|
+
}
|
|
115
|
+
export function flushAlignedOutboundQueue(client) {
|
|
116
|
+
const queue = alignedOutboundQueues.get(client);
|
|
117
|
+
queue?.flush((wire) => client.sendWire(wire));
|
|
118
|
+
}
|
|
119
|
+
export function getAlignedOutboundQueueSize(client) {
|
|
120
|
+
return alignedOutboundQueues.get(client)?.snapshot().length ?? 0;
|
|
121
|
+
}
|
|
122
|
+
async function sendAlignedAckableEnvelope(params) {
|
|
123
|
+
const traceId = params.client.nextTraceId();
|
|
124
|
+
const env = {
|
|
125
|
+
version: "2",
|
|
126
|
+
event: params.eventName,
|
|
127
|
+
trace_id: traceId,
|
|
128
|
+
emitted_at: Date.now(),
|
|
129
|
+
chat_id: params.chatId,
|
|
130
|
+
payload: params.payload,
|
|
131
|
+
};
|
|
132
|
+
const wire = JSON.stringify(env);
|
|
133
|
+
const queue = getAlignedOutboundQueue(params.client, params.account, params.log);
|
|
134
|
+
const messageErrorTracker = getAlignedMessageErrorTracker(params.client, params.account, params.log);
|
|
135
|
+
const isReady = () => {
|
|
136
|
+
const state = params.client.state;
|
|
137
|
+
return params.client.transportState === "open" && (!state || state === "connected");
|
|
138
|
+
};
|
|
139
|
+
const isDisconnected = () => params.client.state === "disconnected";
|
|
140
|
+
return await new Promise((resolve, reject) => {
|
|
141
|
+
let state = "queued";
|
|
142
|
+
let timer;
|
|
143
|
+
let rawListenerRegistered = false;
|
|
144
|
+
let removeCloseListener;
|
|
145
|
+
let removeStateListener;
|
|
146
|
+
const clearAckTimer = () => {
|
|
147
|
+
if (!timer)
|
|
148
|
+
return;
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
timer = undefined;
|
|
151
|
+
};
|
|
152
|
+
const removeRawListener = () => {
|
|
153
|
+
if (!rawListenerRegistered)
|
|
154
|
+
return;
|
|
155
|
+
params.client.off("raw", onRaw);
|
|
156
|
+
rawListenerRegistered = false;
|
|
157
|
+
};
|
|
158
|
+
const cleanup = () => {
|
|
159
|
+
messageErrorTracker.pending.delete(traceId);
|
|
160
|
+
clearAckTimer();
|
|
161
|
+
removeRawListener();
|
|
162
|
+
removeCloseListener?.();
|
|
163
|
+
removeCloseListener = undefined;
|
|
164
|
+
removeStateListener?.();
|
|
165
|
+
removeStateListener = undefined;
|
|
166
|
+
};
|
|
167
|
+
const fail = (err) => {
|
|
168
|
+
if (state === "acked" || state === "failed")
|
|
169
|
+
return;
|
|
170
|
+
state = "failed";
|
|
171
|
+
cleanup();
|
|
172
|
+
reject(err);
|
|
173
|
+
};
|
|
174
|
+
const logAck = (event, action, fields) => {
|
|
175
|
+
params.log?.info?.(formatWsLog({
|
|
176
|
+
event,
|
|
177
|
+
accountId: params.account.accountId,
|
|
178
|
+
...(alignedOutboundContexts.get(params.client)?.() ?? {
|
|
179
|
+
attempt: 1,
|
|
180
|
+
reconnectCount: 0,
|
|
181
|
+
state: isReady() ? "ready" : "reconnecting",
|
|
182
|
+
}),
|
|
183
|
+
action,
|
|
184
|
+
fields: [
|
|
185
|
+
["event_name", params.eventName],
|
|
186
|
+
["trace_id", traceId],
|
|
187
|
+
["chat_id", params.chatId],
|
|
188
|
+
...fields,
|
|
189
|
+
],
|
|
190
|
+
}));
|
|
191
|
+
};
|
|
192
|
+
function onRaw(ack) {
|
|
193
|
+
if (ack.trace_id !== traceId)
|
|
194
|
+
return;
|
|
195
|
+
if (ack.event === "message.error") {
|
|
196
|
+
if (state === "acked" || state === "failed")
|
|
197
|
+
return;
|
|
198
|
+
const payload = ack.payload;
|
|
199
|
+
const code = typeof payload.code === "string" && payload.code ? payload.code : "unknown";
|
|
200
|
+
const message = typeof payload.message === "string" && payload.message ? payload.message : "message send failed";
|
|
201
|
+
fail(new MessageSendError(traceId, code, message, ack.chat_id));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (ack.event !== "message.ack")
|
|
205
|
+
return;
|
|
206
|
+
if (state === "acked" || state === "failed")
|
|
207
|
+
return;
|
|
208
|
+
state = "acked";
|
|
209
|
+
cleanup();
|
|
210
|
+
const payload = ack.payload;
|
|
211
|
+
logAck("ack_received", "resolve", [["message_id", payload.message_id]]);
|
|
212
|
+
resolve(ack);
|
|
213
|
+
}
|
|
214
|
+
const startAckTimer = () => {
|
|
215
|
+
if (state === "acked" || state === "failed")
|
|
216
|
+
return;
|
|
217
|
+
state = "written_waiting_ack";
|
|
218
|
+
messageErrorTracker.pending.add(traceId);
|
|
219
|
+
clearAckTimer();
|
|
220
|
+
removeRawListener();
|
|
221
|
+
params.client.on("raw", onRaw);
|
|
222
|
+
rawListenerRegistered = true;
|
|
223
|
+
timer = setTimeout(() => {
|
|
224
|
+
logAck("ack_timeout", "reject_no_reconnect", [["timeout_ms", params.account.ack.timeout]]);
|
|
225
|
+
fail(new Error(`ack timeout after ${params.account.ack.timeout}ms for trace_id=${traceId}`));
|
|
226
|
+
}, params.account.ack.timeout);
|
|
227
|
+
};
|
|
228
|
+
const item = {
|
|
229
|
+
eventName: params.eventName,
|
|
230
|
+
traceId,
|
|
231
|
+
chatId: params.chatId,
|
|
232
|
+
wire,
|
|
233
|
+
onWrite: startAckTimer,
|
|
234
|
+
onDrop: () => {
|
|
235
|
+
fail(new Error(`send queue full; dropped ${params.eventName} before write for trace_id=${traceId}`));
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
function isTerminalClose(close) {
|
|
239
|
+
return close?.code === 1000 || close?.reason === "client close" || close?.reason === "auth failed";
|
|
240
|
+
}
|
|
241
|
+
function onClose(close) {
|
|
242
|
+
if (state === "acked" || state === "failed")
|
|
243
|
+
return;
|
|
244
|
+
if (isTerminalClose(close)) {
|
|
245
|
+
const reason = typeof close?.reason === "string" && close.reason ? close.reason : "websocket closed";
|
|
246
|
+
queue.remove(item);
|
|
247
|
+
fail(new Error(`send cancelled because ${reason}`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (state !== "written_waiting_ack")
|
|
251
|
+
return;
|
|
252
|
+
clearAckTimer();
|
|
253
|
+
removeRawListener();
|
|
254
|
+
state = "queued";
|
|
255
|
+
queue.enqueue(item);
|
|
256
|
+
}
|
|
257
|
+
function onState(next) {
|
|
258
|
+
if (state === "acked" || state === "failed" || next?.to !== "disconnected")
|
|
259
|
+
return;
|
|
260
|
+
queue.remove(item);
|
|
261
|
+
fail(new Error("send cancelled because client disconnected"));
|
|
262
|
+
}
|
|
263
|
+
removeCloseListener = addAlignedOutboundCloseHandler(params.client, onClose);
|
|
264
|
+
removeStateListener = addAlignedOutboundStateHandler(params.client, onState);
|
|
265
|
+
if (!isReady()) {
|
|
266
|
+
if (isDisconnected()) {
|
|
267
|
+
fail(new Error("send cancelled because client disconnected"));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
queue.enqueue(item);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
queue.enqueue(item);
|
|
275
|
+
queue.flush((queuedWire) => params.client.sendWire(queuedWire));
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// The queue keeps the failed frame at the head for reconnect retry, so
|
|
279
|
+
// keep this promise pending until the frame is written+acked, dropped, or timed out.
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
8
283
|
/**
|
|
9
284
|
* Parse an agent-initiated outbound recipient string into the new-protocol
|
|
10
285
|
* `chat_id` + `chat_type` pair.
|
|
@@ -19,6 +294,8 @@ import { getOpenclawClawlingClient, getOpenclawClawlingRuntime, waitForOpenclawC
|
|
|
19
294
|
* - `clawchat:group:{chat_id}` → group
|
|
20
295
|
* - `openclaw-clawchat:direct:{chat_id}` → direct
|
|
21
296
|
* - `openclaw-clawchat:group:{chat_id}` → group
|
|
297
|
+
* - `direct:{chat_id}` → direct (host-normalized)
|
|
298
|
+
* - `group:{chat_id}` → group (host-normalized)
|
|
22
299
|
* - bare `{chat_id}` → direct (backward compat)
|
|
23
300
|
*/
|
|
24
301
|
export function parseOpenclawRecipient(to) {
|
|
@@ -30,6 +307,12 @@ export function parseOpenclawRecipient(to) {
|
|
|
30
307
|
return { chatId: raw, chatType: "direct" };
|
|
31
308
|
const scheme = raw.slice(0, firstColon).toLowerCase();
|
|
32
309
|
const rest = raw.slice(firstColon + 1);
|
|
310
|
+
if (scheme === "direct" || scheme === "group") {
|
|
311
|
+
const chatId = rest.trim();
|
|
312
|
+
if (!chatId)
|
|
313
|
+
throw new Error(`openclaw-clawchat: missing chat_id in "${to}"`);
|
|
314
|
+
return { chatId, chatType: scheme };
|
|
315
|
+
}
|
|
33
316
|
if (scheme !== "cc" && scheme !== "clawchat" && scheme !== CHANNEL_ID) {
|
|
34
317
|
return { chatId: raw, chatType: "direct" };
|
|
35
318
|
}
|
|
@@ -56,41 +339,66 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
56
339
|
}
|
|
57
340
|
const mentions = params.mentions ?? [];
|
|
58
341
|
const textFragments = text ? textToFragments(text) : [];
|
|
59
|
-
//
|
|
60
|
-
// with one of the
|
|
342
|
+
// Each MediaItem object is structurally compatible
|
|
343
|
+
// with one of the local narrow Fragment members (ImageFragment / FileFragment /
|
|
61
344
|
// AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
|
|
62
345
|
// shape lets us build a single uniform array without a per-kind switch.
|
|
63
346
|
const fragments = [...textFragments, ...richFragments, ...mediaFragments];
|
|
64
|
-
const useReply = params.replyCtx
|
|
65
|
-
|
|
66
|
-
params.log?.info?.(`[${params.account.accountId}] openclaw-clawchat replyCtx + media: downgraded to sendMessage`);
|
|
67
|
-
}
|
|
347
|
+
const useReply = Boolean(params.replyCtx);
|
|
348
|
+
const messageId = params.messageId;
|
|
68
349
|
let ack;
|
|
69
350
|
let mode;
|
|
70
351
|
if (useReply && params.replyCtx) {
|
|
71
352
|
mode = "reply";
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
353
|
+
const payload = {
|
|
354
|
+
...(messageId ? { message_id: messageId } : {}),
|
|
355
|
+
message_mode: "normal",
|
|
356
|
+
message: {
|
|
357
|
+
body: { fragments },
|
|
358
|
+
context: {
|
|
359
|
+
mentions,
|
|
360
|
+
reply: {
|
|
361
|
+
reply_to_msg_id: params.replyCtx.replyToMessageId,
|
|
362
|
+
reply_preview: {
|
|
363
|
+
id: params.replyCtx.replyPreviewSenderId,
|
|
364
|
+
nick_name: params.replyCtx.replyPreviewNickName,
|
|
365
|
+
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
80
369
|
},
|
|
81
|
-
|
|
82
|
-
|
|
370
|
+
};
|
|
371
|
+
ack = await sendAlignedAckableEnvelope({
|
|
372
|
+
client: params.client,
|
|
373
|
+
account: params.account,
|
|
374
|
+
eventName: "message.reply",
|
|
375
|
+
chatId: params.to.chatId,
|
|
376
|
+
payload,
|
|
377
|
+
...(params.log ? { log: params.log } : {}),
|
|
83
378
|
});
|
|
84
379
|
}
|
|
85
380
|
else {
|
|
86
381
|
mode = "send";
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
382
|
+
const payload = {
|
|
383
|
+
...(messageId ? { message_id: messageId } : {}),
|
|
384
|
+
message_mode: "normal",
|
|
385
|
+
message: {
|
|
386
|
+
body: { fragments },
|
|
387
|
+
context: { mentions, reply: null },
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
ack = await sendAlignedAckableEnvelope({
|
|
391
|
+
client: params.client,
|
|
392
|
+
account: params.account,
|
|
393
|
+
eventName: "message.send",
|
|
394
|
+
chatId: params.to.chatId,
|
|
395
|
+
payload,
|
|
396
|
+
...(params.log ? { log: params.log } : {}),
|
|
92
397
|
});
|
|
93
398
|
}
|
|
399
|
+
if (messageId && ack.payload.message_id !== messageId) {
|
|
400
|
+
throw new Error(`ack message_id mismatch: expected ${messageId} got ${ack.payload.message_id}`);
|
|
401
|
+
}
|
|
94
402
|
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}`);
|
|
95
403
|
return {
|
|
96
404
|
messageId: ack.payload.message_id,
|
|
@@ -117,11 +425,48 @@ export async function sendOpenclawClawlingMedia(params) {
|
|
|
117
425
|
to: params.to,
|
|
118
426
|
text: params.text ?? "",
|
|
119
427
|
mediaFragments: params.mediaFragments,
|
|
428
|
+
...(params.messageId ? { messageId: params.messageId } : {}),
|
|
120
429
|
...(params.replyCtx ? { replyCtx: params.replyCtx } : {}),
|
|
121
430
|
...(params.mentions ? { mentions: params.mentions } : {}),
|
|
122
431
|
...(params.log ? { log: params.log } : {}),
|
|
123
432
|
});
|
|
124
433
|
}
|
|
434
|
+
function mintOutboundMessageId(account) {
|
|
435
|
+
return `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
436
|
+
}
|
|
437
|
+
function resolveChannelOutboundStore() {
|
|
438
|
+
try {
|
|
439
|
+
const runtime = getOpenclawClawlingRuntime();
|
|
440
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
441
|
+
return getClawChatStore({
|
|
442
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function claimChannelOutbound(params) {
|
|
450
|
+
const store = resolveChannelOutboundStore();
|
|
451
|
+
if (!store)
|
|
452
|
+
return null;
|
|
453
|
+
try {
|
|
454
|
+
return store.claimMessageOnce({
|
|
455
|
+
platform: "openclaw",
|
|
456
|
+
accountId: params.account.accountId,
|
|
457
|
+
kind: "message",
|
|
458
|
+
direction: "outbound",
|
|
459
|
+
eventType: "message.send",
|
|
460
|
+
chatId: params.target.chatId,
|
|
461
|
+
messageId: params.messageId,
|
|
462
|
+
text: params.text,
|
|
463
|
+
raw: params.raw,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
125
470
|
export const openclawClawlingOutbound = {
|
|
126
471
|
deliveryMode: "direct",
|
|
127
472
|
chunker: (text, limit) => chunkMarkdownText(text, limit),
|
|
@@ -133,18 +478,38 @@ export const openclawClawlingOutbound = {
|
|
|
133
478
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
134
479
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
135
480
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
481
|
+
const target = parseOpenclawRecipient(to);
|
|
482
|
+
const messageId = mintOutboundMessageId(account);
|
|
483
|
+
const trimmedText = text.trim();
|
|
484
|
+
if (!trimmedText) {
|
|
485
|
+
throw new Error("openclaw-clawchat sendText requires non-empty text");
|
|
486
|
+
}
|
|
487
|
+
const claimed = claimChannelOutbound({
|
|
488
|
+
account,
|
|
489
|
+
target,
|
|
490
|
+
messageId,
|
|
491
|
+
text: trimmedText,
|
|
492
|
+
raw: { target, mode: "channel-sendText" },
|
|
493
|
+
});
|
|
494
|
+
if (claimed === false) {
|
|
495
|
+
throw new Error("openclaw-clawchat outbound duplicate claim; message not sent");
|
|
496
|
+
}
|
|
497
|
+
if (claimed === null) {
|
|
498
|
+
throw new Error("openclaw-clawchat outbound message claim failed");
|
|
499
|
+
}
|
|
136
500
|
const result = await sendOpenclawClawlingText({
|
|
137
501
|
client,
|
|
138
502
|
account,
|
|
139
|
-
to:
|
|
503
|
+
to: target,
|
|
140
504
|
text,
|
|
505
|
+
messageId,
|
|
141
506
|
});
|
|
142
507
|
return {
|
|
143
508
|
to,
|
|
144
|
-
messageId: result?.messageId ??
|
|
509
|
+
messageId: result?.messageId ?? messageId,
|
|
145
510
|
};
|
|
146
511
|
},
|
|
147
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots }) => {
|
|
512
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
148
513
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
149
514
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
150
515
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
@@ -160,21 +525,40 @@ export const openclawClawlingOutbound = {
|
|
|
160
525
|
const mediaFragments = await uploadOutboundMedia([mediaUrl.trim()], {
|
|
161
526
|
apiClient,
|
|
162
527
|
runtime,
|
|
528
|
+
...(mediaAccess ? { mediaAccess } : {}),
|
|
163
529
|
...(mediaLocalRoots ? { mediaLocalRoots } : {}),
|
|
530
|
+
...(mediaReadFile ? { mediaReadFile } : {}),
|
|
164
531
|
});
|
|
165
532
|
if (mediaFragments.length === 0) {
|
|
166
533
|
throw new Error(`openclaw-clawchat failed to upload media: ${mediaUrl}`);
|
|
167
534
|
}
|
|
535
|
+
const target = parseOpenclawRecipient(to);
|
|
536
|
+
const messageId = mintOutboundMessageId(account);
|
|
537
|
+
const claimText = (text ?? "").trim();
|
|
538
|
+
const claimed = claimChannelOutbound({
|
|
539
|
+
account,
|
|
540
|
+
target,
|
|
541
|
+
messageId,
|
|
542
|
+
text: claimText,
|
|
543
|
+
raw: { target, mode: "channel-sendMedia", mediaCount: mediaFragments.length },
|
|
544
|
+
});
|
|
545
|
+
if (claimed === false) {
|
|
546
|
+
throw new Error("openclaw-clawchat outbound duplicate claim; message not sent");
|
|
547
|
+
}
|
|
548
|
+
if (claimed === null) {
|
|
549
|
+
throw new Error("openclaw-clawchat outbound message claim failed");
|
|
550
|
+
}
|
|
168
551
|
const result = await sendOpenclawClawlingMedia({
|
|
169
552
|
client,
|
|
170
553
|
account,
|
|
171
|
-
to:
|
|
554
|
+
to: target,
|
|
172
555
|
text,
|
|
173
556
|
mediaFragments,
|
|
557
|
+
messageId,
|
|
174
558
|
});
|
|
175
559
|
return {
|
|
176
560
|
to,
|
|
177
|
-
messageId: result?.messageId ??
|
|
561
|
+
messageId: result?.messageId ?? messageId,
|
|
178
562
|
};
|
|
179
563
|
},
|
|
180
564
|
}),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const EVENT = {
|
|
2
|
+
CONNECT_CHALLENGE: "connect.challenge",
|
|
3
|
+
CONNECT: "connect",
|
|
4
|
+
HELLO_OK: "hello-ok",
|
|
5
|
+
HELLO_FAIL: "hello-fail",
|
|
6
|
+
MESSAGE_SEND: "message.send",
|
|
7
|
+
MESSAGE_ACK: "message.ack",
|
|
8
|
+
MESSAGE_ERROR: "message.error",
|
|
9
|
+
MESSAGE_REPLY: "message.reply",
|
|
10
|
+
MESSAGE_CREATED: "message.created",
|
|
11
|
+
MESSAGE_ADD: "message.add",
|
|
12
|
+
MESSAGE_DONE: "message.done",
|
|
13
|
+
MESSAGE_FAILED: "message.failed",
|
|
14
|
+
TYPING_UPDATE: "typing.update",
|
|
15
|
+
CHAT_METADATA_INVALIDATED: "chat.metadata.invalidated",
|
|
16
|
+
OFFLINE_BATCH: "offline.batch",
|
|
17
|
+
OFFLINE_ACK: "offline.ack",
|
|
18
|
+
OFFLINE_DONE: "offline.done",
|
|
19
|
+
PING: "ping",
|
|
20
|
+
PONG: "pong",
|
|
21
|
+
};
|
|
22
|
+
export class AuthError extends Error {
|
|
23
|
+
name = "AuthError";
|
|
24
|
+
}
|
|
25
|
+
export class TransportError extends Error {
|
|
26
|
+
name = "TransportError";
|
|
27
|
+
}
|
|
28
|
+
export class ProtocolError extends Error {
|
|
29
|
+
envelope;
|
|
30
|
+
name = "ProtocolError";
|
|
31
|
+
constructor(message, envelope) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.envelope = envelope;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class AckTimeoutError extends Error {
|
|
37
|
+
traceId;
|
|
38
|
+
timeoutMs;
|
|
39
|
+
name = "AckTimeoutError";
|
|
40
|
+
constructor(traceId, timeoutMs) {
|
|
41
|
+
super(`ack timeout after ${timeoutMs}ms for trace_id=${traceId}`);
|
|
42
|
+
this.traceId = traceId;
|
|
43
|
+
this.timeoutMs = timeoutMs;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class MessageSendError extends Error {
|
|
47
|
+
traceId;
|
|
48
|
+
code;
|
|
49
|
+
chatId;
|
|
50
|
+
name = "MessageSendError";
|
|
51
|
+
constructor(traceId, code, message, chatId) {
|
|
52
|
+
super(`message.error ${code}: ${message} for trace_id=${traceId}`);
|
|
53
|
+
this.traceId = traceId;
|
|
54
|
+
this.code = code;
|
|
55
|
+
this.chatId = chatId;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class StateError extends Error {
|
|
59
|
+
name = "StateError";
|
|
60
|
+
}
|
|
61
|
+
export function isBusinessDispatchEvent(event) {
|
|
62
|
+
return event === EVENT.MESSAGE_SEND || event === EVENT.MESSAGE_REPLY || event === EVENT.MESSAGE_DONE;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/protocol.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local narrow guards for inbound protocol envelopes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* payload to `
|
|
4
|
+
* Raw client events hand us `Envelope<unknown>`. Before casting the
|
|
5
|
+
* payload to `MessagePayload` we run these cheap structural checks
|
|
6
6
|
* so runtime errors surface as skipped messages, not crashes.
|
|
7
7
|
*/
|
|
8
8
|
export function isInboundMessagePayload(payload) {
|
|
@@ -31,8 +31,3 @@ export function hasRenderableText(message) {
|
|
|
31
31
|
typeof f.url === "string" &&
|
|
32
32
|
f.url.trim().length > 0)));
|
|
33
33
|
}
|
|
34
|
-
export function isGroupSender(sender) {
|
|
35
|
-
if (!sender || typeof sender !== "object")
|
|
36
|
-
return false;
|
|
37
|
-
return sender.type === "group";
|
|
38
|
-
}
|