@dhfpub/clawpool-openclaw 0.4.1
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 +305 -0
- package/dist/index.js +4667 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +82 -0
- package/skills/clawpool-auth-access/SKILL.md +233 -0
- package/skills/clawpool-auth-access/agents/openai.yaml +4 -0
- package/skills/clawpool-auth-access/references/api-contract.md +135 -0
- package/skills/clawpool-auth-access/references/clawpool-concepts.md +29 -0
- package/skills/clawpool-auth-access/references/openclaw-setup.md +154 -0
- package/skills/clawpool-auth-access/references/user-replies.md +29 -0
- package/skills/clawpool-auth-access/scripts/clawpool_auth.py +1550 -0
- package/skills/message-send/SKILL.md +225 -0
- package/skills/message-unsend/SKILL.md +242 -0
- package/skills/message-unsend/flowchart.mermaid +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4667 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
3
|
+
|
|
4
|
+
// src/channel.ts
|
|
5
|
+
import {
|
|
6
|
+
applyAccountNameToChannelSection as applyAccountNameToChannelSection2,
|
|
7
|
+
deleteAccountFromConfigSection,
|
|
8
|
+
formatPairingApproveHint,
|
|
9
|
+
setAccountEnabledInConfigSection
|
|
10
|
+
} from "openclaw/plugin-sdk/core";
|
|
11
|
+
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
12
|
+
|
|
13
|
+
// src/account-id.ts
|
|
14
|
+
var DEFAULT_ACCOUNT_ID = "default";
|
|
15
|
+
function normalizeOptionalAccountId(raw) {
|
|
16
|
+
const value = String(raw ?? "").trim();
|
|
17
|
+
if (!value) {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function normalizeAccountId(raw) {
|
|
23
|
+
return normalizeOptionalAccountId(raw) ?? DEFAULT_ACCOUNT_ID;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/actions.ts
|
|
27
|
+
import {
|
|
28
|
+
jsonResult,
|
|
29
|
+
readStringParam
|
|
30
|
+
} from "openclaw/plugin-sdk/channel-runtime";
|
|
31
|
+
|
|
32
|
+
// src/accounts.ts
|
|
33
|
+
function rawAibotConfig(cfg) {
|
|
34
|
+
return cfg.channels?.clawpool ?? {};
|
|
35
|
+
}
|
|
36
|
+
function listConfiguredAccountIds(cfg) {
|
|
37
|
+
const accounts = rawAibotConfig(cfg).accounts;
|
|
38
|
+
if (!accounts || typeof accounts !== "object") {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return Object.keys(accounts).filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
function listAibotAccountIds(cfg) {
|
|
44
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
45
|
+
if (ids.length === 0) {
|
|
46
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
47
|
+
}
|
|
48
|
+
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
49
|
+
}
|
|
50
|
+
function resolveDefaultAibotAccountId(cfg) {
|
|
51
|
+
const aibotCfg = rawAibotConfig(cfg);
|
|
52
|
+
const preferred = normalizeOptionalAccountId(aibotCfg.defaultAccount);
|
|
53
|
+
if (preferred && listAibotAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)) {
|
|
54
|
+
return preferred;
|
|
55
|
+
}
|
|
56
|
+
const ids = listAibotAccountIds(cfg);
|
|
57
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
58
|
+
return DEFAULT_ACCOUNT_ID;
|
|
59
|
+
}
|
|
60
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
61
|
+
}
|
|
62
|
+
function resolveAccountRawConfig(cfg, accountId) {
|
|
63
|
+
const aibotCfg = rawAibotConfig(cfg);
|
|
64
|
+
const { accounts: _ignoredAccounts, defaultAccount: _ignoredDefault, ...base } = aibotCfg;
|
|
65
|
+
const account = aibotCfg.accounts?.[accountId] ?? {};
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
...account
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function normalizeNonEmpty(value) {
|
|
72
|
+
const s = String(value ?? "").trim();
|
|
73
|
+
return s;
|
|
74
|
+
}
|
|
75
|
+
function normalizeAgentId(value) {
|
|
76
|
+
return normalizeNonEmpty(value);
|
|
77
|
+
}
|
|
78
|
+
function appendAgentIdToWsUrl(rawWsUrl, agentId) {
|
|
79
|
+
if (!rawWsUrl) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
const direct = rawWsUrl.replaceAll("{agent_id}", encodeURIComponent(agentId));
|
|
83
|
+
if (!agentId) {
|
|
84
|
+
return direct;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(direct);
|
|
88
|
+
if (!parsed.searchParams.get("agent_id")) {
|
|
89
|
+
parsed.searchParams.set("agent_id", agentId);
|
|
90
|
+
}
|
|
91
|
+
return parsed.toString();
|
|
92
|
+
} catch {
|
|
93
|
+
if (direct.includes("agent_id=")) {
|
|
94
|
+
return direct;
|
|
95
|
+
}
|
|
96
|
+
return direct.includes("?") ? `${direct}&agent_id=${encodeURIComponent(agentId)}` : `${direct}?agent_id=${encodeURIComponent(agentId)}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function resolveWsUrl(merged, agentId) {
|
|
100
|
+
const envWs = normalizeNonEmpty(process.env.CLAWPOOL_WS_URL);
|
|
101
|
+
const cfgWs = normalizeNonEmpty(merged.wsUrl);
|
|
102
|
+
const ws = cfgWs || envWs;
|
|
103
|
+
if (ws) {
|
|
104
|
+
return appendAgentIdToWsUrl(ws, agentId);
|
|
105
|
+
}
|
|
106
|
+
if (!agentId) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
return `ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=${encodeURIComponent(agentId)}`;
|
|
110
|
+
}
|
|
111
|
+
function redactAibotWsUrl(wsUrl) {
|
|
112
|
+
if (!wsUrl) {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const parsed = new URL(wsUrl);
|
|
117
|
+
if (parsed.searchParams.has("agent_id")) {
|
|
118
|
+
parsed.searchParams.set("agent_id", "***");
|
|
119
|
+
}
|
|
120
|
+
return parsed.toString();
|
|
121
|
+
} catch {
|
|
122
|
+
return wsUrl.replace(/(agent_id=)[^&]+/g, "$1***");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function resolveAibotAccount(params) {
|
|
126
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
127
|
+
const merged = resolveAccountRawConfig(params.cfg, accountId);
|
|
128
|
+
const baseEnabled = rawAibotConfig(params.cfg).enabled !== false;
|
|
129
|
+
const accountEnabled = merged.enabled !== false;
|
|
130
|
+
const enabled = baseEnabled && accountEnabled;
|
|
131
|
+
const agentId = normalizeAgentId(merged.agentId || process.env.CLAWPOOL_AGENT_ID);
|
|
132
|
+
const apiKey = normalizeNonEmpty(merged.apiKey || process.env.CLAWPOOL_API_KEY);
|
|
133
|
+
const wsUrl = resolveWsUrl(merged, agentId);
|
|
134
|
+
const configured = Boolean(wsUrl && agentId && apiKey);
|
|
135
|
+
return {
|
|
136
|
+
accountId,
|
|
137
|
+
name: normalizeNonEmpty(merged.name) || void 0,
|
|
138
|
+
enabled,
|
|
139
|
+
configured,
|
|
140
|
+
wsUrl,
|
|
141
|
+
agentId,
|
|
142
|
+
apiKey,
|
|
143
|
+
config: merged
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function normalizeAibotSessionTarget(raw) {
|
|
147
|
+
const trimmed = String(raw ?? "").trim();
|
|
148
|
+
if (!trimmed) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
return trimmed.replace(/^clawpool:/i, "").replace(/^session:/i, "").trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/client.ts
|
|
155
|
+
import { randomUUID } from "node:crypto";
|
|
156
|
+
|
|
157
|
+
// src/protocol-send.ts
|
|
158
|
+
var AIBOT_PROTOCOL_SEND_RATE_LIMIT = 8;
|
|
159
|
+
var AIBOT_PROTOCOL_SEND_RATE_WINDOW_MS = 1e4;
|
|
160
|
+
var AIBOT_PROTOCOL_SEND_RETRYABLE_CODE = 4008;
|
|
161
|
+
var AIBOT_PROTOCOL_SEND_RETRY_MAX_ATTEMPTS = 3;
|
|
162
|
+
var AIBOT_PROTOCOL_SEND_RETRY_BASE_DELAY_MS = 600;
|
|
163
|
+
var AIBOT_PROTOCOL_SEND_RETRY_MAX_DELAY_MS = 2e3;
|
|
164
|
+
var AIBOT_PROTOCOL_SEND_RATE_SAFETY_DELAY_MS = 100;
|
|
165
|
+
function isRetryableAibotSendCode(code) {
|
|
166
|
+
return Number(code) === AIBOT_PROTOCOL_SEND_RETRYABLE_CODE;
|
|
167
|
+
}
|
|
168
|
+
function resolveAibotSendRetryMaxAttempts() {
|
|
169
|
+
return AIBOT_PROTOCOL_SEND_RETRY_MAX_ATTEMPTS;
|
|
170
|
+
}
|
|
171
|
+
function resolveAibotSendRetryDelayMs(attempt) {
|
|
172
|
+
const normalizedAttempt = Math.max(1, Math.floor(attempt));
|
|
173
|
+
const multiplier = 2 ** Math.max(0, normalizedAttempt - 1);
|
|
174
|
+
return Math.min(
|
|
175
|
+
AIBOT_PROTOCOL_SEND_RETRY_MAX_DELAY_MS,
|
|
176
|
+
AIBOT_PROTOCOL_SEND_RETRY_BASE_DELAY_MS * multiplier
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function pruneAibotSendWindow(sentAtMs, nowMs) {
|
|
180
|
+
return sentAtMs.filter((value) => nowMs - value < AIBOT_PROTOCOL_SEND_RATE_WINDOW_MS);
|
|
181
|
+
}
|
|
182
|
+
function computeAibotSendThrottleDelayMs(sentAtMs, nowMs) {
|
|
183
|
+
const recent = pruneAibotSendWindow(sentAtMs, nowMs);
|
|
184
|
+
if (recent.length < AIBOT_PROTOCOL_SEND_RATE_LIMIT) {
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
const earliest = recent[0] ?? nowMs;
|
|
188
|
+
return Math.max(1, earliest + AIBOT_PROTOCOL_SEND_RATE_WINDOW_MS - nowMs + AIBOT_PROTOCOL_SEND_RATE_SAFETY_DELAY_MS);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/protocol-text.ts
|
|
192
|
+
var AIBOT_PROTOCOL_MAX_RUNES = 2e3;
|
|
193
|
+
var AIBOT_PROTOCOL_MAX_BYTES = 12 * 1024;
|
|
194
|
+
var DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT = 1200;
|
|
195
|
+
var DEFAULT_STREAM_CHUNK_LIMIT = 48;
|
|
196
|
+
function clampPositiveInt(value, fallback) {
|
|
197
|
+
const parsed = Number(value);
|
|
198
|
+
if (!Number.isFinite(parsed)) {
|
|
199
|
+
return fallback;
|
|
200
|
+
}
|
|
201
|
+
return Math.max(1, Math.floor(parsed));
|
|
202
|
+
}
|
|
203
|
+
function resolveOutboundTextChunkLimit(value) {
|
|
204
|
+
return Math.min(
|
|
205
|
+
AIBOT_PROTOCOL_MAX_RUNES,
|
|
206
|
+
clampPositiveInt(value, DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
function resolveStreamTextChunkLimit(value) {
|
|
210
|
+
return Math.min(
|
|
211
|
+
AIBOT_PROTOCOL_MAX_RUNES,
|
|
212
|
+
clampPositiveInt(value, DEFAULT_STREAM_CHUNK_LIMIT)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function splitTextForAibotProtocol(text, preferredRunes) {
|
|
216
|
+
const source = String(text ?? "");
|
|
217
|
+
if (!source) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const runeLimit = Math.min(AIBOT_PROTOCOL_MAX_RUNES, Math.max(1, Math.floor(preferredRunes)));
|
|
221
|
+
const chunks = [];
|
|
222
|
+
let current = "";
|
|
223
|
+
let currentRunes = 0;
|
|
224
|
+
let currentBytes = 0;
|
|
225
|
+
for (const rune of source) {
|
|
226
|
+
const runeBytes = Buffer.byteLength(rune, "utf8");
|
|
227
|
+
const nextRunes = currentRunes + 1;
|
|
228
|
+
const nextBytes = currentBytes + runeBytes;
|
|
229
|
+
const exceedPreferredRunes = nextRunes > runeLimit;
|
|
230
|
+
const exceedProtocolBytes = nextBytes > AIBOT_PROTOCOL_MAX_BYTES;
|
|
231
|
+
if (current && (exceedPreferredRunes || exceedProtocolBytes)) {
|
|
232
|
+
chunks.push(current);
|
|
233
|
+
current = "";
|
|
234
|
+
currentRunes = 0;
|
|
235
|
+
currentBytes = 0;
|
|
236
|
+
}
|
|
237
|
+
current += rune;
|
|
238
|
+
currentRunes += 1;
|
|
239
|
+
currentBytes += runeBytes;
|
|
240
|
+
}
|
|
241
|
+
if (current) {
|
|
242
|
+
chunks.push(current);
|
|
243
|
+
}
|
|
244
|
+
return chunks;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/client.ts
|
|
248
|
+
var DEFAULT_RECONNECT_BASE_MS = 2e3;
|
|
249
|
+
var DEFAULT_RECONNECT_MAX_MS = 3e4;
|
|
250
|
+
var DEFAULT_RECONNECT_STABLE_MS = 3e4;
|
|
251
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
252
|
+
var DEFAULT_HEARTBEAT_SEC = 30;
|
|
253
|
+
function buildAuthPayload(account) {
|
|
254
|
+
return {
|
|
255
|
+
agent_id: account.agentId,
|
|
256
|
+
api_key: account.apiKey,
|
|
257
|
+
client: "openclaw-clawpool",
|
|
258
|
+
client_type: "openclaw"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
var AibotPacketError = class extends Error {
|
|
262
|
+
cmd;
|
|
263
|
+
code;
|
|
264
|
+
constructor(cmd, code, message) {
|
|
265
|
+
super(`clawpool ${cmd}: code=${code} msg=${message}`);
|
|
266
|
+
this.name = "AibotPacketError";
|
|
267
|
+
this.cmd = cmd;
|
|
268
|
+
this.code = code;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
function clampInt(value, fallback, min, max) {
|
|
272
|
+
const n = Number(value);
|
|
273
|
+
if (!Number.isFinite(n)) {
|
|
274
|
+
return fallback;
|
|
275
|
+
}
|
|
276
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
277
|
+
}
|
|
278
|
+
function buildFastRetryDelays(baseDelayMs) {
|
|
279
|
+
const first = Math.max(100, Math.min(300, Math.floor(baseDelayMs / 4)));
|
|
280
|
+
const second = Math.max(first, Math.min(1e3, Math.floor(baseDelayMs / 2)));
|
|
281
|
+
return [first, second];
|
|
282
|
+
}
|
|
283
|
+
function randomIntInclusive(min, max) {
|
|
284
|
+
const boundedMin = Math.floor(min);
|
|
285
|
+
const boundedMax = Math.floor(max);
|
|
286
|
+
if (boundedMax <= boundedMin) {
|
|
287
|
+
return boundedMin;
|
|
288
|
+
}
|
|
289
|
+
return boundedMin + Math.floor(Math.random() * (boundedMax - boundedMin + 1));
|
|
290
|
+
}
|
|
291
|
+
function normalizeCloseReason(value) {
|
|
292
|
+
const reason = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
293
|
+
if (!reason) {
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
return reason.slice(0, 160);
|
|
297
|
+
}
|
|
298
|
+
function redactWsUrlForLog(wsUrl) {
|
|
299
|
+
if (!wsUrl) {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const parsed = new URL(wsUrl);
|
|
304
|
+
if (parsed.searchParams.has("agent_id")) {
|
|
305
|
+
parsed.searchParams.set("agent_id", "***");
|
|
306
|
+
}
|
|
307
|
+
return parsed.toString();
|
|
308
|
+
} catch {
|
|
309
|
+
return wsUrl.replace(/(agent_id=)[^&]+/g, "$1***");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function parseHeartbeatSec(payload) {
|
|
313
|
+
return clampInt(payload.heartbeat_sec, DEFAULT_HEARTBEAT_SEC, 5, 300);
|
|
314
|
+
}
|
|
315
|
+
async function sleepWithAbort(ms, abortSignal) {
|
|
316
|
+
if (ms <= 0 || abortSignal.aborted) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
await new Promise((resolve) => {
|
|
320
|
+
let settled = false;
|
|
321
|
+
let timer = null;
|
|
322
|
+
function finish() {
|
|
323
|
+
if (settled) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
settled = true;
|
|
327
|
+
if (timer) {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
}
|
|
330
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
331
|
+
resolve();
|
|
332
|
+
}
|
|
333
|
+
function onAbort() {
|
|
334
|
+
finish();
|
|
335
|
+
}
|
|
336
|
+
timer = setTimeout(finish, ms);
|
|
337
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async function sleep(ms) {
|
|
341
|
+
if (ms <= 0) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await new Promise((resolve) => {
|
|
345
|
+
setTimeout(resolve, ms);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function resolveReconnectPolicy(account) {
|
|
349
|
+
const baseDelayMs = clampInt(account.config.reconnectMs, DEFAULT_RECONNECT_BASE_MS, 100, 6e4);
|
|
350
|
+
const fallbackMaxMs = Math.max(DEFAULT_RECONNECT_MAX_MS, baseDelayMs * 8);
|
|
351
|
+
const maxDelayMs = clampInt(account.config.reconnectMaxMs, fallbackMaxMs, baseDelayMs, 3e5);
|
|
352
|
+
const stableConnectionMs = clampInt(
|
|
353
|
+
account.config.reconnectStableMs,
|
|
354
|
+
DEFAULT_RECONNECT_STABLE_MS,
|
|
355
|
+
1e3,
|
|
356
|
+
6e5
|
|
357
|
+
);
|
|
358
|
+
const connectTimeoutMs = clampInt(
|
|
359
|
+
account.config.connectTimeoutMs,
|
|
360
|
+
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
361
|
+
1e3,
|
|
362
|
+
6e4
|
|
363
|
+
);
|
|
364
|
+
const fastRetryDelaysMs = buildFastRetryDelays(baseDelayMs);
|
|
365
|
+
return {
|
|
366
|
+
baseDelayMs,
|
|
367
|
+
maxDelayMs,
|
|
368
|
+
stableConnectionMs,
|
|
369
|
+
fastRetryDelaysMs,
|
|
370
|
+
authPenaltyAttemptFloor: fastRetryDelaysMs.length + 4,
|
|
371
|
+
connectTimeoutMs
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
var AuthRejectedError = class extends Error {
|
|
375
|
+
code;
|
|
376
|
+
constructor(code, message) {
|
|
377
|
+
super(`clawpool auth failed: code=${code}, msg=${message}`);
|
|
378
|
+
this.name = "AuthRejectedError";
|
|
379
|
+
this.code = code;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function parseCode(payload) {
|
|
383
|
+
const n = Number(payload.code ?? 0);
|
|
384
|
+
if (Number.isFinite(n)) {
|
|
385
|
+
return n;
|
|
386
|
+
}
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
function parseMessage(payload) {
|
|
390
|
+
const s = String(payload.msg ?? "").trim();
|
|
391
|
+
return s || "unknown error";
|
|
392
|
+
}
|
|
393
|
+
function parseKickedReason(payload) {
|
|
394
|
+
const reason = String(payload.reason ?? payload.msg ?? "").trim();
|
|
395
|
+
return reason || "unknown";
|
|
396
|
+
}
|
|
397
|
+
async function wsDataToText(data) {
|
|
398
|
+
if (typeof data === "string") {
|
|
399
|
+
return data;
|
|
400
|
+
}
|
|
401
|
+
if (data instanceof ArrayBuffer) {
|
|
402
|
+
return Buffer.from(data).toString("utf8");
|
|
403
|
+
}
|
|
404
|
+
if (ArrayBuffer.isView(data)) {
|
|
405
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
|
|
406
|
+
}
|
|
407
|
+
if (data && typeof data.text === "function") {
|
|
408
|
+
return data.text();
|
|
409
|
+
}
|
|
410
|
+
return String(data ?? "");
|
|
411
|
+
}
|
|
412
|
+
var AibotWsClient = class {
|
|
413
|
+
account;
|
|
414
|
+
callbacks;
|
|
415
|
+
reconnectPolicy;
|
|
416
|
+
ws = null;
|
|
417
|
+
running = false;
|
|
418
|
+
seq = Date.now();
|
|
419
|
+
loopPromise = null;
|
|
420
|
+
pending = /* @__PURE__ */ new Map();
|
|
421
|
+
pendingStreamHighSurrogate = /* @__PURE__ */ new Map();
|
|
422
|
+
sendMsgWindowBySession = /* @__PURE__ */ new Map();
|
|
423
|
+
reconnectPenaltyAttemptFloor = 0;
|
|
424
|
+
connectionSerial = 0;
|
|
425
|
+
activeConnectionSerial = 0;
|
|
426
|
+
keepaliveTimer = null;
|
|
427
|
+
keepaliveInFlight = false;
|
|
428
|
+
lastConnectionError = "";
|
|
429
|
+
lastConnectionErrorLogAt = 0;
|
|
430
|
+
suppressedConnectionErrors = 0;
|
|
431
|
+
lastReconnectLogAt = 0;
|
|
432
|
+
suppressedReconnectLogs = 0;
|
|
433
|
+
status = {
|
|
434
|
+
running: false,
|
|
435
|
+
connected: false,
|
|
436
|
+
authed: false,
|
|
437
|
+
lastError: null,
|
|
438
|
+
lastConnectAt: null,
|
|
439
|
+
lastDisconnectAt: null
|
|
440
|
+
};
|
|
441
|
+
constructor(account, callbacks = {}) {
|
|
442
|
+
this.account = account;
|
|
443
|
+
this.callbacks = callbacks;
|
|
444
|
+
this.reconnectPolicy = resolveReconnectPolicy(account);
|
|
445
|
+
}
|
|
446
|
+
logInfo(message) {
|
|
447
|
+
this.callbacks.logger?.info?.(`[clawpool] [${this.account.accountId}] ${message}`);
|
|
448
|
+
}
|
|
449
|
+
logWarn(message) {
|
|
450
|
+
this.callbacks.logger?.warn?.(`[clawpool] [${this.account.accountId}] ${message}`);
|
|
451
|
+
}
|
|
452
|
+
logError(message) {
|
|
453
|
+
this.callbacks.logger?.error?.(`[clawpool] [${this.account.accountId}] ${message}`);
|
|
454
|
+
}
|
|
455
|
+
logConnectionError(message) {
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
const sameAsLast = this.lastConnectionError === message;
|
|
458
|
+
const shouldLog = !sameAsLast || now - this.lastConnectionErrorLogAt >= 3e4 || this.suppressedConnectionErrors >= 10;
|
|
459
|
+
if (!shouldLog) {
|
|
460
|
+
this.suppressedConnectionErrors += 1;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const repeats = this.suppressedConnectionErrors;
|
|
464
|
+
this.lastConnectionError = message;
|
|
465
|
+
this.lastConnectionErrorLogAt = now;
|
|
466
|
+
this.suppressedConnectionErrors = 0;
|
|
467
|
+
if (repeats > 0) {
|
|
468
|
+
this.logWarn(`connection error: ${message} (suppressed=${repeats})`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.logWarn(`connection error: ${message}`);
|
|
472
|
+
}
|
|
473
|
+
logReconnectPlan(params) {
|
|
474
|
+
const now = Date.now();
|
|
475
|
+
const important = params.attempt <= 3 || params.authRejected || params.penaltyFloor > 0 || params.stable || params.attempt % 10 === 0;
|
|
476
|
+
const shouldLog = important || now - this.lastReconnectLogAt >= 3e4;
|
|
477
|
+
if (!shouldLog) {
|
|
478
|
+
this.suppressedReconnectLogs += 1;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const suppressed = this.suppressedReconnectLogs;
|
|
482
|
+
this.suppressedReconnectLogs = 0;
|
|
483
|
+
this.lastReconnectLogAt = now;
|
|
484
|
+
this.logInfo(
|
|
485
|
+
`reconnect scheduled in ${params.delayMs}ms attempt=${params.attempt} stable=${params.stable} authRejected=${params.authRejected} penaltyFloor=${params.penaltyFloor} suppressed=${suppressed}`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
getStatus() {
|
|
489
|
+
return { ...this.status };
|
|
490
|
+
}
|
|
491
|
+
async start(abortSignal) {
|
|
492
|
+
if (this.running) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
this.running = true;
|
|
496
|
+
this.updateStatus({ running: true, lastError: null });
|
|
497
|
+
this.logInfo(
|
|
498
|
+
`client start ws=${redactWsUrlForLog(this.account.wsUrl)} reconnectBaseMs=${this.reconnectPolicy.baseDelayMs} reconnectMaxMs=${this.reconnectPolicy.maxDelayMs} reconnectStableMs=${this.reconnectPolicy.stableConnectionMs} connectTimeoutMs=${this.reconnectPolicy.connectTimeoutMs}`
|
|
499
|
+
);
|
|
500
|
+
this.loopPromise = this.runLoop(abortSignal);
|
|
501
|
+
void this.loopPromise.catch((err) => {
|
|
502
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
503
|
+
this.updateStatus({
|
|
504
|
+
running: false,
|
|
505
|
+
connected: false,
|
|
506
|
+
authed: false,
|
|
507
|
+
lastError: msg,
|
|
508
|
+
lastDisconnectAt: Date.now()
|
|
509
|
+
});
|
|
510
|
+
this.logError(`run loop crashed: ${msg}`);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
stop() {
|
|
514
|
+
this.running = false;
|
|
515
|
+
this.stopKeepalive();
|
|
516
|
+
this.rejectAllPending(new Error("clawpool client stopped"));
|
|
517
|
+
this.safeCloseWs("client_stopped");
|
|
518
|
+
this.updateStatus({
|
|
519
|
+
running: false,
|
|
520
|
+
connected: false,
|
|
521
|
+
authed: false,
|
|
522
|
+
lastDisconnectAt: Date.now()
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async waitUntilStopped() {
|
|
526
|
+
await this.loopPromise;
|
|
527
|
+
}
|
|
528
|
+
async sendText(sessionId, text, opts = {}) {
|
|
529
|
+
this.ensureReady();
|
|
530
|
+
const clientMsgId = opts.clientMsgId || `clawpool_${randomUUID()}`;
|
|
531
|
+
const payload = this.buildSendTextPayload(sessionId, text, clientMsgId, opts);
|
|
532
|
+
try {
|
|
533
|
+
return await this.sendMessageWithRetry(sessionId, payload, opts.timeoutMs ?? 2e4, "sendText");
|
|
534
|
+
} catch (err) {
|
|
535
|
+
if (!this.isMessageTooLargeError(err)) {
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
return this.sendSplitTextAfterSizeError(sessionId, text, clientMsgId, opts);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async sendMedia(sessionId, mediaUrl, caption = "", opts = {}) {
|
|
542
|
+
this.ensureReady();
|
|
543
|
+
const clientMsgId = opts.clientMsgId || `clawpool_${randomUUID()}`;
|
|
544
|
+
const payload = this.buildSendMediaPayload(sessionId, mediaUrl, caption, clientMsgId, opts);
|
|
545
|
+
try {
|
|
546
|
+
return await this.sendMessageWithRetry(sessionId, payload, opts.timeoutMs ?? 3e4, "sendMedia");
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (!this.isMessageTooLargeError(err) || !caption) {
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
return this.sendMediaCaptionAfterSizeError(sessionId, mediaUrl, caption, clientMsgId, opts);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async bindSessionRoute(channel, accountId, routeSessionKey, sessionId, opts = {}) {
|
|
555
|
+
this.ensureReady();
|
|
556
|
+
const normalizedChannel = String(channel ?? "").trim().toLowerCase();
|
|
557
|
+
const normalizedAccountID = String(accountId ?? "").trim();
|
|
558
|
+
const normalizedRouteSessionKey = String(routeSessionKey ?? "").trim();
|
|
559
|
+
const normalizedSessionID = String(sessionId ?? "").trim();
|
|
560
|
+
if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey || !normalizedSessionID) {
|
|
561
|
+
throw new Error("clawpool session_route_bind requires channel/account_id/route_session_key/session_id");
|
|
562
|
+
}
|
|
563
|
+
this.logInfo(
|
|
564
|
+
`session_route_bind request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
|
|
565
|
+
);
|
|
566
|
+
const packet = await this.request(
|
|
567
|
+
"session_route_bind",
|
|
568
|
+
{
|
|
569
|
+
channel: normalizedChannel,
|
|
570
|
+
account_id: normalizedAccountID,
|
|
571
|
+
route_session_key: normalizedRouteSessionKey,
|
|
572
|
+
session_id: normalizedSessionID
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
expected: ["send_ack", "send_nack", "error"],
|
|
576
|
+
timeoutMs: opts.timeoutMs ?? 1e4
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
if (packet.cmd !== "send_ack") {
|
|
580
|
+
this.logWarn(
|
|
581
|
+
`session_route_bind nack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
|
|
582
|
+
);
|
|
583
|
+
throw this.packetError(packet);
|
|
584
|
+
}
|
|
585
|
+
this.logInfo(
|
|
586
|
+
`session_route_bind ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
|
|
587
|
+
);
|
|
588
|
+
return packet.payload;
|
|
589
|
+
}
|
|
590
|
+
async resolveSessionRoute(channel, accountId, routeSessionKey, opts = {}) {
|
|
591
|
+
this.ensureReady();
|
|
592
|
+
const normalizedChannel = String(channel ?? "").trim().toLowerCase();
|
|
593
|
+
const normalizedAccountID = String(accountId ?? "").trim();
|
|
594
|
+
const normalizedRouteSessionKey = String(routeSessionKey ?? "").trim();
|
|
595
|
+
if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey) {
|
|
596
|
+
throw new Error("clawpool session_route_resolve requires channel/account_id/route_session_key");
|
|
597
|
+
}
|
|
598
|
+
this.logInfo(
|
|
599
|
+
`session_route_resolve request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey}`
|
|
600
|
+
);
|
|
601
|
+
const packet = await this.request(
|
|
602
|
+
"session_route_resolve",
|
|
603
|
+
{
|
|
604
|
+
channel: normalizedChannel,
|
|
605
|
+
account_id: normalizedAccountID,
|
|
606
|
+
route_session_key: normalizedRouteSessionKey
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
expected: ["send_ack", "send_nack", "error"],
|
|
610
|
+
timeoutMs: opts.timeoutMs ?? 1e4
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
if (packet.cmd !== "send_ack") {
|
|
614
|
+
this.logWarn(
|
|
615
|
+
`session_route_resolve nack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey}`
|
|
616
|
+
);
|
|
617
|
+
throw this.packetError(packet);
|
|
618
|
+
}
|
|
619
|
+
const payload = packet.payload;
|
|
620
|
+
const normalizedSessionID = String(payload.session_id ?? "").trim();
|
|
621
|
+
if (!normalizedSessionID) {
|
|
622
|
+
throw new Error("clawpool session_route_resolve ack missing session_id");
|
|
623
|
+
}
|
|
624
|
+
this.logInfo(
|
|
625
|
+
`session_route_resolve ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
|
|
626
|
+
);
|
|
627
|
+
return {
|
|
628
|
+
...payload,
|
|
629
|
+
channel: String(payload.channel ?? normalizedChannel),
|
|
630
|
+
account_id: String(payload.account_id ?? normalizedAccountID),
|
|
631
|
+
route_session_key: String(payload.route_session_key ?? normalizedRouteSessionKey),
|
|
632
|
+
session_id: normalizedSessionID
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
async sendStreamChunk(sessionId, deltaContent, opts) {
|
|
636
|
+
this.ensureReady();
|
|
637
|
+
const normalizedDeltaContent = this.normalizeStreamDeltaContent(
|
|
638
|
+
opts.clientMsgId,
|
|
639
|
+
deltaContent,
|
|
640
|
+
opts.isFinish === true
|
|
641
|
+
);
|
|
642
|
+
if (!normalizedDeltaContent && !opts.isFinish) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const payload = {
|
|
646
|
+
session_id: sessionId,
|
|
647
|
+
client_msg_id: opts.clientMsgId,
|
|
648
|
+
delta_content: normalizedDeltaContent,
|
|
649
|
+
is_finish: opts.isFinish ?? false
|
|
650
|
+
};
|
|
651
|
+
const eventId = String(opts.eventId ?? "").trim();
|
|
652
|
+
if (eventId) {
|
|
653
|
+
payload.event_id = eventId;
|
|
654
|
+
}
|
|
655
|
+
if (opts.quotedMessageId) {
|
|
656
|
+
payload.quoted_message_id = opts.quotedMessageId;
|
|
657
|
+
}
|
|
658
|
+
if (opts.isFinish) {
|
|
659
|
+
const packet = await this.request("client_stream_chunk", payload, {
|
|
660
|
+
expected: ["send_ack", "send_nack", "error"],
|
|
661
|
+
timeoutMs: opts.timeoutMs ?? 2e4
|
|
662
|
+
});
|
|
663
|
+
if (packet.cmd !== "send_ack") {
|
|
664
|
+
throw this.packetError(packet);
|
|
665
|
+
}
|
|
666
|
+
return packet.payload;
|
|
667
|
+
}
|
|
668
|
+
this.sendPacket("client_stream_chunk", payload);
|
|
669
|
+
}
|
|
670
|
+
async deleteMessage(sessionId, msgId, opts = {}) {
|
|
671
|
+
this.ensureReady();
|
|
672
|
+
const normalizedSessionId = String(sessionId ?? "").trim();
|
|
673
|
+
if (!normalizedSessionId) {
|
|
674
|
+
throw new Error("clawpool delete_msg requires session_id");
|
|
675
|
+
}
|
|
676
|
+
const normalizedMsgId = String(msgId ?? "").trim();
|
|
677
|
+
if (!/^\d+$/.test(normalizedMsgId)) {
|
|
678
|
+
throw new Error("clawpool delete_msg requires numeric msg_id");
|
|
679
|
+
}
|
|
680
|
+
const packet = await this.request(
|
|
681
|
+
"delete_msg",
|
|
682
|
+
{
|
|
683
|
+
session_id: normalizedSessionId,
|
|
684
|
+
msg_id: normalizedMsgId
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
expected: ["send_ack", "send_nack", "error"],
|
|
688
|
+
timeoutMs: opts.timeoutMs ?? 2e4
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
if (packet.cmd !== "send_ack") {
|
|
692
|
+
throw this.packetError(packet);
|
|
693
|
+
}
|
|
694
|
+
return packet.payload;
|
|
695
|
+
}
|
|
696
|
+
ackEvent(eventId, payload = {}) {
|
|
697
|
+
this.ensureReady();
|
|
698
|
+
const normalizedEventId = String(eventId ?? "").trim();
|
|
699
|
+
if (!normalizedEventId) {
|
|
700
|
+
throw new Error("clawpool event_ack requires event_id");
|
|
701
|
+
}
|
|
702
|
+
const ackPayload = {
|
|
703
|
+
event_id: normalizedEventId,
|
|
704
|
+
received_at: Math.floor(payload.receivedAt ?? Date.now())
|
|
705
|
+
};
|
|
706
|
+
const sessionId = String(payload.sessionId ?? "").trim();
|
|
707
|
+
if (sessionId) {
|
|
708
|
+
ackPayload.session_id = sessionId;
|
|
709
|
+
}
|
|
710
|
+
const msgId = String(payload.msgId ?? "").trim();
|
|
711
|
+
if (/^\d+$/.test(msgId)) {
|
|
712
|
+
ackPayload.msg_id = msgId;
|
|
713
|
+
}
|
|
714
|
+
this.sendPacket("event_ack", ackPayload);
|
|
715
|
+
}
|
|
716
|
+
sendEventResult(payload) {
|
|
717
|
+
this.ensureReady();
|
|
718
|
+
const eventId = String(payload.event_id ?? "").trim();
|
|
719
|
+
const status = String(payload.status ?? "").trim();
|
|
720
|
+
if (!eventId) {
|
|
721
|
+
throw new Error("clawpool event_result requires event_id");
|
|
722
|
+
}
|
|
723
|
+
if (!status) {
|
|
724
|
+
throw new Error("clawpool event_result requires status");
|
|
725
|
+
}
|
|
726
|
+
const packet = {
|
|
727
|
+
event_id: eventId,
|
|
728
|
+
status
|
|
729
|
+
};
|
|
730
|
+
const code = String(payload.code ?? "").trim();
|
|
731
|
+
if (code) {
|
|
732
|
+
packet.code = code;
|
|
733
|
+
}
|
|
734
|
+
const msg = String(payload.msg ?? "").trim();
|
|
735
|
+
if (msg) {
|
|
736
|
+
packet.msg = msg;
|
|
737
|
+
}
|
|
738
|
+
const updatedAt = Number(payload.updated_at);
|
|
739
|
+
if (Number.isFinite(updatedAt) && updatedAt > 0) {
|
|
740
|
+
packet.updated_at = Math.floor(updatedAt);
|
|
741
|
+
}
|
|
742
|
+
this.sendPacket("event_result", packet);
|
|
743
|
+
}
|
|
744
|
+
sendEventStopAck(payload) {
|
|
745
|
+
this.ensureReady();
|
|
746
|
+
const eventId = String(payload.event_id ?? "").trim();
|
|
747
|
+
if (!eventId) {
|
|
748
|
+
throw new Error("clawpool event_stop_ack requires event_id");
|
|
749
|
+
}
|
|
750
|
+
const packet = {
|
|
751
|
+
event_id: eventId,
|
|
752
|
+
accepted: payload.accepted === true
|
|
753
|
+
};
|
|
754
|
+
const stopId = String(payload.stop_id ?? "").trim();
|
|
755
|
+
if (stopId) {
|
|
756
|
+
packet.stop_id = stopId;
|
|
757
|
+
}
|
|
758
|
+
const updatedAt = Number(payload.updated_at);
|
|
759
|
+
if (Number.isFinite(updatedAt) && updatedAt > 0) {
|
|
760
|
+
packet.updated_at = Math.floor(updatedAt);
|
|
761
|
+
}
|
|
762
|
+
this.sendPacket("event_stop_ack", packet);
|
|
763
|
+
}
|
|
764
|
+
sendEventStopResult(payload) {
|
|
765
|
+
this.ensureReady();
|
|
766
|
+
const eventId = String(payload.event_id ?? "").trim();
|
|
767
|
+
const status = String(payload.status ?? "").trim();
|
|
768
|
+
if (!eventId) {
|
|
769
|
+
throw new Error("clawpool event_stop_result requires event_id");
|
|
770
|
+
}
|
|
771
|
+
if (!status) {
|
|
772
|
+
throw new Error("clawpool event_stop_result requires status");
|
|
773
|
+
}
|
|
774
|
+
const packet = {
|
|
775
|
+
event_id: eventId,
|
|
776
|
+
status
|
|
777
|
+
};
|
|
778
|
+
const stopId = String(payload.stop_id ?? "").trim();
|
|
779
|
+
if (stopId) {
|
|
780
|
+
packet.stop_id = stopId;
|
|
781
|
+
}
|
|
782
|
+
const code = String(payload.code ?? "").trim();
|
|
783
|
+
if (code) {
|
|
784
|
+
packet.code = code;
|
|
785
|
+
}
|
|
786
|
+
const msg = String(payload.msg ?? "").trim();
|
|
787
|
+
if (msg) {
|
|
788
|
+
packet.msg = msg;
|
|
789
|
+
}
|
|
790
|
+
const updatedAt = Number(payload.updated_at);
|
|
791
|
+
if (Number.isFinite(updatedAt) && updatedAt > 0) {
|
|
792
|
+
packet.updated_at = Math.floor(updatedAt);
|
|
793
|
+
}
|
|
794
|
+
this.sendPacket("event_stop_result", packet);
|
|
795
|
+
}
|
|
796
|
+
setSessionComposing(sessionId, active, opts = {}) {
|
|
797
|
+
this.ensureReady();
|
|
798
|
+
const normalizedSessionId = String(sessionId ?? "").trim();
|
|
799
|
+
if (!normalizedSessionId) {
|
|
800
|
+
throw new Error("clawpool session_activity_set requires session_id");
|
|
801
|
+
}
|
|
802
|
+
const payload = {
|
|
803
|
+
session_id: normalizedSessionId,
|
|
804
|
+
kind: "composing",
|
|
805
|
+
active
|
|
806
|
+
};
|
|
807
|
+
const refEventId = String(opts.refEventId ?? "").trim();
|
|
808
|
+
if (refEventId) {
|
|
809
|
+
payload.ref_event_id = refEventId;
|
|
810
|
+
}
|
|
811
|
+
const refMsgId = String(opts.refMsgId ?? "").trim();
|
|
812
|
+
if (/^\d+$/.test(refMsgId)) {
|
|
813
|
+
payload.ref_msg_id = refMsgId;
|
|
814
|
+
}
|
|
815
|
+
this.sendPacket("session_activity_set", payload);
|
|
816
|
+
}
|
|
817
|
+
async runLoop(abortSignal) {
|
|
818
|
+
let attempt = 0;
|
|
819
|
+
while (this.running && !abortSignal.aborted) {
|
|
820
|
+
let uptimeMs = 0;
|
|
821
|
+
let authRejected = false;
|
|
822
|
+
let shouldReconnect = true;
|
|
823
|
+
const cycle = attempt + 1;
|
|
824
|
+
try {
|
|
825
|
+
const outcome = await this.connectOnce(abortSignal, cycle);
|
|
826
|
+
uptimeMs = outcome.uptimeMs;
|
|
827
|
+
shouldReconnect = !outcome.aborted;
|
|
828
|
+
if (!outcome.aborted) {
|
|
829
|
+
const codeText = outcome.closeCode != null ? String(outcome.closeCode) : "-";
|
|
830
|
+
const reasonText = outcome.closeReason ? ` reason=${outcome.closeReason}` : "";
|
|
831
|
+
this.logWarn(
|
|
832
|
+
`websocket closed cause=${outcome.cause} code=${codeText}${reasonText} uptimeMs=${uptimeMs}`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
} catch (err) {
|
|
836
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
837
|
+
authRejected = err instanceof AuthRejectedError;
|
|
838
|
+
this.updateStatus({
|
|
839
|
+
connected: false,
|
|
840
|
+
authed: false,
|
|
841
|
+
lastError: msg,
|
|
842
|
+
lastDisconnectAt: Date.now()
|
|
843
|
+
});
|
|
844
|
+
this.logConnectionError(msg);
|
|
845
|
+
}
|
|
846
|
+
if (!this.running || abortSignal.aborted || !shouldReconnect) {
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
const stable = uptimeMs >= this.reconnectPolicy.stableConnectionMs;
|
|
850
|
+
if (stable) {
|
|
851
|
+
attempt = 0;
|
|
852
|
+
}
|
|
853
|
+
attempt += 1;
|
|
854
|
+
if (authRejected) {
|
|
855
|
+
attempt = Math.max(attempt, this.reconnectPolicy.authPenaltyAttemptFloor);
|
|
856
|
+
}
|
|
857
|
+
const penaltyFloor = this.consumeReconnectPenaltyAttemptFloor();
|
|
858
|
+
if (penaltyFloor > 0) {
|
|
859
|
+
attempt = Math.max(attempt, penaltyFloor);
|
|
860
|
+
}
|
|
861
|
+
const delay = this.resolveReconnectDelayMs(attempt);
|
|
862
|
+
this.logReconnectPlan({
|
|
863
|
+
delayMs: delay,
|
|
864
|
+
attempt,
|
|
865
|
+
stable,
|
|
866
|
+
authRejected,
|
|
867
|
+
penaltyFloor
|
|
868
|
+
});
|
|
869
|
+
await sleepWithAbort(delay, abortSignal);
|
|
870
|
+
}
|
|
871
|
+
this.stop();
|
|
872
|
+
}
|
|
873
|
+
async connectOnce(abortSignal, cycle) {
|
|
874
|
+
const connSerial = this.nextConnectionSerial();
|
|
875
|
+
this.logInfo(`websocket connect begin conn=${connSerial} cycle=${cycle}`);
|
|
876
|
+
const ws = await this.openWebSocket(this.account.wsUrl, abortSignal);
|
|
877
|
+
this.ws = ws;
|
|
878
|
+
this.activeConnectionSerial = connSerial;
|
|
879
|
+
const connectedAt = Date.now();
|
|
880
|
+
this.updateStatus({
|
|
881
|
+
connected: true,
|
|
882
|
+
authed: false,
|
|
883
|
+
lastError: null,
|
|
884
|
+
lastConnectAt: connectedAt
|
|
885
|
+
});
|
|
886
|
+
this.logInfo(`websocket connected conn=${connSerial}`);
|
|
887
|
+
const onMessage = (event) => {
|
|
888
|
+
void this.handleMessageEvent(event.data, connSerial).catch((err) => {
|
|
889
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
890
|
+
this.logError(`handle message failed conn=${connSerial}: ${message}`);
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
const onClose = () => {
|
|
894
|
+
this.stopKeepalive();
|
|
895
|
+
this.updateStatus({
|
|
896
|
+
connected: false,
|
|
897
|
+
authed: false,
|
|
898
|
+
lastDisconnectAt: Date.now()
|
|
899
|
+
});
|
|
900
|
+
this.rejectAllPending(new Error("clawpool websocket closed"));
|
|
901
|
+
if (this.ws === ws && ws.readyState !== WebSocket.OPEN) {
|
|
902
|
+
this.ws = null;
|
|
903
|
+
if (this.activeConnectionSerial === connSerial) {
|
|
904
|
+
this.activeConnectionSerial = 0;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
const onError = () => {
|
|
909
|
+
this.stopKeepalive();
|
|
910
|
+
this.updateStatus({
|
|
911
|
+
connected: false,
|
|
912
|
+
authed: false,
|
|
913
|
+
lastDisconnectAt: Date.now()
|
|
914
|
+
});
|
|
915
|
+
this.rejectAllPending(new Error("clawpool websocket error"));
|
|
916
|
+
};
|
|
917
|
+
ws.addEventListener("message", onMessage);
|
|
918
|
+
ws.addEventListener("close", onClose);
|
|
919
|
+
ws.addEventListener("error", onError);
|
|
920
|
+
try {
|
|
921
|
+
const auth = await this.authenticate(connSerial);
|
|
922
|
+
this.startKeepalive(ws, connSerial, auth.heartbeatSec);
|
|
923
|
+
const outcome = await this.waitForCloseOrAbort(ws, abortSignal);
|
|
924
|
+
return {
|
|
925
|
+
...outcome,
|
|
926
|
+
uptimeMs: Math.max(0, Date.now() - connectedAt)
|
|
927
|
+
};
|
|
928
|
+
} catch (err) {
|
|
929
|
+
this.safeCloseSpecificWs(ws, "connect_once_error");
|
|
930
|
+
throw err;
|
|
931
|
+
} finally {
|
|
932
|
+
ws.removeEventListener("message", onMessage);
|
|
933
|
+
ws.removeEventListener("close", onClose);
|
|
934
|
+
ws.removeEventListener("error", onError);
|
|
935
|
+
this.stopKeepalive();
|
|
936
|
+
this.safeCloseSpecificWs(ws, "connect_once_finally");
|
|
937
|
+
if (this.activeConnectionSerial === connSerial) {
|
|
938
|
+
this.activeConnectionSerial = 0;
|
|
939
|
+
}
|
|
940
|
+
this.logInfo(`websocket connect end conn=${connSerial}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async openWebSocket(url, abortSignal) {
|
|
944
|
+
return new Promise((resolve, reject) => {
|
|
945
|
+
const ws = new WebSocket(url);
|
|
946
|
+
let done = false;
|
|
947
|
+
const timeoutMs = this.reconnectPolicy.connectTimeoutMs;
|
|
948
|
+
let timer = null;
|
|
949
|
+
const closeWs = () => {
|
|
950
|
+
try {
|
|
951
|
+
ws.close();
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
const onOpen = () => {
|
|
956
|
+
finish(() => resolve(ws));
|
|
957
|
+
};
|
|
958
|
+
const onError = () => {
|
|
959
|
+
finish(() => reject(new Error("clawpool websocket connect failed")));
|
|
960
|
+
};
|
|
961
|
+
const onAbort = () => {
|
|
962
|
+
finish(() => {
|
|
963
|
+
closeWs();
|
|
964
|
+
reject(new Error("aborted"));
|
|
965
|
+
});
|
|
966
|
+
};
|
|
967
|
+
const finish = (fn) => {
|
|
968
|
+
if (done) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
done = true;
|
|
972
|
+
if (timer) {
|
|
973
|
+
clearTimeout(timer);
|
|
974
|
+
}
|
|
975
|
+
ws.removeEventListener("open", onOpen);
|
|
976
|
+
ws.removeEventListener("error", onError);
|
|
977
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
978
|
+
fn();
|
|
979
|
+
};
|
|
980
|
+
timer = setTimeout(() => {
|
|
981
|
+
finish(() => {
|
|
982
|
+
closeWs();
|
|
983
|
+
reject(new Error("clawpool websocket connect timeout"));
|
|
984
|
+
});
|
|
985
|
+
}, timeoutMs);
|
|
986
|
+
ws.addEventListener("open", onOpen);
|
|
987
|
+
ws.addEventListener("error", onError);
|
|
988
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
async waitForCloseOrAbort(ws, abortSignal) {
|
|
992
|
+
return new Promise((resolve) => {
|
|
993
|
+
let settled = false;
|
|
994
|
+
const closeWs = () => {
|
|
995
|
+
this.safeCloseSpecificWs(ws);
|
|
996
|
+
};
|
|
997
|
+
function finish(result) {
|
|
998
|
+
if (settled) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
settled = true;
|
|
1002
|
+
ws.removeEventListener("close", onClose);
|
|
1003
|
+
ws.removeEventListener("error", onError);
|
|
1004
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
1005
|
+
resolve(result);
|
|
1006
|
+
}
|
|
1007
|
+
function onClose(event) {
|
|
1008
|
+
const close = event;
|
|
1009
|
+
const code = Number(close.code);
|
|
1010
|
+
finish({
|
|
1011
|
+
cause: "close",
|
|
1012
|
+
aborted: false,
|
|
1013
|
+
closeCode: Number.isFinite(code) ? code : void 0,
|
|
1014
|
+
closeReason: normalizeCloseReason(close.reason)
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
function onError() {
|
|
1018
|
+
finish({
|
|
1019
|
+
cause: "error",
|
|
1020
|
+
aborted: false
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
function onAbort() {
|
|
1024
|
+
closeWs();
|
|
1025
|
+
finish({
|
|
1026
|
+
cause: "abort",
|
|
1027
|
+
aborted: true
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
ws.addEventListener("close", onClose);
|
|
1031
|
+
ws.addEventListener("error", onError);
|
|
1032
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
async authenticate(connSerial) {
|
|
1036
|
+
this.logInfo(`auth begin conn=${connSerial}`);
|
|
1037
|
+
const packet = await this.request(
|
|
1038
|
+
"auth",
|
|
1039
|
+
buildAuthPayload(this.account),
|
|
1040
|
+
{
|
|
1041
|
+
expected: ["auth_ack"],
|
|
1042
|
+
timeoutMs: 1e4,
|
|
1043
|
+
requireAuthed: false
|
|
1044
|
+
}
|
|
1045
|
+
);
|
|
1046
|
+
const payload = packet.payload ?? {};
|
|
1047
|
+
const code = parseCode(payload);
|
|
1048
|
+
if (code !== 0) {
|
|
1049
|
+
throw new AuthRejectedError(code, parseMessage(payload));
|
|
1050
|
+
}
|
|
1051
|
+
const heartbeatSec = parseHeartbeatSec(payload);
|
|
1052
|
+
const protocol = String(payload.protocol ?? "").trim() || void 0;
|
|
1053
|
+
this.updateStatus({ authed: true, lastError: null });
|
|
1054
|
+
this.logInfo(
|
|
1055
|
+
`auth success conn=${connSerial} heartbeatSec=${heartbeatSec} protocol=${protocol ?? "-"}`
|
|
1056
|
+
);
|
|
1057
|
+
return {
|
|
1058
|
+
heartbeatSec,
|
|
1059
|
+
protocol
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
async handleMessageEvent(data, connSerial) {
|
|
1063
|
+
const text = await wsDataToText(data);
|
|
1064
|
+
if (!text) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const resolvedConnSerial = (connSerial ?? this.activeConnectionSerial) || 0;
|
|
1068
|
+
let packet;
|
|
1069
|
+
try {
|
|
1070
|
+
packet = JSON.parse(text);
|
|
1071
|
+
} catch {
|
|
1072
|
+
this.logWarn(
|
|
1073
|
+
`ignored non-json message conn=${resolvedConnSerial} bytes=${text.length} preview=${JSON.stringify(text.slice(0, 200))}`
|
|
1074
|
+
);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const cmd = String(packet.cmd ?? "").trim();
|
|
1078
|
+
const seq = Number(packet.seq ?? 0);
|
|
1079
|
+
if (cmd !== "ping") {
|
|
1080
|
+
this.logInfo(
|
|
1081
|
+
`inbound packet conn=${resolvedConnSerial} cmd=${cmd || "-"} seq=${seq} bytes=${text.length}`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
if (cmd === "event_stop") {
|
|
1085
|
+
const payload = packet.payload;
|
|
1086
|
+
this.logInfo(
|
|
1087
|
+
`received stop-related packet cmd=${cmd} eventId=${String(payload.event_id ?? "").trim() || "-"} sessionId=${String(payload.session_id ?? "").trim() || "-"} stopId=${String(payload.stop_id ?? "").trim() || "-"} seq=${seq} bytes=${text.length}`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (cmd === "ping") {
|
|
1091
|
+
this.sendPacket("pong", { ts: Date.now() }, seq > 0 ? seq : void 0, false);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (cmd === "event_msg") {
|
|
1095
|
+
this.callbacks.onEventMsg?.(packet.payload);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (cmd === "event_react") {
|
|
1099
|
+
this.callbacks.onEventReact?.(packet.payload);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (cmd === "event_revoke") {
|
|
1103
|
+
this.callbacks.onEventRevoke?.(packet.payload);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (cmd === "event_stop") {
|
|
1107
|
+
const payload = packet.payload;
|
|
1108
|
+
this.logInfo(
|
|
1109
|
+
`received event_stop eventId=${String(payload.event_id ?? "").trim() || "-"} sessionId=${String(payload.session_id ?? "").trim() || "-"} stopId=${String(payload.stop_id ?? "").trim() || "-"} seq=${seq}`
|
|
1110
|
+
);
|
|
1111
|
+
this.callbacks.onEventStop?.(packet.payload);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (cmd === "kicked") {
|
|
1115
|
+
const payload = packet.payload ?? {};
|
|
1116
|
+
const reason = parseKickedReason(payload);
|
|
1117
|
+
if (reason === "replaced_by_new_connection") {
|
|
1118
|
+
this.reconnectPenaltyAttemptFloor = Math.max(
|
|
1119
|
+
this.reconnectPenaltyAttemptFloor,
|
|
1120
|
+
this.reconnectPolicy.fastRetryDelaysMs.length + 5
|
|
1121
|
+
);
|
|
1122
|
+
this.logWarn(
|
|
1123
|
+
`apply reconnect penalty for kicked replacement penaltyFloor=${this.reconnectPenaltyAttemptFloor}`
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
this.logWarn(`connection kicked by server reason=${reason}`);
|
|
1127
|
+
this.safeCloseWs("kicked_by_server");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const pending = this.pending.get(seq);
|
|
1131
|
+
if (pending && pending.expected.has(cmd)) {
|
|
1132
|
+
this.pending.delete(seq);
|
|
1133
|
+
clearTimeout(pending.timer);
|
|
1134
|
+
pending.resolve(packet);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
ensureReady(requireAuthed = true) {
|
|
1139
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1140
|
+
throw new Error("clawpool websocket is not open");
|
|
1141
|
+
}
|
|
1142
|
+
if (requireAuthed && !this.status.authed) {
|
|
1143
|
+
throw new Error("clawpool websocket is not authed");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
nextSeq() {
|
|
1147
|
+
this.seq += 1;
|
|
1148
|
+
return this.seq;
|
|
1149
|
+
}
|
|
1150
|
+
sendPacket(cmd, payload, seq, requireAuthed = true) {
|
|
1151
|
+
this.ensureReady(requireAuthed);
|
|
1152
|
+
const outSeq = seq ?? this.nextSeq();
|
|
1153
|
+
const packet = {
|
|
1154
|
+
cmd,
|
|
1155
|
+
seq: outSeq,
|
|
1156
|
+
payload
|
|
1157
|
+
};
|
|
1158
|
+
if (cmd === "event_stop_ack" || cmd === "event_stop_result") {
|
|
1159
|
+
this.logInfo(
|
|
1160
|
+
`send stop-related packet cmd=${cmd} eventId=${String(payload.event_id ?? "").trim() || "-"} stopId=${String(payload.stop_id ?? "").trim() || "-"} status=${String(payload.status ?? "").trim() || "-"} accepted=${String(payload.accepted ?? "").trim() || "-"} seq=${outSeq}`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
this.ws?.send(JSON.stringify(packet));
|
|
1164
|
+
return outSeq;
|
|
1165
|
+
}
|
|
1166
|
+
async request(cmd, payload, opts) {
|
|
1167
|
+
this.ensureReady(opts.requireAuthed ?? true);
|
|
1168
|
+
const seq = this.nextSeq();
|
|
1169
|
+
const expected = new Set(opts.expected);
|
|
1170
|
+
return new Promise((resolve, reject) => {
|
|
1171
|
+
const timer = setTimeout(() => {
|
|
1172
|
+
this.pending.delete(seq);
|
|
1173
|
+
reject(new Error(`${cmd} timeout`));
|
|
1174
|
+
}, opts.timeoutMs);
|
|
1175
|
+
this.pending.set(seq, {
|
|
1176
|
+
expected,
|
|
1177
|
+
resolve,
|
|
1178
|
+
reject,
|
|
1179
|
+
timer
|
|
1180
|
+
});
|
|
1181
|
+
try {
|
|
1182
|
+
const packet = {
|
|
1183
|
+
cmd,
|
|
1184
|
+
seq,
|
|
1185
|
+
payload
|
|
1186
|
+
};
|
|
1187
|
+
this.ws?.send(JSON.stringify(packet));
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
this.pending.delete(seq);
|
|
1190
|
+
clearTimeout(timer);
|
|
1191
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
rejectAllPending(err) {
|
|
1196
|
+
const pendingCount = this.pending.size;
|
|
1197
|
+
if (pendingCount > 0) {
|
|
1198
|
+
this.logWarn(`reject pending requests count=${pendingCount} reason=${err.message}`);
|
|
1199
|
+
}
|
|
1200
|
+
for (const [seq, pending] of this.pending.entries()) {
|
|
1201
|
+
this.pending.delete(seq);
|
|
1202
|
+
clearTimeout(pending.timer);
|
|
1203
|
+
pending.reject(err);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
buildSendTextPayload(sessionId, text, clientMsgId, opts) {
|
|
1207
|
+
const payload = {
|
|
1208
|
+
session_id: sessionId,
|
|
1209
|
+
client_msg_id: clientMsgId,
|
|
1210
|
+
msg_type: 1,
|
|
1211
|
+
content: text
|
|
1212
|
+
};
|
|
1213
|
+
const eventId = String(opts.eventId ?? "").trim();
|
|
1214
|
+
if (eventId) {
|
|
1215
|
+
payload.event_id = eventId;
|
|
1216
|
+
}
|
|
1217
|
+
if (opts.quotedMessageId) {
|
|
1218
|
+
payload.quoted_message_id = opts.quotedMessageId;
|
|
1219
|
+
}
|
|
1220
|
+
if (opts.extra && Object.keys(opts.extra).length > 0) {
|
|
1221
|
+
payload.extra = opts.extra;
|
|
1222
|
+
}
|
|
1223
|
+
return payload;
|
|
1224
|
+
}
|
|
1225
|
+
buildSendMediaPayload(sessionId, mediaUrl, caption, clientMsgId, opts) {
|
|
1226
|
+
const payload = {
|
|
1227
|
+
session_id: sessionId,
|
|
1228
|
+
client_msg_id: clientMsgId,
|
|
1229
|
+
msg_type: opts.msgType ?? 2,
|
|
1230
|
+
content: caption || "[media]",
|
|
1231
|
+
media_url: mediaUrl
|
|
1232
|
+
};
|
|
1233
|
+
const eventId = String(opts.eventId ?? "").trim();
|
|
1234
|
+
if (eventId) {
|
|
1235
|
+
payload.event_id = eventId;
|
|
1236
|
+
}
|
|
1237
|
+
if (opts.quotedMessageId) {
|
|
1238
|
+
payload.quoted_message_id = opts.quotedMessageId;
|
|
1239
|
+
}
|
|
1240
|
+
if (opts.extra && Object.keys(opts.extra).length > 0) {
|
|
1241
|
+
payload.extra = opts.extra;
|
|
1242
|
+
}
|
|
1243
|
+
return payload;
|
|
1244
|
+
}
|
|
1245
|
+
async sendMessageWithRetry(sessionId, payload, timeoutMs, action) {
|
|
1246
|
+
const maxAttempts = resolveAibotSendRetryMaxAttempts();
|
|
1247
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1248
|
+
await this.awaitSendMsgSlot(sessionId);
|
|
1249
|
+
const packet = await this.request("send_msg", payload, {
|
|
1250
|
+
expected: ["send_ack", "send_nack", "error"],
|
|
1251
|
+
timeoutMs
|
|
1252
|
+
});
|
|
1253
|
+
if (packet.cmd === "send_ack") {
|
|
1254
|
+
return packet.payload;
|
|
1255
|
+
}
|
|
1256
|
+
const err = this.packetError(packet);
|
|
1257
|
+
if (this.isRetryableSendError(err) && attempt < maxAttempts) {
|
|
1258
|
+
const delayMs = resolveAibotSendRetryDelayMs(attempt);
|
|
1259
|
+
this.logWarn(
|
|
1260
|
+
`${action} rate limited sessionId=${sessionId} attempt=${attempt}/${maxAttempts} delayMs=${delayMs}`
|
|
1261
|
+
);
|
|
1262
|
+
await sleep(delayMs);
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
throw err;
|
|
1266
|
+
}
|
|
1267
|
+
throw new Error(`clawpool ${action} exhausted retry attempts`);
|
|
1268
|
+
}
|
|
1269
|
+
async sendSplitTextAfterSizeError(sessionId, text, clientMsgId, opts) {
|
|
1270
|
+
const chunks = splitTextForAibotProtocol(text, DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT);
|
|
1271
|
+
if (chunks.length <= 1) {
|
|
1272
|
+
throw new Error(`clawpool sendText size recovery failed clientMsgId=${clientMsgId}`);
|
|
1273
|
+
}
|
|
1274
|
+
this.logWarn(
|
|
1275
|
+
`sendText size recovery sessionId=${sessionId} clientMsgId=${clientMsgId} chunkCount=${chunks.length}`
|
|
1276
|
+
);
|
|
1277
|
+
let lastAck = null;
|
|
1278
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
1279
|
+
const chunkClientMsgId = this.buildChunkClientMsgId(clientMsgId, index + 1);
|
|
1280
|
+
lastAck = await this.sendMessageWithRetry(
|
|
1281
|
+
sessionId,
|
|
1282
|
+
this.buildSendTextPayload(sessionId, chunks[index] ?? "", chunkClientMsgId, opts),
|
|
1283
|
+
opts.timeoutMs ?? 2e4,
|
|
1284
|
+
"sendText"
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
if (lastAck == null) {
|
|
1288
|
+
throw new Error(`clawpool sendText size recovery produced no outbound chunks clientMsgId=${clientMsgId}`);
|
|
1289
|
+
}
|
|
1290
|
+
return lastAck;
|
|
1291
|
+
}
|
|
1292
|
+
async sendMediaCaptionAfterSizeError(sessionId, mediaUrl, caption, clientMsgId, opts) {
|
|
1293
|
+
const chunks = splitTextForAibotProtocol(caption, DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT);
|
|
1294
|
+
if (chunks.length <= 1) {
|
|
1295
|
+
throw new Error(`clawpool sendMedia size recovery failed clientMsgId=${clientMsgId}`);
|
|
1296
|
+
}
|
|
1297
|
+
this.logWarn(
|
|
1298
|
+
`sendMedia size recovery sessionId=${sessionId} clientMsgId=${clientMsgId} chunkCount=${chunks.length}`
|
|
1299
|
+
);
|
|
1300
|
+
const mediaAck = await this.sendMessageWithRetry(
|
|
1301
|
+
sessionId,
|
|
1302
|
+
this.buildSendMediaPayload(
|
|
1303
|
+
sessionId,
|
|
1304
|
+
mediaUrl,
|
|
1305
|
+
chunks[0] ?? "",
|
|
1306
|
+
`${clientMsgId}_media`,
|
|
1307
|
+
opts
|
|
1308
|
+
),
|
|
1309
|
+
opts.timeoutMs ?? 3e4,
|
|
1310
|
+
"sendMedia"
|
|
1311
|
+
);
|
|
1312
|
+
for (let index = 1; index < chunks.length; index++) {
|
|
1313
|
+
const chunkClientMsgId = this.buildChunkClientMsgId(clientMsgId, index);
|
|
1314
|
+
await this.sendMessageWithRetry(
|
|
1315
|
+
sessionId,
|
|
1316
|
+
this.buildSendTextPayload(sessionId, chunks[index] ?? "", chunkClientMsgId, opts),
|
|
1317
|
+
opts.timeoutMs ?? 2e4,
|
|
1318
|
+
"sendText"
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
return mediaAck;
|
|
1322
|
+
}
|
|
1323
|
+
async awaitSendMsgSlot(sessionId) {
|
|
1324
|
+
const normalizedSessionId = String(sessionId ?? "").trim();
|
|
1325
|
+
if (!normalizedSessionId) {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
for (; ; ) {
|
|
1329
|
+
const now = Date.now();
|
|
1330
|
+
const recent = pruneAibotSendWindow(this.sendMsgWindowBySession.get(normalizedSessionId) ?? [], now);
|
|
1331
|
+
const waitMs = computeAibotSendThrottleDelayMs(recent, now);
|
|
1332
|
+
this.sendMsgWindowBySession.set(normalizedSessionId, recent);
|
|
1333
|
+
if (waitMs <= 0) {
|
|
1334
|
+
recent.push(now);
|
|
1335
|
+
this.sendMsgWindowBySession.set(normalizedSessionId, recent);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
this.logWarn(
|
|
1339
|
+
`send_msg pacing sessionId=${normalizedSessionId} queued=${recent.length} waitMs=${waitMs}`
|
|
1340
|
+
);
|
|
1341
|
+
await sleep(waitMs);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
isRetryableSendError(err) {
|
|
1345
|
+
return err instanceof AibotPacketError && isRetryableAibotSendCode(err.code);
|
|
1346
|
+
}
|
|
1347
|
+
isMessageTooLargeError(err) {
|
|
1348
|
+
return err instanceof AibotPacketError && err.code === 4004;
|
|
1349
|
+
}
|
|
1350
|
+
buildChunkClientMsgId(clientMsgId, chunkIndex) {
|
|
1351
|
+
return `${clientMsgId}_chunk${chunkIndex}`;
|
|
1352
|
+
}
|
|
1353
|
+
packetError(packet) {
|
|
1354
|
+
const payload = packet.payload;
|
|
1355
|
+
const code = Number(payload.code ?? 0);
|
|
1356
|
+
const msg = String(payload.msg ?? packet.cmd ?? "unknown error");
|
|
1357
|
+
return new AibotPacketError(packet.cmd, code, msg);
|
|
1358
|
+
}
|
|
1359
|
+
normalizeStreamDeltaContent(clientMsgId, deltaContent, isFinish) {
|
|
1360
|
+
const carry = this.pendingStreamHighSurrogate.get(clientMsgId) ?? "";
|
|
1361
|
+
this.pendingStreamHighSurrogate.delete(clientMsgId);
|
|
1362
|
+
let normalized = `${carry}${String(deltaContent ?? "")}`;
|
|
1363
|
+
if (!normalized) {
|
|
1364
|
+
return "";
|
|
1365
|
+
}
|
|
1366
|
+
if (isFinish && !deltaContent && carry) {
|
|
1367
|
+
this.logWarn(`dropping dangling high surrogate at stream finish clientMsgId=${clientMsgId}`);
|
|
1368
|
+
return "";
|
|
1369
|
+
}
|
|
1370
|
+
if (!isFinish && this.endsWithHighSurrogate(normalized)) {
|
|
1371
|
+
this.pendingStreamHighSurrogate.set(clientMsgId, normalized.slice(-1));
|
|
1372
|
+
normalized = normalized.slice(0, -1);
|
|
1373
|
+
} else if (isFinish && this.endsWithHighSurrogate(normalized)) {
|
|
1374
|
+
this.logWarn(`dropping dangling high surrogate at stream finish clientMsgId=${clientMsgId}`);
|
|
1375
|
+
normalized = normalized.slice(0, -1);
|
|
1376
|
+
}
|
|
1377
|
+
return normalized;
|
|
1378
|
+
}
|
|
1379
|
+
endsWithHighSurrogate(value) {
|
|
1380
|
+
if (!value) {
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
const code = value.charCodeAt(value.length - 1);
|
|
1384
|
+
return code >= 55296 && code <= 56319;
|
|
1385
|
+
}
|
|
1386
|
+
nextConnectionSerial() {
|
|
1387
|
+
this.connectionSerial += 1;
|
|
1388
|
+
return this.connectionSerial;
|
|
1389
|
+
}
|
|
1390
|
+
resolveKeepalivePolicy(heartbeatSec) {
|
|
1391
|
+
const defaultIntervalMs = Math.max(5e3, Math.min(2e4, Math.floor(heartbeatSec * 1e3 / 2)));
|
|
1392
|
+
const intervalMs = clampInt(
|
|
1393
|
+
this.account.config.keepalivePingMs,
|
|
1394
|
+
defaultIntervalMs,
|
|
1395
|
+
2e3,
|
|
1396
|
+
6e4
|
|
1397
|
+
);
|
|
1398
|
+
const defaultTimeoutMs = Math.max(3e3, Math.min(15e3, Math.floor(intervalMs * 0.8)));
|
|
1399
|
+
const timeoutMs = clampInt(
|
|
1400
|
+
this.account.config.keepaliveTimeoutMs,
|
|
1401
|
+
defaultTimeoutMs,
|
|
1402
|
+
1e3,
|
|
1403
|
+
6e4
|
|
1404
|
+
);
|
|
1405
|
+
return {
|
|
1406
|
+
intervalMs,
|
|
1407
|
+
timeoutMs
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
startKeepalive(ws, connSerial, heartbeatSec) {
|
|
1411
|
+
this.stopKeepalive();
|
|
1412
|
+
const policy = this.resolveKeepalivePolicy(heartbeatSec);
|
|
1413
|
+
this.logInfo(
|
|
1414
|
+
`keepalive start conn=${connSerial} intervalMs=${policy.intervalMs} timeoutMs=${policy.timeoutMs} serverHeartbeatSec=${heartbeatSec}`
|
|
1415
|
+
);
|
|
1416
|
+
this.keepaliveTimer = setInterval(() => {
|
|
1417
|
+
void this.runKeepaliveProbe(ws, connSerial, policy.timeoutMs);
|
|
1418
|
+
}, policy.intervalMs);
|
|
1419
|
+
}
|
|
1420
|
+
stopKeepalive() {
|
|
1421
|
+
if (this.keepaliveTimer) {
|
|
1422
|
+
clearInterval(this.keepaliveTimer);
|
|
1423
|
+
this.keepaliveTimer = null;
|
|
1424
|
+
}
|
|
1425
|
+
this.keepaliveInFlight = false;
|
|
1426
|
+
}
|
|
1427
|
+
async runKeepaliveProbe(ws, connSerial, timeoutMs) {
|
|
1428
|
+
if (!this.running || this.ws !== ws || ws.readyState !== WebSocket.OPEN || !this.status.authed) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
if (this.keepaliveInFlight) {
|
|
1432
|
+
this.logWarn(`keepalive overlap detected conn=${connSerial}, force reconnect`);
|
|
1433
|
+
this.safeCloseSpecificWs(ws, "keepalive_overlap");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
this.keepaliveInFlight = true;
|
|
1437
|
+
const startedAt = Date.now();
|
|
1438
|
+
try {
|
|
1439
|
+
await this.request(
|
|
1440
|
+
"ping",
|
|
1441
|
+
{
|
|
1442
|
+
ts: startedAt,
|
|
1443
|
+
source: "clawpool_keepalive"
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
expected: ["pong"],
|
|
1447
|
+
timeoutMs
|
|
1448
|
+
}
|
|
1449
|
+
);
|
|
1450
|
+
const latencyMs = Math.max(0, Date.now() - startedAt);
|
|
1451
|
+
if (latencyMs >= 2e3) {
|
|
1452
|
+
this.logWarn(`keepalive high latency conn=${connSerial} latencyMs=${latencyMs}`);
|
|
1453
|
+
}
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1456
|
+
this.logWarn(`keepalive failed conn=${connSerial} err=${msg}, force reconnect`);
|
|
1457
|
+
if (this.ws === ws) {
|
|
1458
|
+
this.safeCloseSpecificWs(ws, "keepalive_probe_failed");
|
|
1459
|
+
}
|
|
1460
|
+
} finally {
|
|
1461
|
+
this.keepaliveInFlight = false;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
resolveReconnectDelayMs(attempt) {
|
|
1465
|
+
if (attempt <= 0) {
|
|
1466
|
+
return 0;
|
|
1467
|
+
}
|
|
1468
|
+
const fastRetryDelays = this.reconnectPolicy.fastRetryDelaysMs;
|
|
1469
|
+
if (attempt <= fastRetryDelays.length) {
|
|
1470
|
+
return fastRetryDelays[attempt - 1] ?? this.reconnectPolicy.baseDelayMs;
|
|
1471
|
+
}
|
|
1472
|
+
const exponent = attempt - fastRetryDelays.length - 1;
|
|
1473
|
+
const uncapped = this.reconnectPolicy.baseDelayMs * 2 ** exponent;
|
|
1474
|
+
const capped = Math.min(this.reconnectPolicy.maxDelayMs, Math.floor(uncapped));
|
|
1475
|
+
const jitterFloor = Math.max(100, Math.floor(capped * 0.5));
|
|
1476
|
+
return randomIntInclusive(jitterFloor, capped);
|
|
1477
|
+
}
|
|
1478
|
+
consumeReconnectPenaltyAttemptFloor() {
|
|
1479
|
+
const floor = this.reconnectPenaltyAttemptFloor;
|
|
1480
|
+
this.reconnectPenaltyAttemptFloor = 0;
|
|
1481
|
+
return floor;
|
|
1482
|
+
}
|
|
1483
|
+
safeCloseSpecificWs(ws, reason = "") {
|
|
1484
|
+
try {
|
|
1485
|
+
ws.close();
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
if (this.ws === ws) {
|
|
1489
|
+
this.ws = null;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
safeCloseWs(reason = "") {
|
|
1493
|
+
if (!this.ws) {
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
this.safeCloseSpecificWs(this.ws, reason);
|
|
1497
|
+
}
|
|
1498
|
+
updateStatus(patch) {
|
|
1499
|
+
this.status = {
|
|
1500
|
+
...this.status,
|
|
1501
|
+
...patch
|
|
1502
|
+
};
|
|
1503
|
+
this.callbacks.onStatus?.(this.getStatus());
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
var activeClients = /* @__PURE__ */ new Map();
|
|
1507
|
+
function setActiveAibotClient(accountId, client) {
|
|
1508
|
+
if (!accountId) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (!client) {
|
|
1512
|
+
activeClients.delete(accountId);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
activeClients.set(accountId, client);
|
|
1516
|
+
}
|
|
1517
|
+
function clearActiveAibotClient(accountId, client) {
|
|
1518
|
+
if (!accountId) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (activeClients.get(accountId) !== client) {
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
activeClients.delete(accountId);
|
|
1525
|
+
}
|
|
1526
|
+
function getActiveAibotClient(accountId) {
|
|
1527
|
+
if (!accountId) {
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
return activeClients.get(accountId) ?? null;
|
|
1531
|
+
}
|
|
1532
|
+
function requireActiveAibotClient(accountId) {
|
|
1533
|
+
const client = getActiveAibotClient(accountId);
|
|
1534
|
+
if (!client) {
|
|
1535
|
+
throw new Error(
|
|
1536
|
+
`clawpool account "${accountId}" is not connected; start the gateway channel runtime first`
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
return client;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/silent-unsend-completion.ts
|
|
1543
|
+
var COMPLETION_TTL_MS = 5 * 60 * 1e3;
|
|
1544
|
+
var completedAtByMessageId = /* @__PURE__ */ new Map();
|
|
1545
|
+
function normalizeMessageId(value) {
|
|
1546
|
+
const normalized = String(value ?? "").trim();
|
|
1547
|
+
if (!/^\d+$/.test(normalized)) {
|
|
1548
|
+
return "";
|
|
1549
|
+
}
|
|
1550
|
+
return normalized;
|
|
1551
|
+
}
|
|
1552
|
+
function pruneExpired(now) {
|
|
1553
|
+
for (const [messageId, completedAt] of completedAtByMessageId) {
|
|
1554
|
+
if (now - completedAt > COMPLETION_TTL_MS) {
|
|
1555
|
+
completedAtByMessageId.delete(messageId);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function markSilentUnsendCompleted(messageId) {
|
|
1560
|
+
const normalizedMessageId = normalizeMessageId(messageId);
|
|
1561
|
+
if (!normalizedMessageId) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const now = Date.now();
|
|
1565
|
+
pruneExpired(now);
|
|
1566
|
+
completedAtByMessageId.set(normalizedMessageId, now);
|
|
1567
|
+
}
|
|
1568
|
+
function consumeSilentUnsendCompleted(messageId) {
|
|
1569
|
+
const normalizedMessageId = normalizeMessageId(messageId);
|
|
1570
|
+
if (!normalizedMessageId) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
pruneExpired(Date.now());
|
|
1574
|
+
const hadCompletion = completedAtByMessageId.has(normalizedMessageId);
|
|
1575
|
+
if (hadCompletion) {
|
|
1576
|
+
completedAtByMessageId.delete(normalizedMessageId);
|
|
1577
|
+
}
|
|
1578
|
+
return hadCompletion;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/target-resolver.ts
|
|
1582
|
+
var aibotSessionIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1583
|
+
function isAibotSessionID(value) {
|
|
1584
|
+
const normalized = String(value ?? "").trim();
|
|
1585
|
+
if (!normalized) {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
return aibotSessionIDPattern.test(normalized);
|
|
1589
|
+
}
|
|
1590
|
+
function normalizeAibotSessionTarget2(raw) {
|
|
1591
|
+
const trimmed = String(raw ?? "").trim();
|
|
1592
|
+
if (!trimmed) {
|
|
1593
|
+
return "";
|
|
1594
|
+
}
|
|
1595
|
+
return trimmed.replace(/^clawpool:/i, "").replace(/^session:/i, "").trim();
|
|
1596
|
+
}
|
|
1597
|
+
function buildRouteSessionKeyCandidates(rawTarget, normalizedTarget) {
|
|
1598
|
+
if (rawTarget === normalizedTarget) {
|
|
1599
|
+
return [rawTarget];
|
|
1600
|
+
}
|
|
1601
|
+
return [rawTarget, normalizedTarget].filter((candidate) => candidate.length > 0);
|
|
1602
|
+
}
|
|
1603
|
+
async function resolveAibotOutboundTarget(params) {
|
|
1604
|
+
const rawTarget = String(params.to ?? "").trim();
|
|
1605
|
+
if (!rawTarget) {
|
|
1606
|
+
throw new Error("clawpool outbound target must be non-empty");
|
|
1607
|
+
}
|
|
1608
|
+
const normalizedTarget = normalizeAibotSessionTarget2(rawTarget);
|
|
1609
|
+
if (!normalizedTarget) {
|
|
1610
|
+
throw new Error("clawpool outbound target must contain session_id or route_session_key");
|
|
1611
|
+
}
|
|
1612
|
+
if (isAibotSessionID(normalizedTarget)) {
|
|
1613
|
+
return {
|
|
1614
|
+
sessionId: normalizedTarget,
|
|
1615
|
+
rawTarget,
|
|
1616
|
+
normalizedTarget,
|
|
1617
|
+
resolveSource: "direct"
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
if (/^\d+$/.test(normalizedTarget)) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`clawpool outbound target "${rawTarget}" is numeric; expected session_id(UUID) or route.sessionKey`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
const routeSessionKeyCandidates = buildRouteSessionKeyCandidates(rawTarget, normalizedTarget);
|
|
1626
|
+
let lastResolveError = null;
|
|
1627
|
+
for (const routeSessionKey of routeSessionKeyCandidates) {
|
|
1628
|
+
try {
|
|
1629
|
+
const ack = await params.client.resolveSessionRoute("clawpool", params.accountId, routeSessionKey);
|
|
1630
|
+
const sessionId = String(ack.session_id ?? "").trim();
|
|
1631
|
+
if (!isAibotSessionID(sessionId)) {
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`session_route_resolve returned invalid session_id for route_session_key="${routeSessionKey}"`
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
sessionId,
|
|
1638
|
+
rawTarget,
|
|
1639
|
+
normalizedTarget,
|
|
1640
|
+
resolveSource: "sessionRouteMap"
|
|
1641
|
+
};
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
lastResolveError = err instanceof Error ? err : new Error(String(err));
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (lastResolveError) {
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
`clawpool outbound target resolve failed target="${rawTarget}" accountId=${params.accountId}: ${lastResolveError.message}`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
throw new Error(`clawpool outbound target resolve failed target="${rawTarget}" accountId=${params.accountId}`);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/delete-target-resolver.ts
|
|
1655
|
+
async function resolveAibotDeleteTarget(params) {
|
|
1656
|
+
const rawTarget = String(params.sessionId ?? "").trim() || String(params.to ?? "").trim() || String(params.topic ?? "").trim() || String(params.currentChannelId ?? "").trim();
|
|
1657
|
+
if (!rawTarget) {
|
|
1658
|
+
return "";
|
|
1659
|
+
}
|
|
1660
|
+
const resolved = await resolveAibotOutboundTarget({
|
|
1661
|
+
client: params.client,
|
|
1662
|
+
accountId: params.accountId,
|
|
1663
|
+
to: rawTarget
|
|
1664
|
+
});
|
|
1665
|
+
return resolved.sessionId;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/silent-unsend-plan.ts
|
|
1669
|
+
function normalizeMessageId2(value) {
|
|
1670
|
+
const normalized = String(value ?? "").trim();
|
|
1671
|
+
if (!/^\d+$/.test(normalized)) {
|
|
1672
|
+
return "";
|
|
1673
|
+
}
|
|
1674
|
+
return normalized;
|
|
1675
|
+
}
|
|
1676
|
+
async function resolveSilentUnsendPlan(params) {
|
|
1677
|
+
const targetMessageId = normalizeMessageId2(params.messageId);
|
|
1678
|
+
if (!targetMessageId) {
|
|
1679
|
+
throw new Error("Clawpool unsend requires numeric messageId.");
|
|
1680
|
+
}
|
|
1681
|
+
const targetSessionId = await resolveAibotDeleteTarget({
|
|
1682
|
+
client: params.client,
|
|
1683
|
+
accountId: params.accountId,
|
|
1684
|
+
sessionId: params.targetSessionId,
|
|
1685
|
+
to: params.targetTo,
|
|
1686
|
+
topic: params.targetTopic,
|
|
1687
|
+
currentChannelId: params.currentChannelId
|
|
1688
|
+
});
|
|
1689
|
+
if (!targetSessionId) {
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
"Clawpool unsend requires sessionId or to, or must be used inside an active Clawpool conversation."
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
const targetDelete = {
|
|
1695
|
+
sessionId: targetSessionId,
|
|
1696
|
+
messageId: targetMessageId
|
|
1697
|
+
};
|
|
1698
|
+
const currentMessageId = normalizeMessageId2(params.currentMessageId);
|
|
1699
|
+
if (!currentMessageId) {
|
|
1700
|
+
return { targetDelete };
|
|
1701
|
+
}
|
|
1702
|
+
if (currentMessageId === targetMessageId) {
|
|
1703
|
+
return {
|
|
1704
|
+
targetDelete,
|
|
1705
|
+
completionMessageId: currentMessageId
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
const currentChannelId = String(params.currentChannelId ?? "").trim();
|
|
1709
|
+
if (!currentChannelId) {
|
|
1710
|
+
return { targetDelete };
|
|
1711
|
+
}
|
|
1712
|
+
const currentSessionId = await resolveAibotDeleteTarget({
|
|
1713
|
+
client: params.client,
|
|
1714
|
+
accountId: params.accountId,
|
|
1715
|
+
currentChannelId
|
|
1716
|
+
});
|
|
1717
|
+
if (!currentSessionId) {
|
|
1718
|
+
throw new Error("Clawpool unsend could not resolve the current command message session.");
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
targetDelete,
|
|
1722
|
+
commandDelete: {
|
|
1723
|
+
sessionId: currentSessionId,
|
|
1724
|
+
messageId: currentMessageId
|
|
1725
|
+
},
|
|
1726
|
+
completionMessageId: currentMessageId
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/actions.ts
|
|
1731
|
+
var WS_ACTIONS = /* @__PURE__ */ new Set(["unsend", "delete"]);
|
|
1732
|
+
var DISCOVERABLE_ACTIONS = ["unsend", "delete"];
|
|
1733
|
+
function toSnakeCaseKey(key) {
|
|
1734
|
+
return key.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
1735
|
+
}
|
|
1736
|
+
function readStringishParam(params, key) {
|
|
1737
|
+
const value = readStringParam(params, key);
|
|
1738
|
+
if (value) {
|
|
1739
|
+
return value;
|
|
1740
|
+
}
|
|
1741
|
+
const snakeKey = toSnakeCaseKey(key);
|
|
1742
|
+
const raw = (Object.hasOwn(params, key) ? params[key] : void 0) ?? (snakeKey !== key && Object.hasOwn(params, snakeKey) ? params[snakeKey] : void 0);
|
|
1743
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
1744
|
+
return String(raw);
|
|
1745
|
+
}
|
|
1746
|
+
return void 0;
|
|
1747
|
+
}
|
|
1748
|
+
var aibotMessageActions = {
|
|
1749
|
+
listActions: ({ cfg }) => {
|
|
1750
|
+
const hasConfiguredAccount = listAibotAccountIds(cfg).map((accountId) => resolveAibotAccount({ cfg, accountId })).some((account) => account.enabled && account.configured);
|
|
1751
|
+
if (!hasConfiguredAccount) {
|
|
1752
|
+
return [];
|
|
1753
|
+
}
|
|
1754
|
+
return DISCOVERABLE_ACTIONS;
|
|
1755
|
+
},
|
|
1756
|
+
supportsAction: ({ action }) => {
|
|
1757
|
+
const normalizedAction = String(action ?? "").trim();
|
|
1758
|
+
return WS_ACTIONS.has(normalizedAction);
|
|
1759
|
+
},
|
|
1760
|
+
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
|
1761
|
+
const normalizedAction = String(action ?? "").trim();
|
|
1762
|
+
if (!WS_ACTIONS.has(normalizedAction)) {
|
|
1763
|
+
throw new Error(`Clawpool action ${normalizedAction} is not supported`);
|
|
1764
|
+
}
|
|
1765
|
+
const account = resolveAibotAccount({ cfg, accountId });
|
|
1766
|
+
if (!account.enabled) {
|
|
1767
|
+
throw new Error(`Clawpool account "${account.accountId}" is disabled.`);
|
|
1768
|
+
}
|
|
1769
|
+
if (!account.configured) {
|
|
1770
|
+
throw new Error(`Clawpool account "${account.accountId}" is not configured.`);
|
|
1771
|
+
}
|
|
1772
|
+
const client = requireActiveAibotClient(account.accountId);
|
|
1773
|
+
const messageId = readStringishParam(params, "messageId") ?? readStringishParam(params, "msgId");
|
|
1774
|
+
if (!messageId) {
|
|
1775
|
+
throw new Error("Clawpool unsend requires messageId.");
|
|
1776
|
+
}
|
|
1777
|
+
const plan = await resolveSilentUnsendPlan({
|
|
1778
|
+
client,
|
|
1779
|
+
accountId: account.accountId,
|
|
1780
|
+
messageId,
|
|
1781
|
+
targetSessionId: readStringishParam(params, "sessionId"),
|
|
1782
|
+
targetTo: readStringishParam(params, "to"),
|
|
1783
|
+
targetTopic: readStringishParam(params, "topic"),
|
|
1784
|
+
currentChannelId: toolContext?.currentChannelId,
|
|
1785
|
+
currentMessageId: toolContext?.currentMessageId
|
|
1786
|
+
});
|
|
1787
|
+
const ack = await client.deleteMessage(plan.targetDelete.sessionId, plan.targetDelete.messageId);
|
|
1788
|
+
if (plan.commandDelete) {
|
|
1789
|
+
await client.deleteMessage(plan.commandDelete.sessionId, plan.commandDelete.messageId);
|
|
1790
|
+
}
|
|
1791
|
+
if (plan.completionMessageId) {
|
|
1792
|
+
markSilentUnsendCompleted(plan.completionMessageId);
|
|
1793
|
+
}
|
|
1794
|
+
return jsonResult({
|
|
1795
|
+
ok: true,
|
|
1796
|
+
deleted: true,
|
|
1797
|
+
unsent: normalizedAction === "unsend",
|
|
1798
|
+
messageId: String(ack.msg_id ?? messageId),
|
|
1799
|
+
sessionId: String(ack.session_id ?? plan.targetDelete.sessionId)
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
// src/exec-approval-adapter-payload.ts
|
|
1805
|
+
var DEFAULT_ALLOWED_DECISIONS = ["allow-once", "allow-always", "deny"];
|
|
1806
|
+
function normalizeText(value) {
|
|
1807
|
+
return String(value ?? "").replace(/\r\n/g, "\n").trim();
|
|
1808
|
+
}
|
|
1809
|
+
function stripUndefinedFields(record) {
|
|
1810
|
+
const next = {};
|
|
1811
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1812
|
+
if (value !== void 0) {
|
|
1813
|
+
next[key] = value;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return next;
|
|
1817
|
+
}
|
|
1818
|
+
function buildFence(text, language) {
|
|
1819
|
+
let fence = "```";
|
|
1820
|
+
while (text.includes(fence)) {
|
|
1821
|
+
fence += "`";
|
|
1822
|
+
}
|
|
1823
|
+
return `${fence}${language ?? ""}
|
|
1824
|
+
${text}
|
|
1825
|
+
${fence}`;
|
|
1826
|
+
}
|
|
1827
|
+
function resolveHost(value) {
|
|
1828
|
+
return normalizeText(value) === "node" ? "node" : "gateway";
|
|
1829
|
+
}
|
|
1830
|
+
function resolveApprovalSlug(approvalId) {
|
|
1831
|
+
return approvalId.length <= 8 ? approvalId : approvalId.slice(0, 8);
|
|
1832
|
+
}
|
|
1833
|
+
function resolveCommandText(params) {
|
|
1834
|
+
return normalizeText(params.commandPreview) || normalizeText(params.command);
|
|
1835
|
+
}
|
|
1836
|
+
function buildPendingApprovalText(params) {
|
|
1837
|
+
const lines = [];
|
|
1838
|
+
const warningText = params.warningText?.trim();
|
|
1839
|
+
if (warningText) {
|
|
1840
|
+
lines.push(warningText);
|
|
1841
|
+
}
|
|
1842
|
+
lines.push("Approval required.");
|
|
1843
|
+
lines.push("Run:");
|
|
1844
|
+
lines.push(buildFence(`/approve ${params.approvalCommandId} allow-once`, "txt"));
|
|
1845
|
+
lines.push("Pending command:");
|
|
1846
|
+
lines.push(buildFence(params.command, "sh"));
|
|
1847
|
+
lines.push("Other options:");
|
|
1848
|
+
lines.push(
|
|
1849
|
+
buildFence(
|
|
1850
|
+
`/approve ${params.approvalCommandId} allow-always
|
|
1851
|
+
/approve ${params.approvalCommandId} deny`,
|
|
1852
|
+
"txt"
|
|
1853
|
+
)
|
|
1854
|
+
);
|
|
1855
|
+
const info = [];
|
|
1856
|
+
info.push(`Host: ${params.host}`);
|
|
1857
|
+
if (params.nodeId) {
|
|
1858
|
+
info.push(`Node: ${params.nodeId}`);
|
|
1859
|
+
}
|
|
1860
|
+
if (params.cwd) {
|
|
1861
|
+
info.push(`CWD: ${params.cwd}`);
|
|
1862
|
+
}
|
|
1863
|
+
if (params.expiresInSeconds !== void 0) {
|
|
1864
|
+
info.push(`Expires in: ${params.expiresInSeconds}s`);
|
|
1865
|
+
}
|
|
1866
|
+
info.push(`Full id: \`${params.approvalId}\``);
|
|
1867
|
+
lines.push(info.join("\n"));
|
|
1868
|
+
return lines.join("\n\n");
|
|
1869
|
+
}
|
|
1870
|
+
function decisionLabel(decision) {
|
|
1871
|
+
if (decision === "allow-once") {
|
|
1872
|
+
return "allowed once";
|
|
1873
|
+
}
|
|
1874
|
+
if (decision === "allow-always") {
|
|
1875
|
+
return "allowed always";
|
|
1876
|
+
}
|
|
1877
|
+
return "denied";
|
|
1878
|
+
}
|
|
1879
|
+
function mapResolvedStatus(decision) {
|
|
1880
|
+
if (decision === "allow-once") {
|
|
1881
|
+
return "resolved-allow-once";
|
|
1882
|
+
}
|
|
1883
|
+
if (decision === "allow-always") {
|
|
1884
|
+
return "resolved-allow-always";
|
|
1885
|
+
}
|
|
1886
|
+
return "resolved-deny";
|
|
1887
|
+
}
|
|
1888
|
+
function buildResolvedApprovalText(params) {
|
|
1889
|
+
const by = params.resolvedBy ? ` Resolved by ${params.resolvedBy}.` : "";
|
|
1890
|
+
return `\u2705 Exec approval ${decisionLabel(params.decision)}.${by} ID: ${params.approvalId}`;
|
|
1891
|
+
}
|
|
1892
|
+
function buildClawpoolPendingExecApprovalPayload(params) {
|
|
1893
|
+
const approvalId = normalizeText(params.request.id);
|
|
1894
|
+
const command = resolveCommandText(params.request.request);
|
|
1895
|
+
if (!approvalId || !command) {
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
const approvalSlug = resolveApprovalSlug(approvalId);
|
|
1899
|
+
const approvalCommandId = approvalId;
|
|
1900
|
+
const host = resolveHost(params.request.request.host);
|
|
1901
|
+
const nodeId = normalizeText(params.request.request.nodeId) || void 0;
|
|
1902
|
+
const cwd = normalizeText(params.request.request.cwd) || void 0;
|
|
1903
|
+
const expiresInSeconds = Math.max(
|
|
1904
|
+
0,
|
|
1905
|
+
Math.round((params.request.expiresAtMs - params.nowMs) / 1e3)
|
|
1906
|
+
);
|
|
1907
|
+
return {
|
|
1908
|
+
text: buildPendingApprovalText({
|
|
1909
|
+
approvalId,
|
|
1910
|
+
approvalCommandId,
|
|
1911
|
+
command,
|
|
1912
|
+
host,
|
|
1913
|
+
nodeId,
|
|
1914
|
+
cwd,
|
|
1915
|
+
expiresInSeconds
|
|
1916
|
+
}),
|
|
1917
|
+
channelData: {
|
|
1918
|
+
execApproval: {
|
|
1919
|
+
approvalId,
|
|
1920
|
+
approvalSlug,
|
|
1921
|
+
allowedDecisions: [...DEFAULT_ALLOWED_DECISIONS]
|
|
1922
|
+
},
|
|
1923
|
+
clawpool: {
|
|
1924
|
+
execApproval: stripUndefinedFields({
|
|
1925
|
+
approval_command_id: approvalCommandId,
|
|
1926
|
+
command,
|
|
1927
|
+
host,
|
|
1928
|
+
node_id: nodeId,
|
|
1929
|
+
cwd,
|
|
1930
|
+
expires_in_seconds: expiresInSeconds
|
|
1931
|
+
})
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function buildClawpoolResolvedExecApprovalPayload(params) {
|
|
1937
|
+
const approvalId = normalizeText(params.resolved.id);
|
|
1938
|
+
if (!approvalId) {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
const decision = params.resolved.decision;
|
|
1942
|
+
const resolvedBy = normalizeText(params.resolved.resolvedBy) || void 0;
|
|
1943
|
+
const host = params.resolved.request?.host ? resolveHost(params.resolved.request.host) : void 0;
|
|
1944
|
+
const nodeId = normalizeText(params.resolved.request?.nodeId) || void 0;
|
|
1945
|
+
const approvalCommandId = approvalId;
|
|
1946
|
+
const summary = `Exec approval ${decisionLabel(decision)}.`;
|
|
1947
|
+
const detailText = resolvedBy ? `Resolved by ${resolvedBy}.` : void 0;
|
|
1948
|
+
const structuredStatus = stripUndefinedFields({
|
|
1949
|
+
status: mapResolvedStatus(decision),
|
|
1950
|
+
summary,
|
|
1951
|
+
detail_text: detailText,
|
|
1952
|
+
approval_id: approvalId,
|
|
1953
|
+
approval_command_id: approvalCommandId,
|
|
1954
|
+
host,
|
|
1955
|
+
node_id: nodeId,
|
|
1956
|
+
decision,
|
|
1957
|
+
resolved_by_id: resolvedBy
|
|
1958
|
+
});
|
|
1959
|
+
return {
|
|
1960
|
+
text: buildResolvedApprovalText({
|
|
1961
|
+
approvalId,
|
|
1962
|
+
decision,
|
|
1963
|
+
resolvedBy
|
|
1964
|
+
}),
|
|
1965
|
+
channelData: {
|
|
1966
|
+
clawpool: {
|
|
1967
|
+
execStatus: structuredStatus
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// src/channel-exec-approvals.ts
|
|
1974
|
+
function hasConfiguredApprovers(values) {
|
|
1975
|
+
return values?.some((value) => {
|
|
1976
|
+
const normalized = String(value ?? "").trim();
|
|
1977
|
+
return normalized.length > 0;
|
|
1978
|
+
}) ?? false;
|
|
1979
|
+
}
|
|
1980
|
+
function isClawpoolExecApprovalClientEnabled(params) {
|
|
1981
|
+
const account = resolveAibotAccount(params);
|
|
1982
|
+
return Boolean(
|
|
1983
|
+
account.config.execApprovals?.enabled && hasConfiguredApprovers(account.config.execApprovals.approvers)
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
var clawpoolExecApprovalAdapter = {
|
|
1987
|
+
getInitiatingSurfaceState: ({ cfg, accountId }) => isClawpoolExecApprovalClientEnabled({ cfg, accountId }) ? { kind: "enabled" } : { kind: "disabled" },
|
|
1988
|
+
shouldSuppressLocalPrompt: ({ cfg, accountId, payload }) => {
|
|
1989
|
+
if (!isClawpoolExecApprovalClientEnabled({ cfg, accountId })) {
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
const channelData = payload.channelData;
|
|
1993
|
+
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
const execApproval = channelData.execApproval;
|
|
1997
|
+
return Boolean(execApproval) && typeof execApproval === "object" && !Array.isArray(execApproval);
|
|
1998
|
+
},
|
|
1999
|
+
hasConfiguredDmRoute: () => false,
|
|
2000
|
+
shouldSuppressForwardingFallback: () => false,
|
|
2001
|
+
buildPendingPayload: (params) => buildClawpoolPendingExecApprovalPayload(params),
|
|
2002
|
+
buildResolvedPayload: (params) => buildClawpoolResolvedExecApprovalPayload(params),
|
|
2003
|
+
beforeDeliverPending: () => void 0
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
// src/monitor.ts
|
|
2007
|
+
import {
|
|
2008
|
+
createReplyPrefixOptions
|
|
2009
|
+
} from "openclaw/plugin-sdk/channel-runtime";
|
|
2010
|
+
|
|
2011
|
+
// src/active-reply-runs.ts
|
|
2012
|
+
var runsByEvent = /* @__PURE__ */ new Map();
|
|
2013
|
+
var eventKeyBySession = /* @__PURE__ */ new Map();
|
|
2014
|
+
function buildEventKey(accountId, eventId) {
|
|
2015
|
+
return `${String(accountId ?? "").trim()}:${String(eventId ?? "").trim()}`;
|
|
2016
|
+
}
|
|
2017
|
+
function buildSessionKey(accountId, sessionId) {
|
|
2018
|
+
return `${String(accountId ?? "").trim()}:${String(sessionId ?? "").trim()}`;
|
|
2019
|
+
}
|
|
2020
|
+
function registerActiveReplyRun(params) {
|
|
2021
|
+
const accountId = String(params.accountId ?? "").trim();
|
|
2022
|
+
const eventId = String(params.eventId ?? "").trim();
|
|
2023
|
+
const sessionId = String(params.sessionId ?? "").trim();
|
|
2024
|
+
if (!accountId || !eventId || !sessionId) {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
const eventKey = buildEventKey(accountId, eventId);
|
|
2028
|
+
const sessionKey = buildSessionKey(accountId, sessionId);
|
|
2029
|
+
const existingEventKey = eventKeyBySession.get(sessionKey);
|
|
2030
|
+
if (existingEventKey && existingEventKey !== eventKey) {
|
|
2031
|
+
const existing = runsByEvent.get(existingEventKey);
|
|
2032
|
+
if (existing) {
|
|
2033
|
+
existing.abortReason = existing.abortReason || "superseded_by_new_event";
|
|
2034
|
+
existing.controller.abort(existing.abortReason);
|
|
2035
|
+
runsByEvent.delete(existingEventKey);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
const run = {
|
|
2039
|
+
accountId,
|
|
2040
|
+
eventId,
|
|
2041
|
+
sessionId,
|
|
2042
|
+
controller: params.controller,
|
|
2043
|
+
stopRequested: false
|
|
2044
|
+
};
|
|
2045
|
+
runsByEvent.set(eventKey, run);
|
|
2046
|
+
eventKeyBySession.set(sessionKey, eventKey);
|
|
2047
|
+
return run;
|
|
2048
|
+
}
|
|
2049
|
+
function resolveActiveReplyRun(params) {
|
|
2050
|
+
const accountId = String(params.accountId ?? "").trim();
|
|
2051
|
+
if (!accountId) {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
const eventId = String(params.eventId ?? "").trim();
|
|
2055
|
+
if (eventId) {
|
|
2056
|
+
return runsByEvent.get(buildEventKey(accountId, eventId)) ?? null;
|
|
2057
|
+
}
|
|
2058
|
+
const sessionId = String(params.sessionId ?? "").trim();
|
|
2059
|
+
if (!sessionId) {
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
const eventKey = eventKeyBySession.get(buildSessionKey(accountId, sessionId));
|
|
2063
|
+
if (!eventKey) {
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
return runsByEvent.get(eventKey) ?? null;
|
|
2067
|
+
}
|
|
2068
|
+
function clearActiveReplyRun(run) {
|
|
2069
|
+
if (!run) {
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
const eventKey = buildEventKey(run.accountId, run.eventId);
|
|
2073
|
+
const sessionKey = buildSessionKey(run.accountId, run.sessionId);
|
|
2074
|
+
const current = runsByEvent.get(eventKey);
|
|
2075
|
+
if (current === run) {
|
|
2076
|
+
runsByEvent.delete(eventKey);
|
|
2077
|
+
}
|
|
2078
|
+
if (eventKeyBySession.get(sessionKey) === eventKey) {
|
|
2079
|
+
eventKeyBySession.delete(sessionKey);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// src/reply-text-guard.ts
|
|
2084
|
+
var NETWORK_ERROR_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
2085
|
+
var TIMEOUT_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
2086
|
+
var CONTEXT_OVERFLOW_MESSAGE = "\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u8FC7\u957F\uFF0C\u8BF7\u65B0\u5F00\u4F1A\u8BDD\u540E\u91CD\u8BD5\u3002";
|
|
2087
|
+
var GENERIC_STOP_MESSAGE = "\u4E0A\u6E38\u670D\u52A1\u5F02\u5E38\u4E2D\u65AD\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
2088
|
+
function guardInternalReplyText(rawText) {
|
|
2089
|
+
const normalized = String(rawText ?? "").trim();
|
|
2090
|
+
if (!normalized) {
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
if (/^Unhandled stop reason:\s*network_error$/i.test(normalized)) {
|
|
2094
|
+
return {
|
|
2095
|
+
code: "upstream_network_error",
|
|
2096
|
+
rawText: normalized,
|
|
2097
|
+
userText: NETWORK_ERROR_MESSAGE
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
if (/^LLM request timed out\.?$/i.test(normalized)) {
|
|
2101
|
+
return {
|
|
2102
|
+
code: "upstream_timeout",
|
|
2103
|
+
rawText: normalized,
|
|
2104
|
+
userText: TIMEOUT_MESSAGE
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
if (normalized.startsWith("Context overflow: prompt too large for the model.")) {
|
|
2108
|
+
return {
|
|
2109
|
+
code: "upstream_context_overflow",
|
|
2110
|
+
rawText: normalized,
|
|
2111
|
+
userText: CONTEXT_OVERFLOW_MESSAGE
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
if (/^Unhandled stop reason:\s*[a-z0-9_]+$/i.test(normalized)) {
|
|
2115
|
+
return {
|
|
2116
|
+
code: "upstream_stop_reason",
|
|
2117
|
+
rawText: normalized,
|
|
2118
|
+
userText: GENERIC_STOP_MESSAGE
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
return null;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/upstream-retry.ts
|
|
2125
|
+
var DEFAULT_UPSTREAM_RETRY_MAX_ATTEMPTS = 3;
|
|
2126
|
+
var DEFAULT_UPSTREAM_RETRY_BASE_DELAY_MS = 300;
|
|
2127
|
+
var DEFAULT_UPSTREAM_RETRY_MAX_DELAY_MS = 2e3;
|
|
2128
|
+
function clampInt2(value, fallback, min, max) {
|
|
2129
|
+
const n = Number(value);
|
|
2130
|
+
if (!Number.isFinite(n)) {
|
|
2131
|
+
return fallback;
|
|
2132
|
+
}
|
|
2133
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
2134
|
+
}
|
|
2135
|
+
function resolveUpstreamRetryPolicy(account) {
|
|
2136
|
+
const maxAttempts = clampInt2(
|
|
2137
|
+
account.config.upstreamRetryMaxAttempts,
|
|
2138
|
+
DEFAULT_UPSTREAM_RETRY_MAX_ATTEMPTS,
|
|
2139
|
+
1,
|
|
2140
|
+
5
|
|
2141
|
+
);
|
|
2142
|
+
const baseDelayMs = clampInt2(
|
|
2143
|
+
account.config.upstreamRetryBaseDelayMs,
|
|
2144
|
+
DEFAULT_UPSTREAM_RETRY_BASE_DELAY_MS,
|
|
2145
|
+
0,
|
|
2146
|
+
1e4
|
|
2147
|
+
);
|
|
2148
|
+
const maxDelayMs = clampInt2(
|
|
2149
|
+
account.config.upstreamRetryMaxDelayMs,
|
|
2150
|
+
DEFAULT_UPSTREAM_RETRY_MAX_DELAY_MS,
|
|
2151
|
+
baseDelayMs,
|
|
2152
|
+
3e4
|
|
2153
|
+
);
|
|
2154
|
+
return {
|
|
2155
|
+
maxAttempts,
|
|
2156
|
+
baseDelayMs,
|
|
2157
|
+
maxDelayMs
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function isRetryableGuardedReply(guarded) {
|
|
2161
|
+
if (!guarded) {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
return guarded.code === "upstream_network_error" || guarded.code === "upstream_timeout";
|
|
2165
|
+
}
|
|
2166
|
+
function resolveUpstreamRetryDelayMs(policy, attempt) {
|
|
2167
|
+
if (attempt <= 0) {
|
|
2168
|
+
return 0;
|
|
2169
|
+
}
|
|
2170
|
+
const exponent = Math.max(0, attempt - 1);
|
|
2171
|
+
const delay = policy.baseDelayMs * 2 ** exponent;
|
|
2172
|
+
return Math.min(policy.maxDelayMs, Math.floor(delay));
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/runtime.ts
|
|
2176
|
+
var runtime = null;
|
|
2177
|
+
function setAibotRuntime(next) {
|
|
2178
|
+
runtime = next;
|
|
2179
|
+
}
|
|
2180
|
+
function getAibotRuntime() {
|
|
2181
|
+
if (!runtime) {
|
|
2182
|
+
throw new Error("Aibot runtime not initialized");
|
|
2183
|
+
}
|
|
2184
|
+
return runtime;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// src/quoted-reply-body.ts
|
|
2188
|
+
function buildBodyWithQuotedReplyId(rawBody, quotedMessageId) {
|
|
2189
|
+
if (!quotedMessageId) {
|
|
2190
|
+
return rawBody;
|
|
2191
|
+
}
|
|
2192
|
+
return `[quoted_message_id=${quotedMessageId}]
|
|
2193
|
+
${rawBody}`;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// src/inbound-event-dedupe.ts
|
|
2197
|
+
var DEFAULT_INBOUND_EVENT_TTL_MS = 10 * 60 * 1e3;
|
|
2198
|
+
var recentInboundEvents = /* @__PURE__ */ new Map();
|
|
2199
|
+
function normalizeKeyPart(value) {
|
|
2200
|
+
return String(value ?? "").trim();
|
|
2201
|
+
}
|
|
2202
|
+
function resolveInboundEventKey(params) {
|
|
2203
|
+
const accountId = normalizeKeyPart(params.accountId);
|
|
2204
|
+
const eventId = normalizeKeyPart(params.eventId);
|
|
2205
|
+
if (eventId) {
|
|
2206
|
+
return `account:${accountId}:event:${eventId}`;
|
|
2207
|
+
}
|
|
2208
|
+
const sessionId = normalizeKeyPart(params.sessionId);
|
|
2209
|
+
const messageSid = normalizeKeyPart(params.messageSid);
|
|
2210
|
+
return `account:${accountId}:message:${sessionId}:${messageSid}`;
|
|
2211
|
+
}
|
|
2212
|
+
function resolveTTL(ttlMs) {
|
|
2213
|
+
const normalized = Number(ttlMs);
|
|
2214
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
2215
|
+
return DEFAULT_INBOUND_EVENT_TTL_MS;
|
|
2216
|
+
}
|
|
2217
|
+
return Math.floor(normalized);
|
|
2218
|
+
}
|
|
2219
|
+
function pruneExpiredInboundEvents(nowMs) {
|
|
2220
|
+
for (const [key, record] of recentInboundEvents.entries()) {
|
|
2221
|
+
if (record.expiresAt <= nowMs) {
|
|
2222
|
+
recentInboundEvents.delete(key);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
function claimInboundEvent(params) {
|
|
2227
|
+
const nowMs = Number.isFinite(Number(params.nowMs)) ? Math.floor(Number(params.nowMs)) : Date.now();
|
|
2228
|
+
const ttlMs = resolveTTL(params.ttlMs);
|
|
2229
|
+
pruneExpiredInboundEvents(nowMs);
|
|
2230
|
+
const key = resolveInboundEventKey(params);
|
|
2231
|
+
const existing = recentInboundEvents.get(key);
|
|
2232
|
+
if (existing && existing.expiresAt > nowMs) {
|
|
2233
|
+
return {
|
|
2234
|
+
duplicate: true,
|
|
2235
|
+
confirmed: existing.confirmed,
|
|
2236
|
+
claim: {
|
|
2237
|
+
key,
|
|
2238
|
+
confirmed: existing.confirmed
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
recentInboundEvents.set(key, {
|
|
2243
|
+
expiresAt: nowMs + ttlMs,
|
|
2244
|
+
confirmed: false
|
|
2245
|
+
});
|
|
2246
|
+
return {
|
|
2247
|
+
duplicate: false,
|
|
2248
|
+
confirmed: false,
|
|
2249
|
+
claim: {
|
|
2250
|
+
key,
|
|
2251
|
+
confirmed: false
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
function confirmInboundEvent(claim, params) {
|
|
2256
|
+
const key = normalizeKeyPart(claim.key);
|
|
2257
|
+
if (!key) {
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
const nowMs = Number.isFinite(Number(params?.nowMs)) ? Math.floor(Number(params?.nowMs)) : Date.now();
|
|
2261
|
+
const ttlMs = resolveTTL(params?.ttlMs);
|
|
2262
|
+
pruneExpiredInboundEvents(nowMs);
|
|
2263
|
+
recentInboundEvents.set(key, {
|
|
2264
|
+
expiresAt: nowMs + ttlMs,
|
|
2265
|
+
confirmed: true
|
|
2266
|
+
});
|
|
2267
|
+
claim.confirmed = true;
|
|
2268
|
+
}
|
|
2269
|
+
function releaseInboundEvent(claim) {
|
|
2270
|
+
const key = normalizeKeyPart(claim.key);
|
|
2271
|
+
if (!key || claim.confirmed) {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const current = recentInboundEvents.get(key);
|
|
2275
|
+
if (current && !current.confirmed) {
|
|
2276
|
+
recentInboundEvents.delete(key);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// src/exec-approval-card.ts
|
|
2281
|
+
var BIZ_CARD_EXTRA_KEY = "biz_card";
|
|
2282
|
+
var BIZ_CARD_VERSION = 1;
|
|
2283
|
+
var EXEC_APPROVAL_CARD_TYPE = "exec_approval";
|
|
2284
|
+
function normalizeDecision(value) {
|
|
2285
|
+
const normalized = String(value ?? "").trim();
|
|
2286
|
+
if (normalized === "allow-once" || normalized === "allow-always" || normalized === "deny") {
|
|
2287
|
+
return normalized;
|
|
2288
|
+
}
|
|
2289
|
+
return void 0;
|
|
2290
|
+
}
|
|
2291
|
+
function normalizeText2(value) {
|
|
2292
|
+
return String(value ?? "").replace(/\r\n/g, "\n").trim();
|
|
2293
|
+
}
|
|
2294
|
+
function summarizeTextPrefix(text) {
|
|
2295
|
+
const normalized = normalizeText2(text);
|
|
2296
|
+
if (!normalized) {
|
|
2297
|
+
return "";
|
|
2298
|
+
}
|
|
2299
|
+
const firstLine = normalized.split("\n", 1)[0]?.trim() ?? "";
|
|
2300
|
+
if (!firstLine) {
|
|
2301
|
+
return "";
|
|
2302
|
+
}
|
|
2303
|
+
return firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
|
|
2304
|
+
}
|
|
2305
|
+
function getExecApprovalReplyMetadata(payload) {
|
|
2306
|
+
const channelData = payload.channelData;
|
|
2307
|
+
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
const execApproval = channelData.execApproval;
|
|
2311
|
+
if (!execApproval || typeof execApproval !== "object" || Array.isArray(execApproval)) {
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
const record = execApproval;
|
|
2315
|
+
const approvalId = normalizeText2(record.approvalId);
|
|
2316
|
+
const approvalSlug = normalizeText2(record.approvalSlug);
|
|
2317
|
+
if (!approvalId || !approvalSlug) {
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
const allowedDecisions = Array.isArray(record.allowedDecisions) ? record.allowedDecisions.map(normalizeDecision).filter((value) => Boolean(value)) : [];
|
|
2321
|
+
return {
|
|
2322
|
+
approvalId,
|
|
2323
|
+
approvalSlug,
|
|
2324
|
+
allowedDecisions: allowedDecisions.length > 0 ? allowedDecisions : ["allow-once", "allow-always", "deny"]
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
function getStructuredClawpoolExecApproval(payload) {
|
|
2328
|
+
const channelData = payload.channelData;
|
|
2329
|
+
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
|
|
2330
|
+
return null;
|
|
2331
|
+
}
|
|
2332
|
+
const clawpool = channelData.clawpool;
|
|
2333
|
+
if (!clawpool || typeof clawpool !== "object" || Array.isArray(clawpool)) {
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
const execApproval = clawpool.execApproval;
|
|
2337
|
+
if (!execApproval || typeof execApproval !== "object" || Array.isArray(execApproval)) {
|
|
2338
|
+
return null;
|
|
2339
|
+
}
|
|
2340
|
+
const record = execApproval;
|
|
2341
|
+
const approvalCommandId = normalizeText2(record.approval_command_id);
|
|
2342
|
+
const command = normalizeText2(record.command);
|
|
2343
|
+
const host = normalizeText2(record.host);
|
|
2344
|
+
if (!approvalCommandId || !command || !host) {
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
const expiresValue = Number(record.expires_in_seconds);
|
|
2348
|
+
return {
|
|
2349
|
+
approvalCommandId,
|
|
2350
|
+
command,
|
|
2351
|
+
host,
|
|
2352
|
+
nodeId: normalizeText2(record.node_id) || void 0,
|
|
2353
|
+
cwd: normalizeText2(record.cwd) || void 0,
|
|
2354
|
+
expiresInSeconds: Number.isFinite(expiresValue) && expiresValue >= 0 ? Math.floor(expiresValue) : void 0,
|
|
2355
|
+
warningText: normalizeText2(record.warning_text) || void 0
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function diagnoseExecApprovalPayload(payload) {
|
|
2359
|
+
const rawText = String(payload.text ?? "");
|
|
2360
|
+
const textPrefix = summarizeTextPrefix(rawText);
|
|
2361
|
+
const channelData = payload.channelData;
|
|
2362
|
+
const hasChannelData = Boolean(channelData) && typeof channelData === "object" && !Array.isArray(channelData);
|
|
2363
|
+
const execApproval = hasChannelData ? channelData.execApproval : void 0;
|
|
2364
|
+
const hasExecApprovalField = Boolean(execApproval) && typeof execApproval === "object" && !Array.isArray(execApproval);
|
|
2365
|
+
const execApprovalRecord = hasExecApprovalField ? execApproval : void 0;
|
|
2366
|
+
const approvalId = normalizeText2(execApprovalRecord?.approvalId);
|
|
2367
|
+
const approvalSlug = normalizeText2(execApprovalRecord?.approvalSlug);
|
|
2368
|
+
const allowedDecisionCount = Array.isArray(execApprovalRecord?.allowedDecisions) ? execApprovalRecord.allowedDecisions.length : 0;
|
|
2369
|
+
const structured = getStructuredClawpoolExecApproval(payload);
|
|
2370
|
+
const hasClawpoolApprovalField = Boolean(structured);
|
|
2371
|
+
const isCandidate = hasExecApprovalField || hasClawpoolApprovalField;
|
|
2372
|
+
if (!isCandidate) {
|
|
2373
|
+
return {
|
|
2374
|
+
isCandidate: false,
|
|
2375
|
+
matched: false,
|
|
2376
|
+
reason: "non-approval-payload",
|
|
2377
|
+
hasChannelData,
|
|
2378
|
+
hasExecApprovalField,
|
|
2379
|
+
hasClawpoolApprovalField,
|
|
2380
|
+
allowedDecisionCount,
|
|
2381
|
+
commandDetected: false,
|
|
2382
|
+
textPrefix
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
if (!hasChannelData) {
|
|
2386
|
+
return {
|
|
2387
|
+
isCandidate: true,
|
|
2388
|
+
matched: false,
|
|
2389
|
+
reason: "missing-channel-data",
|
|
2390
|
+
hasChannelData,
|
|
2391
|
+
hasExecApprovalField,
|
|
2392
|
+
hasClawpoolApprovalField,
|
|
2393
|
+
allowedDecisionCount,
|
|
2394
|
+
commandDetected: false,
|
|
2395
|
+
textPrefix
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
if (!hasExecApprovalField) {
|
|
2399
|
+
return {
|
|
2400
|
+
isCandidate: true,
|
|
2401
|
+
matched: false,
|
|
2402
|
+
reason: "missing-exec-approval-channel-data",
|
|
2403
|
+
hasChannelData,
|
|
2404
|
+
hasExecApprovalField,
|
|
2405
|
+
hasClawpoolApprovalField,
|
|
2406
|
+
allowedDecisionCount,
|
|
2407
|
+
commandDetected: Boolean(structured?.command),
|
|
2408
|
+
approvalCommandId: structured?.approvalCommandId,
|
|
2409
|
+
host: structured?.host,
|
|
2410
|
+
nodeId: structured?.nodeId,
|
|
2411
|
+
cwd: structured?.cwd,
|
|
2412
|
+
expiresInSeconds: structured?.expiresInSeconds,
|
|
2413
|
+
textPrefix
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
if (!structured) {
|
|
2417
|
+
return {
|
|
2418
|
+
isCandidate: true,
|
|
2419
|
+
matched: false,
|
|
2420
|
+
reason: "missing-clawpool-channel-data",
|
|
2421
|
+
hasChannelData,
|
|
2422
|
+
hasExecApprovalField,
|
|
2423
|
+
hasClawpoolApprovalField,
|
|
2424
|
+
approvalId: approvalId || void 0,
|
|
2425
|
+
approvalSlug: approvalSlug || void 0,
|
|
2426
|
+
allowedDecisionCount,
|
|
2427
|
+
commandDetected: false,
|
|
2428
|
+
textPrefix
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
if (!approvalId || !approvalSlug) {
|
|
2432
|
+
return {
|
|
2433
|
+
isCandidate: true,
|
|
2434
|
+
matched: false,
|
|
2435
|
+
reason: "missing-approval-identifiers",
|
|
2436
|
+
hasChannelData,
|
|
2437
|
+
hasExecApprovalField,
|
|
2438
|
+
hasClawpoolApprovalField,
|
|
2439
|
+
approvalId: approvalId || void 0,
|
|
2440
|
+
approvalSlug: approvalSlug || void 0,
|
|
2441
|
+
allowedDecisionCount,
|
|
2442
|
+
approvalCommandId: structured.approvalCommandId,
|
|
2443
|
+
commandDetected: Boolean(structured.command),
|
|
2444
|
+
host: structured.host || void 0,
|
|
2445
|
+
nodeId: structured.nodeId,
|
|
2446
|
+
cwd: structured.cwd,
|
|
2447
|
+
expiresInSeconds: structured.expiresInSeconds,
|
|
2448
|
+
textPrefix
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
if (!structured.approvalCommandId) {
|
|
2452
|
+
return {
|
|
2453
|
+
isCandidate: true,
|
|
2454
|
+
matched: false,
|
|
2455
|
+
reason: "missing-approval-command-id",
|
|
2456
|
+
hasChannelData,
|
|
2457
|
+
hasExecApprovalField,
|
|
2458
|
+
hasClawpoolApprovalField,
|
|
2459
|
+
approvalId,
|
|
2460
|
+
approvalSlug,
|
|
2461
|
+
allowedDecisionCount,
|
|
2462
|
+
commandDetected: Boolean(structured.command),
|
|
2463
|
+
host: structured.host || void 0,
|
|
2464
|
+
nodeId: structured.nodeId,
|
|
2465
|
+
cwd: structured.cwd,
|
|
2466
|
+
expiresInSeconds: structured.expiresInSeconds,
|
|
2467
|
+
textPrefix
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
if (!structured.command) {
|
|
2471
|
+
return {
|
|
2472
|
+
isCandidate: true,
|
|
2473
|
+
matched: false,
|
|
2474
|
+
reason: "missing-pending-command",
|
|
2475
|
+
hasChannelData,
|
|
2476
|
+
hasExecApprovalField,
|
|
2477
|
+
hasClawpoolApprovalField,
|
|
2478
|
+
approvalId,
|
|
2479
|
+
approvalSlug,
|
|
2480
|
+
allowedDecisionCount,
|
|
2481
|
+
approvalCommandId: structured.approvalCommandId,
|
|
2482
|
+
commandDetected: false,
|
|
2483
|
+
host: structured.host || void 0,
|
|
2484
|
+
nodeId: structured.nodeId,
|
|
2485
|
+
cwd: structured.cwd,
|
|
2486
|
+
expiresInSeconds: structured.expiresInSeconds,
|
|
2487
|
+
textPrefix
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
if (!structured.host) {
|
|
2491
|
+
return {
|
|
2492
|
+
isCandidate: true,
|
|
2493
|
+
matched: false,
|
|
2494
|
+
reason: "missing-host",
|
|
2495
|
+
hasChannelData,
|
|
2496
|
+
hasExecApprovalField,
|
|
2497
|
+
hasClawpoolApprovalField,
|
|
2498
|
+
approvalId,
|
|
2499
|
+
approvalSlug,
|
|
2500
|
+
allowedDecisionCount,
|
|
2501
|
+
approvalCommandId: structured.approvalCommandId,
|
|
2502
|
+
commandDetected: true,
|
|
2503
|
+
nodeId: structured.nodeId,
|
|
2504
|
+
cwd: structured.cwd,
|
|
2505
|
+
expiresInSeconds: structured.expiresInSeconds,
|
|
2506
|
+
textPrefix
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
return {
|
|
2510
|
+
isCandidate: true,
|
|
2511
|
+
matched: true,
|
|
2512
|
+
reason: "ok",
|
|
2513
|
+
hasChannelData,
|
|
2514
|
+
hasExecApprovalField,
|
|
2515
|
+
hasClawpoolApprovalField,
|
|
2516
|
+
approvalId,
|
|
2517
|
+
approvalSlug,
|
|
2518
|
+
allowedDecisionCount,
|
|
2519
|
+
approvalCommandId: structured.approvalCommandId,
|
|
2520
|
+
commandDetected: true,
|
|
2521
|
+
host: structured.host,
|
|
2522
|
+
nodeId: structured.nodeId,
|
|
2523
|
+
cwd: structured.cwd,
|
|
2524
|
+
expiresInSeconds: structured.expiresInSeconds,
|
|
2525
|
+
textPrefix
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
function buildExecApprovalFallbackText(params) {
|
|
2529
|
+
const compactCommand = params.command.replace(/\s+/g, " ").trim();
|
|
2530
|
+
const summaryCommand = compactCommand.length > 160 ? `${compactCommand.slice(0, 157)}...` : compactCommand;
|
|
2531
|
+
return `[Exec Approval] ${summaryCommand} (${params.host})
|
|
2532
|
+
/approve ${params.approvalCommandId} allow-once`;
|
|
2533
|
+
}
|
|
2534
|
+
function buildExecApprovalCardEnvelope(payload) {
|
|
2535
|
+
const metadata = getExecApprovalReplyMetadata(payload);
|
|
2536
|
+
const structured = getStructuredClawpoolExecApproval(payload);
|
|
2537
|
+
if (!metadata || !structured) {
|
|
2538
|
+
return void 0;
|
|
2539
|
+
}
|
|
2540
|
+
const cardPayload = {
|
|
2541
|
+
approval_id: metadata.approvalId,
|
|
2542
|
+
approval_slug: metadata.approvalSlug,
|
|
2543
|
+
approval_command_id: structured.approvalCommandId,
|
|
2544
|
+
command: structured.command,
|
|
2545
|
+
host: structured.host,
|
|
2546
|
+
allowed_decisions: metadata.allowedDecisions
|
|
2547
|
+
};
|
|
2548
|
+
if (structured.nodeId) {
|
|
2549
|
+
cardPayload.node_id = structured.nodeId;
|
|
2550
|
+
}
|
|
2551
|
+
if (structured.cwd) {
|
|
2552
|
+
cardPayload.cwd = structured.cwd;
|
|
2553
|
+
}
|
|
2554
|
+
if (structured.warningText) {
|
|
2555
|
+
cardPayload.warning_text = structured.warningText;
|
|
2556
|
+
}
|
|
2557
|
+
if (structured.expiresInSeconds !== void 0) {
|
|
2558
|
+
cardPayload.expires_in_seconds = structured.expiresInSeconds;
|
|
2559
|
+
}
|
|
2560
|
+
return {
|
|
2561
|
+
extra: {
|
|
2562
|
+
[BIZ_CARD_EXTRA_KEY]: {
|
|
2563
|
+
version: BIZ_CARD_VERSION,
|
|
2564
|
+
type: EXEC_APPROVAL_CARD_TYPE,
|
|
2565
|
+
payload: cardPayload
|
|
2566
|
+
},
|
|
2567
|
+
channel_data: payload.channelData ?? {}
|
|
2568
|
+
},
|
|
2569
|
+
fallbackText: buildExecApprovalFallbackText({
|
|
2570
|
+
approvalCommandId: structured.approvalCommandId,
|
|
2571
|
+
command: structured.command,
|
|
2572
|
+
host: structured.host
|
|
2573
|
+
})
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// src/exec-status-card.ts
|
|
2578
|
+
var BIZ_CARD_EXTRA_KEY2 = "biz_card";
|
|
2579
|
+
var BIZ_CARD_VERSION2 = 1;
|
|
2580
|
+
var EXEC_STATUS_CARD_TYPE = "exec_status";
|
|
2581
|
+
function normalizeText3(value) {
|
|
2582
|
+
return String(value ?? "").replace(/\r\n/g, "\n").trim();
|
|
2583
|
+
}
|
|
2584
|
+
function stripUndefinedFields2(record) {
|
|
2585
|
+
const next = {};
|
|
2586
|
+
for (const [key, value] of Object.entries(record)) {
|
|
2587
|
+
if (value !== void 0) {
|
|
2588
|
+
next[key] = value;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return next;
|
|
2592
|
+
}
|
|
2593
|
+
function buildExecStatusFallbackText(parsed) {
|
|
2594
|
+
const summary = parsed.summary.replace(/\s+/g, " ").trim();
|
|
2595
|
+
const compactSummary = summary.length > 180 ? `${summary.slice(0, 177)}...` : summary;
|
|
2596
|
+
return `[Exec Status] ${compactSummary}`;
|
|
2597
|
+
}
|
|
2598
|
+
function buildExecStatusExtra(parsed) {
|
|
2599
|
+
return {
|
|
2600
|
+
[BIZ_CARD_EXTRA_KEY2]: {
|
|
2601
|
+
version: BIZ_CARD_VERSION2,
|
|
2602
|
+
type: EXEC_STATUS_CARD_TYPE,
|
|
2603
|
+
payload: stripUndefinedFields2(parsed)
|
|
2604
|
+
},
|
|
2605
|
+
channel_data: {
|
|
2606
|
+
clawpool: {
|
|
2607
|
+
execStatus: stripUndefinedFields2(parsed)
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
function parseStructuredExecStatus(payload) {
|
|
2613
|
+
const channelData = payload.channelData;
|
|
2614
|
+
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
|
|
2615
|
+
return null;
|
|
2616
|
+
}
|
|
2617
|
+
const clawpool = channelData.clawpool;
|
|
2618
|
+
if (!clawpool || typeof clawpool !== "object" || Array.isArray(clawpool)) {
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
const execStatus = clawpool.execStatus;
|
|
2622
|
+
if (!execStatus || typeof execStatus !== "object" || Array.isArray(execStatus)) {
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
const record = execStatus;
|
|
2626
|
+
const status = normalizeText3(record.status);
|
|
2627
|
+
const summary = normalizeText3(record.summary);
|
|
2628
|
+
const allowedStatuses = /* @__PURE__ */ new Set([
|
|
2629
|
+
"approval-expired",
|
|
2630
|
+
"approval-forwarded",
|
|
2631
|
+
"approval-unavailable",
|
|
2632
|
+
"resolved-allow-once",
|
|
2633
|
+
"resolved-allow-always",
|
|
2634
|
+
"resolved-deny",
|
|
2635
|
+
"running",
|
|
2636
|
+
"finished",
|
|
2637
|
+
"denied"
|
|
2638
|
+
]);
|
|
2639
|
+
if (!allowedStatuses.has(status) || !summary) {
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
return stripUndefinedFields2({
|
|
2643
|
+
status,
|
|
2644
|
+
summary,
|
|
2645
|
+
detail_text: normalizeText3(record.detail_text) || void 0,
|
|
2646
|
+
approval_id: normalizeText3(record.approval_id) || void 0,
|
|
2647
|
+
approval_command_id: normalizeText3(record.approval_command_id) || void 0,
|
|
2648
|
+
host: normalizeText3(record.host) || void 0,
|
|
2649
|
+
node_id: normalizeText3(record.node_id) || void 0,
|
|
2650
|
+
session_id: normalizeText3(record.session_id) || void 0,
|
|
2651
|
+
reason: normalizeText3(record.reason) || void 0,
|
|
2652
|
+
decision: record.decision === "allow-once" || record.decision === "allow-always" || record.decision === "deny" ? record.decision : void 0,
|
|
2653
|
+
resolved_by_id: normalizeText3(record.resolved_by_id) || void 0,
|
|
2654
|
+
command: normalizeText3(record.command) || void 0,
|
|
2655
|
+
exit_label: normalizeText3(record.exit_label) || void 0,
|
|
2656
|
+
channel_label: normalizeText3(record.channel_label) || void 0,
|
|
2657
|
+
warning_text: normalizeText3(record.warning_text) || void 0
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
function buildExecStatusCardEnvelope(payload) {
|
|
2661
|
+
const parsed = parseStructuredExecStatus(payload);
|
|
2662
|
+
if (!parsed) {
|
|
2663
|
+
return void 0;
|
|
2664
|
+
}
|
|
2665
|
+
return {
|
|
2666
|
+
extra: buildExecStatusExtra(parsed),
|
|
2667
|
+
fallbackText: buildExecStatusFallbackText(parsed)
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
function buildExecApprovalResolutionReply(params) {
|
|
2671
|
+
const decisionLabel2 = params.decision === "allow-once" ? "Allow once" : params.decision === "allow-always" ? "Allow always" : "Deny";
|
|
2672
|
+
const actorId = params.actorId.trim() || "unknown";
|
|
2673
|
+
const summary = `${decisionLabel2} selected by ${actorId}.`;
|
|
2674
|
+
const detailText = params.reason?.trim() ? `Reason: ${params.reason.trim()}` : void 0;
|
|
2675
|
+
const payload = stripUndefinedFields2({
|
|
2676
|
+
status: params.decision === "allow-once" ? "resolved-allow-once" : params.decision === "allow-always" ? "resolved-allow-always" : "resolved-deny",
|
|
2677
|
+
summary,
|
|
2678
|
+
detail_text: detailText,
|
|
2679
|
+
approval_id: params.approvalId.trim(),
|
|
2680
|
+
approval_command_id: params.approvalCommandId.trim(),
|
|
2681
|
+
decision: params.decision,
|
|
2682
|
+
reason: params.reason?.trim() || void 0,
|
|
2683
|
+
resolved_by_id: actorId
|
|
2684
|
+
});
|
|
2685
|
+
return {
|
|
2686
|
+
extra: buildExecStatusExtra(payload),
|
|
2687
|
+
fallbackText: buildExecStatusFallbackText(payload)
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// src/outbound-envelope.ts
|
|
2692
|
+
function buildAibotOutboundEnvelope(payload) {
|
|
2693
|
+
const execApprovalDiagnostic = diagnoseExecApprovalPayload(payload);
|
|
2694
|
+
const execApprovalCard = buildExecApprovalCardEnvelope(payload);
|
|
2695
|
+
const execStatusCard = execApprovalCard ? void 0 : buildExecStatusCardEnvelope(payload);
|
|
2696
|
+
const envelope = execApprovalCard ?? execStatusCard;
|
|
2697
|
+
return {
|
|
2698
|
+
text: envelope?.fallbackText ?? String(payload.text ?? ""),
|
|
2699
|
+
extra: envelope?.extra,
|
|
2700
|
+
cardKind: execApprovalCard ? "exec_approval" : execStatusCard ? "exec_status" : void 0,
|
|
2701
|
+
execApprovalDiagnostic
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// src/exec-approval-command.ts
|
|
2706
|
+
var COMMAND_REGEX = /^\/approve(?:@[^\s]+)?(?:\s|$)/i;
|
|
2707
|
+
var DIRECTIVE_REGEX = /\[\[exec-approval-resolution\|(.+?)\]\]/i;
|
|
2708
|
+
var DECISION_ALIASES = {
|
|
2709
|
+
allow: "allow-once",
|
|
2710
|
+
once: "allow-once",
|
|
2711
|
+
"allow-once": "allow-once",
|
|
2712
|
+
allowonce: "allow-once",
|
|
2713
|
+
always: "allow-always",
|
|
2714
|
+
"allow-always": "allow-always",
|
|
2715
|
+
allowalways: "allow-always",
|
|
2716
|
+
deny: "deny",
|
|
2717
|
+
reject: "deny",
|
|
2718
|
+
block: "deny"
|
|
2719
|
+
};
|
|
2720
|
+
var EXEC_APPROVAL_USAGE = "Usage: /approve <id> allow-once|allow-always|deny";
|
|
2721
|
+
function decodeDirectiveValue(rawValue) {
|
|
2722
|
+
const normalized = rawValue.trim();
|
|
2723
|
+
if (!normalized) {
|
|
2724
|
+
return void 0;
|
|
2725
|
+
}
|
|
2726
|
+
if (!normalized.includes("%")) {
|
|
2727
|
+
return normalized;
|
|
2728
|
+
}
|
|
2729
|
+
try {
|
|
2730
|
+
return decodeURIComponent(normalized);
|
|
2731
|
+
} catch {
|
|
2732
|
+
return normalized;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
function parseExecApprovalResolutionDirective(raw) {
|
|
2736
|
+
const match = DIRECTIVE_REGEX.exec(String(raw ?? ""));
|
|
2737
|
+
if (!match) {
|
|
2738
|
+
return { matched: false };
|
|
2739
|
+
}
|
|
2740
|
+
const body = String(match[1] ?? "").trim();
|
|
2741
|
+
if (!body) {
|
|
2742
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2743
|
+
}
|
|
2744
|
+
const payload = /* @__PURE__ */ new Map();
|
|
2745
|
+
for (const segment of body.split("|")) {
|
|
2746
|
+
const normalizedSegment = segment.trim();
|
|
2747
|
+
if (!normalizedSegment) {
|
|
2748
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2749
|
+
}
|
|
2750
|
+
const separatorIndex = normalizedSegment.indexOf("=");
|
|
2751
|
+
if (separatorIndex <= 0 || separatorIndex >= normalizedSegment.length - 1) {
|
|
2752
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2753
|
+
}
|
|
2754
|
+
const key = normalizedSegment.slice(0, separatorIndex).trim();
|
|
2755
|
+
const rawValue = normalizedSegment.slice(separatorIndex + 1);
|
|
2756
|
+
const value = decodeDirectiveValue(rawValue);
|
|
2757
|
+
if (!key || !value) {
|
|
2758
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2759
|
+
}
|
|
2760
|
+
payload.set(key, value);
|
|
2761
|
+
}
|
|
2762
|
+
const decision = DECISION_ALIASES[String(payload.get("decision") ?? "").toLowerCase()];
|
|
2763
|
+
const approvalId = String(payload.get("approval_id") ?? "").trim() || void 0;
|
|
2764
|
+
const approvalCommandId = String(
|
|
2765
|
+
payload.get("approval_command_id") ?? payload.get("approval_id") ?? payload.get("approval_slug") ?? ""
|
|
2766
|
+
).trim();
|
|
2767
|
+
if (!decision || !approvalCommandId) {
|
|
2768
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2769
|
+
}
|
|
2770
|
+
const reason = String(payload.get("reason") ?? "").trim() || void 0;
|
|
2771
|
+
return {
|
|
2772
|
+
matched: true,
|
|
2773
|
+
ok: true,
|
|
2774
|
+
id: approvalCommandId,
|
|
2775
|
+
approvalId,
|
|
2776
|
+
approvalCommandId,
|
|
2777
|
+
decision,
|
|
2778
|
+
reason
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
function parseExecApprovalCommand(raw) {
|
|
2782
|
+
const directiveParsed = parseExecApprovalResolutionDirective(raw);
|
|
2783
|
+
if (directiveParsed.matched) {
|
|
2784
|
+
return directiveParsed;
|
|
2785
|
+
}
|
|
2786
|
+
const trimmed = String(raw ?? "").trim();
|
|
2787
|
+
const commandMatch = trimmed.match(COMMAND_REGEX);
|
|
2788
|
+
if (!commandMatch) {
|
|
2789
|
+
return { matched: false };
|
|
2790
|
+
}
|
|
2791
|
+
const rest = trimmed.slice(commandMatch[0].length).trim();
|
|
2792
|
+
if (!rest) {
|
|
2793
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2794
|
+
}
|
|
2795
|
+
const tokens = rest.split(/\s+/).filter(Boolean);
|
|
2796
|
+
if (tokens.length < 2) {
|
|
2797
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2798
|
+
}
|
|
2799
|
+
const first = tokens[0].toLowerCase();
|
|
2800
|
+
const second = tokens[1].toLowerCase();
|
|
2801
|
+
const firstDecision = DECISION_ALIASES[first];
|
|
2802
|
+
if (firstDecision) {
|
|
2803
|
+
const id = tokens.slice(1).join(" ").trim();
|
|
2804
|
+
if (!id) {
|
|
2805
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2806
|
+
}
|
|
2807
|
+
return {
|
|
2808
|
+
matched: true,
|
|
2809
|
+
ok: true,
|
|
2810
|
+
decision: firstDecision,
|
|
2811
|
+
id,
|
|
2812
|
+
approvalCommandId: id
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
const secondDecision = DECISION_ALIASES[second];
|
|
2816
|
+
if (!secondDecision) {
|
|
2817
|
+
return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
|
|
2818
|
+
}
|
|
2819
|
+
return {
|
|
2820
|
+
matched: true,
|
|
2821
|
+
ok: true,
|
|
2822
|
+
decision: secondDecision,
|
|
2823
|
+
id: tokens[0],
|
|
2824
|
+
approvalCommandId: tokens[0]
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// src/exec-approvals.ts
|
|
2829
|
+
function normalizeExecApprovalConfig(config) {
|
|
2830
|
+
const approvers = (config?.approvers ?? []).map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
2831
|
+
return {
|
|
2832
|
+
enabled: Boolean(config?.enabled && approvers.length > 0),
|
|
2833
|
+
approvers
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
function formatCommandFailure(result) {
|
|
2837
|
+
const parts = [String(result.stderr ?? "").trim(), String(result.stdout ?? "").trim()].filter(Boolean).flatMap((text) => text.split(/\r?\n/)).map((line) => line.trim()).filter(Boolean);
|
|
2838
|
+
if (parts.length > 0) {
|
|
2839
|
+
return parts.at(-1) ?? "unknown error";
|
|
2840
|
+
}
|
|
2841
|
+
if (result.signal) {
|
|
2842
|
+
return `signal=${result.signal}`;
|
|
2843
|
+
}
|
|
2844
|
+
if (result.code !== null) {
|
|
2845
|
+
return `exit code ${result.code}`;
|
|
2846
|
+
}
|
|
2847
|
+
return result.termination || "unknown failure";
|
|
2848
|
+
}
|
|
2849
|
+
function resolveOpenClawCliArgvPrefix() {
|
|
2850
|
+
const execPath = String(process.execPath ?? "").trim();
|
|
2851
|
+
const scriptPath = String(process.argv[1] ?? "").trim();
|
|
2852
|
+
if (execPath && scriptPath) {
|
|
2853
|
+
return [execPath, scriptPath];
|
|
2854
|
+
}
|
|
2855
|
+
return ["openclaw"];
|
|
2856
|
+
}
|
|
2857
|
+
function buildExecApprovalResolveArgv(params) {
|
|
2858
|
+
const cliArgvPrefix = params.cliArgvPrefix && params.cliArgvPrefix.length > 0 ? params.cliArgvPrefix : resolveOpenClawCliArgvPrefix();
|
|
2859
|
+
const timeoutMs = Math.max(1e3, Math.floor(params.timeoutMs ?? 15e3));
|
|
2860
|
+
return [
|
|
2861
|
+
...cliArgvPrefix,
|
|
2862
|
+
"gateway",
|
|
2863
|
+
"call",
|
|
2864
|
+
"exec.approval.resolve",
|
|
2865
|
+
"--json",
|
|
2866
|
+
"--timeout",
|
|
2867
|
+
String(timeoutMs),
|
|
2868
|
+
"--params",
|
|
2869
|
+
JSON.stringify({
|
|
2870
|
+
id: params.id,
|
|
2871
|
+
decision: params.decision
|
|
2872
|
+
})
|
|
2873
|
+
];
|
|
2874
|
+
}
|
|
2875
|
+
async function submitExecApprovalDecision(params) {
|
|
2876
|
+
const runner = params.runner ?? params.runtime.system.runCommandWithTimeout;
|
|
2877
|
+
const timeoutMs = Math.max(1e3, Math.floor(params.timeoutMs ?? 15e3));
|
|
2878
|
+
const argv = buildExecApprovalResolveArgv({
|
|
2879
|
+
cliArgvPrefix: params.cliArgvPrefix,
|
|
2880
|
+
id: params.id,
|
|
2881
|
+
decision: params.decision,
|
|
2882
|
+
timeoutMs
|
|
2883
|
+
});
|
|
2884
|
+
const result = await runner(argv, { timeoutMs });
|
|
2885
|
+
if (result.termination !== "exit" || result.code !== 0) {
|
|
2886
|
+
throw new Error(formatCommandFailure(result));
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
function isExecApprovalApprover(params) {
|
|
2890
|
+
const senderId = String(params.senderId ?? "").trim();
|
|
2891
|
+
if (!senderId) {
|
|
2892
|
+
return false;
|
|
2893
|
+
}
|
|
2894
|
+
const config = normalizeExecApprovalConfig(params.account.config.execApprovals);
|
|
2895
|
+
if (!config.enabled) {
|
|
2896
|
+
return false;
|
|
2897
|
+
}
|
|
2898
|
+
return config.approvers.includes(senderId);
|
|
2899
|
+
}
|
|
2900
|
+
function disabledReplyText(accountId) {
|
|
2901
|
+
return `\u274C ClawPool exec approvals are not enabled for account ${accountId}.`;
|
|
2902
|
+
}
|
|
2903
|
+
function unauthorizedReplyText() {
|
|
2904
|
+
return "\u274C You are not authorized to approve exec requests on ClawPool.";
|
|
2905
|
+
}
|
|
2906
|
+
function successReplyText(command) {
|
|
2907
|
+
return `\u2705 Exec approval ${command.decision} submitted for ${command.id}.`;
|
|
2908
|
+
}
|
|
2909
|
+
function failureReplyText(message) {
|
|
2910
|
+
return `\u274C Failed to submit approval: ${message}`;
|
|
2911
|
+
}
|
|
2912
|
+
async function handleExecApprovalCommand(params) {
|
|
2913
|
+
const parsed = parseExecApprovalCommand(params.rawBody);
|
|
2914
|
+
if (!parsed.matched) {
|
|
2915
|
+
return { handled: false };
|
|
2916
|
+
}
|
|
2917
|
+
if (!parsed.ok) {
|
|
2918
|
+
return {
|
|
2919
|
+
handled: true,
|
|
2920
|
+
replyText: parsed.error
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
const config = normalizeExecApprovalConfig(params.account.config.execApprovals);
|
|
2924
|
+
if (!config.enabled) {
|
|
2925
|
+
return {
|
|
2926
|
+
handled: true,
|
|
2927
|
+
replyText: disabledReplyText(params.account.accountId)
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
if (!isExecApprovalApprover({ account: params.account, senderId: params.senderId })) {
|
|
2931
|
+
return {
|
|
2932
|
+
handled: true,
|
|
2933
|
+
replyText: unauthorizedReplyText()
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
try {
|
|
2937
|
+
await submitExecApprovalDecision({
|
|
2938
|
+
runtime: params.runtime,
|
|
2939
|
+
id: parsed.id,
|
|
2940
|
+
decision: parsed.decision,
|
|
2941
|
+
timeoutMs: params.timeoutMs,
|
|
2942
|
+
runner: params.runner,
|
|
2943
|
+
cliArgvPrefix: params.cliArgvPrefix
|
|
2944
|
+
});
|
|
2945
|
+
const actorId = String(params.senderId ?? "").trim();
|
|
2946
|
+
const approvalId = String(parsed.approvalId ?? "").trim();
|
|
2947
|
+
return {
|
|
2948
|
+
handled: true,
|
|
2949
|
+
replyText: successReplyText(parsed),
|
|
2950
|
+
...approvalId ? {
|
|
2951
|
+
replyExtra: buildExecApprovalResolutionReply({
|
|
2952
|
+
approvalId,
|
|
2953
|
+
approvalCommandId: parsed.approvalCommandId,
|
|
2954
|
+
decision: parsed.decision,
|
|
2955
|
+
actorId: actorId || "unknown",
|
|
2956
|
+
reason: parsed.reason
|
|
2957
|
+
}).extra
|
|
2958
|
+
} : {}
|
|
2959
|
+
};
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
return {
|
|
2962
|
+
handled: true,
|
|
2963
|
+
replyText: failureReplyText(err instanceof Error ? err.message : String(err))
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/revoke-event.ts
|
|
2969
|
+
function toStringId(value) {
|
|
2970
|
+
return String(value ?? "").trim();
|
|
2971
|
+
}
|
|
2972
|
+
function resolveChatType(sessionType) {
|
|
2973
|
+
if (sessionType === 1) {
|
|
2974
|
+
return "direct";
|
|
2975
|
+
}
|
|
2976
|
+
if (sessionType === 2) {
|
|
2977
|
+
return "group";
|
|
2978
|
+
}
|
|
2979
|
+
throw new Error(`clawpool revoke event has unsupported session_type=${sessionType}`);
|
|
2980
|
+
}
|
|
2981
|
+
function enqueueRevokeSystemEvent(params) {
|
|
2982
|
+
const sessionId = toStringId(params.event.session_id);
|
|
2983
|
+
const messageId = toStringId(params.event.msg_id);
|
|
2984
|
+
const senderId = toStringId(params.event.sender_id);
|
|
2985
|
+
const sessionType = Number(params.event.session_type);
|
|
2986
|
+
if (!sessionId || !messageId) {
|
|
2987
|
+
throw new Error(
|
|
2988
|
+
`invalid event_revoke payload: session_id=${sessionId || "<empty>"} msg_id=${messageId || "<empty>"}`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
const chatType = resolveChatType(sessionType);
|
|
2992
|
+
const route = params.core.channel.routing.resolveAgentRoute({
|
|
2993
|
+
cfg: params.config,
|
|
2994
|
+
channel: "clawpool",
|
|
2995
|
+
accountId: params.account.accountId,
|
|
2996
|
+
peer: {
|
|
2997
|
+
kind: chatType,
|
|
2998
|
+
id: sessionId
|
|
2999
|
+
}
|
|
3000
|
+
});
|
|
3001
|
+
const metadataParts = [`session_id=${sessionId}`, `msg_id=${messageId}`];
|
|
3002
|
+
if (senderId) {
|
|
3003
|
+
metadataParts.push(`sender_id=${senderId}`);
|
|
3004
|
+
}
|
|
3005
|
+
const text = `Clawpool ${chatType} message deleted [${metadataParts.join(" ")}]`;
|
|
3006
|
+
params.core.system.enqueueSystemEvent(text, {
|
|
3007
|
+
sessionKey: route.sessionKey,
|
|
3008
|
+
contextKey: `clawpool:revoke:${sessionId}:${messageId}`
|
|
3009
|
+
});
|
|
3010
|
+
return {
|
|
3011
|
+
messageId,
|
|
3012
|
+
sessionId,
|
|
3013
|
+
sessionKey: route.sessionKey,
|
|
3014
|
+
text
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// src/reply-dispatch-outcome.ts
|
|
3019
|
+
function hasPositiveCount(value) {
|
|
3020
|
+
if (typeof value === "number") {
|
|
3021
|
+
return Number.isFinite(value) && value > 0;
|
|
3022
|
+
}
|
|
3023
|
+
if (Array.isArray(value)) {
|
|
3024
|
+
return value.some(hasPositiveCount);
|
|
3025
|
+
}
|
|
3026
|
+
if (value && typeof value === "object") {
|
|
3027
|
+
return Object.values(value).some(hasPositiveCount);
|
|
3028
|
+
}
|
|
3029
|
+
return false;
|
|
3030
|
+
}
|
|
3031
|
+
function shouldTreatDispatchAsRespondedWithoutVisibleOutput(result) {
|
|
3032
|
+
if (!result || typeof result !== "object") {
|
|
3033
|
+
return false;
|
|
3034
|
+
}
|
|
3035
|
+
const typedResult = result;
|
|
3036
|
+
if (typedResult.queuedFinal === true) {
|
|
3037
|
+
return true;
|
|
3038
|
+
}
|
|
3039
|
+
if (hasPositiveCount(typedResult.counts)) {
|
|
3040
|
+
return true;
|
|
3041
|
+
}
|
|
3042
|
+
return false;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// src/aibot-payload-delivery.ts
|
|
3046
|
+
import {
|
|
3047
|
+
resolveOutboundMediaUrls,
|
|
3048
|
+
sendMediaWithLeadingCaption
|
|
3049
|
+
} from "openclaw/plugin-sdk/reply-payload";
|
|
3050
|
+
|
|
3051
|
+
// src/outbound-text-delivery-plan.ts
|
|
3052
|
+
function buildAibotTextSendPlan(params) {
|
|
3053
|
+
const plan = [];
|
|
3054
|
+
let chunkIndex = 0;
|
|
3055
|
+
for (const chunk of params.chunks) {
|
|
3056
|
+
const normalized = String(chunk ?? "");
|
|
3057
|
+
if (!normalized) {
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
chunkIndex += 1;
|
|
3061
|
+
const clientMsgId = params.stableClientMsgId ? `${params.stableClientMsgId}_chunk${chunkIndex}` : void 0;
|
|
3062
|
+
const extra = chunkIndex === 1 ? params.firstChunkExtra : void 0;
|
|
3063
|
+
plan.push({
|
|
3064
|
+
text: normalized,
|
|
3065
|
+
...clientMsgId ? { clientMsgId } : {},
|
|
3066
|
+
...extra ? { extra } : {}
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
return plan;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// src/aibot-payload-delivery.ts
|
|
3073
|
+
function resolveAckMessageId(ack, fallback) {
|
|
3074
|
+
const raw = ack.msg_id ?? ack.client_msg_id ?? fallback;
|
|
3075
|
+
const normalized = String(raw ?? "").trim();
|
|
3076
|
+
return normalized || void 0;
|
|
3077
|
+
}
|
|
3078
|
+
async function deliverAibotPayload(params) {
|
|
3079
|
+
const mediaUrls = resolveOutboundMediaUrls(params.payload);
|
|
3080
|
+
const textChunks = splitTextForAibotProtocol(
|
|
3081
|
+
params.text,
|
|
3082
|
+
resolveOutboundTextChunkLimit(params.account.config.maxChunkChars)
|
|
3083
|
+
);
|
|
3084
|
+
const textSendPlan = buildAibotTextSendPlan({
|
|
3085
|
+
chunks: textChunks,
|
|
3086
|
+
stableClientMsgId: params.stableClientMsgId,
|
|
3087
|
+
firstChunkExtra: params.extra
|
|
3088
|
+
});
|
|
3089
|
+
let firstMessageId;
|
|
3090
|
+
let sent = false;
|
|
3091
|
+
let notifiedFirstVisibleSend = false;
|
|
3092
|
+
const markVisibleDelivery = () => {
|
|
3093
|
+
if (notifiedFirstVisibleSend) {
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
notifiedFirstVisibleSend = true;
|
|
3097
|
+
params.onFirstVisibleSend?.();
|
|
3098
|
+
};
|
|
3099
|
+
const mediaSent = await sendMediaWithLeadingCaption({
|
|
3100
|
+
mediaUrls,
|
|
3101
|
+
caption: textSendPlan[0]?.text ?? "",
|
|
3102
|
+
send: async ({ mediaUrl, caption }) => {
|
|
3103
|
+
if (params.abortSignal?.aborted) {
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
const ack = await params.client.sendMedia(params.sessionId, mediaUrl, caption ?? "", {
|
|
3107
|
+
eventId: params.eventId,
|
|
3108
|
+
quotedMessageId: params.quotedMessageId,
|
|
3109
|
+
clientMsgId: params.stableClientMsgId ? `${params.stableClientMsgId}_media` : void 0,
|
|
3110
|
+
extra: params.extra
|
|
3111
|
+
});
|
|
3112
|
+
firstMessageId ??= resolveAckMessageId(
|
|
3113
|
+
ack,
|
|
3114
|
+
params.stableClientMsgId ? `${params.stableClientMsgId}_media` : void 0
|
|
3115
|
+
);
|
|
3116
|
+
sent = true;
|
|
3117
|
+
markVisibleDelivery();
|
|
3118
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3119
|
+
},
|
|
3120
|
+
onError: (error) => {
|
|
3121
|
+
params.onMediaError?.(error);
|
|
3122
|
+
params.statusSink?.({ lastError: String(error) });
|
|
3123
|
+
}
|
|
3124
|
+
});
|
|
3125
|
+
if (mediaSent) {
|
|
3126
|
+
for (const chunkPlan of textSendPlan.slice(1)) {
|
|
3127
|
+
if (params.abortSignal?.aborted) {
|
|
3128
|
+
return { sent, firstMessageId };
|
|
3129
|
+
}
|
|
3130
|
+
const ack = await params.client.sendText(params.sessionId, chunkPlan.text, {
|
|
3131
|
+
eventId: params.eventId,
|
|
3132
|
+
quotedMessageId: params.quotedMessageId,
|
|
3133
|
+
clientMsgId: chunkPlan.clientMsgId
|
|
3134
|
+
});
|
|
3135
|
+
firstMessageId ??= resolveAckMessageId(ack, chunkPlan.clientMsgId);
|
|
3136
|
+
sent = true;
|
|
3137
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3138
|
+
}
|
|
3139
|
+
return { sent: true, firstMessageId };
|
|
3140
|
+
}
|
|
3141
|
+
if (textSendPlan.length === 0) {
|
|
3142
|
+
return { sent: false, firstMessageId };
|
|
3143
|
+
}
|
|
3144
|
+
for (const chunkPlan of textSendPlan) {
|
|
3145
|
+
if (params.abortSignal?.aborted) {
|
|
3146
|
+
return { sent, firstMessageId };
|
|
3147
|
+
}
|
|
3148
|
+
const ack = await params.client.sendText(params.sessionId, chunkPlan.text, {
|
|
3149
|
+
eventId: params.eventId,
|
|
3150
|
+
quotedMessageId: params.quotedMessageId,
|
|
3151
|
+
clientMsgId: chunkPlan.clientMsgId,
|
|
3152
|
+
extra: chunkPlan.extra
|
|
3153
|
+
});
|
|
3154
|
+
firstMessageId ??= resolveAckMessageId(ack, chunkPlan.clientMsgId);
|
|
3155
|
+
sent = true;
|
|
3156
|
+
markVisibleDelivery();
|
|
3157
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3158
|
+
}
|
|
3159
|
+
return { sent, firstMessageId };
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
// src/monitor.ts
|
|
3163
|
+
var activeMonitorClients = /* @__PURE__ */ new Map();
|
|
3164
|
+
function registerActiveMonitor(accountId, client) {
|
|
3165
|
+
if (!accountId) {
|
|
3166
|
+
return null;
|
|
3167
|
+
}
|
|
3168
|
+
const previous = activeMonitorClients.get(accountId) ?? null;
|
|
3169
|
+
activeMonitorClients.set(accountId, client);
|
|
3170
|
+
return previous === client ? null : previous;
|
|
3171
|
+
}
|
|
3172
|
+
function isActiveMonitor(accountId, client) {
|
|
3173
|
+
if (!accountId) {
|
|
3174
|
+
return false;
|
|
3175
|
+
}
|
|
3176
|
+
return activeMonitorClients.get(accountId) === client;
|
|
3177
|
+
}
|
|
3178
|
+
function clearActiveMonitor(accountId, client) {
|
|
3179
|
+
if (!accountId) {
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
if (activeMonitorClients.get(accountId) !== client) {
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
activeMonitorClients.delete(accountId);
|
|
3186
|
+
}
|
|
3187
|
+
function toStringId2(value) {
|
|
3188
|
+
const text = String(value ?? "").trim();
|
|
3189
|
+
return text;
|
|
3190
|
+
}
|
|
3191
|
+
function toTimestampMs(value) {
|
|
3192
|
+
const n = Number(value);
|
|
3193
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
3194
|
+
return void 0;
|
|
3195
|
+
}
|
|
3196
|
+
if (n < 1e12) {
|
|
3197
|
+
return Math.floor(n * 1e3);
|
|
3198
|
+
}
|
|
3199
|
+
return Math.floor(n);
|
|
3200
|
+
}
|
|
3201
|
+
function normalizeNumericMessageId(value) {
|
|
3202
|
+
const raw = toStringId2(value);
|
|
3203
|
+
if (!raw) {
|
|
3204
|
+
return void 0;
|
|
3205
|
+
}
|
|
3206
|
+
return /^\d+$/.test(raw) ? raw : void 0;
|
|
3207
|
+
}
|
|
3208
|
+
function resolveStreamChunkChars(account) {
|
|
3209
|
+
return resolveStreamTextChunkLimit(account.config.streamChunkChars);
|
|
3210
|
+
}
|
|
3211
|
+
function resolveStreamChunkDelayMs(account) {
|
|
3212
|
+
return Math.max(0, Math.floor(account.config.streamChunkDelayMs ?? 0));
|
|
3213
|
+
}
|
|
3214
|
+
function resolveStreamFinishDelayMs(account) {
|
|
3215
|
+
return resolveStreamChunkDelayMs(account);
|
|
3216
|
+
}
|
|
3217
|
+
var composingRenewIntervalMs = 8e3;
|
|
3218
|
+
function sleep2(ms) {
|
|
3219
|
+
if (ms <= 0) {
|
|
3220
|
+
return Promise.resolve();
|
|
3221
|
+
}
|
|
3222
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3223
|
+
}
|
|
3224
|
+
function resolveAbortReason(signal) {
|
|
3225
|
+
const reason = String(signal?.reason ?? "").trim();
|
|
3226
|
+
return reason || "-";
|
|
3227
|
+
}
|
|
3228
|
+
function buildEventLogContext(params) {
|
|
3229
|
+
const parts = [
|
|
3230
|
+
`eventId=${params.eventId || "-"}`,
|
|
3231
|
+
`sessionId=${params.sessionId}`,
|
|
3232
|
+
`messageSid=${params.messageSid}`
|
|
3233
|
+
];
|
|
3234
|
+
if (params.clientMsgId) {
|
|
3235
|
+
parts.push(`clientMsgId=${params.clientMsgId}`);
|
|
3236
|
+
}
|
|
3237
|
+
if (params.outboundCounter !== void 0) {
|
|
3238
|
+
parts.push(`outboundCounter=${params.outboundCounter}`);
|
|
3239
|
+
}
|
|
3240
|
+
return parts.join(" ");
|
|
3241
|
+
}
|
|
3242
|
+
async function deliverAibotStreamBlock(params) {
|
|
3243
|
+
const chunks = splitTextForAibotProtocol(params.text, resolveStreamChunkChars(params.account));
|
|
3244
|
+
const chunkDelayMs = resolveStreamChunkDelayMs(params.account);
|
|
3245
|
+
let didSend = false;
|
|
3246
|
+
const context = buildEventLogContext({
|
|
3247
|
+
eventId: params.eventId,
|
|
3248
|
+
sessionId: params.sessionId,
|
|
3249
|
+
messageSid: params.messageSid,
|
|
3250
|
+
clientMsgId: params.clientMsgId
|
|
3251
|
+
});
|
|
3252
|
+
params.runtime.log(
|
|
3253
|
+
`[clawpool:${params.account.accountId}] stream block split into ${chunks.length} chunk(s) ${context} textLen=${params.text.length} chunkDelayMs=${chunkDelayMs}`
|
|
3254
|
+
);
|
|
3255
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
3256
|
+
if (params.abortSignal?.aborted) {
|
|
3257
|
+
params.runtime.log(
|
|
3258
|
+
`[clawpool:${params.account.accountId}] stream chunk abort before send ${context} chunkIndex=${index + 1}/${chunks.length} didSend=${didSend} abortReason=${resolveAbortReason(params.abortSignal)}`
|
|
3259
|
+
);
|
|
3260
|
+
return didSend;
|
|
3261
|
+
}
|
|
3262
|
+
const chunk = chunks[index];
|
|
3263
|
+
const normalized = String(chunk ?? "");
|
|
3264
|
+
if (!normalized) {
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
params.runtime.log(
|
|
3268
|
+
`[clawpool:${params.account.accountId}] stream chunk send ${context} chunkIndex=${index + 1}/${chunks.length} deltaLen=${normalized.length}`
|
|
3269
|
+
);
|
|
3270
|
+
await params.client.sendStreamChunk(params.sessionId, normalized, {
|
|
3271
|
+
eventId: params.eventId,
|
|
3272
|
+
clientMsgId: params.clientMsgId,
|
|
3273
|
+
quotedMessageId: params.quotedMessageId,
|
|
3274
|
+
isFinish: false
|
|
3275
|
+
});
|
|
3276
|
+
didSend = true;
|
|
3277
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3278
|
+
if (chunkDelayMs > 0 && index < chunks.length - 1) {
|
|
3279
|
+
await sleep2(chunkDelayMs);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return didSend;
|
|
3283
|
+
}
|
|
3284
|
+
async function deliverAibotMessage(params) {
|
|
3285
|
+
const { payload, client, account, sessionId, quotedMessageId, runtime: runtime2, statusSink, stableClientMsgId } = params;
|
|
3286
|
+
const core = getAibotRuntime();
|
|
3287
|
+
const tableMode = params.tableMode ?? "code";
|
|
3288
|
+
const outboundEnvelope = buildAibotOutboundEnvelope(payload);
|
|
3289
|
+
const execApprovalDiagnostic = outboundEnvelope.execApprovalDiagnostic;
|
|
3290
|
+
if (execApprovalDiagnostic.isCandidate) {
|
|
3291
|
+
runtime2.log(
|
|
3292
|
+
`[clawpool:${account.accountId}] exec approval outbound diagnostic eventId=${params.eventId || "-"} sessionId=${sessionId} clientMsgId=${stableClientMsgId || "-"} matched=${execApprovalDiagnostic.matched ? "true" : "false"} reason=${execApprovalDiagnostic.reason} hasChannelData=${execApprovalDiagnostic.hasChannelData ? "true" : "false"} hasExecApprovalField=${execApprovalDiagnostic.hasExecApprovalField ? "true" : "false"} approvalId=${execApprovalDiagnostic.approvalId || "-"} approvalSlug=${execApprovalDiagnostic.approvalSlug || "-"} approvalCommandId=${execApprovalDiagnostic.approvalCommandId || "-"} commandDetected=${execApprovalDiagnostic.commandDetected ? "true" : "false"} host=${execApprovalDiagnostic.host || "-"} nodeId=${execApprovalDiagnostic.nodeId || "-"} cwd=${execApprovalDiagnostic.cwd || "-"} expiresInSeconds=${execApprovalDiagnostic.expiresInSeconds ?? "-"} allowedDecisionCount=${execApprovalDiagnostic.allowedDecisionCount} textPrefix=${JSON.stringify(execApprovalDiagnostic.textPrefix)} bizCard=${outboundEnvelope.cardKind ?? "none"}`
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
const rawText = outboundEnvelope.text;
|
|
3296
|
+
const text = core.channel.text.convertMarkdownTables(rawText, tableMode);
|
|
3297
|
+
const delivery = await deliverAibotPayload({
|
|
3298
|
+
payload,
|
|
3299
|
+
text,
|
|
3300
|
+
extra: outboundEnvelope.extra,
|
|
3301
|
+
client,
|
|
3302
|
+
account,
|
|
3303
|
+
sessionId,
|
|
3304
|
+
abortSignal: params.abortSignal,
|
|
3305
|
+
eventId: params.eventId,
|
|
3306
|
+
quotedMessageId,
|
|
3307
|
+
stableClientMsgId,
|
|
3308
|
+
onMediaError: (error) => {
|
|
3309
|
+
runtime2.error(`clawpool media send failed: ${String(error)}`);
|
|
3310
|
+
},
|
|
3311
|
+
statusSink
|
|
3312
|
+
});
|
|
3313
|
+
return delivery.sent;
|
|
3314
|
+
}
|
|
3315
|
+
async function bindSessionRouteMapping(params) {
|
|
3316
|
+
const routeSessionKey = String(params.routeSessionKey ?? "").trim();
|
|
3317
|
+
const sessionId = String(params.sessionId ?? "").trim();
|
|
3318
|
+
if (!routeSessionKey || !sessionId) {
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
try {
|
|
3322
|
+
params.runtime.log(
|
|
3323
|
+
`[clawpool:${params.account.accountId}] session route bind begin routeSessionKey=${routeSessionKey} sessionId=${sessionId}`
|
|
3324
|
+
);
|
|
3325
|
+
await params.client.bindSessionRoute(
|
|
3326
|
+
"clawpool",
|
|
3327
|
+
params.account.accountId,
|
|
3328
|
+
routeSessionKey,
|
|
3329
|
+
sessionId
|
|
3330
|
+
);
|
|
3331
|
+
params.runtime.log(
|
|
3332
|
+
`[clawpool:${params.account.accountId}] session route bind success routeSessionKey=${routeSessionKey} sessionId=${sessionId}`
|
|
3333
|
+
);
|
|
3334
|
+
} catch (err) {
|
|
3335
|
+
const reason = `clawpool session route bind failed routeSessionKey=${routeSessionKey} sessionId=${sessionId}: ${String(err)}`;
|
|
3336
|
+
params.runtime.error(`[clawpool:${params.account.accountId}] ${reason}`);
|
|
3337
|
+
params.statusSink?.({ lastError: reason });
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
function handleEventStop(params) {
|
|
3341
|
+
const eventId = toStringId2(params.payload.event_id);
|
|
3342
|
+
const sessionId = toStringId2(params.payload.session_id);
|
|
3343
|
+
const stopId = toStringId2(params.payload.stop_id);
|
|
3344
|
+
if (!eventId || !sessionId) {
|
|
3345
|
+
const reason = `invalid event_stop payload: event_id=${eventId || "<empty>"} session_id=${sessionId || "<empty>"}`;
|
|
3346
|
+
params.runtime.error(`[clawpool:${params.account.accountId}] ${reason}`);
|
|
3347
|
+
params.statusSink?.({ lastError: reason });
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
params.runtime.log(
|
|
3351
|
+
`[clawpool:${params.account.accountId}] event_stop begin eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"} acceptedPayload=${JSON.stringify(params.payload)}`
|
|
3352
|
+
);
|
|
3353
|
+
try {
|
|
3354
|
+
params.client.sendEventStopAck({
|
|
3355
|
+
stop_id: stopId,
|
|
3356
|
+
event_id: eventId,
|
|
3357
|
+
accepted: true,
|
|
3358
|
+
updated_at: Date.now()
|
|
3359
|
+
});
|
|
3360
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3361
|
+
params.runtime.log(
|
|
3362
|
+
`[clawpool:${params.account.accountId}] event_stop_ack sent eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"}`
|
|
3363
|
+
);
|
|
3364
|
+
} catch (err) {
|
|
3365
|
+
const reason = `event_stop_ack failed eventId=${eventId} sessionId=${sessionId}: ${String(err)}`;
|
|
3366
|
+
params.runtime.error(`[clawpool:${params.account.accountId}] ${reason}`);
|
|
3367
|
+
params.statusSink?.({ lastError: reason });
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
const activeRun = resolveActiveReplyRun({
|
|
3371
|
+
accountId: params.account.accountId,
|
|
3372
|
+
eventId,
|
|
3373
|
+
sessionId
|
|
3374
|
+
});
|
|
3375
|
+
params.runtime.log(
|
|
3376
|
+
`[clawpool:${params.account.accountId}] event_stop resolve_active_run eventId=${eventId} sessionId=${sessionId} found=${activeRun ? "true" : "false"} stopRequested=${activeRun?.stopRequested === true} aborted=${activeRun?.controller.signal.aborted === true} abortReason=${activeRun ? resolveAbortReason(activeRun.controller.signal) : "-"}`
|
|
3377
|
+
);
|
|
3378
|
+
if (!activeRun) {
|
|
3379
|
+
params.client.sendEventStopResult({
|
|
3380
|
+
stop_id: stopId,
|
|
3381
|
+
event_id: eventId,
|
|
3382
|
+
status: "already_finished",
|
|
3383
|
+
updated_at: Date.now()
|
|
3384
|
+
});
|
|
3385
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3386
|
+
params.runtime.log(
|
|
3387
|
+
`[clawpool:${params.account.accountId}] event_stop already_finished eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"}`
|
|
3388
|
+
);
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
activeRun.stopRequested = true;
|
|
3392
|
+
activeRun.stopId = stopId;
|
|
3393
|
+
activeRun.abortReason = "owner_requested_stop";
|
|
3394
|
+
if (!activeRun.controller.signal.aborted) {
|
|
3395
|
+
activeRun.controller.abort(activeRun.abortReason);
|
|
3396
|
+
}
|
|
3397
|
+
params.runtime.log(
|
|
3398
|
+
`[clawpool:${params.account.accountId}] owner stop requested eventId=${eventId} sessionId=${sessionId} stopId=${stopId || "-"} aborted=${activeRun.controller.signal.aborted} abortReason=${resolveAbortReason(activeRun.controller.signal)}`
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
function reportHandledCommandResult(params) {
|
|
3402
|
+
if (!params.eventId) {
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
try {
|
|
3406
|
+
params.client.sendEventResult({
|
|
3407
|
+
event_id: params.eventId,
|
|
3408
|
+
status: params.status,
|
|
3409
|
+
code: params.code,
|
|
3410
|
+
msg: params.msg,
|
|
3411
|
+
updated_at: Date.now()
|
|
3412
|
+
});
|
|
3413
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3414
|
+
} catch (err) {
|
|
3415
|
+
params.runtime.error(
|
|
3416
|
+
`[clawpool:${params.account.accountId}] command event result send failed eventId=${params.eventId} status=${params.status}: ${String(err)}`
|
|
3417
|
+
);
|
|
3418
|
+
params.statusSink?.({ lastError: String(err) });
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
async function sendHandledCommandReply(params) {
|
|
3422
|
+
await params.client.sendText(params.sessionId, params.replyText, {
|
|
3423
|
+
eventId: params.eventId,
|
|
3424
|
+
quotedMessageId: params.quotedMessageId,
|
|
3425
|
+
extra: params.replyExtra
|
|
3426
|
+
});
|
|
3427
|
+
params.statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3428
|
+
params.runtime.log(
|
|
3429
|
+
`[clawpool:${params.account.accountId}] command reply sent eventId=${params.eventId || "-"} sessionId=${params.sessionId} quotedMessageId=${params.quotedMessageId || "-"} textLen=${params.replyText.length}`
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
async function processEvent(params) {
|
|
3433
|
+
const { event, account, config, runtime: runtime2, client, statusSink } = params;
|
|
3434
|
+
const core = getAibotRuntime();
|
|
3435
|
+
const sessionId = toStringId2(event.session_id);
|
|
3436
|
+
const messageSid = toStringId2(event.msg_id);
|
|
3437
|
+
const rawBody = String(event.content ?? "").trim();
|
|
3438
|
+
if (!sessionId || !messageSid || !rawBody) {
|
|
3439
|
+
const reason = `invalid event_msg payload: session_id=${sessionId || "<empty>"} msg_id=${messageSid || "<empty>"}`;
|
|
3440
|
+
runtime2.error(`[clawpool:${account.accountId}] ${reason}`);
|
|
3441
|
+
statusSink?.({ lastError: reason });
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
const eventId = toStringId2(event.event_id);
|
|
3445
|
+
const quotedMessageId = normalizeNumericMessageId(event.quoted_message_id);
|
|
3446
|
+
const bodyForAgent = buildBodyWithQuotedReplyId(rawBody, quotedMessageId);
|
|
3447
|
+
const senderId = toStringId2(event.sender_id);
|
|
3448
|
+
const isGroup = Number(event.session_type ?? 0) === 2 || String(event.event_type ?? "").startsWith("group_");
|
|
3449
|
+
const chatType = isGroup ? "group" : "direct";
|
|
3450
|
+
const createdAt = toTimestampMs(event.created_at);
|
|
3451
|
+
const baseLogContext = buildEventLogContext({
|
|
3452
|
+
eventId,
|
|
3453
|
+
sessionId,
|
|
3454
|
+
messageSid
|
|
3455
|
+
});
|
|
3456
|
+
let visibleOutputSent = false;
|
|
3457
|
+
const inboundEvent = claimInboundEvent({
|
|
3458
|
+
accountId: account.accountId,
|
|
3459
|
+
eventId,
|
|
3460
|
+
sessionId,
|
|
3461
|
+
messageSid
|
|
3462
|
+
});
|
|
3463
|
+
if (inboundEvent.duplicate) {
|
|
3464
|
+
runtime2.log(
|
|
3465
|
+
`[clawpool:${account.accountId}] skip duplicate inbound event ${baseLogContext} confirmed=${inboundEvent.confirmed}`
|
|
3466
|
+
);
|
|
3467
|
+
if (inboundEvent.confirmed && eventId) {
|
|
3468
|
+
try {
|
|
3469
|
+
client.ackEvent(eventId, {
|
|
3470
|
+
sessionId,
|
|
3471
|
+
msgId: messageSid,
|
|
3472
|
+
receivedAt: Date.now()
|
|
3473
|
+
});
|
|
3474
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3475
|
+
} catch (err) {
|
|
3476
|
+
runtime2.error(
|
|
3477
|
+
`[clawpool:${account.accountId}] duplicate event ack failed eventId=${eventId}: ${String(err)}`
|
|
3478
|
+
);
|
|
3479
|
+
statusSink?.({ lastError: String(err) });
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
runtime2.log(
|
|
3485
|
+
`[clawpool:${account.accountId}] inbound event ${baseLogContext} chatType=${chatType} bodyLen=${rawBody.length} quotedMessageId=${quotedMessageId || "-"}`
|
|
3486
|
+
);
|
|
3487
|
+
let inboundEventAccepted = false;
|
|
3488
|
+
const commandOutcome = await handleExecApprovalCommand({
|
|
3489
|
+
rawBody,
|
|
3490
|
+
senderId,
|
|
3491
|
+
account,
|
|
3492
|
+
runtime: core
|
|
3493
|
+
});
|
|
3494
|
+
if (commandOutcome.handled) {
|
|
3495
|
+
try {
|
|
3496
|
+
if (eventId) {
|
|
3497
|
+
client.ackEvent(eventId, {
|
|
3498
|
+
sessionId,
|
|
3499
|
+
msgId: messageSid,
|
|
3500
|
+
receivedAt: Date.now()
|
|
3501
|
+
});
|
|
3502
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3503
|
+
}
|
|
3504
|
+
confirmInboundEvent(inboundEvent.claim);
|
|
3505
|
+
inboundEventAccepted = true;
|
|
3506
|
+
await sendHandledCommandReply({
|
|
3507
|
+
client,
|
|
3508
|
+
sessionId,
|
|
3509
|
+
replyText: commandOutcome.replyText,
|
|
3510
|
+
replyExtra: commandOutcome.replyExtra,
|
|
3511
|
+
eventId,
|
|
3512
|
+
quotedMessageId: normalizeNumericMessageId(messageSid),
|
|
3513
|
+
account,
|
|
3514
|
+
runtime: runtime2,
|
|
3515
|
+
statusSink
|
|
3516
|
+
});
|
|
3517
|
+
reportHandledCommandResult({
|
|
3518
|
+
client,
|
|
3519
|
+
eventId,
|
|
3520
|
+
status: "responded",
|
|
3521
|
+
code: "clawpool_exec_approval_command_handled",
|
|
3522
|
+
msg: "exec approval command handled",
|
|
3523
|
+
account,
|
|
3524
|
+
runtime: runtime2,
|
|
3525
|
+
statusSink
|
|
3526
|
+
});
|
|
3527
|
+
return;
|
|
3528
|
+
} catch (err) {
|
|
3529
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3530
|
+
runtime2.error(
|
|
3531
|
+
`[clawpool:${account.accountId}] exec approval command failed ${baseLogContext}: ${message}`
|
|
3532
|
+
);
|
|
3533
|
+
statusSink?.({ lastError: message });
|
|
3534
|
+
reportHandledCommandResult({
|
|
3535
|
+
client,
|
|
3536
|
+
eventId,
|
|
3537
|
+
status: "failed",
|
|
3538
|
+
code: "clawpool_exec_approval_command_failed",
|
|
3539
|
+
msg: message,
|
|
3540
|
+
account,
|
|
3541
|
+
runtime: runtime2,
|
|
3542
|
+
statusSink
|
|
3543
|
+
});
|
|
3544
|
+
throw err;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
const runAbortController = new AbortController();
|
|
3548
|
+
const activeRun = registerActiveReplyRun({
|
|
3549
|
+
accountId: account.accountId,
|
|
3550
|
+
eventId: eventId || `${sessionId}:${messageSid}`,
|
|
3551
|
+
sessionId,
|
|
3552
|
+
controller: runAbortController
|
|
3553
|
+
});
|
|
3554
|
+
runtime2.log(
|
|
3555
|
+
`[clawpool:${account.accountId}] active reply run registered eventId=${eventId || `${sessionId}:${messageSid}`} sessionId=${sessionId} messageSid=${messageSid} activeRun=${activeRun ? "true" : "false"}`
|
|
3556
|
+
);
|
|
3557
|
+
try {
|
|
3558
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
3559
|
+
cfg: config,
|
|
3560
|
+
channel: "clawpool",
|
|
3561
|
+
accountId: account.accountId,
|
|
3562
|
+
peer: {
|
|
3563
|
+
kind: isGroup ? "group" : "direct",
|
|
3564
|
+
id: sessionId
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
3568
|
+
agentId: route.agentId
|
|
3569
|
+
});
|
|
3570
|
+
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
3571
|
+
storePath,
|
|
3572
|
+
sessionKey: route.sessionKey
|
|
3573
|
+
});
|
|
3574
|
+
const fromLabel = isGroup ? `group:${sessionId}/${senderId || "unknown"}` : `user:${senderId || "unknown"}`;
|
|
3575
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
|
|
3576
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
3577
|
+
channel: "Clawpool",
|
|
3578
|
+
from: fromLabel,
|
|
3579
|
+
timestamp: createdAt,
|
|
3580
|
+
previousTimestamp,
|
|
3581
|
+
envelope: envelopeOptions,
|
|
3582
|
+
body: bodyForAgent
|
|
3583
|
+
});
|
|
3584
|
+
const from = isGroup ? `clawpool:group:${sessionId}:${senderId || "unknown"}` : `clawpool:${senderId || "unknown"}`;
|
|
3585
|
+
const to = `clawpool:${sessionId}`;
|
|
3586
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
3587
|
+
Body: body,
|
|
3588
|
+
BodyForAgent: bodyForAgent,
|
|
3589
|
+
RawBody: rawBody,
|
|
3590
|
+
CommandBody: rawBody,
|
|
3591
|
+
// Clawpool inbound text is end-user chat content; do not parse it as OpenClaw slash/bang commands.
|
|
3592
|
+
BodyForCommands: "",
|
|
3593
|
+
From: from,
|
|
3594
|
+
To: to,
|
|
3595
|
+
SessionKey: route.sessionKey,
|
|
3596
|
+
AccountId: route.accountId,
|
|
3597
|
+
ChatType: chatType,
|
|
3598
|
+
ConversationLabel: fromLabel,
|
|
3599
|
+
SenderName: senderId || void 0,
|
|
3600
|
+
SenderId: senderId || void 0,
|
|
3601
|
+
CommandAuthorized: false,
|
|
3602
|
+
Provider: "clawpool",
|
|
3603
|
+
Surface: "clawpool",
|
|
3604
|
+
MessageSid: messageSid,
|
|
3605
|
+
// This field carries the inbound quoted message id from end user (event.quoted_message_id).
|
|
3606
|
+
// It is not the outbound reply anchor used when plugin sends replies back to Aibot.
|
|
3607
|
+
ReplyToMessageSid: quotedMessageId,
|
|
3608
|
+
OriginatingChannel: "clawpool",
|
|
3609
|
+
OriginatingTo: to
|
|
3610
|
+
});
|
|
3611
|
+
const routeSessionKey = ctxPayload.SessionKey ?? route.sessionKey;
|
|
3612
|
+
await core.channel.session.recordInboundSession({
|
|
3613
|
+
storePath,
|
|
3614
|
+
sessionKey: routeSessionKey,
|
|
3615
|
+
ctx: ctxPayload,
|
|
3616
|
+
onRecordError: (err) => runtime2.error(`clawpool session meta update failed: ${String(err)}`)
|
|
3617
|
+
});
|
|
3618
|
+
await bindSessionRouteMapping({
|
|
3619
|
+
client,
|
|
3620
|
+
account,
|
|
3621
|
+
runtime: runtime2,
|
|
3622
|
+
sessionId,
|
|
3623
|
+
routeSessionKey,
|
|
3624
|
+
statusSink: statusSink ? (patch) => statusSink({ lastError: patch.lastError }) : void 0
|
|
3625
|
+
});
|
|
3626
|
+
if (eventId) {
|
|
3627
|
+
try {
|
|
3628
|
+
client.ackEvent(eventId, {
|
|
3629
|
+
sessionId,
|
|
3630
|
+
msgId: messageSid,
|
|
3631
|
+
receivedAt: Date.now()
|
|
3632
|
+
});
|
|
3633
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3634
|
+
confirmInboundEvent(inboundEvent.claim);
|
|
3635
|
+
inboundEventAccepted = true;
|
|
3636
|
+
} catch (err) {
|
|
3637
|
+
runtime2.error(`[clawpool:${account.accountId}] event ack failed eventId=${eventId}: ${String(err)}`);
|
|
3638
|
+
statusSink?.({ lastError: String(err) });
|
|
3639
|
+
}
|
|
3640
|
+
} else {
|
|
3641
|
+
confirmInboundEvent(inboundEvent.claim);
|
|
3642
|
+
inboundEventAccepted = true;
|
|
3643
|
+
}
|
|
3644
|
+
const outboundQuotedMessageId = normalizeNumericMessageId(event.msg_id);
|
|
3645
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
3646
|
+
cfg: config,
|
|
3647
|
+
agentId: route.agentId,
|
|
3648
|
+
channel: "clawpool",
|
|
3649
|
+
accountId: account.accountId
|
|
3650
|
+
});
|
|
3651
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
3652
|
+
cfg: config,
|
|
3653
|
+
channel: "clawpool",
|
|
3654
|
+
accountId: account.accountId
|
|
3655
|
+
});
|
|
3656
|
+
const streamClientMsgId = `reply_${messageSid}_stream`;
|
|
3657
|
+
const retryPolicy = resolveUpstreamRetryPolicy(account);
|
|
3658
|
+
let composingSet = false;
|
|
3659
|
+
let composingRenewTimer = null;
|
|
3660
|
+
let eventResultReported = false;
|
|
3661
|
+
let stopResultReported = false;
|
|
3662
|
+
const setComposing = (active) => {
|
|
3663
|
+
try {
|
|
3664
|
+
client.setSessionComposing(sessionId, active, {
|
|
3665
|
+
refEventId: eventId || void 0,
|
|
3666
|
+
refMsgId: outboundQuotedMessageId
|
|
3667
|
+
});
|
|
3668
|
+
composingSet = active;
|
|
3669
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3670
|
+
} catch (err) {
|
|
3671
|
+
runtime2.error(
|
|
3672
|
+
`[clawpool:${account.accountId}] session activity update failed eventId=${eventId || "-"} sessionId=${sessionId} active=${active}: ${String(err)}`
|
|
3673
|
+
);
|
|
3674
|
+
statusSink?.({ lastError: String(err) });
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
const stopComposingRenewal = () => {
|
|
3678
|
+
if (composingRenewTimer) {
|
|
3679
|
+
clearTimeout(composingRenewTimer);
|
|
3680
|
+
composingRenewTimer = null;
|
|
3681
|
+
}
|
|
3682
|
+
};
|
|
3683
|
+
const scheduleComposingRenewal = () => {
|
|
3684
|
+
if (!composingSet || eventResultReported || visibleOutputSent || composingRenewTimer) {
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
composingRenewTimer = setTimeout(() => {
|
|
3688
|
+
composingRenewTimer = null;
|
|
3689
|
+
if (!composingSet || eventResultReported || visibleOutputSent) {
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
setComposing(true);
|
|
3693
|
+
scheduleComposingRenewal();
|
|
3694
|
+
}, composingRenewIntervalMs);
|
|
3695
|
+
};
|
|
3696
|
+
const reportEventResult = (status, code = "", msg = "") => {
|
|
3697
|
+
if (eventResultReported) {
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
eventResultReported = true;
|
|
3701
|
+
stopComposingRenewal();
|
|
3702
|
+
if (!eventId) {
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
try {
|
|
3706
|
+
client.sendEventResult({
|
|
3707
|
+
event_id: eventId,
|
|
3708
|
+
status,
|
|
3709
|
+
code: code || void 0,
|
|
3710
|
+
msg: msg || void 0,
|
|
3711
|
+
updated_at: Date.now()
|
|
3712
|
+
});
|
|
3713
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3714
|
+
} catch (err) {
|
|
3715
|
+
runtime2.error(
|
|
3716
|
+
`[clawpool:${account.accountId}] event result send failed eventId=${eventId} status=${status}: ${String(err)}`
|
|
3717
|
+
);
|
|
3718
|
+
statusSink?.({ lastError: String(err) });
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
const reportStopResult = (status, code = "", msg = "") => {
|
|
3722
|
+
if (stopResultReported || !eventId || !activeRun?.stopRequested) {
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
stopResultReported = true;
|
|
3726
|
+
try {
|
|
3727
|
+
client.sendEventStopResult({
|
|
3728
|
+
stop_id: activeRun.stopId,
|
|
3729
|
+
event_id: eventId,
|
|
3730
|
+
status,
|
|
3731
|
+
code: code || void 0,
|
|
3732
|
+
msg: msg || void 0,
|
|
3733
|
+
updated_at: Date.now()
|
|
3734
|
+
});
|
|
3735
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3736
|
+
runtime2.log(
|
|
3737
|
+
`[clawpool:${account.accountId}] event_stop_result sent eventId=${eventId} stopId=${activeRun.stopId || "-"} status=${status} code=${code || "-"} msg=${msg || "-"}`
|
|
3738
|
+
);
|
|
3739
|
+
} catch (err) {
|
|
3740
|
+
runtime2.error(
|
|
3741
|
+
`[clawpool:${account.accountId}] event_stop_result send failed eventId=${eventId} status=${status}: ${String(err)}`
|
|
3742
|
+
);
|
|
3743
|
+
statusSink?.({ lastError: String(err) });
|
|
3744
|
+
}
|
|
3745
|
+
};
|
|
3746
|
+
const clearComposing = () => {
|
|
3747
|
+
stopComposingRenewal();
|
|
3748
|
+
if (composingSet) {
|
|
3749
|
+
setComposing(false);
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
const markVisibleOutputSent = () => {
|
|
3753
|
+
if (visibleOutputSent) {
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
visibleOutputSent = true;
|
|
3757
|
+
clearComposing();
|
|
3758
|
+
reportEventResult("responded");
|
|
3759
|
+
};
|
|
3760
|
+
setComposing(true);
|
|
3761
|
+
scheduleComposingRenewal();
|
|
3762
|
+
try {
|
|
3763
|
+
for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) {
|
|
3764
|
+
let hasSentBlock = false;
|
|
3765
|
+
let outboundCounter = 0;
|
|
3766
|
+
let attemptHasOutbound = false;
|
|
3767
|
+
let retryGuardedText = null;
|
|
3768
|
+
const attemptLabel = `${attempt}/${retryPolicy.maxAttempts}`;
|
|
3769
|
+
const finishStreamIfNeeded = async () => {
|
|
3770
|
+
if (!hasSentBlock) {
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
if (runAbortController.signal.aborted) {
|
|
3774
|
+
runtime2.log(
|
|
3775
|
+
`[clawpool:${account.accountId}] skip stream finish due to abort ${buildEventLogContext({
|
|
3776
|
+
eventId,
|
|
3777
|
+
sessionId,
|
|
3778
|
+
messageSid,
|
|
3779
|
+
clientMsgId: streamClientMsgId
|
|
3780
|
+
})} abortReason=${resolveAbortReason(runAbortController.signal)}`
|
|
3781
|
+
);
|
|
3782
|
+
hasSentBlock = false;
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
hasSentBlock = false;
|
|
3786
|
+
try {
|
|
3787
|
+
const finishContext = buildEventLogContext({
|
|
3788
|
+
eventId,
|
|
3789
|
+
sessionId,
|
|
3790
|
+
messageSid,
|
|
3791
|
+
clientMsgId: streamClientMsgId
|
|
3792
|
+
});
|
|
3793
|
+
const finishDelayMs = resolveStreamFinishDelayMs(account);
|
|
3794
|
+
if (finishDelayMs > 0) {
|
|
3795
|
+
runtime2.log(
|
|
3796
|
+
`[clawpool:${account.accountId}] stream finish delay ${finishContext} delayMs=${finishDelayMs}`
|
|
3797
|
+
);
|
|
3798
|
+
await sleep2(finishDelayMs);
|
|
3799
|
+
}
|
|
3800
|
+
runtime2.log(
|
|
3801
|
+
`[clawpool:${account.accountId}] stream finish ${finishContext}`
|
|
3802
|
+
);
|
|
3803
|
+
await client.sendStreamChunk(sessionId, "", {
|
|
3804
|
+
eventId,
|
|
3805
|
+
clientMsgId: streamClientMsgId,
|
|
3806
|
+
quotedMessageId: outboundQuotedMessageId,
|
|
3807
|
+
isFinish: true
|
|
3808
|
+
});
|
|
3809
|
+
attemptHasOutbound = true;
|
|
3810
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
3811
|
+
} catch (err) {
|
|
3812
|
+
runtime2.error(`[clawpool:${account.accountId}] stream finish failed: ${String(err)}`);
|
|
3813
|
+
statusSink?.({ lastError: String(err) });
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
3816
|
+
const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
3817
|
+
ctx: ctxPayload,
|
|
3818
|
+
cfg: config,
|
|
3819
|
+
dispatcherOptions: {
|
|
3820
|
+
...prefixOptions,
|
|
3821
|
+
deliver: async (payload, info) => {
|
|
3822
|
+
outboundCounter++;
|
|
3823
|
+
const outPayload = payload;
|
|
3824
|
+
const guardedText = guardInternalReplyText(String(outPayload.text ?? ""));
|
|
3825
|
+
const normalizedPayload = guardedText ? { ...outPayload, text: guardedText.userText } : outPayload;
|
|
3826
|
+
const hasMedia = Boolean(normalizedPayload.mediaUrl) || (normalizedPayload.mediaUrls?.length ?? 0) > 0;
|
|
3827
|
+
const text = core.channel.text.convertMarkdownTables(normalizedPayload.text ?? "", tableMode);
|
|
3828
|
+
const streamedTextAlreadyVisible = hasSentBlock;
|
|
3829
|
+
const deliverContext = buildEventLogContext({
|
|
3830
|
+
eventId,
|
|
3831
|
+
sessionId,
|
|
3832
|
+
messageSid,
|
|
3833
|
+
clientMsgId: info.kind === "block" ? streamClientMsgId : `reply_${messageSid}_${outboundCounter}`,
|
|
3834
|
+
outboundCounter
|
|
3835
|
+
});
|
|
3836
|
+
runtime2.log(
|
|
3837
|
+
`[clawpool:${account.accountId}] deliver ${deliverContext} kind=${info.kind} textLen=${text.length} hasMedia=${hasMedia} streamedBefore=${streamedTextAlreadyVisible}`
|
|
3838
|
+
);
|
|
3839
|
+
if (guardedText) {
|
|
3840
|
+
runtime2.error(
|
|
3841
|
+
`[clawpool:${account.accountId}] rewrite internal reply text ${deliverContext} code=${guardedText.code} raw=${JSON.stringify(guardedText.rawText)}`
|
|
3842
|
+
);
|
|
3843
|
+
}
|
|
3844
|
+
if (guardedText && retryGuardedText == null && isRetryableGuardedReply(guardedText) && !attemptHasOutbound && !hasSentBlock) {
|
|
3845
|
+
retryGuardedText = guardedText;
|
|
3846
|
+
runtime2.log(
|
|
3847
|
+
`[clawpool:${account.accountId}] defer guarded upstream reply for retry ${deliverContext} attempt=${attemptLabel} code=${guardedText.code}`
|
|
3848
|
+
);
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
if (retryGuardedText && !attemptHasOutbound && !hasSentBlock) {
|
|
3852
|
+
runtime2.log(
|
|
3853
|
+
`[clawpool:${account.accountId}] skip outbound while retry pending ${deliverContext} attempt=${attemptLabel} code=${retryGuardedText.code}`
|
|
3854
|
+
);
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
if (info.kind === "block" && !guardedText && !hasMedia && text) {
|
|
3858
|
+
const didSendBlock = await deliverAibotStreamBlock({
|
|
3859
|
+
text,
|
|
3860
|
+
client,
|
|
3861
|
+
account,
|
|
3862
|
+
sessionId,
|
|
3863
|
+
abortSignal: runAbortController.signal,
|
|
3864
|
+
eventId,
|
|
3865
|
+
messageSid,
|
|
3866
|
+
quotedMessageId: outboundQuotedMessageId,
|
|
3867
|
+
clientMsgId: streamClientMsgId,
|
|
3868
|
+
runtime: runtime2,
|
|
3869
|
+
statusSink
|
|
3870
|
+
});
|
|
3871
|
+
hasSentBlock = hasSentBlock || didSendBlock;
|
|
3872
|
+
attemptHasOutbound = attemptHasOutbound || didSendBlock;
|
|
3873
|
+
if (didSendBlock) {
|
|
3874
|
+
markVisibleOutputSent();
|
|
3875
|
+
}
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
await finishStreamIfNeeded();
|
|
3879
|
+
if (info.kind === "final" && streamedTextAlreadyVisible && !hasMedia && text) {
|
|
3880
|
+
runtime2.log(
|
|
3881
|
+
`[clawpool:${account.accountId}] skip final text after streamed block ${deliverContext} textLen=${text.length}`
|
|
3882
|
+
);
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
const stableClientMsgId = `reply_${messageSid}_${outboundCounter}`;
|
|
3886
|
+
runtime2.log(
|
|
3887
|
+
`[clawpool:${account.accountId}] deliver message ${buildEventLogContext({
|
|
3888
|
+
eventId,
|
|
3889
|
+
sessionId,
|
|
3890
|
+
messageSid,
|
|
3891
|
+
clientMsgId: stableClientMsgId,
|
|
3892
|
+
outboundCounter
|
|
3893
|
+
})} textLen=${text.length} hasMedia=${hasMedia}`
|
|
3894
|
+
);
|
|
3895
|
+
const didSendMessage = await deliverAibotMessage({
|
|
3896
|
+
payload: normalizedPayload,
|
|
3897
|
+
client,
|
|
3898
|
+
account,
|
|
3899
|
+
sessionId,
|
|
3900
|
+
abortSignal: runAbortController.signal,
|
|
3901
|
+
eventId,
|
|
3902
|
+
quotedMessageId: outboundQuotedMessageId,
|
|
3903
|
+
runtime: runtime2,
|
|
3904
|
+
statusSink,
|
|
3905
|
+
stableClientMsgId,
|
|
3906
|
+
tableMode
|
|
3907
|
+
});
|
|
3908
|
+
attemptHasOutbound = attemptHasOutbound || didSendMessage;
|
|
3909
|
+
if (didSendMessage) {
|
|
3910
|
+
markVisibleOutputSent();
|
|
3911
|
+
}
|
|
3912
|
+
},
|
|
3913
|
+
onError: (err, info) => {
|
|
3914
|
+
runtime2.error(`[clawpool:${account.accountId}] ${info.kind} reply failed: ${String(err)}`);
|
|
3915
|
+
statusSink?.({ lastError: String(err) });
|
|
3916
|
+
}
|
|
3917
|
+
},
|
|
3918
|
+
replyOptions: {
|
|
3919
|
+
abortSignal: runAbortController.signal,
|
|
3920
|
+
onModelSelected
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3923
|
+
runtime2.log(
|
|
3924
|
+
`[clawpool:${account.accountId}] dispatch complete ${baseLogContext} attempt=${attemptLabel} queuedFinal=${dispatchResult.queuedFinal} counts=${JSON.stringify(dispatchResult.counts)}`
|
|
3925
|
+
);
|
|
3926
|
+
await finishStreamIfNeeded();
|
|
3927
|
+
if (!visibleOutputSent && consumeSilentUnsendCompleted(messageSid)) {
|
|
3928
|
+
runtime2.log(
|
|
3929
|
+
`[clawpool:${account.accountId}] silent unsend completed ${baseLogContext} attempt=${attemptLabel}`
|
|
3930
|
+
);
|
|
3931
|
+
reportEventResult("responded");
|
|
3932
|
+
}
|
|
3933
|
+
if (!visibleOutputSent && shouldTreatDispatchAsRespondedWithoutVisibleOutput(dispatchResult)) {
|
|
3934
|
+
runtime2.log(
|
|
3935
|
+
`[clawpool:${account.accountId}] dispatch completed without visible reply but produced actionable outcome ${baseLogContext} attempt=${attemptLabel}`
|
|
3936
|
+
);
|
|
3937
|
+
reportEventResult("responded");
|
|
3938
|
+
}
|
|
3939
|
+
if (retryGuardedText && !attemptHasOutbound) {
|
|
3940
|
+
if (attempt < retryPolicy.maxAttempts) {
|
|
3941
|
+
const delayMs = resolveUpstreamRetryDelayMs(retryPolicy, attempt);
|
|
3942
|
+
runtime2.error(
|
|
3943
|
+
`[clawpool:${account.accountId}] upstream guarded reply retry ${baseLogContext} code=${retryGuardedText.code} attempt=${attemptLabel} next=${attempt + 1}/${retryPolicy.maxAttempts} delayMs=${delayMs}`
|
|
3944
|
+
);
|
|
3945
|
+
if (delayMs > 0) {
|
|
3946
|
+
await sleep2(delayMs);
|
|
3947
|
+
}
|
|
3948
|
+
continue;
|
|
3949
|
+
}
|
|
3950
|
+
outboundCounter++;
|
|
3951
|
+
const stableClientMsgId = `reply_${messageSid}_${outboundCounter}`;
|
|
3952
|
+
runtime2.error(
|
|
3953
|
+
`[clawpool:${account.accountId}] upstream guarded reply retry exhausted ${baseLogContext} code=${retryGuardedText.code} attempts=${retryPolicy.maxAttempts}`
|
|
3954
|
+
);
|
|
3955
|
+
const didSendMessage = await deliverAibotMessage({
|
|
3956
|
+
payload: {
|
|
3957
|
+
text: retryGuardedText.userText
|
|
3958
|
+
},
|
|
3959
|
+
client,
|
|
3960
|
+
account,
|
|
3961
|
+
sessionId,
|
|
3962
|
+
abortSignal: runAbortController.signal,
|
|
3963
|
+
eventId,
|
|
3964
|
+
quotedMessageId: outboundQuotedMessageId,
|
|
3965
|
+
runtime: runtime2,
|
|
3966
|
+
statusSink,
|
|
3967
|
+
stableClientMsgId,
|
|
3968
|
+
tableMode
|
|
3969
|
+
});
|
|
3970
|
+
attemptHasOutbound = attemptHasOutbound || didSendMessage;
|
|
3971
|
+
if (didSendMessage) {
|
|
3972
|
+
markVisibleOutputSent();
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
break;
|
|
3976
|
+
}
|
|
3977
|
+
if (!visibleOutputSent && !eventResultReported) {
|
|
3978
|
+
reportEventResult("failed", "clawpool_no_outbound_reply", "no outbound reply emitted");
|
|
3979
|
+
}
|
|
3980
|
+
} catch (err) {
|
|
3981
|
+
if (runAbortController.signal.aborted) {
|
|
3982
|
+
runtime2.log(
|
|
3983
|
+
`[clawpool:${account.accountId}] dispatch aborted ${baseLogContext} stopRequested=${activeRun?.stopRequested === true} abortReason=${resolveAbortReason(runAbortController.signal)}`
|
|
3984
|
+
);
|
|
3985
|
+
clearComposing();
|
|
3986
|
+
if (activeRun?.stopRequested) {
|
|
3987
|
+
if (!visibleOutputSent) {
|
|
3988
|
+
reportEventResult("canceled", "owner_requested_stop", "owner requested stop");
|
|
3989
|
+
}
|
|
3990
|
+
reportStopResult("stopped", "owner_requested_stop", "owner requested stop");
|
|
3991
|
+
return;
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
if (!visibleOutputSent) {
|
|
3995
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3996
|
+
reportEventResult("failed", "clawpool_dispatch_failed", message);
|
|
3997
|
+
}
|
|
3998
|
+
reportStopResult("failed", "clawpool_stop_failed", err instanceof Error ? err.message : String(err));
|
|
3999
|
+
throw err;
|
|
4000
|
+
} finally {
|
|
4001
|
+
stopComposingRenewal();
|
|
4002
|
+
if (composingSet) {
|
|
4003
|
+
setComposing(false);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
} finally {
|
|
4007
|
+
runtime2.log(
|
|
4008
|
+
`[clawpool:${account.accountId}] active reply run clearing eventId=${activeRun?.eventId || "-"} sessionId=${activeRun?.sessionId || sessionId} stopRequested=${activeRun?.stopRequested === true} abortReason=${activeRun ? resolveAbortReason(activeRun.controller.signal) : "-"} visibleOutputSent=${visibleOutputSent}`
|
|
4009
|
+
);
|
|
4010
|
+
clearActiveReplyRun(activeRun);
|
|
4011
|
+
if (!inboundEventAccepted) {
|
|
4012
|
+
releaseInboundEvent(inboundEvent.claim);
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
async function monitorAibotProvider(options) {
|
|
4017
|
+
const { account, config, runtime: runtime2, abortSignal, statusSink } = options;
|
|
4018
|
+
let client;
|
|
4019
|
+
const guardedStatusSink = (patch) => {
|
|
4020
|
+
if (!isActiveMonitor(account.accountId, client)) {
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
statusSink?.(patch);
|
|
4024
|
+
};
|
|
4025
|
+
client = new AibotWsClient(account, {
|
|
4026
|
+
logger: {
|
|
4027
|
+
info: (message) => runtime2.log(message),
|
|
4028
|
+
warn: (message) => runtime2.log(`[warn] ${message}`),
|
|
4029
|
+
error: (message) => runtime2.error(message),
|
|
4030
|
+
debug: (message) => runtime2.log(message)
|
|
4031
|
+
},
|
|
4032
|
+
onStatus: (status) => {
|
|
4033
|
+
guardedStatusSink({
|
|
4034
|
+
running: status.running,
|
|
4035
|
+
connected: status.connected,
|
|
4036
|
+
lastError: status.lastError,
|
|
4037
|
+
lastConnectAt: status.lastConnectAt ?? void 0,
|
|
4038
|
+
lastDisconnectAt: status.lastDisconnectAt ?? void 0
|
|
4039
|
+
});
|
|
4040
|
+
},
|
|
4041
|
+
onEventMsg: (event) => {
|
|
4042
|
+
if (!isActiveMonitor(account.accountId, client)) {
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
guardedStatusSink({ lastInboundAt: Date.now() });
|
|
4046
|
+
void processEvent({
|
|
4047
|
+
event,
|
|
4048
|
+
account,
|
|
4049
|
+
config,
|
|
4050
|
+
runtime: runtime2,
|
|
4051
|
+
client,
|
|
4052
|
+
statusSink: guardedStatusSink
|
|
4053
|
+
}).catch((err) => {
|
|
4054
|
+
if (!isActiveMonitor(account.accountId, client)) {
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4058
|
+
runtime2.error(`[clawpool:${account.accountId}] process event failed: ${msg}`);
|
|
4059
|
+
guardedStatusSink({ lastError: msg });
|
|
4060
|
+
});
|
|
4061
|
+
},
|
|
4062
|
+
onEventRevoke: (event) => {
|
|
4063
|
+
if (!isActiveMonitor(account.accountId, client)) {
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
guardedStatusSink({ lastInboundAt: Date.now() });
|
|
4067
|
+
try {
|
|
4068
|
+
const eventId = String(event.event_id ?? "").trim();
|
|
4069
|
+
if (eventId) {
|
|
4070
|
+
client.ackEvent(eventId, {
|
|
4071
|
+
sessionId: event.session_id,
|
|
4072
|
+
msgId: event.msg_id
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
const revokeEvent = enqueueRevokeSystemEvent({
|
|
4076
|
+
core: getAibotRuntime(),
|
|
4077
|
+
event,
|
|
4078
|
+
account,
|
|
4079
|
+
config
|
|
4080
|
+
});
|
|
4081
|
+
runtime2.log(
|
|
4082
|
+
`[clawpool:${account.accountId}] inbound revoke sessionId=${revokeEvent.sessionId} messageSid=${revokeEvent.messageId} routeSessionKey=${revokeEvent.sessionKey}`
|
|
4083
|
+
);
|
|
4084
|
+
} catch (err) {
|
|
4085
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4086
|
+
runtime2.error(`[clawpool:${account.accountId}] process revoke event failed: ${msg}`);
|
|
4087
|
+
guardedStatusSink({ lastError: msg });
|
|
4088
|
+
}
|
|
4089
|
+
},
|
|
4090
|
+
onEventStop: (payload) => {
|
|
4091
|
+
if (!isActiveMonitor(account.accountId, client)) {
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
guardedStatusSink({ lastInboundAt: Date.now() });
|
|
4095
|
+
handleEventStop({
|
|
4096
|
+
payload,
|
|
4097
|
+
account,
|
|
4098
|
+
runtime: runtime2,
|
|
4099
|
+
client,
|
|
4100
|
+
statusSink: guardedStatusSink
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
});
|
|
4104
|
+
const previousClient = registerActiveMonitor(account.accountId, client);
|
|
4105
|
+
if (previousClient) {
|
|
4106
|
+
runtime2.log(`[clawpool:${account.accountId}] stopping superseded clawpool monitor before restart`);
|
|
4107
|
+
previousClient.stop();
|
|
4108
|
+
}
|
|
4109
|
+
setActiveAibotClient(account.accountId, client);
|
|
4110
|
+
try {
|
|
4111
|
+
await client.start(abortSignal);
|
|
4112
|
+
} catch (err) {
|
|
4113
|
+
clearActiveAibotClient(account.accountId, client);
|
|
4114
|
+
clearActiveMonitor(account.accountId, client);
|
|
4115
|
+
throw err;
|
|
4116
|
+
}
|
|
4117
|
+
void client.waitUntilStopped().catch((err) => {
|
|
4118
|
+
if (!isActiveMonitor(account.accountId, client)) {
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4122
|
+
runtime2.error(`[clawpool:${account.accountId}] background run loop failed: ${msg}`);
|
|
4123
|
+
guardedStatusSink({ lastError: msg });
|
|
4124
|
+
}).finally(() => {
|
|
4125
|
+
clearActiveAibotClient(account.accountId, client);
|
|
4126
|
+
clearActiveMonitor(account.accountId, client);
|
|
4127
|
+
});
|
|
4128
|
+
return {
|
|
4129
|
+
stop: () => {
|
|
4130
|
+
clearActiveAibotClient(account.accountId, client);
|
|
4131
|
+
clearActiveMonitor(account.accountId, client);
|
|
4132
|
+
client.stop();
|
|
4133
|
+
}
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
// src/setup-config.ts
|
|
4138
|
+
import {
|
|
4139
|
+
applyAccountNameToChannelSection,
|
|
4140
|
+
migrateBaseNameToDefaultAccount
|
|
4141
|
+
} from "openclaw/plugin-sdk/core";
|
|
4142
|
+
function resolveSetupValues(input) {
|
|
4143
|
+
const apiKey = String(input.token ?? input.appToken ?? "").trim();
|
|
4144
|
+
const wsUrl = String(input.httpUrl ?? input.webhookUrl ?? input.url ?? "").trim();
|
|
4145
|
+
const agentId = String(input.userId ?? "").trim();
|
|
4146
|
+
return {
|
|
4147
|
+
apiKey: apiKey || void 0,
|
|
4148
|
+
wsUrl: wsUrl || void 0,
|
|
4149
|
+
agentId: agentId || void 0
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
function applySetupAccountConfig(params) {
|
|
4153
|
+
const { cfg, accountId, name, values } = params;
|
|
4154
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
4155
|
+
cfg,
|
|
4156
|
+
channelKey: "clawpool",
|
|
4157
|
+
accountId,
|
|
4158
|
+
name
|
|
4159
|
+
});
|
|
4160
|
+
const next = accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
|
|
4161
|
+
cfg: namedConfig,
|
|
4162
|
+
channelKey: "clawpool"
|
|
4163
|
+
}) : namedConfig;
|
|
4164
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
4165
|
+
return {
|
|
4166
|
+
...next,
|
|
4167
|
+
channels: {
|
|
4168
|
+
...next.channels,
|
|
4169
|
+
clawpool: {
|
|
4170
|
+
...next.channels?.clawpool,
|
|
4171
|
+
enabled: true,
|
|
4172
|
+
...values.apiKey ? { apiKey: values.apiKey } : {},
|
|
4173
|
+
...values.wsUrl ? { wsUrl: values.wsUrl } : {},
|
|
4174
|
+
...values.agentId ? { agentId: values.agentId } : {}
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
return {
|
|
4180
|
+
...next,
|
|
4181
|
+
channels: {
|
|
4182
|
+
...next.channels,
|
|
4183
|
+
clawpool: {
|
|
4184
|
+
...next.channels?.clawpool,
|
|
4185
|
+
enabled: true,
|
|
4186
|
+
accounts: {
|
|
4187
|
+
...next.channels?.clawpool?.accounts ?? {},
|
|
4188
|
+
[accountId]: {
|
|
4189
|
+
...next.channels?.clawpool?.accounts?.[accountId],
|
|
4190
|
+
enabled: true,
|
|
4191
|
+
...values.apiKey ? { apiKey: values.apiKey } : {},
|
|
4192
|
+
...values.wsUrl ? { wsUrl: values.wsUrl } : {},
|
|
4193
|
+
...values.agentId ? { agentId: values.agentId } : {}
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
// src/channel.ts
|
|
4202
|
+
var meta = {
|
|
4203
|
+
id: "clawpool",
|
|
4204
|
+
label: "Clawpool",
|
|
4205
|
+
selectionLabel: "Clawpool",
|
|
4206
|
+
docsPath: "/channels/clawpool",
|
|
4207
|
+
blurb: "Bridge OpenClaw to Clawpool over the ClawPool Agent API WebSocket.",
|
|
4208
|
+
aliases: ["cp", "clowpool"],
|
|
4209
|
+
order: 90
|
|
4210
|
+
};
|
|
4211
|
+
function normalizeQuotedMessageId(rawInput) {
|
|
4212
|
+
const raw = String(rawInput ?? "").trim();
|
|
4213
|
+
if (!raw) {
|
|
4214
|
+
return void 0;
|
|
4215
|
+
}
|
|
4216
|
+
if (/^\d+$/.test(raw)) {
|
|
4217
|
+
return raw;
|
|
4218
|
+
}
|
|
4219
|
+
const parsed = raw.split(":").at(-1)?.trim() ?? "";
|
|
4220
|
+
if (/^\d+$/.test(parsed)) {
|
|
4221
|
+
return parsed;
|
|
4222
|
+
}
|
|
4223
|
+
return void 0;
|
|
4224
|
+
}
|
|
4225
|
+
function logAibotOutboundAdapter(message) {
|
|
4226
|
+
console.info(`[clawpool:outbound] ${message}`);
|
|
4227
|
+
}
|
|
4228
|
+
function asAibotChannelConfig(cfg) {
|
|
4229
|
+
return cfg.channels?.clawpool ?? {};
|
|
4230
|
+
}
|
|
4231
|
+
function buildAccountSnapshot(params) {
|
|
4232
|
+
const { account, runtime: runtime2 } = params;
|
|
4233
|
+
return {
|
|
4234
|
+
accountId: account.accountId,
|
|
4235
|
+
name: account.name,
|
|
4236
|
+
enabled: account.enabled,
|
|
4237
|
+
configured: account.configured,
|
|
4238
|
+
running: runtime2?.running ?? false,
|
|
4239
|
+
connected: runtime2?.connected ?? false,
|
|
4240
|
+
lastError: runtime2?.lastError ?? null,
|
|
4241
|
+
lastStartAt: runtime2?.lastStartAt ?? null,
|
|
4242
|
+
lastStopAt: runtime2?.lastStopAt ?? null,
|
|
4243
|
+
lastInboundAt: runtime2?.lastInboundAt ?? null,
|
|
4244
|
+
lastOutboundAt: runtime2?.lastOutboundAt ?? null,
|
|
4245
|
+
dmPolicy: account.config.dmPolicy ?? "open",
|
|
4246
|
+
tokenSource: account.apiKey ? "config" : "none"
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
var AibotConfigSchema = {
|
|
4250
|
+
type: "object",
|
|
4251
|
+
additionalProperties: true,
|
|
4252
|
+
properties: {}
|
|
4253
|
+
};
|
|
4254
|
+
function chunkTextForOutbound(text, limit) {
|
|
4255
|
+
if (!text) return [];
|
|
4256
|
+
if (limit <= 0 || text.length <= limit) return [text];
|
|
4257
|
+
const chunks = [];
|
|
4258
|
+
let remaining = text;
|
|
4259
|
+
while (remaining.length > limit) {
|
|
4260
|
+
const window = remaining.slice(0, limit);
|
|
4261
|
+
const lastNewline = window.lastIndexOf("\n");
|
|
4262
|
+
const lastSpace = window.lastIndexOf(" ");
|
|
4263
|
+
const candidateBreak = lastNewline > 0 ? lastNewline : lastSpace;
|
|
4264
|
+
const breakIdx = Number.isFinite(candidateBreak) && candidateBreak > 0 && candidateBreak <= limit ? candidateBreak : limit;
|
|
4265
|
+
const chunk = remaining.slice(0, breakIdx).trimEnd();
|
|
4266
|
+
if (chunk.length > 0) chunks.push(chunk);
|
|
4267
|
+
const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx] ?? "");
|
|
4268
|
+
const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0));
|
|
4269
|
+
remaining = remaining.slice(nextStart).trimStart();
|
|
4270
|
+
}
|
|
4271
|
+
if (remaining.length) chunks.push(remaining);
|
|
4272
|
+
return chunks;
|
|
4273
|
+
}
|
|
4274
|
+
var aibotPlugin = {
|
|
4275
|
+
id: "clawpool",
|
|
4276
|
+
meta,
|
|
4277
|
+
capabilities: {
|
|
4278
|
+
chatTypes: ["direct", "group"],
|
|
4279
|
+
media: true,
|
|
4280
|
+
reactions: true,
|
|
4281
|
+
unsend: true,
|
|
4282
|
+
threads: false,
|
|
4283
|
+
polls: false,
|
|
4284
|
+
nativeCommands: false,
|
|
4285
|
+
blockStreaming: true
|
|
4286
|
+
},
|
|
4287
|
+
actions: aibotMessageActions,
|
|
4288
|
+
reload: {
|
|
4289
|
+
configPrefixes: ["channels.clawpool"]
|
|
4290
|
+
},
|
|
4291
|
+
configSchema: {
|
|
4292
|
+
schema: AibotConfigSchema
|
|
4293
|
+
},
|
|
4294
|
+
config: {
|
|
4295
|
+
listAccountIds: (cfg) => listAibotAccountIds(cfg),
|
|
4296
|
+
resolveAccount: (cfg, accountId) => resolveAibotAccount({ cfg, accountId }),
|
|
4297
|
+
defaultAccountId: (cfg) => resolveDefaultAibotAccountId(cfg),
|
|
4298
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
|
|
4299
|
+
cfg,
|
|
4300
|
+
sectionKey: "clawpool",
|
|
4301
|
+
accountId,
|
|
4302
|
+
enabled,
|
|
4303
|
+
allowTopLevel: true
|
|
4304
|
+
}),
|
|
4305
|
+
deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
|
|
4306
|
+
cfg,
|
|
4307
|
+
sectionKey: "clawpool",
|
|
4308
|
+
accountId,
|
|
4309
|
+
clearBaseFields: [
|
|
4310
|
+
"name",
|
|
4311
|
+
"wsUrl",
|
|
4312
|
+
"agentId",
|
|
4313
|
+
"apiKey",
|
|
4314
|
+
"reconnectMs",
|
|
4315
|
+
"reconnectMaxMs",
|
|
4316
|
+
"reconnectStableMs",
|
|
4317
|
+
"connectTimeoutMs",
|
|
4318
|
+
"keepalivePingMs",
|
|
4319
|
+
"keepaliveTimeoutMs",
|
|
4320
|
+
"upstreamRetryMaxAttempts",
|
|
4321
|
+
"upstreamRetryBaseDelayMs",
|
|
4322
|
+
"upstreamRetryMaxDelayMs",
|
|
4323
|
+
"maxChunkChars",
|
|
4324
|
+
"execApprovals",
|
|
4325
|
+
"dmPolicy",
|
|
4326
|
+
"allowFrom",
|
|
4327
|
+
"defaultTo"
|
|
4328
|
+
]
|
|
4329
|
+
}),
|
|
4330
|
+
isConfigured: (account) => account.configured,
|
|
4331
|
+
describeAccount: (account, cfg) => {
|
|
4332
|
+
const root = asAibotChannelConfig(cfg);
|
|
4333
|
+
return {
|
|
4334
|
+
accountId: account.accountId,
|
|
4335
|
+
name: account.name,
|
|
4336
|
+
enabled: account.enabled,
|
|
4337
|
+
configured: account.configured,
|
|
4338
|
+
running: false,
|
|
4339
|
+
connected: false,
|
|
4340
|
+
lastError: account.configured ? null : "missing wsUrl/agentId/apiKey (or CLAWPOOL_WS_URL/CLAWPOOL_AGENT_ID/CLAWPOOL_API_KEY)",
|
|
4341
|
+
dmPolicy: account.config.dmPolicy ?? "open",
|
|
4342
|
+
tokenSource: account.apiKey ? "config" : "none",
|
|
4343
|
+
mode: "block_streaming",
|
|
4344
|
+
baseUrl: redactAibotWsUrl(account.wsUrl),
|
|
4345
|
+
allowFrom: account.config.allowFrom?.map((entry) => String(entry).trim()).filter(Boolean) ?? [],
|
|
4346
|
+
nameSource: root.accounts?.[account.accountId]?.name ? "account" : "base"
|
|
4347
|
+
};
|
|
4348
|
+
},
|
|
4349
|
+
resolveAllowFrom: ({ cfg, accountId }) => resolveAibotAccount({ cfg, accountId }).config.allowFrom?.map((entry) => String(entry)) ?? [],
|
|
4350
|
+
formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
|
|
4351
|
+
resolveDefaultTo: ({ cfg, accountId }) => resolveAibotAccount({ cfg, accountId }).config.defaultTo?.trim() || void 0
|
|
4352
|
+
},
|
|
4353
|
+
setup: {
|
|
4354
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
4355
|
+
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection2({
|
|
4356
|
+
cfg,
|
|
4357
|
+
channelKey: "clawpool",
|
|
4358
|
+
accountId,
|
|
4359
|
+
name
|
|
4360
|
+
}),
|
|
4361
|
+
validateInput: ({ input }) => {
|
|
4362
|
+
const values = resolveSetupValues(input);
|
|
4363
|
+
const hasAny = Boolean(values.apiKey || values.wsUrl || values.agentId);
|
|
4364
|
+
if (!hasAny) {
|
|
4365
|
+
return "clawpool setup requires at least one of: --token(api key), --http-url(ws url), --user-id(agent id)";
|
|
4366
|
+
}
|
|
4367
|
+
return null;
|
|
4368
|
+
},
|
|
4369
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
4370
|
+
const values = resolveSetupValues(input);
|
|
4371
|
+
return applySetupAccountConfig({
|
|
4372
|
+
cfg,
|
|
4373
|
+
accountId,
|
|
4374
|
+
name: input.name,
|
|
4375
|
+
values
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
},
|
|
4379
|
+
security: {
|
|
4380
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
4381
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
4382
|
+
const isAccountScoped = Boolean(cfg.channels?.clawpool?.accounts?.[resolvedAccountId]);
|
|
4383
|
+
const basePath = isAccountScoped ? `channels.clawpool.accounts.${resolvedAccountId}.` : "channels.clawpool.";
|
|
4384
|
+
return {
|
|
4385
|
+
policy: account.config.dmPolicy ?? "open",
|
|
4386
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
4387
|
+
policyPath: `${basePath}dmPolicy`,
|
|
4388
|
+
allowFromPath: basePath,
|
|
4389
|
+
approveHint: formatPairingApproveHint("clawpool")
|
|
4390
|
+
};
|
|
4391
|
+
}
|
|
4392
|
+
},
|
|
4393
|
+
messaging: {
|
|
4394
|
+
normalizeTarget: (raw) => normalizeAibotSessionTarget(raw),
|
|
4395
|
+
targetResolver: {
|
|
4396
|
+
looksLikeId: (raw) => Boolean(normalizeAibotSessionTarget(raw)),
|
|
4397
|
+
hint: "<session_id|route.sessionKey>"
|
|
4398
|
+
}
|
|
4399
|
+
},
|
|
4400
|
+
execApprovals: clawpoolExecApprovalAdapter,
|
|
4401
|
+
threading: {
|
|
4402
|
+
buildToolContext: ({ context, hasRepliedRef }) => ({
|
|
4403
|
+
currentChannelId: context.To?.trim() || void 0,
|
|
4404
|
+
currentMessageId: context.CurrentMessageId != null ? String(context.CurrentMessageId) : void 0,
|
|
4405
|
+
hasRepliedRef
|
|
4406
|
+
})
|
|
4407
|
+
},
|
|
4408
|
+
agentPrompt: {
|
|
4409
|
+
messageToolHints: () => [
|
|
4410
|
+
"- Clawpool `action=unsend` is a silent cleanup action: unsend the target `messageId`, unsend the recall command message when applicable, then end with `NO_REPLY` and do not send any confirmation text. Omit `sessionId`/`to` only when targeting the current Clawpool chat."
|
|
4411
|
+
]
|
|
4412
|
+
},
|
|
4413
|
+
outbound: {
|
|
4414
|
+
deliveryMode: "direct",
|
|
4415
|
+
chunker: chunkTextForOutbound,
|
|
4416
|
+
chunkerMode: "markdown",
|
|
4417
|
+
textChunkLimit: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT,
|
|
4418
|
+
sendText: async ({ cfg, to, text, accountId, replyToId }) => {
|
|
4419
|
+
const account = resolveAibotAccount({ cfg, accountId });
|
|
4420
|
+
const client = requireActiveAibotClient(account.accountId);
|
|
4421
|
+
const rawTarget = String(to ?? "").trim() || "-";
|
|
4422
|
+
logAibotOutboundAdapter(
|
|
4423
|
+
`sendText target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
|
|
4424
|
+
);
|
|
4425
|
+
let resolvedTarget;
|
|
4426
|
+
try {
|
|
4427
|
+
resolvedTarget = await resolveAibotOutboundTarget({
|
|
4428
|
+
client,
|
|
4429
|
+
accountId: account.accountId,
|
|
4430
|
+
to
|
|
4431
|
+
});
|
|
4432
|
+
} catch (err) {
|
|
4433
|
+
logAibotOutboundAdapter(
|
|
4434
|
+
`sendText target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
|
|
4435
|
+
);
|
|
4436
|
+
throw err;
|
|
4437
|
+
}
|
|
4438
|
+
const sessionId = resolvedTarget.sessionId;
|
|
4439
|
+
const quotedMessageId = normalizeQuotedMessageId(replyToId);
|
|
4440
|
+
logAibotOutboundAdapter(
|
|
4441
|
+
`sendText accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${text.length} quotedMessageId=${quotedMessageId ?? "-"}`
|
|
4442
|
+
);
|
|
4443
|
+
const ack = await client.sendText(sessionId, text, {
|
|
4444
|
+
quotedMessageId
|
|
4445
|
+
});
|
|
4446
|
+
logAibotOutboundAdapter(
|
|
4447
|
+
`sendText ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")}`
|
|
4448
|
+
);
|
|
4449
|
+
return {
|
|
4450
|
+
channel: "clawpool",
|
|
4451
|
+
messageId: String(ack.msg_id ?? ack.client_msg_id ?? Date.now())
|
|
4452
|
+
};
|
|
4453
|
+
},
|
|
4454
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => {
|
|
4455
|
+
const account = resolveAibotAccount({ cfg, accountId });
|
|
4456
|
+
const client = requireActiveAibotClient(account.accountId);
|
|
4457
|
+
const rawTarget = String(to ?? "").trim() || "-";
|
|
4458
|
+
logAibotOutboundAdapter(
|
|
4459
|
+
`sendMedia target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
|
|
4460
|
+
);
|
|
4461
|
+
let resolvedTarget;
|
|
4462
|
+
try {
|
|
4463
|
+
resolvedTarget = await resolveAibotOutboundTarget({
|
|
4464
|
+
client,
|
|
4465
|
+
accountId: account.accountId,
|
|
4466
|
+
to
|
|
4467
|
+
});
|
|
4468
|
+
} catch (err) {
|
|
4469
|
+
logAibotOutboundAdapter(
|
|
4470
|
+
`sendMedia target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
|
|
4471
|
+
);
|
|
4472
|
+
throw err;
|
|
4473
|
+
}
|
|
4474
|
+
const sessionId = resolvedTarget.sessionId;
|
|
4475
|
+
if (!mediaUrl) {
|
|
4476
|
+
throw new Error("clawpool sendMedia requires mediaUrl");
|
|
4477
|
+
}
|
|
4478
|
+
const quotedMessageId = normalizeQuotedMessageId(replyToId);
|
|
4479
|
+
logAibotOutboundAdapter(
|
|
4480
|
+
`sendMedia accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${(text ?? "").length} quotedMessageId=${quotedMessageId ?? "-"} mediaUrl=${mediaUrl}`
|
|
4481
|
+
);
|
|
4482
|
+
const ack = await client.sendMedia(sessionId, mediaUrl, text ?? "", {
|
|
4483
|
+
quotedMessageId
|
|
4484
|
+
});
|
|
4485
|
+
logAibotOutboundAdapter(
|
|
4486
|
+
`sendMedia ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")}`
|
|
4487
|
+
);
|
|
4488
|
+
return {
|
|
4489
|
+
channel: "clawpool",
|
|
4490
|
+
messageId: String(ack.msg_id ?? ack.client_msg_id ?? Date.now())
|
|
4491
|
+
};
|
|
4492
|
+
},
|
|
4493
|
+
sendPayload: async ({ cfg, to, payload, accountId, replyToId }) => {
|
|
4494
|
+
const account = resolveAibotAccount({ cfg, accountId });
|
|
4495
|
+
const client = requireActiveAibotClient(account.accountId);
|
|
4496
|
+
const rawTarget = String(to ?? "").trim() || "-";
|
|
4497
|
+
logAibotOutboundAdapter(
|
|
4498
|
+
`sendPayload target resolve begin accountId=${account.accountId} rawTarget=${rawTarget}`
|
|
4499
|
+
);
|
|
4500
|
+
let resolvedTarget;
|
|
4501
|
+
try {
|
|
4502
|
+
resolvedTarget = await resolveAibotOutboundTarget({
|
|
4503
|
+
client,
|
|
4504
|
+
accountId: account.accountId,
|
|
4505
|
+
to
|
|
4506
|
+
});
|
|
4507
|
+
} catch (err) {
|
|
4508
|
+
logAibotOutboundAdapter(
|
|
4509
|
+
`sendPayload target resolve failed accountId=${account.accountId} rawTarget=${rawTarget} error=${String(err)}`
|
|
4510
|
+
);
|
|
4511
|
+
throw err;
|
|
4512
|
+
}
|
|
4513
|
+
const sessionId = resolvedTarget.sessionId;
|
|
4514
|
+
const quotedMessageId = normalizeQuotedMessageId(replyToId);
|
|
4515
|
+
const envelope = buildAibotOutboundEnvelope(payload);
|
|
4516
|
+
logAibotOutboundAdapter(
|
|
4517
|
+
`sendPayload accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${envelope.text.length} quotedMessageId=${quotedMessageId ?? "-"} cardKind=${envelope.cardKind ?? "none"}`
|
|
4518
|
+
);
|
|
4519
|
+
const delivery = await deliverAibotPayload({
|
|
4520
|
+
payload,
|
|
4521
|
+
text: envelope.text,
|
|
4522
|
+
extra: envelope.extra,
|
|
4523
|
+
client,
|
|
4524
|
+
account,
|
|
4525
|
+
sessionId,
|
|
4526
|
+
quotedMessageId
|
|
4527
|
+
});
|
|
4528
|
+
if (!delivery.sent) {
|
|
4529
|
+
throw new Error("clawpool sendPayload produced no visible delivery");
|
|
4530
|
+
}
|
|
4531
|
+
const messageId = delivery.firstMessageId ?? `clawpool_payload_${Date.now()}`;
|
|
4532
|
+
logAibotOutboundAdapter(
|
|
4533
|
+
`sendPayload ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${messageId} cardKind=${envelope.cardKind ?? "none"}`
|
|
4534
|
+
);
|
|
4535
|
+
return {
|
|
4536
|
+
channel: "clawpool",
|
|
4537
|
+
messageId
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
},
|
|
4541
|
+
status: {
|
|
4542
|
+
defaultRuntime: {
|
|
4543
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
4544
|
+
running: false,
|
|
4545
|
+
connected: false,
|
|
4546
|
+
lastError: null,
|
|
4547
|
+
lastStartAt: null,
|
|
4548
|
+
lastStopAt: null,
|
|
4549
|
+
lastInboundAt: null,
|
|
4550
|
+
lastOutboundAt: null
|
|
4551
|
+
},
|
|
4552
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
4553
|
+
configured: snapshot.configured ?? false,
|
|
4554
|
+
running: snapshot.running ?? false,
|
|
4555
|
+
connected: snapshot.connected ?? false,
|
|
4556
|
+
lastError: snapshot.lastError ?? null,
|
|
4557
|
+
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
4558
|
+
lastOutboundAt: snapshot.lastOutboundAt ?? null
|
|
4559
|
+
}),
|
|
4560
|
+
buildAccountSnapshot: ({ account, runtime: runtime2 }) => buildAccountSnapshot({ account, runtime: runtime2 }),
|
|
4561
|
+
collectStatusIssues: (accounts) => accounts.flatMap((account) => {
|
|
4562
|
+
if (!account.enabled) {
|
|
4563
|
+
return [];
|
|
4564
|
+
}
|
|
4565
|
+
if (!account.configured) {
|
|
4566
|
+
return [
|
|
4567
|
+
{
|
|
4568
|
+
channel: "clawpool",
|
|
4569
|
+
accountId: account.accountId,
|
|
4570
|
+
kind: "config",
|
|
4571
|
+
message: "Clawpool account is not configured. Set wsUrl/agentId/apiKey (or CLAWPOOL_WS_URL/CLAWPOOL_AGENT_ID/CLAWPOOL_API_KEY)."
|
|
4572
|
+
}
|
|
4573
|
+
];
|
|
4574
|
+
}
|
|
4575
|
+
if (account.running && !account.connected) {
|
|
4576
|
+
return [
|
|
4577
|
+
{
|
|
4578
|
+
channel: "clawpool",
|
|
4579
|
+
accountId: account.accountId,
|
|
4580
|
+
kind: "runtime",
|
|
4581
|
+
message: "Clawpool channel is running but not connected."
|
|
4582
|
+
}
|
|
4583
|
+
];
|
|
4584
|
+
}
|
|
4585
|
+
if (typeof account.lastError === "string" && account.lastError.trim()) {
|
|
4586
|
+
return [
|
|
4587
|
+
{
|
|
4588
|
+
channel: "clawpool",
|
|
4589
|
+
accountId: account.accountId,
|
|
4590
|
+
kind: "runtime",
|
|
4591
|
+
message: account.lastError
|
|
4592
|
+
}
|
|
4593
|
+
];
|
|
4594
|
+
}
|
|
4595
|
+
return [];
|
|
4596
|
+
})
|
|
4597
|
+
},
|
|
4598
|
+
gateway: {
|
|
4599
|
+
startAccount: async (ctx) => {
|
|
4600
|
+
const account = ctx.account;
|
|
4601
|
+
if (!account.configured) {
|
|
4602
|
+
throw new Error(
|
|
4603
|
+
`clawpool account "${account.accountId}" not configured: require wsUrl + agentId + apiKey`
|
|
4604
|
+
);
|
|
4605
|
+
}
|
|
4606
|
+
ctx.log?.info?.(
|
|
4607
|
+
`[${account.accountId}] starting clawpool monitor (${redactAibotWsUrl(account.wsUrl)})`
|
|
4608
|
+
);
|
|
4609
|
+
ctx.setStatus({
|
|
4610
|
+
...ctx.getStatus(),
|
|
4611
|
+
running: true,
|
|
4612
|
+
connected: false,
|
|
4613
|
+
lastError: null,
|
|
4614
|
+
lastStartAt: Date.now()
|
|
4615
|
+
});
|
|
4616
|
+
const monitor = await monitorAibotProvider({
|
|
4617
|
+
account,
|
|
4618
|
+
config: ctx.cfg,
|
|
4619
|
+
runtime: ctx.runtime,
|
|
4620
|
+
abortSignal: ctx.abortSignal,
|
|
4621
|
+
statusSink: (patch) => {
|
|
4622
|
+
ctx.setStatus({
|
|
4623
|
+
...ctx.getStatus(),
|
|
4624
|
+
...patch
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
});
|
|
4628
|
+
try {
|
|
4629
|
+
await waitUntilAbort(ctx.abortSignal);
|
|
4630
|
+
} finally {
|
|
4631
|
+
monitor.stop();
|
|
4632
|
+
ctx.setStatus({
|
|
4633
|
+
...ctx.getStatus(),
|
|
4634
|
+
running: false,
|
|
4635
|
+
connected: false,
|
|
4636
|
+
lastStopAt: Date.now()
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
},
|
|
4640
|
+
stopAccount: async (ctx) => {
|
|
4641
|
+
const client = getActiveAibotClient(ctx.accountId);
|
|
4642
|
+
client?.stop();
|
|
4643
|
+
ctx.setStatus({
|
|
4644
|
+
...ctx.getStatus(),
|
|
4645
|
+
running: false,
|
|
4646
|
+
connected: false,
|
|
4647
|
+
lastStopAt: Date.now()
|
|
4648
|
+
});
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
};
|
|
4652
|
+
|
|
4653
|
+
// index.ts
|
|
4654
|
+
var plugin = {
|
|
4655
|
+
id: "clawpool",
|
|
4656
|
+
name: "Clawpool OpenClaw",
|
|
4657
|
+
description: "Clawpool channel plugin backed by Aibot Agent API",
|
|
4658
|
+
configSchema: emptyPluginConfigSchema(),
|
|
4659
|
+
register(api) {
|
|
4660
|
+
setAibotRuntime(api.runtime);
|
|
4661
|
+
api.registerChannel({ plugin: aibotPlugin });
|
|
4662
|
+
}
|
|
4663
|
+
};
|
|
4664
|
+
var index_default = plugin;
|
|
4665
|
+
export {
|
|
4666
|
+
index_default as default
|
|
4667
|
+
};
|