@coolclaw/coolclaw 0.2.10 → 0.3.0

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.
@@ -0,0 +1,1113 @@
1
+ import {
2
+ buildWsUrl,
3
+ resolveAccountToken
4
+ } from "./chunk-Q3NF4NWE.js";
5
+
6
+ // src/runtime.ts
7
+ var _runtime;
8
+ function setCoolclawRuntime(runtime) {
9
+ _runtime = runtime;
10
+ }
11
+ function getCoolclawRuntime() {
12
+ return _runtime;
13
+ }
14
+
15
+ // src/ack-store.ts
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
17
+ import { join } from "path";
18
+ import { homedir } from "os";
19
+ var InMemoryAckStore = class {
20
+ cursors = /* @__PURE__ */ new Map();
21
+ async getLastAckedSeq(accountKey) {
22
+ return this.cursors.get(accountKey) ?? 0;
23
+ }
24
+ async record(accountKey, seq) {
25
+ if (!Number.isInteger(seq) || seq < 1) {
26
+ throw new Error(`Invalid ACK seq: ${seq}`);
27
+ }
28
+ const current = this.cursors.get(accountKey) ?? 0;
29
+ if (seq <= current) {
30
+ return current;
31
+ }
32
+ this.cursors.set(accountKey, seq);
33
+ return seq;
34
+ }
35
+ };
36
+ var FileAckStore = class {
37
+ cursors = /* @__PURE__ */ new Map();
38
+ dir;
39
+ constructor(dir) {
40
+ this.dir = dir ?? join(homedir(), ".openclaw", "extensions", "coolclaw", ".ack-store");
41
+ if (!existsSync(this.dir)) {
42
+ mkdirSync(this.dir, { recursive: true });
43
+ }
44
+ }
45
+ async getLastAckedSeq(accountKey) {
46
+ return this.cursors.get(accountKey) ?? this.load(accountKey);
47
+ }
48
+ async record(accountKey, seq) {
49
+ if (!Number.isInteger(seq) || seq < 1) {
50
+ throw new Error(`Invalid ACK seq: ${seq}`);
51
+ }
52
+ const current = this.cursors.get(accountKey) ?? this.load(accountKey);
53
+ if (seq <= current) {
54
+ return current;
55
+ }
56
+ this.cursors.set(accountKey, seq);
57
+ this.persist(accountKey, seq);
58
+ return seq;
59
+ }
60
+ load(accountKey) {
61
+ const filePath = this.filePath(accountKey);
62
+ if (!existsSync(filePath)) {
63
+ return 0;
64
+ }
65
+ try {
66
+ const text = readFileSync(filePath, "utf-8").trim();
67
+ const value = parseInt(text, 10);
68
+ return Number.isFinite(value) && value >= 0 ? value : 0;
69
+ } catch {
70
+ return 0;
71
+ }
72
+ }
73
+ persist(accountKey, lastAckedSeq) {
74
+ try {
75
+ writeFileSync(this.filePath(accountKey), String(lastAckedSeq), "utf-8");
76
+ } catch {
77
+ }
78
+ }
79
+ filePath(accountKey) {
80
+ const safeName = accountKey.replace(/[^a-zA-Z0-9_-]/g, "_");
81
+ return join(this.dir, `${safeName}.ack`);
82
+ }
83
+ };
84
+
85
+ // src/frame-codec.ts
86
+ import { randomUUID } from "crypto";
87
+ var CoolclawFrameDecodeError = class extends Error {
88
+ constructor(message) {
89
+ super(message);
90
+ this.name = "CoolclawFrameDecodeError";
91
+ }
92
+ };
93
+ function createFrame(type, payload) {
94
+ return {
95
+ v: 1,
96
+ type,
97
+ id: `cli_${randomUUID()}`,
98
+ ts: Date.now(),
99
+ payload
100
+ };
101
+ }
102
+ function encodeFrame(frame) {
103
+ return JSON.stringify(frame);
104
+ }
105
+ function decodeFrame(raw) {
106
+ let value;
107
+ try {
108
+ value = JSON.parse(raw);
109
+ } catch (error) {
110
+ throw new CoolclawFrameDecodeError(`Invalid CoolClaw frame JSON: ${error.message}`);
111
+ }
112
+ if (!isRecord(value)) {
113
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: expected object");
114
+ }
115
+ if (!("v" in value)) {
116
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing v");
117
+ }
118
+ if (value.v !== 1) {
119
+ throw new CoolclawFrameDecodeError("Unsupported CoolClaw frame version");
120
+ }
121
+ if (typeof value.type !== "string" || value.type.length === 0) {
122
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing type");
123
+ }
124
+ if (typeof value.id !== "string" || value.id.length === 0) {
125
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing id");
126
+ }
127
+ if (typeof value.ts !== "number" || !Number.isFinite(value.ts)) {
128
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing ts");
129
+ }
130
+ if ("ack" in value && value.ack !== void 0 && typeof value.ack !== "string") {
131
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: ack must be a string");
132
+ }
133
+ const frame = {
134
+ v: 1,
135
+ type: value.type,
136
+ id: value.id,
137
+ ts: value.ts
138
+ };
139
+ if (typeof value.ack === "string") {
140
+ frame.ack = value.ack;
141
+ }
142
+ if ("payload" in value) {
143
+ frame.payload = value.payload;
144
+ }
145
+ return frame;
146
+ }
147
+ function isRecord(value) {
148
+ return typeof value === "object" && value !== null;
149
+ }
150
+
151
+ // src/inbound.ts
152
+ function mapInboundFrame(frame) {
153
+ if (frame.type === "PRIVATE_MESSAGE") {
154
+ const payload = assertPrivatePayload(frame.payload);
155
+ return {
156
+ id: payload.messageId,
157
+ channel: "coolclaw",
158
+ conversationId: `private:${payload.sender.userType}:${payload.sender.userId}`,
159
+ text: payload.content,
160
+ messageType: payload.messageType,
161
+ seq: payload.seq,
162
+ shouldReply: true,
163
+ // 私聊本身就是与 Bot 对话的意图
164
+ sender: payload.sender,
165
+ recipient: payload.recipient,
166
+ metadata: {
167
+ riddleConversationId: payload.conversationId,
168
+ sentAt: payload.sentAt,
169
+ sourceFrameId: frame.id
170
+ }
171
+ };
172
+ }
173
+ if (frame.type === "GROUP_MESSAGE") {
174
+ const payload = assertGroupPayload(frame.payload);
175
+ return {
176
+ id: payload.messageId,
177
+ channel: "coolclaw",
178
+ conversationId: `group:${payload.groupId}`,
179
+ text: payload.content,
180
+ messageType: payload.messageType,
181
+ seq: payload.seq,
182
+ shouldReply: payload.mentioned,
183
+ // 群聊仅在 @ 时回复
184
+ sender: payload.sender,
185
+ group: {
186
+ groupId: payload.groupId,
187
+ groupName: payload.groupName
188
+ },
189
+ metadata: {
190
+ riddleConversationId: payload.conversationId,
191
+ sentAt: payload.sentAt,
192
+ sourceFrameId: frame.id,
193
+ agentHint: payload.agentHint
194
+ }
195
+ };
196
+ }
197
+ if (frame.type === "SYSTEM_NOTIFICATION" || frame.type === "GAME_EVENT" || frame.type === "CONTENT_TASK") {
198
+ return mapNotificationFrame(frame);
199
+ }
200
+ throw new Error(`Unsupported inbound CoolClaw frame type: ${frame.type}`);
201
+ }
202
+ async function handleInboundFrame(input) {
203
+ const envelope = mapInboundFrame(input.frame);
204
+ if (!envelope) return;
205
+ await ackProcessedSeq(input, envelope);
206
+ await input.dispatch(envelope);
207
+ }
208
+ function mapNotificationFrame(frame) {
209
+ const payload = isRecord2(frame.payload) ? frame.payload : {};
210
+ const seq = typeof payload.seq === "number" ? payload.seq : void 0;
211
+ if (frame.type === "SYSTEM_NOTIFICATION") {
212
+ const title = typeof payload.title === "string" ? payload.title : "System";
213
+ const content = typeof payload.content === "string" ? payload.content : JSON.stringify(payload);
214
+ return {
215
+ id: frame.id,
216
+ channel: "coolclaw",
217
+ conversationId: "notification:system",
218
+ text: `[\u7CFB\u7EDF\u901A\u77E5] ${title}: ${content}`,
219
+ messageType: frame.type,
220
+ seq,
221
+ shouldReply: false,
222
+ metadata: { sourceFrameId: frame.id, payload: frame.payload }
223
+ };
224
+ }
225
+ if (frame.type === "GAME_EVENT") {
226
+ return null;
227
+ }
228
+ if (frame.type === "CONTENT_TASK") {
229
+ const taskType = typeof payload.taskType === "string" ? payload.taskType : "unknown";
230
+ return {
231
+ id: frame.id,
232
+ channel: "coolclaw",
233
+ conversationId: "notification:content_task",
234
+ text: `[\u5185\u5BB9\u4EFB\u52A1] ${taskType}: ${JSON.stringify(payload)}`,
235
+ messageType: frame.type,
236
+ seq,
237
+ shouldReply: false,
238
+ metadata: { sourceFrameId: frame.id, payload: frame.payload }
239
+ };
240
+ }
241
+ return null;
242
+ }
243
+ async function ackProcessedSeq(input, envelope) {
244
+ if (typeof envelope.seq === "number") {
245
+ const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
246
+ await input.sendAck(createFrame("ACK", { lastAckedSeq }));
247
+ }
248
+ }
249
+ function assertPrivatePayload(value) {
250
+ if (!isRecord2(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
251
+ throw new Error("Invalid PRIVATE_MESSAGE payload");
252
+ }
253
+ return {
254
+ seq: readNumber(value, "seq"),
255
+ messageId: readString(value, "messageId"),
256
+ conversationId: readString(value, "conversationId"),
257
+ sender: value.sender,
258
+ recipient: value.recipient,
259
+ messageType: readString(value, "messageType"),
260
+ content: readString(value, "content"),
261
+ mentioned: readBoolean(value, "mentioned"),
262
+ sentAt: readString(value, "sentAt")
263
+ };
264
+ }
265
+ function assertGroupPayload(value) {
266
+ if (!isRecord2(value) || !isUserRef(value.sender)) {
267
+ throw new Error("Invalid GROUP_MESSAGE payload");
268
+ }
269
+ return {
270
+ seq: readNumber(value, "seq"),
271
+ messageId: readString(value, "messageId"),
272
+ groupId: readString(value, "groupId"),
273
+ groupName: readString(value, "groupName"),
274
+ conversationId: readString(value, "conversationId"),
275
+ sender: value.sender,
276
+ messageType: readString(value, "messageType"),
277
+ content: readString(value, "content"),
278
+ mentioned: readBoolean(value, "mentioned"),
279
+ sentAt: readString(value, "sentAt"),
280
+ agentHint: readOptionalString(value, "agentHint")
281
+ };
282
+ }
283
+ function readString(source, key) {
284
+ const value = source[key];
285
+ if (typeof value !== "string" || value.length === 0) {
286
+ throw new Error(`Invalid inbound payload: missing ${key}`);
287
+ }
288
+ return value;
289
+ }
290
+ function readNumber(source, key) {
291
+ const value = source[key];
292
+ if (typeof value !== "number" || !Number.isInteger(value)) {
293
+ throw new Error(`Invalid inbound payload: missing ${key}`);
294
+ }
295
+ return value;
296
+ }
297
+ function readBoolean(source, key) {
298
+ const value = source[key];
299
+ if (typeof value !== "boolean") {
300
+ throw new Error(`Invalid inbound payload: missing ${key}`);
301
+ }
302
+ return value;
303
+ }
304
+ function readOptionalString(source, key) {
305
+ const value = source[key];
306
+ if (value === void 0 || value === null) {
307
+ return void 0;
308
+ }
309
+ if (typeof value !== "string") {
310
+ throw new Error(`Invalid inbound payload: ${key} must be a string`);
311
+ }
312
+ return value;
313
+ }
314
+ function isUserRef(value) {
315
+ return isRecord2(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
316
+ }
317
+ function isRecord2(value) {
318
+ return typeof value === "object" && value !== null;
319
+ }
320
+
321
+ // src/targets.ts
322
+ var TargetParseError = class extends Error {
323
+ constructor(message) {
324
+ super(message);
325
+ this.name = "TargetParseError";
326
+ }
327
+ };
328
+ function parseCoolclawTarget(raw) {
329
+ const normalized = normalizeCoolclawTarget(raw);
330
+ const parts = normalized.split(":");
331
+ if (parts.length !== 3) {
332
+ throw new TargetParseError(`Invalid CoolClaw target: ${raw}`);
333
+ }
334
+ const [channel, type, id] = parts;
335
+ if (channel !== "coolclaw" || id.length === 0) {
336
+ throw new TargetParseError(`Invalid CoolClaw target: ${raw}`);
337
+ }
338
+ if (type === "human") {
339
+ return { kind: "private", userType: "HUMAN", userId: id };
340
+ }
341
+ if (type === "agent") {
342
+ return { kind: "private", userType: "AGENT", userId: id };
343
+ }
344
+ if (type === "group") {
345
+ return { kind: "group", groupId: id };
346
+ }
347
+ throw new TargetParseError(`Invalid CoolClaw target type: ${type}`);
348
+ }
349
+ function normalizeCoolclawTarget(raw) {
350
+ const trimmed = raw.trim();
351
+ const parts = trimmed.split(":");
352
+ if (parts.length !== 3) {
353
+ return trimmed;
354
+ }
355
+ const [channel, type, id] = parts;
356
+ return `${channel.toLowerCase()}:${type.toLowerCase()}:${id.trim()}`;
357
+ }
358
+ function inferCoolclawTargetChatType(raw) {
359
+ try {
360
+ const target = parseCoolclawTarget(raw);
361
+ return target.kind === "private" ? "direct" : "group";
362
+ } catch {
363
+ return void 0;
364
+ }
365
+ }
366
+ function isCoolclawTargetId(raw, normalized = normalizeCoolclawTarget(raw)) {
367
+ try {
368
+ parseCoolclawTarget(normalized);
369
+ return normalized.startsWith("coolclaw:");
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+ async function resolveCoolclawMessagingTarget(raw, preferredKind) {
375
+ const normalized = normalizeCoolclawTarget(raw);
376
+ const target = parseCoolclawTarget(normalized);
377
+ const kind = target.kind === "private" ? "user" : "group";
378
+ if (preferredKind && preferredKind !== kind) {
379
+ return null;
380
+ }
381
+ const [, type, id] = normalized.split(":");
382
+ return {
383
+ to: normalized,
384
+ kind,
385
+ display: `${type}:${id}`,
386
+ source: "normalized"
387
+ };
388
+ }
389
+
390
+ // src/outbound.ts
391
+ async function sendText(input) {
392
+ const target = parseCoolclawTarget(input.target);
393
+ const frame = target.kind === "private" ? createFrame("SEND_PRIVATE", {
394
+ target: {
395
+ userId: target.userId,
396
+ userType: target.userType
397
+ },
398
+ messageType: "TEXT",
399
+ content: input.text
400
+ }) : createFrame("SEND_GROUP", {
401
+ groupId: target.groupId,
402
+ messageType: "TEXT",
403
+ content: input.text
404
+ });
405
+ const response = await input.client.request(frame);
406
+ if (response.ok === false) {
407
+ throw new Error(response.error?.message ?? "CoolClaw message send failed");
408
+ }
409
+ if (!response.messageId) {
410
+ throw new Error("CoolClaw message send response missing messageId");
411
+ }
412
+ return response.messageId;
413
+ }
414
+ async function sendMedia(input) {
415
+ const target = parseCoolclawTarget(input.target);
416
+ const fs = await import("fs/promises");
417
+ const fileBuffer = await fs.readFile(input.filePath);
418
+ const base64Content = fileBuffer.toString("base64");
419
+ const frame = target.kind === "private" ? createFrame("SEND_PRIVATE_MEDIA", {
420
+ target: { userId: target.userId, userType: target.userType },
421
+ messageType: "IMAGE",
422
+ content: base64Content,
423
+ mimeType: input.mimeType ?? "image/png"
424
+ }) : createFrame("SEND_GROUP_MEDIA", {
425
+ groupId: target.groupId,
426
+ messageType: "IMAGE",
427
+ content: base64Content,
428
+ mimeType: input.mimeType ?? "image/png"
429
+ });
430
+ const response = await input.client.request(frame);
431
+ if (response.ok === false) {
432
+ throw new Error(response.error?.message ?? "CoolClaw media send failed");
433
+ }
434
+ if (!response.messageId) {
435
+ throw new Error("CoolClaw media send response missing messageId");
436
+ }
437
+ return response.messageId;
438
+ }
439
+
440
+ // src/ws-client.ts
441
+ import WebSocket from "ws";
442
+ var CoolclawWsClient = class {
443
+ constructor(options) {
444
+ this.options = options;
445
+ }
446
+ socket;
447
+ heartbeatTimer;
448
+ reconnectTimer;
449
+ stopped = true;
450
+ reconnectAttempt = 0;
451
+ pendingRequests = /* @__PURE__ */ new Map();
452
+ async start() {
453
+ this.stopped = false;
454
+ await this.connect();
455
+ }
456
+ async stop() {
457
+ this.stopped = true;
458
+ this.clearHeartbeat();
459
+ this.clearReconnect();
460
+ this.rejectPending(new Error("CoolClaw WSS client stopped"));
461
+ const socket = this.socket;
462
+ this.socket = void 0;
463
+ if (!socket || socket.readyState === WebSocket.CLOSED) {
464
+ return;
465
+ }
466
+ await new Promise((resolve) => {
467
+ socket.once("close", () => resolve());
468
+ socket.close(1e3, "client stopped");
469
+ });
470
+ }
471
+ async request(frame) {
472
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
473
+ throw new Error("CoolClaw WSS client is not connected");
474
+ }
475
+ const requestTimeoutMs = this.options.requestTimeoutMs ?? 1e4;
476
+ return new Promise((resolve, reject) => {
477
+ const timeout = setTimeout(() => {
478
+ this.pendingRequests.delete(frame.id);
479
+ reject(new Error(`CoolClaw request timed out: ${frame.type}`));
480
+ }, requestTimeoutMs);
481
+ this.pendingRequests.set(frame.id, {
482
+ resolve: (payload) => resolve(payload),
483
+ reject,
484
+ timeout
485
+ });
486
+ this.sendFrame(frame);
487
+ });
488
+ }
489
+ sendFrame(frame) {
490
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
491
+ throw new Error("CoolClaw WSS client is not connected");
492
+ }
493
+ this.socket.send(encodeFrame(frame));
494
+ }
495
+ isConnected() {
496
+ return this.socket?.readyState === WebSocket.OPEN;
497
+ }
498
+ async connect() {
499
+ this.notifyState("connecting");
500
+ const lastAckedSeq = await this.options.ackStore.getLastAckedSeq(this.options.accountKey);
501
+ const socket = new WebSocket(buildWsUrl(this.options.gatewayUrl, lastAckedSeq), {
502
+ headers: {
503
+ Authorization: `Bearer ${this.options.token}`,
504
+ "X-CoolClaw-Agent-Id": this.options.agentId,
505
+ "X-CoolClaw-Plugin-Version": this.options.pluginVersion
506
+ }
507
+ });
508
+ this.socket = socket;
509
+ await new Promise((resolve, reject) => {
510
+ let helloReceived = false;
511
+ const failBeforeHello = (error) => {
512
+ if (!helloReceived) {
513
+ cleanupBeforeHello();
514
+ reject(error);
515
+ }
516
+ };
517
+ const cleanupBeforeHello = () => {
518
+ socket.off("error", failBeforeHello);
519
+ socket.off("message", onMessageBeforeHello);
520
+ socket.off("close", onCloseBeforeHello);
521
+ };
522
+ const onMessageBeforeHello = (data) => {
523
+ let frame;
524
+ try {
525
+ frame = decodeFrame(data.toString());
526
+ } catch (error) {
527
+ failBeforeHello(error);
528
+ return;
529
+ }
530
+ this.handleFrame(frame).catch((error) => {
531
+ this.rejectPending(error instanceof Error ? error : new Error(String(error)));
532
+ });
533
+ if (frame.type === "HELLO") {
534
+ helloReceived = true;
535
+ this.reconnectAttempt = 0;
536
+ cleanupBeforeHello();
537
+ socket.on("message", (nextData) => {
538
+ this.handleRawMessage(nextData).catch((error) => {
539
+ this.rejectPending(error instanceof Error ? error : new Error(String(error)));
540
+ });
541
+ });
542
+ socket.on("close", (code) => this.handleClose(code));
543
+ this.startHeartbeat(frame);
544
+ this.notifyState("connected");
545
+ resolve();
546
+ }
547
+ };
548
+ const onCloseBeforeHello = (code) => {
549
+ const error = new Error(`CoolClaw WSS closed before HELLO: ${code}`);
550
+ cleanupBeforeHello();
551
+ if (!this.isTerminalClose(code) && !this.stopped) {
552
+ this.scheduleReconnect();
553
+ }
554
+ reject(error);
555
+ };
556
+ socket.on("error", failBeforeHello);
557
+ socket.on("message", onMessageBeforeHello);
558
+ socket.on("close", onCloseBeforeHello);
559
+ });
560
+ }
561
+ async handleRawMessage(data) {
562
+ await this.handleFrame(decodeFrame(data.toString()));
563
+ }
564
+ async handleFrame(frame) {
565
+ if (frame.ack) {
566
+ const pending = this.pendingRequests.get(frame.ack);
567
+ if (pending) {
568
+ clearTimeout(pending.timeout);
569
+ this.pendingRequests.delete(frame.ack);
570
+ if (frame.type === "ERROR") {
571
+ pending.reject(new Error(readErrorMessage(frame.payload)));
572
+ } else {
573
+ pending.resolve(frame.payload);
574
+ }
575
+ return;
576
+ }
577
+ }
578
+ if (frame.type === "PING") {
579
+ const pong = createFrame("PONG", { clientTime: Date.now() });
580
+ pong.ack = frame.id;
581
+ this.sendFrame(pong);
582
+ return;
583
+ }
584
+ if (frame.type === "PONG" || frame.type === "HELLO" || frame.type === "RESUME_DONE") {
585
+ return;
586
+ }
587
+ await this.options.onFrame?.(frame, this);
588
+ }
589
+ startHeartbeat(helloFrame) {
590
+ this.clearHeartbeat();
591
+ const intervalMs = this.options.heartbeatIntervalMs ?? readPingInterval(helloFrame.payload) ?? 2e4;
592
+ this.heartbeatTimer = setInterval(() => {
593
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
594
+ return;
595
+ }
596
+ this.sendFrame(createFrame("PING", { clientTime: Date.now() }));
597
+ }, intervalMs);
598
+ }
599
+ handleClose(code) {
600
+ this.clearHeartbeat();
601
+ this.rejectPending(new Error(`CoolClaw WSS connection closed: ${code}`));
602
+ this.notifyState("disconnected");
603
+ if (this.stopped || this.isTerminalClose(code)) {
604
+ return;
605
+ }
606
+ this.scheduleReconnect();
607
+ }
608
+ scheduleReconnect() {
609
+ this.clearReconnect();
610
+ this.notifyState("reconnecting");
611
+ const baseDelay = this.options.reconnectDelayMs ?? 1e3;
612
+ const maxDelay = 6e4;
613
+ const attempt = this.reconnectAttempt++;
614
+ const delayMs = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
615
+ this.reconnectTimer = setTimeout(() => {
616
+ if (this.stopped) return;
617
+ this.connect().catch((error) => {
618
+ if (!this.stopped) {
619
+ this.scheduleReconnect();
620
+ }
621
+ });
622
+ }, delayMs);
623
+ }
624
+ notifyState(state) {
625
+ this.options.onStateChange?.(state);
626
+ }
627
+ clearHeartbeat() {
628
+ if (this.heartbeatTimer) {
629
+ clearInterval(this.heartbeatTimer);
630
+ this.heartbeatTimer = void 0;
631
+ }
632
+ }
633
+ clearReconnect() {
634
+ if (this.reconnectTimer) {
635
+ clearTimeout(this.reconnectTimer);
636
+ this.reconnectTimer = void 0;
637
+ }
638
+ }
639
+ rejectPending(error) {
640
+ for (const pending of this.pendingRequests.values()) {
641
+ clearTimeout(pending.timeout);
642
+ pending.reject(error);
643
+ }
644
+ this.pendingRequests.clear();
645
+ }
646
+ isTerminalClose(code) {
647
+ return code === 4001 || code === 4002 || code === 4003 || code === 4004 || code === 4005;
648
+ }
649
+ };
650
+ function readPingInterval(payload) {
651
+ if (!isRecord3(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
652
+ return void 0;
653
+ }
654
+ return payload.pingIntervalMs;
655
+ }
656
+ function readErrorMessage(payload) {
657
+ if (isRecord3(payload) && typeof payload.message === "string") {
658
+ return payload.message;
659
+ }
660
+ return "CoolClaw request failed";
661
+ }
662
+ function isRecord3(value) {
663
+ return typeof value === "object" && value !== null;
664
+ }
665
+
666
+ // src/version.ts
667
+ import { readFileSync as readFileSync2 } from "fs";
668
+ import { join as join2, dirname } from "path";
669
+ import { fileURLToPath } from "url";
670
+ var _version;
671
+ function getPluginVersion() {
672
+ if (_version) return _version;
673
+ try {
674
+ const pkgPath = join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
675
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
676
+ _version = pkg.version ?? "0.0.0";
677
+ } catch {
678
+ _version = "0.0.0";
679
+ }
680
+ return _version;
681
+ }
682
+
683
+ // src/channel.ts
684
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
685
+ import { createAccountStatusSink, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-lifecycle";
686
+ import { logInboundDrop, logAckFailure } from "openclaw/plugin-sdk/channel-logging";
687
+ var runtimeClients = /* @__PURE__ */ new Map();
688
+ function setRuntimeClient(accountKey, client) {
689
+ runtimeClients.set(accountKey, client);
690
+ }
691
+ function getRuntimeClient(accountKey) {
692
+ return runtimeClients.get(accountKey);
693
+ }
694
+ function clearRuntimeClient(accountKey) {
695
+ runtimeClients.delete(accountKey);
696
+ }
697
+ function extractAccountFromConfig(cfg, accountId) {
698
+ const coolclawSection = cfg.channels?.coolclaw;
699
+ const accounts = coolclawSection?.accounts;
700
+ return accounts?.[accountId ?? "default"] ?? {};
701
+ }
702
+ var coolclawChannelPlugin = createChatChannelPlugin({
703
+ base: {
704
+ id: "coolclaw",
705
+ meta: {
706
+ id: "coolclaw",
707
+ label: "CoolClaw",
708
+ selectionLabel: "CoolClaw",
709
+ docsPath: "/plugins/coolclaw",
710
+ blurb: "Connect OpenClaw to the CoolClaw/Riddle chat platform."
711
+ },
712
+ capabilities: {
713
+ chatTypes: ["direct", "group"],
714
+ media: true,
715
+ blockStreaming: true
716
+ },
717
+ streaming: {
718
+ blockStreamingCoalesceDefaults: {
719
+ minChars: 200,
720
+ idleMs: 3e3
721
+ }
722
+ },
723
+ agentPrompt: {
724
+ messageToolHints: () => [
725
+ "To send a message on CoolClaw/Riddle, use the message tool with action='send' and set 'to' to a CoolClaw target like 'coolclaw:human:<userId>', 'coolclaw:agent:<agentId>', or 'coolclaw:group:<groupId>'.",
726
+ "To send an image or file, use the message tool with action='send' and set 'media' to a local file path or a remote URL.",
727
+ "When sending a message to a CoolClaw group, the agent will only reply if it was mentioned in the group message.",
728
+ "When creating a cron job for CoolClaw, set delivery.to to the target CoolClaw ID and delivery.accountId to the current accountId."
729
+ ]
730
+ },
731
+ config: {
732
+ listAccountIds(cfg) {
733
+ return Object.keys(cfg.channels?.coolclaw?.accounts ?? {});
734
+ },
735
+ resolveAccount(cfg, accountId) {
736
+ return extractAccountFromConfig(cfg, accountId);
737
+ },
738
+ defaultAccountId() {
739
+ return "default";
740
+ },
741
+ isConfigured(account) {
742
+ return Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account));
743
+ },
744
+ isEnabled(account) {
745
+ return account?.enabled !== false;
746
+ },
747
+ describeAccount(account) {
748
+ return {
749
+ accountId: "default",
750
+ name: account.name,
751
+ enabled: account.enabled !== false,
752
+ configured: Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account)),
753
+ gatewayUrl: account.gatewayUrl,
754
+ agentId: account.agentId,
755
+ tokenConfigured: Boolean(account.tokenSecretRef || "tokenSecret" in account),
756
+ allowFromCount: account.allowFrom?.length ?? 0,
757
+ dmPolicy: account.dmPolicy ?? "allowlist"
758
+ };
759
+ }
760
+ },
761
+ resolver: {
762
+ async resolveTargets({ inputs }) {
763
+ return inputs.map((input) => {
764
+ try {
765
+ const normalized = normalizeCoolclawTarget(input);
766
+ const [, type, id] = normalized.split(":");
767
+ parseCoolclawTarget(normalized);
768
+ return { input, resolved: true, id: normalized, name: `${type}:${id}` };
769
+ } catch (error) {
770
+ return { input, resolved: false, note: error instanceof Error ? error.message : String(error) };
771
+ }
772
+ });
773
+ }
774
+ },
775
+ messaging: {
776
+ normalizeTarget(raw) {
777
+ try {
778
+ const normalized = normalizeCoolclawTarget(raw);
779
+ parseCoolclawTarget(normalized);
780
+ return normalized;
781
+ } catch {
782
+ return void 0;
783
+ }
784
+ },
785
+ inferTargetChatType({ to }) {
786
+ return inferCoolclawTargetChatType(to);
787
+ },
788
+ targetResolver: {
789
+ hint: "Use coolclaw:human:<id>, coolclaw:agent:<id>, or coolclaw:group:<id>.",
790
+ looksLikeId(raw, normalized) {
791
+ return isCoolclawTargetId(raw, normalized);
792
+ },
793
+ resolveTarget({ input, normalized, preferredKind }) {
794
+ return resolveCoolclawMessagingTarget(normalized || input, preferredKind);
795
+ }
796
+ }
797
+ },
798
+ gateway: {
799
+ async startAccount(ctx) {
800
+ const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
801
+ const token = await resolveAccountToken(account);
802
+ if (!account.gatewayUrl || !account.agentId || !token) {
803
+ ctx.log?.error(`[${ctx.accountId}] CoolClaw account is not fully configured`);
804
+ return;
805
+ }
806
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
807
+ const ackStore = new FileAckStore();
808
+ const statusSink = createAccountStatusSink({
809
+ accountId: ctx.accountId,
810
+ setStatus: ctx.setStatus
811
+ });
812
+ statusSink({ statusState: "connecting" });
813
+ ctx.log?.info(`[${ctx.accountId}] starting CoolClaw provider (${account.gatewayUrl})`);
814
+ await runPassiveAccountLifecycle({
815
+ abortSignal: ctx.abortSignal,
816
+ start: async () => {
817
+ const client = new CoolclawWsClient({
818
+ gatewayUrl: account.gatewayUrl,
819
+ agentId: account.agentId,
820
+ token,
821
+ pluginVersion: getPluginVersion(),
822
+ ackStore,
823
+ accountKey,
824
+ onStateChange: (state) => {
825
+ statusSink({ statusState: state });
826
+ },
827
+ onFrame: async (frame, wsClient) => {
828
+ try {
829
+ await handleInboundFrame({
830
+ frame,
831
+ accountKey,
832
+ ackStore,
833
+ dispatch: async (envelope) => {
834
+ const runtime = getCoolclawRuntime();
835
+ if (!runtime?.channel) {
836
+ logInboundDrop({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
837
+ }), channel: "coolclaw", reason: "runtime not available; skipping dispatch" });
838
+ return;
839
+ }
840
+ try {
841
+ const isGroup = envelope.conversationId.startsWith("group:");
842
+ const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
843
+ if (!runtime.channel.routing?.resolveAgentRoute) {
844
+ throw new Error(
845
+ "CoolClaw requires runtime.channel.routing.resolveAgentRoute. Please upgrade OpenClaw to >=2026.3.22."
846
+ );
847
+ }
848
+ const route = await runtime.channel.routing.resolveAgentRoute({
849
+ cfg: ctx.cfg,
850
+ channel: "coolclaw",
851
+ accountId: ctx.accountId,
852
+ peer
853
+ });
854
+ if (!runtime.channel.session?.resolveStorePath) {
855
+ throw new Error(
856
+ "CoolClaw requires runtime.channel.session.resolveStorePath. Please upgrade OpenClaw to >=2026.3.22."
857
+ );
858
+ }
859
+ const storePath = runtime.channel.session.resolveStorePath(
860
+ ctx.cfg.session?.store,
861
+ { agentId: route.agentId }
862
+ );
863
+ const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : "unknown";
864
+ let deliveryTarget;
865
+ if (envelope.group) {
866
+ deliveryTarget = `coolclaw:group:${envelope.group.groupId}`;
867
+ } else if (envelope.sender) {
868
+ deliveryTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
869
+ } else {
870
+ deliveryTarget = normalizeCoolclawTarget(envelope.conversationId);
871
+ }
872
+ const agentHint = envelope.metadata?.agentHint;
873
+ const bodyForAgent = agentHint ? envelope.text + agentHint : envelope.text;
874
+ const ctxPayload = runtime.channel.reply.finalizeInboundContext({
875
+ Body: envelope.text,
876
+ BodyForAgent: bodyForAgent,
877
+ RawBody: envelope.text,
878
+ CommandBody: envelope.text,
879
+ From: `coolclaw:${senderLabel}`,
880
+ To: deliveryTarget,
881
+ SessionKey: route.sessionKey,
882
+ AccountId: ctx.accountId,
883
+ ChatType: isGroup ? "channel" : "direct",
884
+ CommandAuthorized: true,
885
+ Provider: "coolclaw",
886
+ Surface: "coolclaw",
887
+ Channel: "coolclaw",
888
+ Peer: peer,
889
+ Mentioned: envelope.shouldReply
890
+ });
891
+ const sessionKey = ctxPayload.SessionKey ?? route.sessionKey;
892
+ const mainSessionKey = route.mainSessionKey;
893
+ await runtime.channel.session.recordInboundSession({
894
+ storePath,
895
+ sessionKey,
896
+ ctx: ctxPayload,
897
+ updateLastRoute: mainSessionKey && mainSessionKey !== sessionKey ? { sessionKey: mainSessionKey, channel: "coolclaw", to: deliveryTarget, accountId: ctx.accountId ?? void 0 } : void 0,
898
+ onRecordError: (err) => {
899
+ ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
900
+ }
901
+ });
902
+ await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
903
+ ctx: ctxPayload,
904
+ cfg: ctx.cfg,
905
+ dispatcherOptions: {
906
+ deliver: async (payload) => {
907
+ if (!payload.text) return;
908
+ try {
909
+ let replyTarget;
910
+ if (envelope.group) {
911
+ replyTarget = `coolclaw:group:${envelope.group.groupId}`;
912
+ } else if (envelope.sender) {
913
+ replyTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
914
+ } else {
915
+ replyTarget = normalizeCoolclawTarget(envelope.conversationId);
916
+ }
917
+ await sendText({ client: wsClient, target: replyTarget, text: payload.text });
918
+ } catch (err) {
919
+ ctx.log?.error(`Failed to deliver reply: ${err instanceof Error ? err.message : String(err)}`);
920
+ }
921
+ },
922
+ onError: (err, info) => {
923
+ ctx.log?.error(`Reply dispatch error: ${err instanceof Error ? err.message : String(err)}${info ? ` ${JSON.stringify(info)}` : ""}`);
924
+ }
925
+ }
926
+ });
927
+ } catch (err) {
928
+ ctx.log?.error(`Inbound dispatch error: ${err instanceof Error ? err.message : String(err)}`);
929
+ }
930
+ },
931
+ sendAck: async (ackFrame) => {
932
+ try {
933
+ wsClient.sendFrame(ackFrame);
934
+ ctx.log?.debug?.(`ACK sent: type=${ackFrame.type} lastAckedSeq=${ackFrame.payload?.lastAckedSeq}`);
935
+ } catch (err) {
936
+ logAckFailure({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
937
+ }), channel: "coolclaw", error: err });
938
+ }
939
+ }
940
+ });
941
+ } catch (err) {
942
+ ctx.log?.error(`Frame handling error: ${err instanceof Error ? err.message : String(err)}`);
943
+ }
944
+ }
945
+ });
946
+ await client.start();
947
+ setRuntimeClient(accountKey, client);
948
+ statusSink({ statusState: "connected" });
949
+ ctx.log?.info(`[${ctx.accountId}] CoolClaw provider connected`);
950
+ return client;
951
+ },
952
+ stop: async (client) => {
953
+ await client.stop();
954
+ clearRuntimeClient(accountKey);
955
+ },
956
+ onStop: () => {
957
+ statusSink({ statusState: "disconnected" });
958
+ ctx.log?.info(`[${ctx.accountId}] CoolClaw provider stopped`);
959
+ }
960
+ });
961
+ },
962
+ /** 显式停止账户连接,清理 WebSocket 客户端资源 */
963
+ async stopAccount(ctx) {
964
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
965
+ const client = getRuntimeClient(accountKey);
966
+ if (client) {
967
+ await client.stop();
968
+ clearRuntimeClient(accountKey);
969
+ ctx.log?.info(`[${ctx.accountId}] CoolClaw client stopped via stopAccount`);
970
+ }
971
+ }
972
+ },
973
+ /** auth 适配器 — 支持 openclaw channels login --channel coolclaw 原生命令 */
974
+ auth: {
975
+ async login({ cfg, accountId, verbose }) {
976
+ const resolvedAccountId = accountId?.trim() || "default";
977
+ const account = coolclawChannelPlugin.config.resolveAccount(cfg, resolvedAccountId);
978
+ const { validateAgentToken, runCoolclawSetup } = await import("./setup-T2A3RRG7.js");
979
+ const existingToken = account.tokenSecretRef ? await resolveAccountToken(account) : void 0;
980
+ if (existingToken) {
981
+ const gatewayUrl = account.gatewayUrl ?? "https://agits-xa.baidu.com/riddle";
982
+ const valid = await validateAgentToken(gatewayUrl, existingToken);
983
+ if (valid) {
984
+ if (verbose) console.log("[coolclaw] Already authenticated.");
985
+ return;
986
+ }
987
+ }
988
+ if (verbose) console.log("[coolclaw] Running setup...");
989
+ const result = await runCoolclawSetup({
990
+ gatewayUrl: account.gatewayUrl,
991
+ accountId: resolvedAccountId,
992
+ autoRestart: false
993
+ // login 场景不需要重启网关
994
+ });
995
+ if (result.mode === "register") {
996
+ if (verbose) console.log(`[coolclaw] Agent registered: ${result.agentId}`);
997
+ } else {
998
+ if (verbose) console.log(`[coolclaw] Reusing existing agent: ${result.agentId}`);
999
+ }
1000
+ if (verbose) console.log("[coolclaw] Authentication complete.");
1001
+ }
1002
+ }
1003
+ },
1004
+ security: {
1005
+ dm: {
1006
+ channelKey: "coolclaw",
1007
+ resolvePolicy: (account) => account.dmPolicy ?? "allowlist",
1008
+ resolveAllowFrom: (account) => account.allowFrom ?? [],
1009
+ defaultPolicy: "allowlist",
1010
+ normalizeEntry: (raw) => raw.trim().toLowerCase()
1011
+ }
1012
+ },
1013
+ pairing: {
1014
+ text: {
1015
+ idLabel: "CoolClaw user ID",
1016
+ message: "You are not authorized to message this agent. Send this pairing code to verify:",
1017
+ notify: async ({ id, message }) => {
1018
+ const client = getRuntimeClient("coolclaw:default");
1019
+ if (client) {
1020
+ await sendText({
1021
+ client,
1022
+ target: id,
1023
+ text: message
1024
+ });
1025
+ }
1026
+ }
1027
+ }
1028
+ },
1029
+ threading: {
1030
+ topLevelReplyToMode: "reply"
1031
+ },
1032
+ outbound: {
1033
+ base: {
1034
+ deliveryMode: "direct",
1035
+ resolveTarget({ to }) {
1036
+ if (!to) {
1037
+ return { ok: false, error: new Error("CoolClaw target is required") };
1038
+ }
1039
+ try {
1040
+ const normalized = normalizeCoolclawTarget(to);
1041
+ parseCoolclawTarget(normalized);
1042
+ return { ok: true, to: normalized };
1043
+ } catch (error) {
1044
+ return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
1045
+ }
1046
+ }
1047
+ },
1048
+ attachedResults: {
1049
+ channel: "coolclaw",
1050
+ async sendText(ctx) {
1051
+ const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
1052
+ const token = await resolveAccountToken(account);
1053
+ if (!account.gatewayUrl || !account.agentId || !token) {
1054
+ throw new Error("CoolClaw account is not fully configured");
1055
+ }
1056
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
1057
+ let client = getRuntimeClient(accountKey);
1058
+ if (!client || !client.isConnected()) {
1059
+ client = new CoolclawWsClient({
1060
+ gatewayUrl: account.gatewayUrl,
1061
+ agentId: account.agentId,
1062
+ token,
1063
+ pluginVersion: getPluginVersion(),
1064
+ ackStore: new InMemoryAckStore(),
1065
+ accountKey
1066
+ });
1067
+ await client.start();
1068
+ try {
1069
+ const messageId2 = await sendText({ client, target: ctx.to, text: ctx.text });
1070
+ return { messageId: messageId2, conversationId: ctx.to, timestamp: Date.now() };
1071
+ } finally {
1072
+ await client.stop();
1073
+ }
1074
+ }
1075
+ const messageId = await sendText({ client, target: ctx.to, text: ctx.text });
1076
+ return { messageId, conversationId: ctx.to, timestamp: Date.now() };
1077
+ },
1078
+ async sendMedia(ctx) {
1079
+ const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
1080
+ const token = await resolveAccountToken(account);
1081
+ if (!account.gatewayUrl || !account.agentId || !token) {
1082
+ throw new Error("CoolClaw account is not fully configured");
1083
+ }
1084
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
1085
+ let client = getRuntimeClient(accountKey);
1086
+ if (!client || !client.isConnected()) {
1087
+ client = new CoolclawWsClient({
1088
+ gatewayUrl: account.gatewayUrl,
1089
+ agentId: account.agentId,
1090
+ token,
1091
+ pluginVersion: getPluginVersion(),
1092
+ ackStore: new InMemoryAckStore(),
1093
+ accountKey
1094
+ });
1095
+ await client.start();
1096
+ try {
1097
+ const messageId2 = await sendMedia({ client, target: ctx.to, filePath: ctx.mediaUrl ?? "" });
1098
+ return { messageId: messageId2, conversationId: ctx.to, timestamp: Date.now() };
1099
+ } finally {
1100
+ await client.stop();
1101
+ }
1102
+ }
1103
+ const messageId = await sendMedia({ client, target: ctx.to, filePath: ctx.mediaUrl ?? "" });
1104
+ return { messageId, conversationId: ctx.to, timestamp: Date.now() };
1105
+ }
1106
+ }
1107
+ }
1108
+ });
1109
+
1110
+ export {
1111
+ setCoolclawRuntime,
1112
+ coolclawChannelPlugin
1113
+ };