@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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/index.js +2654 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +68 -0
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
|
+
};
|