@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,588 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { AckTimeoutError, AuthError, EVENT, MessageSendError, ProtocolError, StateError, TransportError, isBusinessDispatchEvent, } from "./protocol-types.js";
|
|
3
|
+
export function createWebSocketTransport(WebSocketCtor = globalThis.WebSocket) {
|
|
4
|
+
let currentState = "closed";
|
|
5
|
+
let socket;
|
|
6
|
+
return {
|
|
7
|
+
get state() {
|
|
8
|
+
return currentState;
|
|
9
|
+
},
|
|
10
|
+
async connect(url, handlers) {
|
|
11
|
+
if (!WebSocketCtor) {
|
|
12
|
+
throw new TransportError("global WebSocket is not available");
|
|
13
|
+
}
|
|
14
|
+
currentState = "connecting";
|
|
15
|
+
const activeSocket = new WebSocketCtor(url);
|
|
16
|
+
socket = activeSocket;
|
|
17
|
+
const isCurrentSocket = () => socket === activeSocket;
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
let opened = false;
|
|
20
|
+
let settled = false;
|
|
21
|
+
const rejectBeforeOpen = (err) => {
|
|
22
|
+
if (opened || settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
reject(err);
|
|
26
|
+
};
|
|
27
|
+
activeSocket.addEventListener("open", () => {
|
|
28
|
+
if (!isCurrentSocket())
|
|
29
|
+
return;
|
|
30
|
+
opened = true;
|
|
31
|
+
settled = true;
|
|
32
|
+
currentState = "open";
|
|
33
|
+
handlers.onOpen();
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
activeSocket.addEventListener("message", (event) => {
|
|
37
|
+
if (!isCurrentSocket())
|
|
38
|
+
return;
|
|
39
|
+
const data = event?.data;
|
|
40
|
+
if (typeof data === "string" || Buffer.isBuffer(data)) {
|
|
41
|
+
handlers.onMessage(data);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (data instanceof ArrayBuffer) {
|
|
45
|
+
handlers.onMessage(Buffer.from(data));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (ArrayBuffer.isView(data)) {
|
|
49
|
+
handlers.onMessage(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
handlers.onMessage(String(data ?? ""));
|
|
53
|
+
});
|
|
54
|
+
activeSocket.addEventListener("close", (event) => {
|
|
55
|
+
if (!isCurrentSocket())
|
|
56
|
+
return;
|
|
57
|
+
currentState = "closed";
|
|
58
|
+
socket = undefined;
|
|
59
|
+
const close = event;
|
|
60
|
+
const code = typeof close?.code === "number" ? close.code : 1006;
|
|
61
|
+
const reason = typeof close?.reason === "string" ? close.reason : "closed";
|
|
62
|
+
handlers.onClose(code, reason);
|
|
63
|
+
rejectBeforeOpen(new TransportError(reason));
|
|
64
|
+
});
|
|
65
|
+
activeSocket.addEventListener("error", (event) => {
|
|
66
|
+
if (!isCurrentSocket())
|
|
67
|
+
return;
|
|
68
|
+
const err = event instanceof Error ? event : new Error("websocket error");
|
|
69
|
+
handlers.onError(err);
|
|
70
|
+
rejectBeforeOpen(err);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
send(data) {
|
|
75
|
+
if (currentState !== "open" || !socket) {
|
|
76
|
+
throw new StateError(`cannot send while transport=${currentState}`);
|
|
77
|
+
}
|
|
78
|
+
socket.send(data);
|
|
79
|
+
},
|
|
80
|
+
close(code = 1000, reason = "client close") {
|
|
81
|
+
if (currentState === "closed")
|
|
82
|
+
return;
|
|
83
|
+
socket?.close(code, reason);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export class ClawChatClient extends EventEmitter {
|
|
88
|
+
opts;
|
|
89
|
+
currentState = "idle";
|
|
90
|
+
connectResolve;
|
|
91
|
+
connectReject;
|
|
92
|
+
heartbeatTimer;
|
|
93
|
+
pongTimer;
|
|
94
|
+
reconnectTimer;
|
|
95
|
+
reconnectAttempts = 0;
|
|
96
|
+
closing = false;
|
|
97
|
+
authFailed = false;
|
|
98
|
+
expectedConnectTraceId;
|
|
99
|
+
pending = new Map();
|
|
100
|
+
handledMessageErrorTraces = new Set();
|
|
101
|
+
sendQueue = [];
|
|
102
|
+
constructor(opts) {
|
|
103
|
+
super();
|
|
104
|
+
this.opts = opts;
|
|
105
|
+
}
|
|
106
|
+
get state() {
|
|
107
|
+
return this.currentState;
|
|
108
|
+
}
|
|
109
|
+
get transportState() {
|
|
110
|
+
return this.opts.transport.state;
|
|
111
|
+
}
|
|
112
|
+
nextTraceId() {
|
|
113
|
+
return this.opts.traceIdFactory();
|
|
114
|
+
}
|
|
115
|
+
hasPendingAckTrace(traceId) {
|
|
116
|
+
return this.pending.has(traceId);
|
|
117
|
+
}
|
|
118
|
+
markMessageErrorHandled(traceId) {
|
|
119
|
+
this.handledMessageErrorTraces.add(traceId);
|
|
120
|
+
}
|
|
121
|
+
async connect() {
|
|
122
|
+
this.closing = false;
|
|
123
|
+
this.authFailed = false;
|
|
124
|
+
const ready = new Promise((resolve, reject) => {
|
|
125
|
+
this.connectResolve = resolve;
|
|
126
|
+
this.connectReject = reject;
|
|
127
|
+
});
|
|
128
|
+
void this.openTransport();
|
|
129
|
+
return await ready;
|
|
130
|
+
}
|
|
131
|
+
close() {
|
|
132
|
+
this.closing = true;
|
|
133
|
+
this.clearTimers();
|
|
134
|
+
this.rejectPending(new StateError("client closed"));
|
|
135
|
+
this.sendQueue.length = 0;
|
|
136
|
+
if (this.opts.transport.state === "closed") {
|
|
137
|
+
this.transition("disconnected");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.opts.transport.close(1000, "client close");
|
|
141
|
+
}
|
|
142
|
+
typing(chatId, isTyping = true) {
|
|
143
|
+
this.sendRawEnvelope({
|
|
144
|
+
version: "2",
|
|
145
|
+
event: EVENT.TYPING_UPDATE,
|
|
146
|
+
trace_id: this.nextTraceId(),
|
|
147
|
+
emitted_at: Date.now(),
|
|
148
|
+
chat_id: chatId,
|
|
149
|
+
payload: { is_typing: isTyping },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
emitRaw(event, payload, routing) {
|
|
153
|
+
this.sendRawEnvelope({
|
|
154
|
+
version: "2",
|
|
155
|
+
event,
|
|
156
|
+
trace_id: this.nextTraceId(),
|
|
157
|
+
emitted_at: Date.now(),
|
|
158
|
+
...(routing?.chat_id ? { chat_id: routing.chat_id } : {}),
|
|
159
|
+
payload,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
sendWire(wire, options = {}) {
|
|
163
|
+
if (!options.bypassReconnectQueue && this.shouldQueueOutboundWire()) {
|
|
164
|
+
this.sendQueue.push(wire);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (this.opts.transport.state !== "open") {
|
|
168
|
+
throw new StateError(`cannot send while transport=${this.opts.transport.state}`);
|
|
169
|
+
}
|
|
170
|
+
this.opts.transport.send(wire);
|
|
171
|
+
}
|
|
172
|
+
sendRawEnvelope(env) {
|
|
173
|
+
this.sendWire(JSON.stringify(env), { bypassReconnectQueue: env.event === EVENT.CONNECT });
|
|
174
|
+
}
|
|
175
|
+
async sendAckableEnvelope(params) {
|
|
176
|
+
const traceId = this.nextTraceId();
|
|
177
|
+
const env = {
|
|
178
|
+
version: "2",
|
|
179
|
+
event: params.eventName,
|
|
180
|
+
trace_id: traceId,
|
|
181
|
+
emitted_at: Date.now(),
|
|
182
|
+
chat_id: params.chatId,
|
|
183
|
+
payload: params.payload,
|
|
184
|
+
};
|
|
185
|
+
return await new Promise((resolve, reject) => {
|
|
186
|
+
const entry = {
|
|
187
|
+
timer: setTimeout(() => { }, 0),
|
|
188
|
+
resolve,
|
|
189
|
+
reject,
|
|
190
|
+
};
|
|
191
|
+
const armTimer = () => {
|
|
192
|
+
entry.timer = setTimeout(() => {
|
|
193
|
+
if (this.opts.ack.autoResendOnTimeout && this.opts.transport.state === "open") {
|
|
194
|
+
try {
|
|
195
|
+
this.sendRawEnvelope(env);
|
|
196
|
+
armTimer();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
this.pending.delete(traceId);
|
|
201
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this.pending.delete(traceId);
|
|
206
|
+
reject(new AckTimeoutError(traceId, this.opts.ack.timeout));
|
|
207
|
+
}, this.opts.ack.timeout);
|
|
208
|
+
};
|
|
209
|
+
armTimer();
|
|
210
|
+
this.pending.set(traceId, entry);
|
|
211
|
+
try {
|
|
212
|
+
this.sendRawEnvelope(env);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
clearTimeout(entry.timer);
|
|
216
|
+
this.pending.delete(traceId);
|
|
217
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async openTransport() {
|
|
222
|
+
this.transition("connecting");
|
|
223
|
+
try {
|
|
224
|
+
await this.opts.transport.connect(this.opts.url, {
|
|
225
|
+
onOpen: () => this.transition("challenging"),
|
|
226
|
+
onMessage: (data) => this.handleWireMessage(data),
|
|
227
|
+
onClose: (code, reason) => this.handleClose(code, reason),
|
|
228
|
+
onError: (err) => this.emitError(new TransportError(err.message)),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
const error = err instanceof Error ? new TransportError(err.message) : new TransportError(String(err));
|
|
233
|
+
this.emitError(error);
|
|
234
|
+
if (this.closing || this.authFailed || !this.opts.reconnect.enabled) {
|
|
235
|
+
this.transition("disconnected");
|
|
236
|
+
this.connectReject?.(error);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.scheduleReconnect(error.message);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
transition(next) {
|
|
243
|
+
const from = this.currentState;
|
|
244
|
+
if (from === next)
|
|
245
|
+
return;
|
|
246
|
+
this.currentState = next;
|
|
247
|
+
this.emit("state", { from, to: next });
|
|
248
|
+
}
|
|
249
|
+
handleWireMessage(data) {
|
|
250
|
+
let env;
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(String(data));
|
|
253
|
+
if (!parsed || typeof parsed !== "object") {
|
|
254
|
+
throw new ProtocolError("invalid envelope", parsed);
|
|
255
|
+
}
|
|
256
|
+
env = parsed;
|
|
257
|
+
if (env.version !== "2" ||
|
|
258
|
+
typeof env.event !== "string" ||
|
|
259
|
+
typeof env.trace_id !== "string" ||
|
|
260
|
+
typeof env.emitted_at !== "number" ||
|
|
261
|
+
!Object.prototype.hasOwnProperty.call(env, "payload")) {
|
|
262
|
+
throw new ProtocolError("invalid envelope", env);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
this.emitError(err instanceof Error ? err : new ProtocolError(String(err)));
|
|
267
|
+
if (this.isHandshaking()) {
|
|
268
|
+
this.failHandshake(err instanceof ProtocolError ? err : new ProtocolError(err instanceof Error ? err.message : String(err)), 4002, "protocol error");
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.emit("raw", env);
|
|
273
|
+
this.dispatchInbound(env);
|
|
274
|
+
}
|
|
275
|
+
dispatchInbound(env) {
|
|
276
|
+
if (env.event === EVENT.CONNECT_CHALLENGE)
|
|
277
|
+
return this.onChallenge(env);
|
|
278
|
+
if (env.event === EVENT.HELLO_OK)
|
|
279
|
+
return this.onHelloOk(env);
|
|
280
|
+
if (env.event === EVENT.HELLO_FAIL)
|
|
281
|
+
return this.onHelloFail(env);
|
|
282
|
+
if (env.event === EVENT.PING)
|
|
283
|
+
return this.onPing(env);
|
|
284
|
+
if (env.event === EVENT.PONG)
|
|
285
|
+
return this.onPong();
|
|
286
|
+
if (env.event === EVENT.MESSAGE_ACK)
|
|
287
|
+
return this.onAck(env);
|
|
288
|
+
if (env.event === EVENT.MESSAGE_ERROR)
|
|
289
|
+
return this.onMessageError(env);
|
|
290
|
+
if (isBusinessDispatchEvent(env.event))
|
|
291
|
+
this.emit("message", env);
|
|
292
|
+
if (env.event === EVENT.MESSAGE_CREATED)
|
|
293
|
+
this.emit("message:created", env);
|
|
294
|
+
if (env.event === EVENT.MESSAGE_ADD)
|
|
295
|
+
this.emit("message:add", env);
|
|
296
|
+
if (env.event === EVENT.MESSAGE_DONE)
|
|
297
|
+
this.emit("message:done", env);
|
|
298
|
+
if (env.event === EVENT.MESSAGE_FAILED)
|
|
299
|
+
this.emit("message:failed", env);
|
|
300
|
+
if (env.event === EVENT.TYPING_UPDATE)
|
|
301
|
+
this.emit("typing", env);
|
|
302
|
+
if (env.event === EVENT.CHAT_METADATA_INVALIDATED)
|
|
303
|
+
this.emit("metadata:invalidated", env);
|
|
304
|
+
if (env.event === EVENT.OFFLINE_DONE)
|
|
305
|
+
this.emit("offline:done");
|
|
306
|
+
}
|
|
307
|
+
onChallenge(env) {
|
|
308
|
+
if (this.currentState !== "challenging") {
|
|
309
|
+
if (this.currentState === "connected" || this.currentState === "reconnecting")
|
|
310
|
+
return;
|
|
311
|
+
this.failHandshake(new ProtocolError("unexpected challenge", env), 4002, "protocol error");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const payload = env.payload && typeof env.payload === "object" ? env.payload : {};
|
|
315
|
+
const nonce = payload.nonce;
|
|
316
|
+
if (typeof nonce !== "string" || !nonce) {
|
|
317
|
+
this.failHandshake(new ProtocolError("missing challenge nonce", env), 4002, "protocol error");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
this.transition("authenticating");
|
|
321
|
+
const connectPayload = {
|
|
322
|
+
token: this.opts.token,
|
|
323
|
+
nonce,
|
|
324
|
+
...(this.opts.deviceId ? { device_id: this.opts.deviceId } : {}),
|
|
325
|
+
capabilities: { multi_device: true, device_replay: true, chat_meta_events: true },
|
|
326
|
+
};
|
|
327
|
+
const traceId = this.nextTraceId();
|
|
328
|
+
this.expectedConnectTraceId = traceId;
|
|
329
|
+
try {
|
|
330
|
+
this.sendRawEnvelope({
|
|
331
|
+
version: "2",
|
|
332
|
+
event: EVENT.CONNECT,
|
|
333
|
+
trace_id: traceId,
|
|
334
|
+
emitted_at: Date.now(),
|
|
335
|
+
payload: connectPayload,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
this.failHandshake(err instanceof Error ? err : new TransportError(String(err)), 4003, "connect send failed");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
onHelloOk(env) {
|
|
343
|
+
const helloError = this.validateHelloEnvelope(env, "hello-ok");
|
|
344
|
+
if (helloError) {
|
|
345
|
+
this.failHandshake(helloError, 4002, "protocol error");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.expectedConnectTraceId = undefined;
|
|
349
|
+
this.emit("hello:ok", env);
|
|
350
|
+
this.transition("connected");
|
|
351
|
+
this.reconnectAttempts = 0;
|
|
352
|
+
this.startHeartbeat();
|
|
353
|
+
try {
|
|
354
|
+
this.flushSendQueue();
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
this.emitError(err instanceof Error ? err : new TransportError(String(err)));
|
|
358
|
+
this.opts.transport.close(4000, "queued send failed");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.connectResolve?.();
|
|
362
|
+
this.connectResolve = undefined;
|
|
363
|
+
this.connectReject = undefined;
|
|
364
|
+
}
|
|
365
|
+
onHelloFail(env) {
|
|
366
|
+
const helloError = this.validateHelloEnvelope(env, "hello-fail");
|
|
367
|
+
if (helloError) {
|
|
368
|
+
this.failHandshake(helloError, 4002, "protocol error");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const payload = env.payload && typeof env.payload === "object" ? env.payload : {};
|
|
372
|
+
const reason = payload.reason;
|
|
373
|
+
if (typeof reason !== "string" || !reason) {
|
|
374
|
+
this.failHandshake(new ProtocolError("invalid hello-fail payload", env), 4002, "protocol error");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const err = new AuthError(typeof reason === "string" ? reason : "authentication failed");
|
|
378
|
+
this.authFailed = true;
|
|
379
|
+
this.expectedConnectTraceId = undefined;
|
|
380
|
+
this.sendQueue.length = 0;
|
|
381
|
+
this.clearTimers();
|
|
382
|
+
this.rejectPending(err);
|
|
383
|
+
this.connectReject?.(err);
|
|
384
|
+
this.connectResolve = undefined;
|
|
385
|
+
this.connectReject = undefined;
|
|
386
|
+
this.emitError(err);
|
|
387
|
+
this.transition("disconnected");
|
|
388
|
+
if (this.opts.transport.state !== "closed") {
|
|
389
|
+
this.opts.transport.close(4001, "auth failed");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
onPing(env) {
|
|
393
|
+
this.sendRawEnvelope({
|
|
394
|
+
version: "2",
|
|
395
|
+
event: EVENT.PONG,
|
|
396
|
+
trace_id: env.trace_id,
|
|
397
|
+
emitted_at: env.emitted_at,
|
|
398
|
+
payload: {},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
onPong() {
|
|
402
|
+
if (this.pongTimer)
|
|
403
|
+
clearTimeout(this.pongTimer);
|
|
404
|
+
this.pongTimer = undefined;
|
|
405
|
+
}
|
|
406
|
+
onAck(env) {
|
|
407
|
+
const entry = this.pending.get(env.trace_id);
|
|
408
|
+
if (!entry)
|
|
409
|
+
return;
|
|
410
|
+
clearTimeout(entry.timer);
|
|
411
|
+
this.pending.delete(env.trace_id);
|
|
412
|
+
entry.resolve(env);
|
|
413
|
+
}
|
|
414
|
+
onMessageError(env) {
|
|
415
|
+
const entry = this.pending.get(env.trace_id);
|
|
416
|
+
if (!entry) {
|
|
417
|
+
if (this.handledMessageErrorTraces.delete(env.trace_id))
|
|
418
|
+
return;
|
|
419
|
+
console.warn(`clawchat.ws unmatched message.error trace_id=${env.trace_id} chat_id=${env.chat_id ?? "-"}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
clearTimeout(entry.timer);
|
|
423
|
+
this.pending.delete(env.trace_id);
|
|
424
|
+
const payload = env.payload && typeof env.payload === "object" ? env.payload : undefined;
|
|
425
|
+
const code = typeof payload?.code === "string" && payload.code ? payload.code : "unknown";
|
|
426
|
+
const message = typeof payload?.message === "string" && payload.message ? payload.message : "message send failed";
|
|
427
|
+
entry.reject(new MessageSendError(env.trace_id, code, message, env.chat_id));
|
|
428
|
+
}
|
|
429
|
+
startHeartbeat() {
|
|
430
|
+
if (!this.opts.heartbeat.enabled)
|
|
431
|
+
return;
|
|
432
|
+
if (this.heartbeatTimer)
|
|
433
|
+
clearInterval(this.heartbeatTimer);
|
|
434
|
+
this.heartbeatTimer = setInterval(() => {
|
|
435
|
+
try {
|
|
436
|
+
this.sendRawEnvelope({
|
|
437
|
+
version: "2",
|
|
438
|
+
event: EVENT.PING,
|
|
439
|
+
trace_id: this.nextTraceId(),
|
|
440
|
+
emitted_at: Date.now(),
|
|
441
|
+
payload: {},
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
this.opts.transport.close(4000, "heartbeat send failed");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (this.pongTimer)
|
|
449
|
+
clearTimeout(this.pongTimer);
|
|
450
|
+
this.pongTimer = setTimeout(() => this.opts.transport.close(4000, "heartbeat timeout"), this.opts.heartbeat.timeout);
|
|
451
|
+
}, this.opts.heartbeat.interval);
|
|
452
|
+
}
|
|
453
|
+
handleClose(code, reason) {
|
|
454
|
+
if (this.currentState === "reconnecting" && this.reconnectTimer) {
|
|
455
|
+
if (this.heartbeatTimer)
|
|
456
|
+
clearInterval(this.heartbeatTimer);
|
|
457
|
+
if (this.pongTimer)
|
|
458
|
+
clearTimeout(this.pongTimer);
|
|
459
|
+
this.heartbeatTimer = undefined;
|
|
460
|
+
this.pongTimer = undefined;
|
|
461
|
+
this.emit("close", { code, reason });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.clearTimers();
|
|
465
|
+
this.emit("close", { code, reason });
|
|
466
|
+
const closeError = new TransportError(reason || "connection closed");
|
|
467
|
+
if (!this.closing)
|
|
468
|
+
this.rejectPending(closeError);
|
|
469
|
+
if (this.closing || this.authFailed || !this.opts.reconnect.enabled) {
|
|
470
|
+
this.transition("disconnected");
|
|
471
|
+
this.connectReject?.(closeError);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.scheduleReconnect(reason || `close ${code}`);
|
|
475
|
+
}
|
|
476
|
+
failHandshake(err, code, reason) {
|
|
477
|
+
this.clearTimers();
|
|
478
|
+
this.rejectPending(err);
|
|
479
|
+
this.expectedConnectTraceId = undefined;
|
|
480
|
+
this.sendQueue.length = 0;
|
|
481
|
+
this.connectReject?.(err);
|
|
482
|
+
this.connectResolve = undefined;
|
|
483
|
+
this.connectReject = undefined;
|
|
484
|
+
this.emitError(err);
|
|
485
|
+
this.transition("disconnected");
|
|
486
|
+
if (this.opts.transport.state !== "closed") {
|
|
487
|
+
this.closing = true;
|
|
488
|
+
this.opts.transport.close(code, reason);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
scheduleReconnect(reason) {
|
|
492
|
+
if (this.reconnectTimer)
|
|
493
|
+
return;
|
|
494
|
+
if (this.reconnectAttempts >= this.opts.reconnect.maxRetries) {
|
|
495
|
+
this.sendQueue.length = 0;
|
|
496
|
+
this.transition("disconnected");
|
|
497
|
+
this.connectReject?.(new TransportError(reason));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.reconnectAttempts += 1;
|
|
501
|
+
const baseDelay = Math.min(this.opts.reconnect.maxDelay, this.opts.reconnect.initialDelay * 2 ** Math.max(0, this.reconnectAttempts - 1));
|
|
502
|
+
const jitter = baseDelay * this.opts.reconnect.jitterRatio * Math.random();
|
|
503
|
+
const delay = Math.round(baseDelay + jitter);
|
|
504
|
+
this.transition("reconnecting");
|
|
505
|
+
this.emit("reconnect:scheduled", { reason, delay });
|
|
506
|
+
this.reconnectTimer = setTimeout(() => {
|
|
507
|
+
this.reconnectTimer = undefined;
|
|
508
|
+
void this.openTransport();
|
|
509
|
+
}, delay);
|
|
510
|
+
}
|
|
511
|
+
clearTimers() {
|
|
512
|
+
if (this.heartbeatTimer)
|
|
513
|
+
clearInterval(this.heartbeatTimer);
|
|
514
|
+
if (this.pongTimer)
|
|
515
|
+
clearTimeout(this.pongTimer);
|
|
516
|
+
if (this.reconnectTimer)
|
|
517
|
+
clearTimeout(this.reconnectTimer);
|
|
518
|
+
this.heartbeatTimer = undefined;
|
|
519
|
+
this.pongTimer = undefined;
|
|
520
|
+
this.reconnectTimer = undefined;
|
|
521
|
+
}
|
|
522
|
+
rejectPending(err) {
|
|
523
|
+
for (const [traceId, entry] of this.pending) {
|
|
524
|
+
clearTimeout(entry.timer);
|
|
525
|
+
this.pending.delete(traceId);
|
|
526
|
+
entry.reject(err);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
emitError(err) {
|
|
530
|
+
if (this.listenerCount("error") > 0)
|
|
531
|
+
this.emit("error", err);
|
|
532
|
+
}
|
|
533
|
+
shouldQueueOutboundWire() {
|
|
534
|
+
if (!this.opts.reconnect.enabled || this.closing || this.authFailed)
|
|
535
|
+
return false;
|
|
536
|
+
if (this.currentState === "reconnecting")
|
|
537
|
+
return true;
|
|
538
|
+
return this.reconnectAttempts > 0 && this.isHandshaking();
|
|
539
|
+
}
|
|
540
|
+
flushSendQueue() {
|
|
541
|
+
while (this.sendQueue.length > 0) {
|
|
542
|
+
const wire = this.sendQueue.shift();
|
|
543
|
+
try {
|
|
544
|
+
this.sendWire(wire);
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
this.sendQueue.unshift(wire);
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
isHandshaking() {
|
|
553
|
+
return this.currentState === "connecting" || this.currentState === "challenging" || this.currentState === "authenticating";
|
|
554
|
+
}
|
|
555
|
+
validateHelloEnvelope(env, eventName) {
|
|
556
|
+
if (this.currentState !== "authenticating" || env.trace_id !== this.expectedConnectTraceId) {
|
|
557
|
+
return new ProtocolError(`unexpected ${eventName}`, env);
|
|
558
|
+
}
|
|
559
|
+
if (!env.payload || typeof env.payload !== "object") {
|
|
560
|
+
return new ProtocolError(`invalid ${eventName} payload`, env);
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
export function createClawChatClient(options) {
|
|
566
|
+
return new ClawChatClient({
|
|
567
|
+
...options,
|
|
568
|
+
transport: options.transport ?? createWebSocketTransport(),
|
|
569
|
+
traceIdFactory: options.traceIdFactory ??
|
|
570
|
+
(() => `trace-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
571
|
+
reconnect: {
|
|
572
|
+
enabled: options.reconnect?.enabled ?? true,
|
|
573
|
+
initialDelay: options.reconnect?.initialDelay ?? 500,
|
|
574
|
+
maxDelay: options.reconnect?.maxDelay ?? 15000,
|
|
575
|
+
maxRetries: options.reconnect?.maxRetries ?? Number.POSITIVE_INFINITY,
|
|
576
|
+
jitterRatio: options.reconnect?.jitterRatio ?? 0.3,
|
|
577
|
+
},
|
|
578
|
+
heartbeat: {
|
|
579
|
+
enabled: options.heartbeat?.enabled ?? true,
|
|
580
|
+
interval: options.heartbeat?.interval ?? 20000,
|
|
581
|
+
timeout: options.heartbeat?.timeout ?? 10000,
|
|
582
|
+
},
|
|
583
|
+
ack: {
|
|
584
|
+
timeout: options.ack?.timeout ?? 15000,
|
|
585
|
+
autoResendOnTimeout: options.ack?.autoResendOnTimeout ?? false,
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function optionalField(value) {
|
|
2
|
+
if (value === null || value === undefined || value === "")
|
|
3
|
+
return "-";
|
|
4
|
+
return String(value);
|
|
5
|
+
}
|
|
6
|
+
export function formatWsLog(input) {
|
|
7
|
+
const base = [
|
|
8
|
+
["event", input.event],
|
|
9
|
+
["account_id", input.accountId],
|
|
10
|
+
["attempt", input.attempt],
|
|
11
|
+
["reconnect_count", input.reconnectCount],
|
|
12
|
+
["state", input.state],
|
|
13
|
+
["action", input.action],
|
|
14
|
+
];
|
|
15
|
+
return [...base, ...(input.fields ?? [])]
|
|
16
|
+
.map(([key, value]) => `${key}=${optionalField(value)}`)
|
|
17
|
+
.join(" ")
|
|
18
|
+
.replace(/^/, "clawchat.ws ");
|
|
19
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { openclawClawlingPlugin } from "./src/channel.ts";
|
|
3
|
+
import { registerOpenclawClawlingCommands } from "./src/commands.ts";
|
|
4
|
+
import { openclawClawlingConfigSchema } from "./src/config.ts";
|
|
5
|
+
import {
|
|
6
|
+
registerClawChatPromptInjection,
|
|
7
|
+
type ClawChatPromptInjectionApi,
|
|
8
|
+
} from "./src/prompt-injection.ts";
|
|
9
|
+
import { setOpenclawClawlingRuntime } from "./src/runtime.ts";
|
|
10
|
+
import { registerOpenclawClawlingTools } from "./src/tools.ts";
|
|
11
|
+
|
|
12
|
+
export default defineChannelPluginEntry({
|
|
13
|
+
id: "clawchat-plugin-openclaw",
|
|
14
|
+
name: "Clawling Chat",
|
|
15
|
+
description: "Clawling Chat Protocol v2 channel plugin",
|
|
16
|
+
plugin: openclawClawlingPlugin,
|
|
17
|
+
configSchema: { schema: openclawClawlingConfigSchema },
|
|
18
|
+
setRuntime: setOpenclawClawlingRuntime,
|
|
19
|
+
registerFull(api) {
|
|
20
|
+
registerOpenclawClawlingCommands(api);
|
|
21
|
+
registerClawChatPromptInjection(api as unknown as ClawChatPromptInjectionApi);
|
|
22
|
+
registerOpenclawClawlingTools(api);
|
|
23
|
+
},
|
|
24
|
+
});
|