@dhfpub/clawpool 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2654 @@
1
+ // index.ts
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+
4
+ // src/channel.ts
5
+ import {
6
+ applyAccountNameToChannelSection as applyAccountNameToChannelSection2,
7
+ chunkTextForOutbound,
8
+ DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID3,
9
+ deleteAccountFromConfigSection,
10
+ formatPairingApproveHint,
11
+ normalizeAccountId as normalizeAccountId3,
12
+ setAccountEnabledInConfigSection,
13
+ waitUntilAbort
14
+ } from "openclaw/plugin-sdk";
15
+
16
+ // src/actions.ts
17
+ import {
18
+ jsonResult,
19
+ readStringParam
20
+ } from "openclaw/plugin-sdk";
21
+
22
+ // src/accounts.ts
23
+ import {
24
+ DEFAULT_ACCOUNT_ID,
25
+ normalizeAccountId,
26
+ normalizeOptionalAccountId
27
+ } from "openclaw/plugin-sdk/account-id";
28
+ function rawAibotConfig(cfg) {
29
+ return cfg.channels?.clawpool ?? {};
30
+ }
31
+ function listConfiguredAccountIds(cfg) {
32
+ const accounts = rawAibotConfig(cfg).accounts;
33
+ if (!accounts || typeof accounts !== "object") {
34
+ return [];
35
+ }
36
+ return Object.keys(accounts).filter(Boolean);
37
+ }
38
+ function listAibotAccountIds(cfg) {
39
+ const ids = listConfiguredAccountIds(cfg);
40
+ if (ids.length === 0) {
41
+ return [DEFAULT_ACCOUNT_ID];
42
+ }
43
+ return ids.toSorted((a, b) => a.localeCompare(b));
44
+ }
45
+ function resolveDefaultAibotAccountId(cfg) {
46
+ const aibotCfg = rawAibotConfig(cfg);
47
+ const preferred = normalizeOptionalAccountId(aibotCfg.defaultAccount);
48
+ if (preferred && listAibotAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)) {
49
+ return preferred;
50
+ }
51
+ const ids = listAibotAccountIds(cfg);
52
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
53
+ return DEFAULT_ACCOUNT_ID;
54
+ }
55
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
56
+ }
57
+ function resolveAccountRawConfig(cfg, accountId) {
58
+ const aibotCfg = rawAibotConfig(cfg);
59
+ const { accounts: _ignoredAccounts, defaultAccount: _ignoredDefault, ...base } = aibotCfg;
60
+ const account = aibotCfg.accounts?.[accountId] ?? {};
61
+ return {
62
+ ...base,
63
+ ...account
64
+ };
65
+ }
66
+ function normalizeNonEmpty(value) {
67
+ const s = String(value ?? "").trim();
68
+ return s;
69
+ }
70
+ function normalizeAgentId(value) {
71
+ return normalizeNonEmpty(value);
72
+ }
73
+ function appendAgentIdToWsUrl(rawWsUrl, agentId) {
74
+ if (!rawWsUrl) {
75
+ return "";
76
+ }
77
+ const direct = rawWsUrl.replaceAll("{agent_id}", encodeURIComponent(agentId));
78
+ if (!agentId) {
79
+ return direct;
80
+ }
81
+ try {
82
+ const parsed = new URL(direct);
83
+ if (!parsed.searchParams.get("agent_id")) {
84
+ parsed.searchParams.set("agent_id", agentId);
85
+ }
86
+ return parsed.toString();
87
+ } catch {
88
+ if (direct.includes("agent_id=")) {
89
+ return direct;
90
+ }
91
+ return direct.includes("?") ? `${direct}&agent_id=${encodeURIComponent(agentId)}` : `${direct}?agent_id=${encodeURIComponent(agentId)}`;
92
+ }
93
+ }
94
+ function resolveWsUrl(merged, agentId) {
95
+ const envWs = normalizeNonEmpty(process.env.CLAWPOOL_WS_URL);
96
+ const cfgWs = normalizeNonEmpty(merged.wsUrl);
97
+ const ws = cfgWs || envWs;
98
+ if (ws) {
99
+ return appendAgentIdToWsUrl(ws, agentId);
100
+ }
101
+ if (!agentId) {
102
+ return "";
103
+ }
104
+ return `ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=${encodeURIComponent(agentId)}`;
105
+ }
106
+ function redactAibotWsUrl(wsUrl) {
107
+ if (!wsUrl) {
108
+ return "";
109
+ }
110
+ try {
111
+ const parsed = new URL(wsUrl);
112
+ if (parsed.searchParams.has("agent_id")) {
113
+ parsed.searchParams.set("agent_id", "***");
114
+ }
115
+ return parsed.toString();
116
+ } catch {
117
+ return wsUrl.replace(/(agent_id=)[^&]+/g, "$1***");
118
+ }
119
+ }
120
+ function resolveAibotAccount(params) {
121
+ const accountId = normalizeAccountId(params.accountId);
122
+ const merged = resolveAccountRawConfig(params.cfg, accountId);
123
+ const baseEnabled = rawAibotConfig(params.cfg).enabled !== false;
124
+ const accountEnabled = merged.enabled !== false;
125
+ const enabled = baseEnabled && accountEnabled;
126
+ const agentId = normalizeAgentId(merged.agentId || process.env.CLAWPOOL_AGENT_ID);
127
+ const apiKey = normalizeNonEmpty(merged.apiKey || process.env.CLAWPOOL_API_KEY);
128
+ const wsUrl = resolveWsUrl(merged, agentId);
129
+ const configured = Boolean(wsUrl && agentId && apiKey);
130
+ return {
131
+ accountId,
132
+ name: normalizeNonEmpty(merged.name) || void 0,
133
+ enabled,
134
+ configured,
135
+ wsUrl,
136
+ agentId,
137
+ apiKey,
138
+ config: merged
139
+ };
140
+ }
141
+ function normalizeAibotSessionTarget(raw) {
142
+ const trimmed = String(raw ?? "").trim();
143
+ if (!trimmed) {
144
+ return "";
145
+ }
146
+ return trimmed.replace(/^clawpool:/i, "").replace(/^session:/i, "").trim();
147
+ }
148
+
149
+ // src/client.ts
150
+ import { randomUUID } from "node:crypto";
151
+ var DEFAULT_RECONNECT_BASE_MS = 2e3;
152
+ var DEFAULT_RECONNECT_MAX_MS = 3e4;
153
+ var DEFAULT_RECONNECT_STABLE_MS = 3e4;
154
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
155
+ var DEFAULT_HEARTBEAT_SEC = 30;
156
+ function clampInt(value, fallback, min, max) {
157
+ const n = Number(value);
158
+ if (!Number.isFinite(n)) {
159
+ return fallback;
160
+ }
161
+ return Math.max(min, Math.min(max, Math.floor(n)));
162
+ }
163
+ function buildFastRetryDelays(baseDelayMs) {
164
+ const first = Math.max(100, Math.min(300, Math.floor(baseDelayMs / 4)));
165
+ const second = Math.max(first, Math.min(1e3, Math.floor(baseDelayMs / 2)));
166
+ return [first, second];
167
+ }
168
+ function randomIntInclusive(min, max) {
169
+ const boundedMin = Math.floor(min);
170
+ const boundedMax = Math.floor(max);
171
+ if (boundedMax <= boundedMin) {
172
+ return boundedMin;
173
+ }
174
+ return boundedMin + Math.floor(Math.random() * (boundedMax - boundedMin + 1));
175
+ }
176
+ function normalizeCloseReason(value) {
177
+ const reason = String(value ?? "").replace(/\s+/g, " ").trim();
178
+ if (!reason) {
179
+ return void 0;
180
+ }
181
+ return reason.slice(0, 160);
182
+ }
183
+ function redactWsUrlForLog(wsUrl) {
184
+ if (!wsUrl) {
185
+ return "";
186
+ }
187
+ try {
188
+ const parsed = new URL(wsUrl);
189
+ if (parsed.searchParams.has("agent_id")) {
190
+ parsed.searchParams.set("agent_id", "***");
191
+ }
192
+ return parsed.toString();
193
+ } catch {
194
+ return wsUrl.replace(/(agent_id=)[^&]+/g, "$1***");
195
+ }
196
+ }
197
+ function parseHeartbeatSec(payload) {
198
+ return clampInt(payload.heartbeat_sec, DEFAULT_HEARTBEAT_SEC, 5, 300);
199
+ }
200
+ async function sleepWithAbort(ms, abortSignal) {
201
+ if (ms <= 0 || abortSignal.aborted) {
202
+ return;
203
+ }
204
+ await new Promise((resolve) => {
205
+ let settled = false;
206
+ let timer = null;
207
+ function finish() {
208
+ if (settled) {
209
+ return;
210
+ }
211
+ settled = true;
212
+ if (timer) {
213
+ clearTimeout(timer);
214
+ }
215
+ abortSignal.removeEventListener("abort", onAbort);
216
+ resolve();
217
+ }
218
+ function onAbort() {
219
+ finish();
220
+ }
221
+ timer = setTimeout(finish, ms);
222
+ abortSignal.addEventListener("abort", onAbort, { once: true });
223
+ });
224
+ }
225
+ function resolveReconnectPolicy(account) {
226
+ const baseDelayMs = clampInt(account.config.reconnectMs, DEFAULT_RECONNECT_BASE_MS, 100, 6e4);
227
+ const fallbackMaxMs = Math.max(DEFAULT_RECONNECT_MAX_MS, baseDelayMs * 8);
228
+ const maxDelayMs = clampInt(account.config.reconnectMaxMs, fallbackMaxMs, baseDelayMs, 3e5);
229
+ const stableConnectionMs = clampInt(
230
+ account.config.reconnectStableMs,
231
+ DEFAULT_RECONNECT_STABLE_MS,
232
+ 1e3,
233
+ 6e5
234
+ );
235
+ const connectTimeoutMs = clampInt(
236
+ account.config.connectTimeoutMs,
237
+ DEFAULT_CONNECT_TIMEOUT_MS,
238
+ 1e3,
239
+ 6e4
240
+ );
241
+ const fastRetryDelaysMs = buildFastRetryDelays(baseDelayMs);
242
+ return {
243
+ baseDelayMs,
244
+ maxDelayMs,
245
+ stableConnectionMs,
246
+ fastRetryDelaysMs,
247
+ authPenaltyAttemptFloor: fastRetryDelaysMs.length + 4,
248
+ connectTimeoutMs
249
+ };
250
+ }
251
+ var AuthRejectedError = class extends Error {
252
+ code;
253
+ constructor(code, message) {
254
+ super(`clawpool auth failed: code=${code}, msg=${message}`);
255
+ this.name = "AuthRejectedError";
256
+ this.code = code;
257
+ }
258
+ };
259
+ function parseCode(payload) {
260
+ const n = Number(payload.code ?? 0);
261
+ if (Number.isFinite(n)) {
262
+ return n;
263
+ }
264
+ return 0;
265
+ }
266
+ function parseMessage(payload) {
267
+ const s = String(payload.msg ?? "").trim();
268
+ return s || "unknown error";
269
+ }
270
+ function parseKickedReason(payload) {
271
+ const reason = String(payload.reason ?? payload.msg ?? "").trim();
272
+ return reason || "unknown";
273
+ }
274
+ async function wsDataToText(data) {
275
+ if (typeof data === "string") {
276
+ return data;
277
+ }
278
+ if (data instanceof ArrayBuffer) {
279
+ return Buffer.from(data).toString("utf8");
280
+ }
281
+ if (ArrayBuffer.isView(data)) {
282
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
283
+ }
284
+ if (data && typeof data.text === "function") {
285
+ return data.text();
286
+ }
287
+ return String(data ?? "");
288
+ }
289
+ var AibotWsClient = class {
290
+ account;
291
+ callbacks;
292
+ reconnectPolicy;
293
+ ws = null;
294
+ running = false;
295
+ seq = Date.now();
296
+ loopPromise = null;
297
+ pending = /* @__PURE__ */ new Map();
298
+ pendingStreamHighSurrogate = /* @__PURE__ */ new Map();
299
+ reconnectPenaltyAttemptFloor = 0;
300
+ connectionSerial = 0;
301
+ keepaliveTimer = null;
302
+ keepaliveInFlight = false;
303
+ lastConnectionError = "";
304
+ lastConnectionErrorLogAt = 0;
305
+ suppressedConnectionErrors = 0;
306
+ lastReconnectLogAt = 0;
307
+ suppressedReconnectLogs = 0;
308
+ status = {
309
+ running: false,
310
+ connected: false,
311
+ authed: false,
312
+ lastError: null,
313
+ lastConnectAt: null,
314
+ lastDisconnectAt: null
315
+ };
316
+ constructor(account, callbacks = {}) {
317
+ this.account = account;
318
+ this.callbacks = callbacks;
319
+ this.reconnectPolicy = resolveReconnectPolicy(account);
320
+ }
321
+ logInfo(message) {
322
+ this.callbacks.logger?.info?.(`[clawpool] [${this.account.accountId}] ${message}`);
323
+ }
324
+ logWarn(message) {
325
+ this.callbacks.logger?.warn?.(`[clawpool] [${this.account.accountId}] ${message}`);
326
+ }
327
+ logError(message) {
328
+ this.callbacks.logger?.error?.(`[clawpool] [${this.account.accountId}] ${message}`);
329
+ }
330
+ logConnectionError(message) {
331
+ const now = Date.now();
332
+ const sameAsLast = this.lastConnectionError === message;
333
+ const shouldLog = !sameAsLast || now - this.lastConnectionErrorLogAt >= 3e4 || this.suppressedConnectionErrors >= 10;
334
+ if (!shouldLog) {
335
+ this.suppressedConnectionErrors += 1;
336
+ return;
337
+ }
338
+ const repeats = this.suppressedConnectionErrors;
339
+ this.lastConnectionError = message;
340
+ this.lastConnectionErrorLogAt = now;
341
+ this.suppressedConnectionErrors = 0;
342
+ if (repeats > 0) {
343
+ this.logWarn(`connection error: ${message} (suppressed=${repeats})`);
344
+ return;
345
+ }
346
+ this.logWarn(`connection error: ${message}`);
347
+ }
348
+ logReconnectPlan(params) {
349
+ const now = Date.now();
350
+ const important = params.attempt <= 3 || params.authRejected || params.penaltyFloor > 0 || params.stable || params.attempt % 10 === 0;
351
+ const shouldLog = important || now - this.lastReconnectLogAt >= 3e4;
352
+ if (!shouldLog) {
353
+ this.suppressedReconnectLogs += 1;
354
+ return;
355
+ }
356
+ const suppressed = this.suppressedReconnectLogs;
357
+ this.suppressedReconnectLogs = 0;
358
+ this.lastReconnectLogAt = now;
359
+ this.logInfo(
360
+ `reconnect scheduled in ${params.delayMs}ms attempt=${params.attempt} stable=${params.stable} authRejected=${params.authRejected} penaltyFloor=${params.penaltyFloor} suppressed=${suppressed}`
361
+ );
362
+ }
363
+ getStatus() {
364
+ return { ...this.status };
365
+ }
366
+ async start(abortSignal) {
367
+ if (this.running) {
368
+ return;
369
+ }
370
+ this.running = true;
371
+ this.updateStatus({ running: true, lastError: null });
372
+ this.logInfo(
373
+ `client start ws=${redactWsUrlForLog(this.account.wsUrl)} reconnectBaseMs=${this.reconnectPolicy.baseDelayMs} reconnectMaxMs=${this.reconnectPolicy.maxDelayMs} reconnectStableMs=${this.reconnectPolicy.stableConnectionMs} connectTimeoutMs=${this.reconnectPolicy.connectTimeoutMs}`
374
+ );
375
+ this.loopPromise = this.runLoop(abortSignal);
376
+ void this.loopPromise.catch((err) => {
377
+ const msg = err instanceof Error ? err.message : String(err);
378
+ this.updateStatus({
379
+ running: false,
380
+ connected: false,
381
+ authed: false,
382
+ lastError: msg,
383
+ lastDisconnectAt: Date.now()
384
+ });
385
+ this.logError(`run loop crashed: ${msg}`);
386
+ });
387
+ }
388
+ stop() {
389
+ this.running = false;
390
+ this.stopKeepalive();
391
+ this.rejectAllPending(new Error("clawpool client stopped"));
392
+ this.safeCloseWs("client_stopped");
393
+ this.updateStatus({
394
+ running: false,
395
+ connected: false,
396
+ authed: false,
397
+ lastDisconnectAt: Date.now()
398
+ });
399
+ }
400
+ async waitUntilStopped() {
401
+ await this.loopPromise;
402
+ }
403
+ async sendText(sessionId, text, opts = {}) {
404
+ this.ensureReady();
405
+ const payload = {
406
+ session_id: sessionId,
407
+ client_msg_id: opts.clientMsgId || `clawpool_${randomUUID()}`,
408
+ msg_type: 1,
409
+ content: text
410
+ };
411
+ if (opts.quotedMessageId) {
412
+ payload.quoted_message_id = opts.quotedMessageId;
413
+ }
414
+ if (opts.extra && Object.keys(opts.extra).length > 0) {
415
+ payload.extra = opts.extra;
416
+ }
417
+ const packet = await this.request("send_msg", payload, {
418
+ expected: ["send_ack", "send_nack", "error"],
419
+ timeoutMs: opts.timeoutMs ?? 2e4
420
+ });
421
+ if (packet.cmd !== "send_ack") {
422
+ throw this.packetError(packet);
423
+ }
424
+ return packet.payload;
425
+ }
426
+ async sendMedia(sessionId, mediaUrl, caption = "", opts = {}) {
427
+ this.ensureReady();
428
+ const payload = {
429
+ session_id: sessionId,
430
+ client_msg_id: opts.clientMsgId || `clawpool_${randomUUID()}`,
431
+ msg_type: opts.msgType ?? 2,
432
+ content: caption || "[media]",
433
+ media_url: mediaUrl
434
+ };
435
+ if (opts.quotedMessageId) {
436
+ payload.quoted_message_id = opts.quotedMessageId;
437
+ }
438
+ if (opts.extra && Object.keys(opts.extra).length > 0) {
439
+ payload.extra = opts.extra;
440
+ }
441
+ const packet = await this.request("send_msg", payload, {
442
+ expected: ["send_ack", "send_nack", "error"],
443
+ timeoutMs: opts.timeoutMs ?? 3e4
444
+ });
445
+ if (packet.cmd !== "send_ack") {
446
+ throw this.packetError(packet);
447
+ }
448
+ return packet.payload;
449
+ }
450
+ async bindSessionRoute(channel2, accountId, routeSessionKey, sessionId, opts = {}) {
451
+ this.ensureReady();
452
+ const normalizedChannel = String(channel2 ?? "").trim().toLowerCase();
453
+ const normalizedAccountID = String(accountId ?? "").trim();
454
+ const normalizedRouteSessionKey = String(routeSessionKey ?? "").trim();
455
+ const normalizedSessionID = String(sessionId ?? "").trim();
456
+ if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey || !normalizedSessionID) {
457
+ throw new Error("clawpool session_route_bind requires channel/account_id/route_session_key/session_id");
458
+ }
459
+ this.logInfo(
460
+ `session_route_bind request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
461
+ );
462
+ const packet = await this.request(
463
+ "session_route_bind",
464
+ {
465
+ channel: normalizedChannel,
466
+ account_id: normalizedAccountID,
467
+ route_session_key: normalizedRouteSessionKey,
468
+ session_id: normalizedSessionID
469
+ },
470
+ {
471
+ expected: ["send_ack", "send_nack", "error"],
472
+ timeoutMs: opts.timeoutMs ?? 1e4
473
+ }
474
+ );
475
+ if (packet.cmd !== "send_ack") {
476
+ this.logWarn(
477
+ `session_route_bind nack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
478
+ );
479
+ throw this.packetError(packet);
480
+ }
481
+ this.logInfo(
482
+ `session_route_bind ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
483
+ );
484
+ return packet.payload;
485
+ }
486
+ async resolveSessionRoute(channel2, accountId, routeSessionKey, opts = {}) {
487
+ this.ensureReady();
488
+ const normalizedChannel = String(channel2 ?? "").trim().toLowerCase();
489
+ const normalizedAccountID = String(accountId ?? "").trim();
490
+ const normalizedRouteSessionKey = String(routeSessionKey ?? "").trim();
491
+ if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey) {
492
+ throw new Error("clawpool session_route_resolve requires channel/account_id/route_session_key");
493
+ }
494
+ this.logInfo(
495
+ `session_route_resolve request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey}`
496
+ );
497
+ const packet = await this.request(
498
+ "session_route_resolve",
499
+ {
500
+ channel: normalizedChannel,
501
+ account_id: normalizedAccountID,
502
+ route_session_key: normalizedRouteSessionKey
503
+ },
504
+ {
505
+ expected: ["send_ack", "send_nack", "error"],
506
+ timeoutMs: opts.timeoutMs ?? 1e4
507
+ }
508
+ );
509
+ if (packet.cmd !== "send_ack") {
510
+ this.logWarn(
511
+ `session_route_resolve nack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey}`
512
+ );
513
+ throw this.packetError(packet);
514
+ }
515
+ const payload = packet.payload;
516
+ const normalizedSessionID = String(payload.session_id ?? "").trim();
517
+ if (!normalizedSessionID) {
518
+ throw new Error("clawpool session_route_resolve ack missing session_id");
519
+ }
520
+ this.logInfo(
521
+ `session_route_resolve ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
522
+ );
523
+ return {
524
+ ...payload,
525
+ channel: String(payload.channel ?? normalizedChannel),
526
+ account_id: String(payload.account_id ?? normalizedAccountID),
527
+ route_session_key: String(payload.route_session_key ?? normalizedRouteSessionKey),
528
+ session_id: normalizedSessionID
529
+ };
530
+ }
531
+ async sendStreamChunk(sessionId, deltaContent, opts) {
532
+ this.ensureReady();
533
+ const normalizedDeltaContent = this.normalizeStreamDeltaContent(
534
+ opts.clientMsgId,
535
+ deltaContent,
536
+ opts.isFinish === true
537
+ );
538
+ if (!normalizedDeltaContent && !opts.isFinish) {
539
+ return;
540
+ }
541
+ const payload = {
542
+ session_id: sessionId,
543
+ client_msg_id: opts.clientMsgId,
544
+ delta_content: normalizedDeltaContent,
545
+ is_finish: opts.isFinish ?? false
546
+ };
547
+ if (opts.quotedMessageId) {
548
+ payload.quoted_message_id = opts.quotedMessageId;
549
+ }
550
+ if (opts.isFinish) {
551
+ const packet = await this.request("client_stream_chunk", payload, {
552
+ expected: ["send_ack", "send_nack", "error"],
553
+ timeoutMs: opts.timeoutMs ?? 2e4
554
+ });
555
+ if (packet.cmd !== "send_ack") {
556
+ throw this.packetError(packet);
557
+ }
558
+ return packet.payload;
559
+ }
560
+ this.sendPacket("client_stream_chunk", payload);
561
+ }
562
+ async deleteMessage(sessionId, msgId, opts = {}) {
563
+ this.ensureReady();
564
+ const normalizedSessionId = String(sessionId ?? "").trim();
565
+ if (!normalizedSessionId) {
566
+ throw new Error("clawpool delete_msg requires session_id");
567
+ }
568
+ const normalizedMsgId = String(msgId ?? "").trim();
569
+ if (!/^\d+$/.test(normalizedMsgId)) {
570
+ throw new Error("clawpool delete_msg requires numeric msg_id");
571
+ }
572
+ const packet = await this.request(
573
+ "delete_msg",
574
+ {
575
+ session_id: normalizedSessionId,
576
+ msg_id: normalizedMsgId
577
+ },
578
+ {
579
+ expected: ["send_ack", "send_nack", "error"],
580
+ timeoutMs: opts.timeoutMs ?? 2e4
581
+ }
582
+ );
583
+ if (packet.cmd !== "send_ack") {
584
+ throw this.packetError(packet);
585
+ }
586
+ return packet.payload;
587
+ }
588
+ ackEvent(eventId, payload = {}) {
589
+ this.ensureReady();
590
+ const normalizedEventId = String(eventId ?? "").trim();
591
+ if (!normalizedEventId) {
592
+ throw new Error("clawpool event_ack requires event_id");
593
+ }
594
+ const ackPayload = {
595
+ event_id: normalizedEventId,
596
+ received_at: Math.floor(payload.receivedAt ?? Date.now())
597
+ };
598
+ const sessionId = String(payload.sessionId ?? "").trim();
599
+ if (sessionId) {
600
+ ackPayload.session_id = sessionId;
601
+ }
602
+ const msgId = String(payload.msgId ?? "").trim();
603
+ if (/^\d+$/.test(msgId)) {
604
+ ackPayload.msg_id = msgId;
605
+ }
606
+ this.sendPacket("event_ack", ackPayload);
607
+ }
608
+ setSessionComposing(sessionId, active, opts = {}) {
609
+ this.ensureReady();
610
+ const normalizedSessionId = String(sessionId ?? "").trim();
611
+ if (!normalizedSessionId) {
612
+ throw new Error("clawpool session_activity_set requires session_id");
613
+ }
614
+ const payload = {
615
+ session_id: normalizedSessionId,
616
+ kind: "composing",
617
+ active
618
+ };
619
+ const refEventId = String(opts.refEventId ?? "").trim();
620
+ if (refEventId) {
621
+ payload.ref_event_id = refEventId;
622
+ }
623
+ const refMsgId = String(opts.refMsgId ?? "").trim();
624
+ if (/^\d+$/.test(refMsgId)) {
625
+ payload.ref_msg_id = refMsgId;
626
+ }
627
+ this.sendPacket("session_activity_set", payload);
628
+ }
629
+ async runLoop(abortSignal) {
630
+ let attempt = 0;
631
+ while (this.running && !abortSignal.aborted) {
632
+ let uptimeMs = 0;
633
+ let authRejected = false;
634
+ let shouldReconnect = true;
635
+ const cycle = attempt + 1;
636
+ try {
637
+ const outcome = await this.connectOnce(abortSignal, cycle);
638
+ uptimeMs = outcome.uptimeMs;
639
+ shouldReconnect = !outcome.aborted;
640
+ if (!outcome.aborted) {
641
+ const codeText = outcome.closeCode != null ? String(outcome.closeCode) : "-";
642
+ const reasonText = outcome.closeReason ? ` reason=${outcome.closeReason}` : "";
643
+ this.logWarn(
644
+ `websocket closed cause=${outcome.cause} code=${codeText}${reasonText} uptimeMs=${uptimeMs}`
645
+ );
646
+ }
647
+ } catch (err) {
648
+ const msg = err instanceof Error ? err.message : String(err);
649
+ authRejected = err instanceof AuthRejectedError;
650
+ this.updateStatus({
651
+ connected: false,
652
+ authed: false,
653
+ lastError: msg,
654
+ lastDisconnectAt: Date.now()
655
+ });
656
+ this.logConnectionError(msg);
657
+ }
658
+ if (!this.running || abortSignal.aborted || !shouldReconnect) {
659
+ break;
660
+ }
661
+ const stable = uptimeMs >= this.reconnectPolicy.stableConnectionMs;
662
+ if (stable) {
663
+ attempt = 0;
664
+ }
665
+ attempt += 1;
666
+ if (authRejected) {
667
+ attempt = Math.max(attempt, this.reconnectPolicy.authPenaltyAttemptFloor);
668
+ }
669
+ const penaltyFloor = this.consumeReconnectPenaltyAttemptFloor();
670
+ if (penaltyFloor > 0) {
671
+ attempt = Math.max(attempt, penaltyFloor);
672
+ }
673
+ const delay = this.resolveReconnectDelayMs(attempt);
674
+ this.logReconnectPlan({
675
+ delayMs: delay,
676
+ attempt,
677
+ stable,
678
+ authRejected,
679
+ penaltyFloor
680
+ });
681
+ await sleepWithAbort(delay, abortSignal);
682
+ }
683
+ this.stop();
684
+ }
685
+ async connectOnce(abortSignal, cycle) {
686
+ const connSerial = this.nextConnectionSerial();
687
+ this.logInfo(`websocket connect begin conn=${connSerial} cycle=${cycle}`);
688
+ const ws = await this.openWebSocket(this.account.wsUrl, abortSignal);
689
+ this.ws = ws;
690
+ const connectedAt = Date.now();
691
+ this.updateStatus({
692
+ connected: true,
693
+ authed: false,
694
+ lastError: null,
695
+ lastConnectAt: connectedAt
696
+ });
697
+ this.logInfo(`websocket connected conn=${connSerial}`);
698
+ const onMessage = (event) => {
699
+ void this.handleMessageEvent(event.data);
700
+ };
701
+ const onClose = () => {
702
+ this.stopKeepalive();
703
+ this.updateStatus({
704
+ connected: false,
705
+ authed: false,
706
+ lastDisconnectAt: Date.now()
707
+ });
708
+ this.rejectAllPending(new Error("clawpool websocket closed"));
709
+ if (this.ws === ws && ws.readyState !== WebSocket.OPEN) {
710
+ this.ws = null;
711
+ }
712
+ };
713
+ const onError = () => {
714
+ this.stopKeepalive();
715
+ this.updateStatus({
716
+ connected: false,
717
+ authed: false,
718
+ lastDisconnectAt: Date.now()
719
+ });
720
+ this.rejectAllPending(new Error("clawpool websocket error"));
721
+ };
722
+ ws.addEventListener("message", onMessage);
723
+ ws.addEventListener("close", onClose);
724
+ ws.addEventListener("error", onError);
725
+ try {
726
+ const auth = await this.authenticate(connSerial);
727
+ this.startKeepalive(ws, connSerial, auth.heartbeatSec);
728
+ const outcome = await this.waitForCloseOrAbort(ws, abortSignal);
729
+ return {
730
+ ...outcome,
731
+ uptimeMs: Math.max(0, Date.now() - connectedAt)
732
+ };
733
+ } catch (err) {
734
+ this.safeCloseSpecificWs(ws, "connect_once_error");
735
+ throw err;
736
+ } finally {
737
+ ws.removeEventListener("message", onMessage);
738
+ ws.removeEventListener("close", onClose);
739
+ ws.removeEventListener("error", onError);
740
+ this.stopKeepalive();
741
+ this.safeCloseSpecificWs(ws, "connect_once_finally");
742
+ this.logInfo(`websocket connect end conn=${connSerial}`);
743
+ }
744
+ }
745
+ async openWebSocket(url, abortSignal) {
746
+ return new Promise((resolve, reject) => {
747
+ const ws = new WebSocket(url);
748
+ let done = false;
749
+ const timeoutMs = this.reconnectPolicy.connectTimeoutMs;
750
+ let timer = null;
751
+ const closeWs = () => {
752
+ try {
753
+ ws.close();
754
+ } catch {
755
+ }
756
+ };
757
+ const onOpen = () => {
758
+ finish(() => resolve(ws));
759
+ };
760
+ const onError = () => {
761
+ finish(() => reject(new Error("clawpool websocket connect failed")));
762
+ };
763
+ const onAbort = () => {
764
+ finish(() => {
765
+ closeWs();
766
+ reject(new Error("aborted"));
767
+ });
768
+ };
769
+ const finish = (fn) => {
770
+ if (done) {
771
+ return;
772
+ }
773
+ done = true;
774
+ if (timer) {
775
+ clearTimeout(timer);
776
+ }
777
+ ws.removeEventListener("open", onOpen);
778
+ ws.removeEventListener("error", onError);
779
+ abortSignal.removeEventListener("abort", onAbort);
780
+ fn();
781
+ };
782
+ timer = setTimeout(() => {
783
+ finish(() => {
784
+ closeWs();
785
+ reject(new Error("clawpool websocket connect timeout"));
786
+ });
787
+ }, timeoutMs);
788
+ ws.addEventListener("open", onOpen);
789
+ ws.addEventListener("error", onError);
790
+ abortSignal.addEventListener("abort", onAbort, { once: true });
791
+ });
792
+ }
793
+ async waitForCloseOrAbort(ws, abortSignal) {
794
+ return new Promise((resolve) => {
795
+ let settled = false;
796
+ const closeWs = () => {
797
+ this.safeCloseSpecificWs(ws);
798
+ };
799
+ function finish(result) {
800
+ if (settled) {
801
+ return;
802
+ }
803
+ settled = true;
804
+ ws.removeEventListener("close", onClose);
805
+ ws.removeEventListener("error", onError);
806
+ abortSignal.removeEventListener("abort", onAbort);
807
+ resolve(result);
808
+ }
809
+ function onClose(event) {
810
+ const close = event;
811
+ const code = Number(close.code);
812
+ finish({
813
+ cause: "close",
814
+ aborted: false,
815
+ closeCode: Number.isFinite(code) ? code : void 0,
816
+ closeReason: normalizeCloseReason(close.reason)
817
+ });
818
+ }
819
+ function onError() {
820
+ finish({
821
+ cause: "error",
822
+ aborted: false
823
+ });
824
+ }
825
+ function onAbort() {
826
+ closeWs();
827
+ finish({
828
+ cause: "abort",
829
+ aborted: true
830
+ });
831
+ }
832
+ ws.addEventListener("close", onClose);
833
+ ws.addEventListener("error", onError);
834
+ abortSignal.addEventListener("abort", onAbort, { once: true });
835
+ });
836
+ }
837
+ async authenticate(connSerial) {
838
+ this.logInfo(`auth begin conn=${connSerial}`);
839
+ const packet = await this.request(
840
+ "auth",
841
+ {
842
+ agent_id: this.account.agentId,
843
+ api_key: this.account.apiKey,
844
+ client: "openclaw-clawpool"
845
+ },
846
+ {
847
+ expected: ["auth_ack"],
848
+ timeoutMs: 1e4,
849
+ requireAuthed: false
850
+ }
851
+ );
852
+ const payload = packet.payload ?? {};
853
+ const code = parseCode(payload);
854
+ if (code !== 0) {
855
+ throw new AuthRejectedError(code, parseMessage(payload));
856
+ }
857
+ const heartbeatSec = parseHeartbeatSec(payload);
858
+ const protocol = String(payload.protocol ?? "").trim() || void 0;
859
+ this.updateStatus({ authed: true, lastError: null });
860
+ this.logInfo(
861
+ `auth success conn=${connSerial} heartbeatSec=${heartbeatSec} protocol=${protocol ?? "-"}`
862
+ );
863
+ return {
864
+ heartbeatSec,
865
+ protocol
866
+ };
867
+ }
868
+ async handleMessageEvent(data) {
869
+ const text = await wsDataToText(data);
870
+ if (!text) {
871
+ return;
872
+ }
873
+ let packet;
874
+ try {
875
+ packet = JSON.parse(text);
876
+ } catch {
877
+ this.logWarn("ignored non-json message");
878
+ return;
879
+ }
880
+ const cmd = String(packet.cmd ?? "").trim();
881
+ const seq = Number(packet.seq ?? 0);
882
+ if (cmd === "ping") {
883
+ this.sendPacket("pong", { ts: Date.now() }, seq > 0 ? seq : void 0, false);
884
+ return;
885
+ }
886
+ if (cmd === "event_msg") {
887
+ this.callbacks.onEventMsg?.(packet.payload);
888
+ return;
889
+ }
890
+ if (cmd === "event_react") {
891
+ this.callbacks.onEventReact?.(packet.payload);
892
+ return;
893
+ }
894
+ if (cmd === "event_revoke") {
895
+ this.callbacks.onEventRevoke?.(packet.payload);
896
+ return;
897
+ }
898
+ if (cmd === "kicked") {
899
+ const payload = packet.payload ?? {};
900
+ const reason = parseKickedReason(payload);
901
+ if (reason === "replaced_by_new_connection") {
902
+ this.reconnectPenaltyAttemptFloor = Math.max(
903
+ this.reconnectPenaltyAttemptFloor,
904
+ this.reconnectPolicy.fastRetryDelaysMs.length + 5
905
+ );
906
+ this.logWarn(
907
+ `apply reconnect penalty for kicked replacement penaltyFloor=${this.reconnectPenaltyAttemptFloor}`
908
+ );
909
+ }
910
+ this.logWarn(`connection kicked by server reason=${reason}`);
911
+ this.safeCloseWs("kicked_by_server");
912
+ return;
913
+ }
914
+ const pending = this.pending.get(seq);
915
+ if (pending && pending.expected.has(cmd)) {
916
+ this.pending.delete(seq);
917
+ clearTimeout(pending.timer);
918
+ pending.resolve(packet);
919
+ return;
920
+ }
921
+ }
922
+ ensureReady(requireAuthed = true) {
923
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
924
+ throw new Error("clawpool websocket is not open");
925
+ }
926
+ if (requireAuthed && !this.status.authed) {
927
+ throw new Error("clawpool websocket is not authed");
928
+ }
929
+ }
930
+ nextSeq() {
931
+ this.seq += 1;
932
+ return this.seq;
933
+ }
934
+ sendPacket(cmd, payload, seq, requireAuthed = true) {
935
+ this.ensureReady(requireAuthed);
936
+ const outSeq = seq ?? this.nextSeq();
937
+ const packet = {
938
+ cmd,
939
+ seq: outSeq,
940
+ payload
941
+ };
942
+ this.ws?.send(JSON.stringify(packet));
943
+ return outSeq;
944
+ }
945
+ async request(cmd, payload, opts) {
946
+ this.ensureReady(opts.requireAuthed ?? true);
947
+ const seq = this.nextSeq();
948
+ const expected = new Set(opts.expected);
949
+ return new Promise((resolve, reject) => {
950
+ const timer = setTimeout(() => {
951
+ this.pending.delete(seq);
952
+ reject(new Error(`${cmd} timeout`));
953
+ }, opts.timeoutMs);
954
+ this.pending.set(seq, {
955
+ expected,
956
+ resolve,
957
+ reject,
958
+ timer
959
+ });
960
+ try {
961
+ const packet = {
962
+ cmd,
963
+ seq,
964
+ payload
965
+ };
966
+ this.ws?.send(JSON.stringify(packet));
967
+ } catch (err) {
968
+ this.pending.delete(seq);
969
+ clearTimeout(timer);
970
+ reject(err instanceof Error ? err : new Error(String(err)));
971
+ }
972
+ });
973
+ }
974
+ rejectAllPending(err) {
975
+ const pendingCount = this.pending.size;
976
+ if (pendingCount > 0) {
977
+ this.logWarn(`reject pending requests count=${pendingCount} reason=${err.message}`);
978
+ }
979
+ for (const [seq, pending] of this.pending.entries()) {
980
+ this.pending.delete(seq);
981
+ clearTimeout(pending.timer);
982
+ pending.reject(err);
983
+ }
984
+ }
985
+ packetError(packet) {
986
+ const payload = packet.payload;
987
+ const code = Number(payload.code ?? 0);
988
+ const msg = String(payload.msg ?? packet.cmd ?? "unknown error");
989
+ return new Error(`clawpool ${packet.cmd}: code=${code} msg=${msg}`);
990
+ }
991
+ normalizeStreamDeltaContent(clientMsgId, deltaContent, isFinish) {
992
+ const carry = this.pendingStreamHighSurrogate.get(clientMsgId) ?? "";
993
+ this.pendingStreamHighSurrogate.delete(clientMsgId);
994
+ let normalized = `${carry}${String(deltaContent ?? "")}`;
995
+ if (!normalized) {
996
+ return "";
997
+ }
998
+ if (isFinish && !deltaContent && carry) {
999
+ this.logWarn(`dropping dangling high surrogate at stream finish clientMsgId=${clientMsgId}`);
1000
+ return "";
1001
+ }
1002
+ if (!isFinish && this.endsWithHighSurrogate(normalized)) {
1003
+ this.pendingStreamHighSurrogate.set(clientMsgId, normalized.slice(-1));
1004
+ normalized = normalized.slice(0, -1);
1005
+ } else if (isFinish && this.endsWithHighSurrogate(normalized)) {
1006
+ this.logWarn(`dropping dangling high surrogate at stream finish clientMsgId=${clientMsgId}`);
1007
+ normalized = normalized.slice(0, -1);
1008
+ }
1009
+ return normalized;
1010
+ }
1011
+ endsWithHighSurrogate(value) {
1012
+ if (!value) {
1013
+ return false;
1014
+ }
1015
+ const code = value.charCodeAt(value.length - 1);
1016
+ return code >= 55296 && code <= 56319;
1017
+ }
1018
+ nextConnectionSerial() {
1019
+ this.connectionSerial += 1;
1020
+ return this.connectionSerial;
1021
+ }
1022
+ resolveKeepalivePolicy(heartbeatSec) {
1023
+ const defaultIntervalMs = Math.max(5e3, Math.min(2e4, Math.floor(heartbeatSec * 1e3 / 2)));
1024
+ const intervalMs = clampInt(
1025
+ this.account.config.keepalivePingMs,
1026
+ defaultIntervalMs,
1027
+ 2e3,
1028
+ 6e4
1029
+ );
1030
+ const defaultTimeoutMs = Math.max(3e3, Math.min(15e3, Math.floor(intervalMs * 0.8)));
1031
+ const timeoutMs = clampInt(
1032
+ this.account.config.keepaliveTimeoutMs,
1033
+ defaultTimeoutMs,
1034
+ 1e3,
1035
+ 6e4
1036
+ );
1037
+ return {
1038
+ intervalMs,
1039
+ timeoutMs
1040
+ };
1041
+ }
1042
+ startKeepalive(ws, connSerial, heartbeatSec) {
1043
+ this.stopKeepalive();
1044
+ const policy = this.resolveKeepalivePolicy(heartbeatSec);
1045
+ this.logInfo(
1046
+ `keepalive start conn=${connSerial} intervalMs=${policy.intervalMs} timeoutMs=${policy.timeoutMs} serverHeartbeatSec=${heartbeatSec}`
1047
+ );
1048
+ this.keepaliveTimer = setInterval(() => {
1049
+ void this.runKeepaliveProbe(ws, connSerial, policy.timeoutMs);
1050
+ }, policy.intervalMs);
1051
+ }
1052
+ stopKeepalive() {
1053
+ if (this.keepaliveTimer) {
1054
+ clearInterval(this.keepaliveTimer);
1055
+ this.keepaliveTimer = null;
1056
+ }
1057
+ this.keepaliveInFlight = false;
1058
+ }
1059
+ async runKeepaliveProbe(ws, connSerial, timeoutMs) {
1060
+ if (!this.running || this.ws !== ws || ws.readyState !== WebSocket.OPEN || !this.status.authed) {
1061
+ return;
1062
+ }
1063
+ if (this.keepaliveInFlight) {
1064
+ this.logWarn(`keepalive overlap detected conn=${connSerial}, force reconnect`);
1065
+ this.safeCloseSpecificWs(ws, "keepalive_overlap");
1066
+ return;
1067
+ }
1068
+ this.keepaliveInFlight = true;
1069
+ const startedAt = Date.now();
1070
+ try {
1071
+ await this.request(
1072
+ "ping",
1073
+ {
1074
+ ts: startedAt,
1075
+ source: "clawpool_keepalive"
1076
+ },
1077
+ {
1078
+ expected: ["pong"],
1079
+ timeoutMs
1080
+ }
1081
+ );
1082
+ const latencyMs = Math.max(0, Date.now() - startedAt);
1083
+ if (latencyMs >= 2e3) {
1084
+ this.logWarn(`keepalive high latency conn=${connSerial} latencyMs=${latencyMs}`);
1085
+ }
1086
+ } catch (err) {
1087
+ const msg = err instanceof Error ? err.message : String(err);
1088
+ this.logWarn(`keepalive failed conn=${connSerial} err=${msg}, force reconnect`);
1089
+ if (this.ws === ws) {
1090
+ this.safeCloseSpecificWs(ws, "keepalive_probe_failed");
1091
+ }
1092
+ } finally {
1093
+ this.keepaliveInFlight = false;
1094
+ }
1095
+ }
1096
+ resolveReconnectDelayMs(attempt) {
1097
+ if (attempt <= 0) {
1098
+ return 0;
1099
+ }
1100
+ const fastRetryDelays = this.reconnectPolicy.fastRetryDelaysMs;
1101
+ if (attempt <= fastRetryDelays.length) {
1102
+ return fastRetryDelays[attempt - 1] ?? this.reconnectPolicy.baseDelayMs;
1103
+ }
1104
+ const exponent = attempt - fastRetryDelays.length - 1;
1105
+ const uncapped = this.reconnectPolicy.baseDelayMs * 2 ** exponent;
1106
+ const capped = Math.min(this.reconnectPolicy.maxDelayMs, Math.floor(uncapped));
1107
+ const jitterFloor = Math.max(100, Math.floor(capped * 0.5));
1108
+ return randomIntInclusive(jitterFloor, capped);
1109
+ }
1110
+ consumeReconnectPenaltyAttemptFloor() {
1111
+ const floor = this.reconnectPenaltyAttemptFloor;
1112
+ this.reconnectPenaltyAttemptFloor = 0;
1113
+ return floor;
1114
+ }
1115
+ safeCloseSpecificWs(ws, reason = "") {
1116
+ try {
1117
+ ws.close();
1118
+ } catch {
1119
+ }
1120
+ if (this.ws === ws) {
1121
+ this.ws = null;
1122
+ }
1123
+ }
1124
+ safeCloseWs(reason = "") {
1125
+ if (!this.ws) {
1126
+ return;
1127
+ }
1128
+ this.safeCloseSpecificWs(this.ws, reason);
1129
+ }
1130
+ updateStatus(patch) {
1131
+ this.status = {
1132
+ ...this.status,
1133
+ ...patch
1134
+ };
1135
+ this.callbacks.onStatus?.(this.getStatus());
1136
+ }
1137
+ };
1138
+ var activeClients = /* @__PURE__ */ new Map();
1139
+ function setActiveAibotClient(accountId, client) {
1140
+ if (!accountId) {
1141
+ return;
1142
+ }
1143
+ if (!client) {
1144
+ activeClients.delete(accountId);
1145
+ return;
1146
+ }
1147
+ activeClients.set(accountId, client);
1148
+ }
1149
+ function clearActiveAibotClient(accountId, client) {
1150
+ if (!accountId) {
1151
+ return;
1152
+ }
1153
+ if (activeClients.get(accountId) !== client) {
1154
+ return;
1155
+ }
1156
+ activeClients.delete(accountId);
1157
+ }
1158
+ function getActiveAibotClient(accountId) {
1159
+ if (!accountId) {
1160
+ return null;
1161
+ }
1162
+ return activeClients.get(accountId) ?? null;
1163
+ }
1164
+ function requireActiveAibotClient(accountId) {
1165
+ const client = getActiveAibotClient(accountId);
1166
+ if (!client) {
1167
+ throw new Error(
1168
+ `clawpool account "${accountId}" is not connected; start the gateway channel runtime first`
1169
+ );
1170
+ }
1171
+ return client;
1172
+ }
1173
+
1174
+ // src/actions.ts
1175
+ var SUPPORTED_AIBOT_MESSAGE_ACTIONS = /* @__PURE__ */ new Set(["unsend", "delete"]);
1176
+ function toSnakeCaseKey(key) {
1177
+ return key.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
1178
+ }
1179
+ function readStringishParam(params, key) {
1180
+ const value = readStringParam(params, key);
1181
+ if (value) {
1182
+ return value;
1183
+ }
1184
+ const snakeKey = toSnakeCaseKey(key);
1185
+ const raw = (Object.hasOwn(params, key) ? params[key] : void 0) ?? (snakeKey !== key && Object.hasOwn(params, snakeKey) ? params[snakeKey] : void 0);
1186
+ if (typeof raw === "number" && Number.isFinite(raw)) {
1187
+ return String(raw);
1188
+ }
1189
+ return void 0;
1190
+ }
1191
+ function resolveDeleteSessionId(params) {
1192
+ const direct = readStringParam(params.params, "sessionId") ?? readStringParam(params.params, "to") ?? params.currentChannelId;
1193
+ return normalizeAibotSessionTarget(direct ?? "");
1194
+ }
1195
+ var aibotMessageActions = {
1196
+ listActions: ({ cfg }) => {
1197
+ const hasConfiguredAccount = listAibotAccountIds(cfg).map((accountId) => resolveAibotAccount({ cfg, accountId })).some((account) => account.enabled && account.configured);
1198
+ if (!hasConfiguredAccount) {
1199
+ return [];
1200
+ }
1201
+ return ["unsend"];
1202
+ },
1203
+ supportsAction: ({ action }) => SUPPORTED_AIBOT_MESSAGE_ACTIONS.has(action),
1204
+ handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
1205
+ if (!SUPPORTED_AIBOT_MESSAGE_ACTIONS.has(action)) {
1206
+ throw new Error(`Clawpool action ${action} is not supported`);
1207
+ }
1208
+ const account = resolveAibotAccount({ cfg, accountId });
1209
+ const client = requireActiveAibotClient(account.accountId);
1210
+ const messageId = readStringishParam(params, "messageId") ?? readStringishParam(params, "msgId");
1211
+ if (!messageId) {
1212
+ throw new Error("Clawpool unsend requires messageId.");
1213
+ }
1214
+ const sessionId = resolveDeleteSessionId({
1215
+ params,
1216
+ currentChannelId: toolContext?.currentChannelId
1217
+ });
1218
+ if (!sessionId) {
1219
+ throw new Error(
1220
+ "Clawpool unsend requires sessionId or to, or must be used inside an active Clawpool conversation."
1221
+ );
1222
+ }
1223
+ const ack = await client.deleteMessage(sessionId, messageId);
1224
+ return jsonResult({
1225
+ ok: true,
1226
+ deleted: true,
1227
+ unsent: action === "unsend",
1228
+ messageId: String(ack.msg_id ?? messageId),
1229
+ sessionId: String(ack.session_id ?? sessionId)
1230
+ });
1231
+ }
1232
+ };
1233
+
1234
+ // src/monitor.ts
1235
+ import {
1236
+ createReplyPrefixOptions,
1237
+ resolveOutboundMediaUrls,
1238
+ sendMediaWithLeadingCaption
1239
+ } from "openclaw/plugin-sdk";
1240
+
1241
+ // src/reply-text-guard.ts
1242
+ var NETWORK_ERROR_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
1243
+ var TIMEOUT_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
1244
+ var CONTEXT_OVERFLOW_MESSAGE = "\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u8FC7\u957F\uFF0C\u8BF7\u65B0\u5F00\u4F1A\u8BDD\u540E\u91CD\u8BD5\u3002";
1245
+ var GENERIC_STOP_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u5F02\u5E38\u4E2D\u65AD\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
1246
+ function guardInternalReplyText(rawText) {
1247
+ const normalized = String(rawText ?? "").trim();
1248
+ if (!normalized) {
1249
+ return null;
1250
+ }
1251
+ if (/^Unhandled stop reason:\s*network_error$/i.test(normalized)) {
1252
+ return {
1253
+ code: "upstream_network_error",
1254
+ rawText: normalized,
1255
+ userText: NETWORK_ERROR_MESSAGE
1256
+ };
1257
+ }
1258
+ if (/^LLM request timed out\.?$/i.test(normalized)) {
1259
+ return {
1260
+ code: "upstream_timeout",
1261
+ rawText: normalized,
1262
+ userText: TIMEOUT_MESSAGE
1263
+ };
1264
+ }
1265
+ if (normalized.startsWith("Context overflow: prompt too large for the model.")) {
1266
+ return {
1267
+ code: "upstream_context_overflow",
1268
+ rawText: normalized,
1269
+ userText: CONTEXT_OVERFLOW_MESSAGE
1270
+ };
1271
+ }
1272
+ if (/^Unhandled stop reason:\s*[a-z0-9_]+$/i.test(normalized)) {
1273
+ return {
1274
+ code: "upstream_stop_reason",
1275
+ rawText: normalized,
1276
+ userText: GENERIC_STOP_MESSAGE
1277
+ };
1278
+ }
1279
+ return null;
1280
+ }
1281
+
1282
+ // src/upstream-retry.ts
1283
+ var DEFAULT_UPSTREAM_RETRY_MAX_ATTEMPTS = 3;
1284
+ var DEFAULT_UPSTREAM_RETRY_BASE_DELAY_MS = 300;
1285
+ var DEFAULT_UPSTREAM_RETRY_MAX_DELAY_MS = 2e3;
1286
+ function clampInt2(value, fallback, min, max) {
1287
+ const n = Number(value);
1288
+ if (!Number.isFinite(n)) {
1289
+ return fallback;
1290
+ }
1291
+ return Math.max(min, Math.min(max, Math.floor(n)));
1292
+ }
1293
+ function resolveUpstreamRetryPolicy(account) {
1294
+ const maxAttempts = clampInt2(
1295
+ account.config.upstreamRetryMaxAttempts,
1296
+ DEFAULT_UPSTREAM_RETRY_MAX_ATTEMPTS,
1297
+ 1,
1298
+ 5
1299
+ );
1300
+ const baseDelayMs = clampInt2(
1301
+ account.config.upstreamRetryBaseDelayMs,
1302
+ DEFAULT_UPSTREAM_RETRY_BASE_DELAY_MS,
1303
+ 0,
1304
+ 1e4
1305
+ );
1306
+ const maxDelayMs = clampInt2(
1307
+ account.config.upstreamRetryMaxDelayMs,
1308
+ DEFAULT_UPSTREAM_RETRY_MAX_DELAY_MS,
1309
+ baseDelayMs,
1310
+ 3e4
1311
+ );
1312
+ return {
1313
+ maxAttempts,
1314
+ baseDelayMs,
1315
+ maxDelayMs
1316
+ };
1317
+ }
1318
+ function isRetryableGuardedReply(guarded) {
1319
+ if (!guarded) {
1320
+ return false;
1321
+ }
1322
+ return guarded.code === "upstream_network_error" || guarded.code === "upstream_timeout";
1323
+ }
1324
+ function resolveUpstreamRetryDelayMs(policy, attempt) {
1325
+ if (attempt <= 0) {
1326
+ return 0;
1327
+ }
1328
+ const exponent = Math.max(0, attempt - 1);
1329
+ const delay = policy.baseDelayMs * 2 ** exponent;
1330
+ return Math.min(policy.maxDelayMs, Math.floor(delay));
1331
+ }
1332
+
1333
+ // src/runtime.ts
1334
+ var runtime = null;
1335
+ function setAibotRuntime(next) {
1336
+ runtime = next;
1337
+ }
1338
+ function getAibotRuntime() {
1339
+ if (!runtime) {
1340
+ throw new Error("Aibot runtime not initialized");
1341
+ }
1342
+ return runtime;
1343
+ }
1344
+
1345
+ // src/quoted-reply-body.ts
1346
+ function buildBodyWithQuotedReplyId(rawBody, quotedMessageId) {
1347
+ if (!quotedMessageId) {
1348
+ return rawBody;
1349
+ }
1350
+ return `[quoted_message_id=${quotedMessageId}]
1351
+ ${rawBody}`;
1352
+ }
1353
+
1354
+ // src/monitor.ts
1355
+ var activeMonitorClients = /* @__PURE__ */ new Map();
1356
+ function registerActiveMonitor(accountId, client) {
1357
+ if (!accountId) {
1358
+ return null;
1359
+ }
1360
+ const previous = activeMonitorClients.get(accountId) ?? null;
1361
+ activeMonitorClients.set(accountId, client);
1362
+ return previous === client ? null : previous;
1363
+ }
1364
+ function isActiveMonitor(accountId, client) {
1365
+ if (!accountId) {
1366
+ return false;
1367
+ }
1368
+ return activeMonitorClients.get(accountId) === client;
1369
+ }
1370
+ function clearActiveMonitor(accountId, client) {
1371
+ if (!accountId) {
1372
+ return;
1373
+ }
1374
+ if (activeMonitorClients.get(accountId) !== client) {
1375
+ return;
1376
+ }
1377
+ activeMonitorClients.delete(accountId);
1378
+ }
1379
+ function toStringId(value) {
1380
+ const text = String(value ?? "").trim();
1381
+ return text;
1382
+ }
1383
+ function toTimestampMs(value) {
1384
+ const n = Number(value);
1385
+ if (!Number.isFinite(n) || n <= 0) {
1386
+ return void 0;
1387
+ }
1388
+ if (n < 1e12) {
1389
+ return Math.floor(n * 1e3);
1390
+ }
1391
+ return Math.floor(n);
1392
+ }
1393
+ function normalizeNumericMessageId(value) {
1394
+ const raw = toStringId(value);
1395
+ if (!raw) {
1396
+ return void 0;
1397
+ }
1398
+ return /^\d+$/.test(raw) ? raw : void 0;
1399
+ }
1400
+ function resolveStreamChunkChars(account) {
1401
+ return Math.max(1, account.config.streamChunkChars ?? 48);
1402
+ }
1403
+ function resolveStreamChunkDelayMs(account) {
1404
+ return Math.max(0, Math.floor(account.config.streamChunkDelayMs ?? 0));
1405
+ }
1406
+ function resolveStreamFinishDelayMs(account) {
1407
+ return resolveStreamChunkDelayMs(account);
1408
+ }
1409
+ function sleep(ms) {
1410
+ if (ms <= 0) {
1411
+ return Promise.resolve();
1412
+ }
1413
+ return new Promise((resolve) => setTimeout(resolve, ms));
1414
+ }
1415
+ function endsWithHighSurrogate(value) {
1416
+ if (!value) {
1417
+ return false;
1418
+ }
1419
+ const code = value.charCodeAt(value.length - 1);
1420
+ return code >= 55296 && code <= 56319;
1421
+ }
1422
+ function startsWithLowSurrogate(value) {
1423
+ if (!value) {
1424
+ return false;
1425
+ }
1426
+ const code = value.charCodeAt(0);
1427
+ return code >= 56320 && code <= 57343;
1428
+ }
1429
+ function splitTextByLengthPreserveContent(text, maxChars) {
1430
+ const source = String(text ?? "");
1431
+ if (!source) {
1432
+ return [];
1433
+ }
1434
+ const limit = Math.max(1, Math.floor(maxChars));
1435
+ const chunks = [];
1436
+ let cursor = 0;
1437
+ while (cursor < source.length) {
1438
+ let end = Math.min(source.length, cursor + limit);
1439
+ if (end < source.length) {
1440
+ const tail = source.slice(cursor, end);
1441
+ const head = source.slice(end, end + 1);
1442
+ if (endsWithHighSurrogate(tail) && startsWithLowSurrogate(head)) {
1443
+ end++;
1444
+ }
1445
+ }
1446
+ const chunk = source.slice(cursor, end);
1447
+ if (!chunk) {
1448
+ break;
1449
+ }
1450
+ chunks.push(chunk);
1451
+ cursor = end;
1452
+ }
1453
+ return chunks;
1454
+ }
1455
+ function buildEventLogContext(params) {
1456
+ const parts = [
1457
+ `eventId=${params.eventId || "-"}`,
1458
+ `sessionId=${params.sessionId}`,
1459
+ `messageSid=${params.messageSid}`
1460
+ ];
1461
+ if (params.clientMsgId) {
1462
+ parts.push(`clientMsgId=${params.clientMsgId}`);
1463
+ }
1464
+ if (params.outboundCounter !== void 0) {
1465
+ parts.push(`outboundCounter=${params.outboundCounter}`);
1466
+ }
1467
+ return parts.join(" ");
1468
+ }
1469
+ async function deliverAibotStreamBlock(params) {
1470
+ const chunks = splitTextByLengthPreserveContent(params.text, resolveStreamChunkChars(params.account));
1471
+ const chunkDelayMs = resolveStreamChunkDelayMs(params.account);
1472
+ let didSend = false;
1473
+ const context = buildEventLogContext({
1474
+ eventId: params.eventId,
1475
+ sessionId: params.sessionId,
1476
+ messageSid: params.messageSid,
1477
+ clientMsgId: params.clientMsgId
1478
+ });
1479
+ params.runtime.log(
1480
+ `[clawpool:${params.account.accountId}] stream block split into ${chunks.length} chunk(s) ${context} textLen=${params.text.length} chunkDelayMs=${chunkDelayMs}`
1481
+ );
1482
+ for (let index = 0; index < chunks.length; index++) {
1483
+ const chunk = chunks[index];
1484
+ const normalized = String(chunk ?? "");
1485
+ if (!normalized) {
1486
+ continue;
1487
+ }
1488
+ params.runtime.log(
1489
+ `[clawpool:${params.account.accountId}] stream chunk send ${context} chunkIndex=${index + 1}/${chunks.length} deltaLen=${normalized.length}`
1490
+ );
1491
+ await params.client.sendStreamChunk(params.sessionId, normalized, {
1492
+ clientMsgId: params.clientMsgId,
1493
+ quotedMessageId: params.quotedMessageId,
1494
+ isFinish: false
1495
+ });
1496
+ didSend = true;
1497
+ params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
1498
+ if (chunkDelayMs > 0 && index < chunks.length - 1) {
1499
+ await sleep(chunkDelayMs);
1500
+ }
1501
+ }
1502
+ return didSend;
1503
+ }
1504
+ async function deliverAibotMessage(params) {
1505
+ const { payload, client, account, sessionId, quotedMessageId, runtime: runtime2, statusSink, stableClientMsgId } = params;
1506
+ const core = getAibotRuntime();
1507
+ const tableMode = params.tableMode ?? "code";
1508
+ const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
1509
+ const mediaSent = await sendMediaWithLeadingCaption({
1510
+ mediaUrls: resolveOutboundMediaUrls(payload),
1511
+ caption: text,
1512
+ send: async ({ mediaUrl, caption }) => {
1513
+ await client.sendMedia(sessionId, mediaUrl, caption ?? "", {
1514
+ quotedMessageId,
1515
+ clientMsgId: stableClientMsgId ? `${stableClientMsgId}_media` : void 0
1516
+ });
1517
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
1518
+ },
1519
+ onError: (error) => {
1520
+ runtime2.error(`clawpool media send failed: ${String(error)}`);
1521
+ statusSink?.({ lastError: String(error) });
1522
+ }
1523
+ });
1524
+ if (mediaSent) {
1525
+ return true;
1526
+ }
1527
+ if (!text) {
1528
+ return false;
1529
+ }
1530
+ const maxChunkChars = Math.max(1, account.config.maxChunkChars ?? 1200);
1531
+ const chunks = splitTextByLengthPreserveContent(text, maxChunkChars);
1532
+ let chunkIndex = 0;
1533
+ for (const chunk of chunks) {
1534
+ chunkIndex++;
1535
+ const normalized = String(chunk ?? "");
1536
+ if (!normalized) {
1537
+ continue;
1538
+ }
1539
+ await client.sendText(sessionId, normalized, {
1540
+ quotedMessageId,
1541
+ clientMsgId: stableClientMsgId ? `${stableClientMsgId}_chunk${chunkIndex}` : void 0
1542
+ });
1543
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
1544
+ }
1545
+ return true;
1546
+ }
1547
+ async function bindSessionRouteMapping(params) {
1548
+ const routeSessionKey = String(params.routeSessionKey ?? "").trim();
1549
+ const sessionId = String(params.sessionId ?? "").trim();
1550
+ if (!routeSessionKey || !sessionId) {
1551
+ return;
1552
+ }
1553
+ try {
1554
+ params.runtime.log(
1555
+ `[clawpool:${params.account.accountId}] session route bind begin routeSessionKey=${routeSessionKey} sessionId=${sessionId}`
1556
+ );
1557
+ await params.client.bindSessionRoute(
1558
+ "clawpool",
1559
+ params.account.accountId,
1560
+ routeSessionKey,
1561
+ sessionId
1562
+ );
1563
+ params.runtime.log(
1564
+ `[clawpool:${params.account.accountId}] session route bind success routeSessionKey=${routeSessionKey} sessionId=${sessionId}`
1565
+ );
1566
+ } catch (err) {
1567
+ const reason = `clawpool session route bind failed routeSessionKey=${routeSessionKey} sessionId=${sessionId}: ${String(err)}`;
1568
+ params.runtime.error(`[clawpool:${params.account.accountId}] ${reason}`);
1569
+ params.statusSink?.({ lastError: reason });
1570
+ }
1571
+ }
1572
+ async function processEvent(params) {
1573
+ const { event, account, config, runtime: runtime2, client, statusSink } = params;
1574
+ const core = getAibotRuntime();
1575
+ const sessionId = toStringId(event.session_id);
1576
+ const messageSid = toStringId(event.msg_id);
1577
+ const rawBody = String(event.content ?? "").trim();
1578
+ if (!sessionId || !messageSid || !rawBody) {
1579
+ const reason = `invalid event_msg payload: session_id=${sessionId || "<empty>"} msg_id=${messageSid || "<empty>"}`;
1580
+ runtime2.error(`[clawpool:${account.accountId}] ${reason}`);
1581
+ statusSink?.({ lastError: reason });
1582
+ return;
1583
+ }
1584
+ const eventId = toStringId(event.event_id);
1585
+ const quotedMessageId = normalizeNumericMessageId(event.quoted_message_id);
1586
+ const bodyForAgent = buildBodyWithQuotedReplyId(rawBody, quotedMessageId);
1587
+ const senderId = toStringId(event.sender_id);
1588
+ const isGroup = Number(event.session_type ?? 0) === 2 || String(event.event_type ?? "").startsWith("group_");
1589
+ const chatType = isGroup ? "group" : "direct";
1590
+ const createdAt = toTimestampMs(event.created_at);
1591
+ const baseLogContext = buildEventLogContext({
1592
+ eventId,
1593
+ sessionId,
1594
+ messageSid
1595
+ });
1596
+ runtime2.log(
1597
+ `[clawpool:${account.accountId}] inbound event ${baseLogContext} chatType=${chatType} bodyLen=${rawBody.length} quotedMessageId=${quotedMessageId || "-"}`
1598
+ );
1599
+ const route = core.channel.routing.resolveAgentRoute({
1600
+ cfg: config,
1601
+ channel: "clawpool",
1602
+ accountId: account.accountId,
1603
+ peer: {
1604
+ kind: isGroup ? "group" : "direct",
1605
+ id: sessionId
1606
+ }
1607
+ });
1608
+ const storePath = core.channel.session.resolveStorePath(config.session?.store, {
1609
+ agentId: route.agentId
1610
+ });
1611
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
1612
+ storePath,
1613
+ sessionKey: route.sessionKey
1614
+ });
1615
+ const fromLabel = isGroup ? `group:${sessionId}/${senderId || "unknown"}` : `user:${senderId || "unknown"}`;
1616
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
1617
+ const body = core.channel.reply.formatAgentEnvelope({
1618
+ channel: "Clawpool",
1619
+ from: fromLabel,
1620
+ timestamp: createdAt,
1621
+ previousTimestamp,
1622
+ envelope: envelopeOptions,
1623
+ body: bodyForAgent
1624
+ });
1625
+ const from = isGroup ? `clawpool:group:${sessionId}:${senderId || "unknown"}` : `clawpool:${senderId || "unknown"}`;
1626
+ const to = `clawpool:${sessionId}`;
1627
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
1628
+ Body: body,
1629
+ BodyForAgent: bodyForAgent,
1630
+ RawBody: rawBody,
1631
+ CommandBody: rawBody,
1632
+ // Clawpool inbound text is end-user chat content; do not parse it as OpenClaw slash/bang commands.
1633
+ BodyForCommands: "",
1634
+ From: from,
1635
+ To: to,
1636
+ SessionKey: route.sessionKey,
1637
+ AccountId: route.accountId,
1638
+ ChatType: chatType,
1639
+ ConversationLabel: fromLabel,
1640
+ SenderName: senderId || void 0,
1641
+ SenderId: senderId || void 0,
1642
+ CommandAuthorized: false,
1643
+ Provider: "clawpool",
1644
+ Surface: "clawpool",
1645
+ MessageSid: messageSid,
1646
+ // This field carries the inbound quoted message id from end user (event.quoted_message_id).
1647
+ // It is not the outbound reply anchor used when plugin sends replies back to Aibot.
1648
+ ReplyToMessageSid: quotedMessageId,
1649
+ OriginatingChannel: "clawpool",
1650
+ OriginatingTo: to
1651
+ });
1652
+ const routeSessionKey = ctxPayload.SessionKey ?? route.sessionKey;
1653
+ await core.channel.session.recordInboundSession({
1654
+ storePath,
1655
+ sessionKey: routeSessionKey,
1656
+ ctx: ctxPayload,
1657
+ onRecordError: (err) => runtime2.error(`clawpool session meta update failed: ${String(err)}`)
1658
+ });
1659
+ await bindSessionRouteMapping({
1660
+ client,
1661
+ account,
1662
+ runtime: runtime2,
1663
+ sessionId,
1664
+ routeSessionKey,
1665
+ statusSink: statusSink ? (patch) => statusSink({ lastError: patch.lastError }) : void 0
1666
+ });
1667
+ if (eventId) {
1668
+ try {
1669
+ client.ackEvent(eventId, {
1670
+ sessionId,
1671
+ msgId: messageSid,
1672
+ receivedAt: Date.now()
1673
+ });
1674
+ } catch (err) {
1675
+ runtime2.error(`[clawpool:${account.accountId}] event ack failed eventId=${eventId}: ${String(err)}`);
1676
+ statusSink?.({ lastError: String(err) });
1677
+ }
1678
+ }
1679
+ const outboundQuotedMessageId = normalizeNumericMessageId(event.msg_id);
1680
+ const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
1681
+ cfg: config,
1682
+ agentId: route.agentId,
1683
+ channel: "clawpool",
1684
+ accountId: account.accountId
1685
+ });
1686
+ const tableMode = core.channel.text.resolveMarkdownTableMode({
1687
+ cfg: config,
1688
+ channel: "clawpool",
1689
+ accountId: account.accountId
1690
+ });
1691
+ const streamClientMsgId = `reply_${messageSid}_stream`;
1692
+ const retryPolicy = resolveUpstreamRetryPolicy(account);
1693
+ let composingSet = false;
1694
+ const setComposing = (active) => {
1695
+ try {
1696
+ client.setSessionComposing(sessionId, active, {
1697
+ refEventId: eventId || void 0,
1698
+ refMsgId: outboundQuotedMessageId
1699
+ });
1700
+ composingSet = active;
1701
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
1702
+ } catch (err) {
1703
+ runtime2.error(
1704
+ `[clawpool:${account.accountId}] session activity update failed eventId=${eventId || "-"} sessionId=${sessionId} active=${active}: ${String(err)}`
1705
+ );
1706
+ statusSink?.({ lastError: String(err) });
1707
+ }
1708
+ };
1709
+ setComposing(true);
1710
+ try {
1711
+ for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) {
1712
+ let hasSentBlock = false;
1713
+ let outboundCounter = 0;
1714
+ let attemptHasOutbound = false;
1715
+ let retryGuardedText = null;
1716
+ const attemptLabel = `${attempt}/${retryPolicy.maxAttempts}`;
1717
+ const finishStreamIfNeeded = async () => {
1718
+ if (!hasSentBlock) {
1719
+ return;
1720
+ }
1721
+ hasSentBlock = false;
1722
+ try {
1723
+ const finishContext = buildEventLogContext({
1724
+ eventId,
1725
+ sessionId,
1726
+ messageSid,
1727
+ clientMsgId: streamClientMsgId
1728
+ });
1729
+ const finishDelayMs = resolveStreamFinishDelayMs(account);
1730
+ if (finishDelayMs > 0) {
1731
+ runtime2.log(
1732
+ `[clawpool:${account.accountId}] stream finish delay ${finishContext} delayMs=${finishDelayMs}`
1733
+ );
1734
+ await sleep(finishDelayMs);
1735
+ }
1736
+ runtime2.log(
1737
+ `[clawpool:${account.accountId}] stream finish ${finishContext}`
1738
+ );
1739
+ await client.sendStreamChunk(sessionId, "", {
1740
+ clientMsgId: streamClientMsgId,
1741
+ quotedMessageId: outboundQuotedMessageId,
1742
+ isFinish: true
1743
+ });
1744
+ attemptHasOutbound = true;
1745
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
1746
+ } catch (err) {
1747
+ runtime2.error(`[clawpool:${account.accountId}] stream finish failed: ${String(err)}`);
1748
+ statusSink?.({ lastError: String(err) });
1749
+ }
1750
+ };
1751
+ const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1752
+ ctx: ctxPayload,
1753
+ cfg: config,
1754
+ dispatcherOptions: {
1755
+ ...prefixOptions,
1756
+ deliver: async (payload, info) => {
1757
+ outboundCounter++;
1758
+ const outPayload = payload;
1759
+ const guardedText = guardInternalReplyText(String(outPayload.text ?? ""));
1760
+ const normalizedPayload = guardedText ? { ...outPayload, text: guardedText.userText } : outPayload;
1761
+ const hasMedia = Boolean(normalizedPayload.mediaUrl) || (normalizedPayload.mediaUrls?.length ?? 0) > 0;
1762
+ const text = core.channel.text.convertMarkdownTables(normalizedPayload.text ?? "", tableMode);
1763
+ const streamedTextAlreadyVisible = hasSentBlock;
1764
+ const deliverContext = buildEventLogContext({
1765
+ eventId,
1766
+ sessionId,
1767
+ messageSid,
1768
+ clientMsgId: info.kind === "block" ? streamClientMsgId : `reply_${messageSid}_${outboundCounter}`,
1769
+ outboundCounter
1770
+ });
1771
+ runtime2.log(
1772
+ `[clawpool:${account.accountId}] deliver ${deliverContext} kind=${info.kind} textLen=${text.length} hasMedia=${hasMedia} streamedBefore=${streamedTextAlreadyVisible}`
1773
+ );
1774
+ if (guardedText) {
1775
+ runtime2.error(
1776
+ `[clawpool:${account.accountId}] rewrite internal reply text ${deliverContext} code=${guardedText.code} raw=${JSON.stringify(guardedText.rawText)}`
1777
+ );
1778
+ }
1779
+ if (guardedText && retryGuardedText == null && isRetryableGuardedReply(guardedText) && !attemptHasOutbound && !hasSentBlock) {
1780
+ retryGuardedText = guardedText;
1781
+ runtime2.log(
1782
+ `[clawpool:${account.accountId}] defer guarded upstream reply for retry ${deliverContext} attempt=${attemptLabel} code=${guardedText.code}`
1783
+ );
1784
+ return;
1785
+ }
1786
+ if (retryGuardedText && !attemptHasOutbound && !hasSentBlock) {
1787
+ runtime2.log(
1788
+ `[clawpool:${account.accountId}] skip outbound while retry pending ${deliverContext} attempt=${attemptLabel} code=${retryGuardedText.code}`
1789
+ );
1790
+ return;
1791
+ }
1792
+ if (info.kind === "block" && !guardedText && !hasMedia && text) {
1793
+ const didSendBlock = await deliverAibotStreamBlock({
1794
+ text,
1795
+ client,
1796
+ account,
1797
+ sessionId,
1798
+ eventId,
1799
+ messageSid,
1800
+ quotedMessageId: outboundQuotedMessageId,
1801
+ clientMsgId: streamClientMsgId,
1802
+ runtime: runtime2,
1803
+ statusSink
1804
+ });
1805
+ hasSentBlock = hasSentBlock || didSendBlock;
1806
+ attemptHasOutbound = attemptHasOutbound || didSendBlock;
1807
+ return;
1808
+ }
1809
+ await finishStreamIfNeeded();
1810
+ if (info.kind === "final" && streamedTextAlreadyVisible && !hasMedia && text) {
1811
+ runtime2.log(
1812
+ `[clawpool:${account.accountId}] skip final text after streamed block ${deliverContext} textLen=${text.length}`
1813
+ );
1814
+ return;
1815
+ }
1816
+ const stableClientMsgId = `reply_${messageSid}_${outboundCounter}`;
1817
+ runtime2.log(
1818
+ `[clawpool:${account.accountId}] deliver message ${buildEventLogContext({
1819
+ eventId,
1820
+ sessionId,
1821
+ messageSid,
1822
+ clientMsgId: stableClientMsgId,
1823
+ outboundCounter
1824
+ })} textLen=${text.length} hasMedia=${hasMedia}`
1825
+ );
1826
+ const didSendMessage = await deliverAibotMessage({
1827
+ payload: normalizedPayload,
1828
+ client,
1829
+ account,
1830
+ sessionId,
1831
+ quotedMessageId: outboundQuotedMessageId,
1832
+ runtime: runtime2,
1833
+ statusSink,
1834
+ stableClientMsgId,
1835
+ tableMode
1836
+ });
1837
+ attemptHasOutbound = attemptHasOutbound || didSendMessage;
1838
+ },
1839
+ onError: (err, info) => {
1840
+ runtime2.error(`[clawpool:${account.accountId}] ${info.kind} reply failed: ${String(err)}`);
1841
+ statusSink?.({ lastError: String(err) });
1842
+ }
1843
+ },
1844
+ replyOptions: {
1845
+ onModelSelected
1846
+ }
1847
+ });
1848
+ runtime2.log(
1849
+ `[clawpool:${account.accountId}] dispatch complete ${baseLogContext} attempt=${attemptLabel} queuedFinal=${dispatchResult.queuedFinal} counts=${JSON.stringify(dispatchResult.counts)}`
1850
+ );
1851
+ await finishStreamIfNeeded();
1852
+ if (retryGuardedText && !attemptHasOutbound) {
1853
+ if (attempt < retryPolicy.maxAttempts) {
1854
+ const delayMs = resolveUpstreamRetryDelayMs(retryPolicy, attempt);
1855
+ runtime2.error(
1856
+ `[clawpool:${account.accountId}] upstream guarded reply retry ${baseLogContext} code=${retryGuardedText.code} attempt=${attemptLabel} next=${attempt + 1}/${retryPolicy.maxAttempts} delayMs=${delayMs}`
1857
+ );
1858
+ if (delayMs > 0) {
1859
+ await sleep(delayMs);
1860
+ }
1861
+ continue;
1862
+ }
1863
+ outboundCounter++;
1864
+ const stableClientMsgId = `reply_${messageSid}_${outboundCounter}`;
1865
+ runtime2.error(
1866
+ `[clawpool:${account.accountId}] upstream guarded reply retry exhausted ${baseLogContext} code=${retryGuardedText.code} attempts=${retryPolicy.maxAttempts}`
1867
+ );
1868
+ const didSendMessage = await deliverAibotMessage({
1869
+ payload: {
1870
+ text: retryGuardedText.userText
1871
+ },
1872
+ client,
1873
+ account,
1874
+ sessionId,
1875
+ quotedMessageId: outboundQuotedMessageId,
1876
+ runtime: runtime2,
1877
+ statusSink,
1878
+ stableClientMsgId,
1879
+ tableMode
1880
+ });
1881
+ attemptHasOutbound = attemptHasOutbound || didSendMessage;
1882
+ }
1883
+ break;
1884
+ }
1885
+ } finally {
1886
+ if (composingSet) {
1887
+ setComposing(false);
1888
+ }
1889
+ }
1890
+ }
1891
+ async function monitorAibotProvider(options) {
1892
+ const { account, config, runtime: runtime2, abortSignal, statusSink } = options;
1893
+ let client;
1894
+ const guardedStatusSink = (patch) => {
1895
+ if (!isActiveMonitor(account.accountId, client)) {
1896
+ return;
1897
+ }
1898
+ statusSink?.(patch);
1899
+ };
1900
+ client = new AibotWsClient(account, {
1901
+ logger: {
1902
+ info: (message) => runtime2.log(message),
1903
+ warn: (message) => runtime2.log(`[warn] ${message}`),
1904
+ error: (message) => runtime2.error(message),
1905
+ debug: (message) => runtime2.log(message)
1906
+ },
1907
+ onStatus: (status) => {
1908
+ guardedStatusSink({
1909
+ running: status.running,
1910
+ connected: status.connected,
1911
+ lastError: status.lastError,
1912
+ lastConnectAt: status.lastConnectAt ?? void 0,
1913
+ lastDisconnectAt: status.lastDisconnectAt ?? void 0
1914
+ });
1915
+ },
1916
+ onEventMsg: (event) => {
1917
+ if (!isActiveMonitor(account.accountId, client)) {
1918
+ return;
1919
+ }
1920
+ guardedStatusSink({ lastInboundAt: Date.now() });
1921
+ void processEvent({
1922
+ event,
1923
+ account,
1924
+ config,
1925
+ runtime: runtime2,
1926
+ client,
1927
+ statusSink: guardedStatusSink
1928
+ }).catch((err) => {
1929
+ if (!isActiveMonitor(account.accountId, client)) {
1930
+ return;
1931
+ }
1932
+ const msg = err instanceof Error ? err.message : String(err);
1933
+ runtime2.error(`[clawpool:${account.accountId}] process event failed: ${msg}`);
1934
+ guardedStatusSink({ lastError: msg });
1935
+ });
1936
+ }
1937
+ });
1938
+ const previousClient = registerActiveMonitor(account.accountId, client);
1939
+ if (previousClient) {
1940
+ runtime2.log(`[clawpool:${account.accountId}] stopping superseded clawpool monitor before restart`);
1941
+ previousClient.stop();
1942
+ }
1943
+ setActiveAibotClient(account.accountId, client);
1944
+ try {
1945
+ await client.start(abortSignal);
1946
+ } catch (err) {
1947
+ clearActiveAibotClient(account.accountId, client);
1948
+ clearActiveMonitor(account.accountId, client);
1949
+ throw err;
1950
+ }
1951
+ void client.waitUntilStopped().catch((err) => {
1952
+ if (!isActiveMonitor(account.accountId, client)) {
1953
+ return;
1954
+ }
1955
+ const msg = err instanceof Error ? err.message : String(err);
1956
+ runtime2.error(`[clawpool:${account.accountId}] background run loop failed: ${msg}`);
1957
+ guardedStatusSink({ lastError: msg });
1958
+ }).finally(() => {
1959
+ clearActiveAibotClient(account.accountId, client);
1960
+ clearActiveMonitor(account.accountId, client);
1961
+ });
1962
+ return {
1963
+ stop: () => {
1964
+ clearActiveAibotClient(account.accountId, client);
1965
+ clearActiveMonitor(account.accountId, client);
1966
+ client.stop();
1967
+ }
1968
+ };
1969
+ }
1970
+
1971
+ // src/onboarding.ts
1972
+ import { normalizeAccountId as normalizeAccountId2 } from "openclaw/plugin-sdk";
1973
+
1974
+ // src/setup-config.ts
1975
+ import {
1976
+ applyAccountNameToChannelSection,
1977
+ DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID2,
1978
+ migrateBaseNameToDefaultAccount
1979
+ } from "openclaw/plugin-sdk";
1980
+ function resolveSetupValues(input) {
1981
+ const apiKey = String(input.token ?? input.appToken ?? "").trim();
1982
+ const wsUrl = String(input.httpUrl ?? input.webhookUrl ?? input.url ?? "").trim();
1983
+ const agentId = String(input.userId ?? "").trim();
1984
+ return {
1985
+ apiKey: apiKey || void 0,
1986
+ wsUrl: wsUrl || void 0,
1987
+ agentId: agentId || void 0
1988
+ };
1989
+ }
1990
+ function applySetupAccountConfig(params) {
1991
+ const { cfg, accountId, name, values } = params;
1992
+ const namedConfig = applyAccountNameToChannelSection({
1993
+ cfg,
1994
+ channelKey: "clawpool",
1995
+ accountId,
1996
+ name
1997
+ });
1998
+ const next = accountId !== DEFAULT_ACCOUNT_ID2 ? migrateBaseNameToDefaultAccount({
1999
+ cfg: namedConfig,
2000
+ channelKey: "clawpool"
2001
+ }) : namedConfig;
2002
+ if (accountId === DEFAULT_ACCOUNT_ID2) {
2003
+ return {
2004
+ ...next,
2005
+ channels: {
2006
+ ...next.channels,
2007
+ clawpool: {
2008
+ ...next.channels?.clawpool,
2009
+ enabled: true,
2010
+ ...values.apiKey ? { apiKey: values.apiKey } : {},
2011
+ ...values.wsUrl ? { wsUrl: values.wsUrl } : {},
2012
+ ...values.agentId ? { agentId: values.agentId } : {}
2013
+ }
2014
+ }
2015
+ };
2016
+ }
2017
+ return {
2018
+ ...next,
2019
+ channels: {
2020
+ ...next.channels,
2021
+ clawpool: {
2022
+ ...next.channels?.clawpool,
2023
+ enabled: true,
2024
+ accounts: {
2025
+ ...next.channels?.clawpool?.accounts ?? {},
2026
+ [accountId]: {
2027
+ ...next.channels?.clawpool?.accounts?.[accountId],
2028
+ enabled: true,
2029
+ ...values.apiKey ? { apiKey: values.apiKey } : {},
2030
+ ...values.wsUrl ? { wsUrl: values.wsUrl } : {},
2031
+ ...values.agentId ? { agentId: values.agentId } : {}
2032
+ }
2033
+ }
2034
+ }
2035
+ }
2036
+ };
2037
+ }
2038
+
2039
+ // src/onboarding.ts
2040
+ var channel = "clawpool";
2041
+ function formatAccountLabel(accountId) {
2042
+ return accountId === "default" ? "default (primary)" : accountId;
2043
+ }
2044
+ function normalizeOptionalAccountId2(raw) {
2045
+ const value = String(raw ?? "").trim();
2046
+ if (!value) {
2047
+ return void 0;
2048
+ }
2049
+ return normalizeAccountId2(value);
2050
+ }
2051
+ function resolveSuggestedWsUrl(agentId) {
2052
+ const cleaned = String(agentId ?? "").trim();
2053
+ if (!cleaned) {
2054
+ return "ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=9992";
2055
+ }
2056
+ return `ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=${encodeURIComponent(cleaned)}`;
2057
+ }
2058
+ async function resolveAccountIdForConfigure(params) {
2059
+ const accountOverride = normalizeOptionalAccountId2(params.accountOverride);
2060
+ if (accountOverride) {
2061
+ return accountOverride;
2062
+ }
2063
+ const defaultAccountId = resolveDefaultAibotAccountId(params.cfg);
2064
+ const accountIds = listAibotAccountIds(params.cfg).filter(Boolean);
2065
+ if (!params.shouldPromptAccountIds || accountIds.length <= 1) {
2066
+ return defaultAccountId;
2067
+ }
2068
+ const selected = await params.prompter.select({
2069
+ message: "Select Clawpool account",
2070
+ options: accountIds.map((accountId) => ({
2071
+ value: accountId,
2072
+ label: formatAccountLabel(accountId)
2073
+ })),
2074
+ initialValue: defaultAccountId
2075
+ });
2076
+ return normalizeAccountId2(selected);
2077
+ }
2078
+ async function promptSetupGuide(prompter) {
2079
+ await prompter.note(
2080
+ [
2081
+ "Clawpool requires three values:",
2082
+ "1) wsUrl (Aibot Agent API ws endpoint)",
2083
+ "2) agentId",
2084
+ "3) apiKey",
2085
+ "Example wsUrl: ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=9992",
2086
+ "Env fallback: CLAWPOOL_WS_URL / CLAWPOOL_AGENT_ID / CLAWPOOL_API_KEY"
2087
+ ].join("\n"),
2088
+ "Clawpool setup"
2089
+ );
2090
+ }
2091
+ var clawpoolOnboardingAdapter = {
2092
+ channel,
2093
+ getStatus: async ({ cfg }) => {
2094
+ const configured = listAibotAccountIds(cfg).some(
2095
+ (accountId) => resolveAibotAccount({ cfg, accountId }).configured
2096
+ );
2097
+ return {
2098
+ channel,
2099
+ configured,
2100
+ statusLines: [`Clawpool: ${configured ? "configured" : "needs wsUrl/agentId/apiKey"}`],
2101
+ selectionHint: configured ? "configured" : "needs setup",
2102
+ quickstartScore: configured ? 2 : 1
2103
+ };
2104
+ },
2105
+ configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
2106
+ const accountId = await resolveAccountIdForConfigure({
2107
+ cfg,
2108
+ prompter,
2109
+ accountOverride: accountOverrides[channel],
2110
+ shouldPromptAccountIds
2111
+ });
2112
+ const current = resolveAibotAccount({ cfg, accountId });
2113
+ const accountConfigured = current.configured;
2114
+ if (accountConfigured) {
2115
+ const keep = await prompter.confirm({
2116
+ message: `Clawpool account "${formatAccountLabel(accountId)}" is already configured. Keep current settings?`,
2117
+ initialValue: true
2118
+ });
2119
+ if (keep) {
2120
+ return { cfg, accountId };
2121
+ }
2122
+ } else {
2123
+ await promptSetupGuide(prompter);
2124
+ }
2125
+ const initialAgentId = String(current.config.agentId ?? process.env.CLAWPOOL_AGENT_ID ?? "").trim();
2126
+ const initialWsUrl = String(current.config.wsUrl ?? process.env.CLAWPOOL_WS_URL ?? "").trim();
2127
+ const wsUrl = String(
2128
+ await prompter.text({
2129
+ message: "Clawpool wsUrl",
2130
+ initialValue: initialWsUrl || resolveSuggestedWsUrl(initialAgentId),
2131
+ placeholder: "ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=9992",
2132
+ validate: (value) => {
2133
+ const resolved = String(value ?? "").trim();
2134
+ if (!resolved) {
2135
+ return "Required";
2136
+ }
2137
+ if (!/^wss?:\/\//i.test(resolved)) {
2138
+ return "Must start with ws:// or wss://";
2139
+ }
2140
+ return void 0;
2141
+ }
2142
+ })
2143
+ ).trim();
2144
+ const agentId = String(
2145
+ await prompter.text({
2146
+ message: "Clawpool agentId",
2147
+ initialValue: initialAgentId || current.agentId,
2148
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
2149
+ })
2150
+ ).trim();
2151
+ const apiKey = String(
2152
+ await prompter.text({
2153
+ message: "Clawpool apiKey",
2154
+ initialValue: String(current.config.apiKey ?? process.env.CLAWPOOL_API_KEY ?? "").trim(),
2155
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
2156
+ })
2157
+ ).trim();
2158
+ const next = applySetupAccountConfig({
2159
+ cfg,
2160
+ accountId,
2161
+ values: {
2162
+ wsUrl,
2163
+ agentId,
2164
+ apiKey
2165
+ }
2166
+ });
2167
+ return {
2168
+ cfg: next,
2169
+ accountId
2170
+ };
2171
+ },
2172
+ disable: (cfg) => ({
2173
+ ...cfg,
2174
+ channels: {
2175
+ ...cfg.channels,
2176
+ clawpool: {
2177
+ ...cfg.channels?.clawpool,
2178
+ enabled: false
2179
+ }
2180
+ }
2181
+ })
2182
+ };
2183
+
2184
+ // src/target-resolver.ts
2185
+ var aibotSessionIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2186
+ function isAibotSessionID(value) {
2187
+ const normalized = String(value ?? "").trim();
2188
+ if (!normalized) {
2189
+ return false;
2190
+ }
2191
+ return aibotSessionIDPattern.test(normalized);
2192
+ }
2193
+ function normalizeAibotSessionTarget2(raw) {
2194
+ const trimmed = String(raw ?? "").trim();
2195
+ if (!trimmed) {
2196
+ return "";
2197
+ }
2198
+ return trimmed.replace(/^clawpool:/i, "").replace(/^session:/i, "").trim();
2199
+ }
2200
+ function buildRouteSessionKeyCandidates(rawTarget, normalizedTarget) {
2201
+ if (rawTarget === normalizedTarget) {
2202
+ return [rawTarget];
2203
+ }
2204
+ return [rawTarget, normalizedTarget].filter((candidate) => candidate.length > 0);
2205
+ }
2206
+ async function resolveAibotOutboundTarget(params) {
2207
+ const rawTarget = String(params.to ?? "").trim();
2208
+ if (!rawTarget) {
2209
+ throw new Error("clawpool outbound target must be non-empty");
2210
+ }
2211
+ const normalizedTarget = normalizeAibotSessionTarget2(rawTarget);
2212
+ if (!normalizedTarget) {
2213
+ throw new Error("clawpool outbound target must contain session_id or route_session_key");
2214
+ }
2215
+ if (isAibotSessionID(normalizedTarget)) {
2216
+ return {
2217
+ sessionId: normalizedTarget,
2218
+ rawTarget,
2219
+ normalizedTarget,
2220
+ resolveSource: "direct"
2221
+ };
2222
+ }
2223
+ if (/^\d+$/.test(normalizedTarget)) {
2224
+ throw new Error(
2225
+ `clawpool outbound target "${rawTarget}" is numeric; expected session_id(UUID) or route.sessionKey`
2226
+ );
2227
+ }
2228
+ const routeSessionKeyCandidates = buildRouteSessionKeyCandidates(rawTarget, normalizedTarget);
2229
+ let lastResolveError = null;
2230
+ for (const routeSessionKey of routeSessionKeyCandidates) {
2231
+ try {
2232
+ const ack = await params.client.resolveSessionRoute("clawpool", params.accountId, routeSessionKey);
2233
+ const sessionId = String(ack.session_id ?? "").trim();
2234
+ if (!isAibotSessionID(sessionId)) {
2235
+ throw new Error(
2236
+ `session_route_resolve returned invalid session_id for route_session_key="${routeSessionKey}"`
2237
+ );
2238
+ }
2239
+ return {
2240
+ sessionId,
2241
+ rawTarget,
2242
+ normalizedTarget,
2243
+ resolveSource: "sessionRouteMap"
2244
+ };
2245
+ } catch (err) {
2246
+ lastResolveError = err instanceof Error ? err : new Error(String(err));
2247
+ }
2248
+ }
2249
+ if (lastResolveError) {
2250
+ throw new Error(
2251
+ `clawpool outbound target resolve failed target="${rawTarget}" accountId=${params.accountId}: ${lastResolveError.message}`
2252
+ );
2253
+ }
2254
+ throw new Error(`clawpool outbound target resolve failed target="${rawTarget}" accountId=${params.accountId}`);
2255
+ }
2256
+
2257
+ // src/channel.ts
2258
+ var meta = {
2259
+ id: "clawpool",
2260
+ label: "Clawpool",
2261
+ selectionLabel: "Clawpool",
2262
+ blurb: "Bridge OpenClaw to Clawpool over the Aibot Agent API WebSocket.",
2263
+ aliases: ["cp", "clowpool"],
2264
+ order: 90
2265
+ };
2266
+ function normalizeQuotedMessageId(rawInput) {
2267
+ const raw = String(rawInput ?? "").trim();
2268
+ if (!raw) {
2269
+ return void 0;
2270
+ }
2271
+ if (/^\d+$/.test(raw)) {
2272
+ return raw;
2273
+ }
2274
+ const parsed = raw.split(":").at(-1)?.trim() ?? "";
2275
+ if (/^\d+$/.test(parsed)) {
2276
+ return parsed;
2277
+ }
2278
+ return void 0;
2279
+ }
2280
+ function logAibotOutboundAdapter(message) {
2281
+ console.info(`[clawpool:outbound] ${message}`);
2282
+ }
2283
+ function asAibotChannelConfig(cfg) {
2284
+ return cfg.channels?.clawpool ?? {};
2285
+ }
2286
+ function buildAccountSnapshot(params) {
2287
+ const { account, runtime: runtime2 } = params;
2288
+ return {
2289
+ accountId: account.accountId,
2290
+ name: account.name,
2291
+ enabled: account.enabled,
2292
+ configured: account.configured,
2293
+ running: runtime2?.running ?? false,
2294
+ connected: runtime2?.connected ?? false,
2295
+ lastError: runtime2?.lastError ?? null,
2296
+ lastStartAt: runtime2?.lastStartAt ?? null,
2297
+ lastStopAt: runtime2?.lastStopAt ?? null,
2298
+ lastInboundAt: runtime2?.lastInboundAt ?? null,
2299
+ lastOutboundAt: runtime2?.lastOutboundAt ?? null,
2300
+ dmPolicy: account.config.dmPolicy ?? "open",
2301
+ tokenSource: account.apiKey ? "config" : "none"
2302
+ };
2303
+ }
2304
+ var AibotConfigSchema = {
2305
+ type: "object",
2306
+ additionalProperties: true,
2307
+ properties: {}
2308
+ };
2309
+ var aibotPlugin = {
2310
+ id: "clawpool",
2311
+ meta,
2312
+ onboarding: clawpoolOnboardingAdapter,
2313
+ capabilities: {
2314
+ chatTypes: ["direct", "group"],
2315
+ media: true,
2316
+ reactions: true,
2317
+ unsend: true,
2318
+ threads: false,
2319
+ polls: false,
2320
+ nativeCommands: false,
2321
+ blockStreaming: true
2322
+ },
2323
+ actions: aibotMessageActions,
2324
+ reload: {
2325
+ configPrefixes: ["channels.clawpool"]
2326
+ },
2327
+ configSchema: {
2328
+ schema: AibotConfigSchema
2329
+ },
2330
+ config: {
2331
+ listAccountIds: (cfg) => listAibotAccountIds(cfg),
2332
+ resolveAccount: (cfg, accountId) => resolveAibotAccount({ cfg, accountId }),
2333
+ defaultAccountId: (cfg) => resolveDefaultAibotAccountId(cfg),
2334
+ setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
2335
+ cfg,
2336
+ sectionKey: "clawpool",
2337
+ accountId,
2338
+ enabled,
2339
+ allowTopLevel: true
2340
+ }),
2341
+ deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
2342
+ cfg,
2343
+ sectionKey: "clawpool",
2344
+ accountId,
2345
+ clearBaseFields: [
2346
+ "name",
2347
+ "wsUrl",
2348
+ "agentId",
2349
+ "apiKey",
2350
+ "reconnectMs",
2351
+ "reconnectMaxMs",
2352
+ "reconnectStableMs",
2353
+ "connectTimeoutMs",
2354
+ "keepalivePingMs",
2355
+ "keepaliveTimeoutMs",
2356
+ "upstreamRetryMaxAttempts",
2357
+ "upstreamRetryBaseDelayMs",
2358
+ "upstreamRetryMaxDelayMs",
2359
+ "maxChunkChars",
2360
+ "dmPolicy",
2361
+ "allowFrom",
2362
+ "defaultTo"
2363
+ ]
2364
+ }),
2365
+ isConfigured: (account) => account.configured,
2366
+ describeAccount: (account, cfg) => {
2367
+ const root = asAibotChannelConfig(cfg);
2368
+ return {
2369
+ accountId: account.accountId,
2370
+ name: account.name,
2371
+ enabled: account.enabled,
2372
+ configured: account.configured,
2373
+ running: false,
2374
+ connected: false,
2375
+ lastError: account.configured ? null : "missing wsUrl/agentId/apiKey (or CLAWPOOL_WS_URL/CLAWPOOL_AGENT_ID/CLAWPOOL_API_KEY)",
2376
+ dmPolicy: account.config.dmPolicy ?? "open",
2377
+ tokenSource: account.apiKey ? "config" : "none",
2378
+ mode: "block_streaming",
2379
+ baseUrl: redactAibotWsUrl(account.wsUrl),
2380
+ allowFrom: account.config.allowFrom?.map((entry) => String(entry).trim()).filter(Boolean) ?? [],
2381
+ nameSource: root.accounts?.[account.accountId]?.name ? "account" : "base"
2382
+ };
2383
+ },
2384
+ resolveAllowFrom: ({ cfg, accountId }) => resolveAibotAccount({ cfg, accountId }).config.allowFrom?.map((entry) => String(entry)) ?? [],
2385
+ formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
2386
+ resolveDefaultTo: ({ cfg, accountId }) => resolveAibotAccount({ cfg, accountId }).config.defaultTo?.trim() || void 0
2387
+ },
2388
+ setup: {
2389
+ resolveAccountId: ({ accountId }) => normalizeAccountId3(accountId),
2390
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection2({
2391
+ cfg,
2392
+ channelKey: "clawpool",
2393
+ accountId,
2394
+ name
2395
+ }),
2396
+ validateInput: ({ input }) => {
2397
+ const values = resolveSetupValues(input);
2398
+ const hasAny = Boolean(values.apiKey || values.wsUrl || values.agentId);
2399
+ if (!hasAny) {
2400
+ return "clawpool setup requires at least one of: --token(api key), --http-url(ws url), --user-id(agent id)";
2401
+ }
2402
+ return null;
2403
+ },
2404
+ applyAccountConfig: ({ cfg, accountId, input }) => {
2405
+ const values = resolveSetupValues(input);
2406
+ return applySetupAccountConfig({
2407
+ cfg,
2408
+ accountId,
2409
+ name: input.name,
2410
+ values
2411
+ });
2412
+ }
2413
+ },
2414
+ security: {
2415
+ resolveDmPolicy: ({ cfg, accountId, account }) => {
2416
+ const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID3;
2417
+ const isAccountScoped = Boolean(cfg.channels?.clawpool?.accounts?.[resolvedAccountId]);
2418
+ const basePath = isAccountScoped ? `channels.clawpool.accounts.${resolvedAccountId}.` : "channels.clawpool.";
2419
+ return {
2420
+ policy: account.config.dmPolicy ?? "open",
2421
+ allowFrom: account.config.allowFrom ?? [],
2422
+ policyPath: `${basePath}dmPolicy`,
2423
+ allowFromPath: basePath,
2424
+ approveHint: formatPairingApproveHint("clawpool")
2425
+ };
2426
+ }
2427
+ },
2428
+ messaging: {
2429
+ normalizeTarget: (raw) => normalizeAibotSessionTarget(raw),
2430
+ targetResolver: {
2431
+ looksLikeId: (raw) => Boolean(normalizeAibotSessionTarget(raw)),
2432
+ hint: "<session_id|route.sessionKey>"
2433
+ }
2434
+ },
2435
+ threading: {
2436
+ buildToolContext: ({ context, hasRepliedRef }) => ({
2437
+ currentChannelId: context.To?.trim() || void 0,
2438
+ currentMessageId: context.CurrentMessageId != null ? String(context.CurrentMessageId) : void 0,
2439
+ hasRepliedRef
2440
+ })
2441
+ },
2442
+ agentPrompt: {
2443
+ messageToolHints: () => [
2444
+ "- Clawpool can only unsend the agent's own previously sent messages. Use `action=unsend` with `messageId`; omit `sessionId`/`to` to target the current Clawpool chat."
2445
+ ]
2446
+ },
2447
+ outbound: {
2448
+ deliveryMode: "direct",
2449
+ chunker: chunkTextForOutbound,
2450
+ chunkerMode: "markdown",
2451
+ textChunkLimit: 1200,
2452
+ sendText: async ({ cfg, to, text, accountId, replyToId }) => {
2453
+ const account = resolveAibotAccount({ cfg, accountId });
2454
+ const client = requireActiveAibotClient(account.accountId);
2455
+ const rawTarget = String(to ?? "").trim() || "-";
2456
+ logAibotOutboundAdapter(
2457
+ `sendText target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
2458
+ );
2459
+ let resolvedTarget;
2460
+ try {
2461
+ resolvedTarget = await resolveAibotOutboundTarget({
2462
+ client,
2463
+ accountId: account.accountId,
2464
+ to
2465
+ });
2466
+ } catch (err) {
2467
+ logAibotOutboundAdapter(
2468
+ `sendText target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
2469
+ );
2470
+ throw err;
2471
+ }
2472
+ const sessionId = resolvedTarget.sessionId;
2473
+ const quotedMessageId = normalizeQuotedMessageId(replyToId);
2474
+ logAibotOutboundAdapter(
2475
+ `sendText accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${text.length} quotedMessageId=${quotedMessageId ?? "-"}`
2476
+ );
2477
+ const ack = await client.sendText(sessionId, text, {
2478
+ quotedMessageId
2479
+ });
2480
+ logAibotOutboundAdapter(
2481
+ `sendText ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")}`
2482
+ );
2483
+ return {
2484
+ channel: "clawpool",
2485
+ messageId: String(ack.msg_id ?? ack.client_msg_id ?? Date.now())
2486
+ };
2487
+ },
2488
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => {
2489
+ const account = resolveAibotAccount({ cfg, accountId });
2490
+ const client = requireActiveAibotClient(account.accountId);
2491
+ const rawTarget = String(to ?? "").trim() || "-";
2492
+ logAibotOutboundAdapter(
2493
+ `sendMedia target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
2494
+ );
2495
+ let resolvedTarget;
2496
+ try {
2497
+ resolvedTarget = await resolveAibotOutboundTarget({
2498
+ client,
2499
+ accountId: account.accountId,
2500
+ to
2501
+ });
2502
+ } catch (err) {
2503
+ logAibotOutboundAdapter(
2504
+ `sendMedia target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
2505
+ );
2506
+ throw err;
2507
+ }
2508
+ const sessionId = resolvedTarget.sessionId;
2509
+ if (!mediaUrl) {
2510
+ throw new Error("clawpool sendMedia requires mediaUrl");
2511
+ }
2512
+ const quotedMessageId = normalizeQuotedMessageId(replyToId);
2513
+ logAibotOutboundAdapter(
2514
+ `sendMedia accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${(text ?? "").length} quotedMessageId=${quotedMessageId ?? "-"} mediaUrl=${mediaUrl}`
2515
+ );
2516
+ const ack = await client.sendMedia(sessionId, mediaUrl, text ?? "", {
2517
+ quotedMessageId
2518
+ });
2519
+ logAibotOutboundAdapter(
2520
+ `sendMedia ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")}`
2521
+ );
2522
+ return {
2523
+ channel: "clawpool",
2524
+ messageId: String(ack.msg_id ?? ack.client_msg_id ?? Date.now())
2525
+ };
2526
+ }
2527
+ },
2528
+ status: {
2529
+ defaultRuntime: {
2530
+ accountId: DEFAULT_ACCOUNT_ID3,
2531
+ running: false,
2532
+ connected: false,
2533
+ lastError: null,
2534
+ lastStartAt: null,
2535
+ lastStopAt: null,
2536
+ lastInboundAt: null,
2537
+ lastOutboundAt: null
2538
+ },
2539
+ buildChannelSummary: ({ snapshot }) => ({
2540
+ configured: snapshot.configured ?? false,
2541
+ running: snapshot.running ?? false,
2542
+ connected: snapshot.connected ?? false,
2543
+ lastError: snapshot.lastError ?? null,
2544
+ lastInboundAt: snapshot.lastInboundAt ?? null,
2545
+ lastOutboundAt: snapshot.lastOutboundAt ?? null
2546
+ }),
2547
+ buildAccountSnapshot: ({ account, runtime: runtime2 }) => buildAccountSnapshot({ account, runtime: runtime2 }),
2548
+ collectStatusIssues: (accounts) => accounts.flatMap((account) => {
2549
+ if (!account.enabled) {
2550
+ return [];
2551
+ }
2552
+ if (!account.configured) {
2553
+ return [
2554
+ {
2555
+ channel: "clawpool",
2556
+ accountId: account.accountId,
2557
+ kind: "config",
2558
+ message: "Clawpool account is not configured. Set wsUrl/agentId/apiKey (or CLAWPOOL_WS_URL/CLAWPOOL_AGENT_ID/CLAWPOOL_API_KEY)."
2559
+ }
2560
+ ];
2561
+ }
2562
+ if (account.running && !account.connected) {
2563
+ return [
2564
+ {
2565
+ channel: "clawpool",
2566
+ accountId: account.accountId,
2567
+ kind: "runtime",
2568
+ message: "Clawpool channel is running but not connected."
2569
+ }
2570
+ ];
2571
+ }
2572
+ if (typeof account.lastError === "string" && account.lastError.trim()) {
2573
+ return [
2574
+ {
2575
+ channel: "clawpool",
2576
+ accountId: account.accountId,
2577
+ kind: "runtime",
2578
+ message: account.lastError
2579
+ }
2580
+ ];
2581
+ }
2582
+ return [];
2583
+ })
2584
+ },
2585
+ gateway: {
2586
+ startAccount: async (ctx) => {
2587
+ const account = ctx.account;
2588
+ if (!account.configured) {
2589
+ throw new Error(
2590
+ `clawpool account "${account.accountId}" not configured: require wsUrl + agentId + apiKey`
2591
+ );
2592
+ }
2593
+ ctx.log?.info?.(
2594
+ `[${account.accountId}] starting clawpool monitor (${redactAibotWsUrl(account.wsUrl)})`
2595
+ );
2596
+ ctx.setStatus({
2597
+ ...ctx.getStatus(),
2598
+ running: true,
2599
+ connected: false,
2600
+ lastError: null,
2601
+ lastStartAt: Date.now()
2602
+ });
2603
+ const monitor = await monitorAibotProvider({
2604
+ account,
2605
+ config: ctx.cfg,
2606
+ runtime: ctx.runtime,
2607
+ abortSignal: ctx.abortSignal,
2608
+ statusSink: (patch) => {
2609
+ ctx.setStatus({
2610
+ ...ctx.getStatus(),
2611
+ ...patch
2612
+ });
2613
+ }
2614
+ });
2615
+ try {
2616
+ await waitUntilAbort(ctx.abortSignal);
2617
+ } finally {
2618
+ monitor.stop();
2619
+ ctx.setStatus({
2620
+ ...ctx.getStatus(),
2621
+ running: false,
2622
+ connected: false,
2623
+ lastStopAt: Date.now()
2624
+ });
2625
+ }
2626
+ },
2627
+ stopAccount: async (ctx) => {
2628
+ const client = getActiveAibotClient(ctx.accountId);
2629
+ client?.stop();
2630
+ ctx.setStatus({
2631
+ ...ctx.getStatus(),
2632
+ running: false,
2633
+ connected: false,
2634
+ lastStopAt: Date.now()
2635
+ });
2636
+ }
2637
+ }
2638
+ };
2639
+
2640
+ // index.ts
2641
+ var plugin = {
2642
+ id: "clawpool",
2643
+ name: "Clawpool",
2644
+ description: "Clawpool channel plugin backed by Aibot Agent API",
2645
+ configSchema: emptyPluginConfigSchema(),
2646
+ register(api) {
2647
+ setAibotRuntime(api.runtime);
2648
+ api.registerChannel({ plugin: aibotPlugin });
2649
+ }
2650
+ };
2651
+ var index_default = plugin;
2652
+ export {
2653
+ index_default as default
2654
+ };