@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +64 -0
- package/README.md +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
package/dist/src/runtime.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, } from "
|
|
1
|
+
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, EVENT, } from "./protocol-types.js";
|
|
2
2
|
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
3
3
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
4
4
|
import { createOpenclawClawlingClient } from "./client.js";
|
|
5
|
+
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
6
|
+
import { ClawlingApiError } from "./api-types.js";
|
|
5
7
|
import { CHANNEL_ID } from "./config.js";
|
|
6
8
|
import { dispatchOpenclawClawlingInbound } from "./inbound.js";
|
|
7
9
|
import { fetchInboundMedia } from "./media-runtime.js";
|
|
8
10
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.js";
|
|
9
11
|
import { sendStreamingText } from "./streaming.js";
|
|
10
|
-
import {
|
|
12
|
+
import { flushAlignedOutboundQueue, getAlignedOutboundQueueSize, setAlignedOutboundLogContext, } from "./outbound.js";
|
|
13
|
+
import { formatWsLog } from "./ws-log.js";
|
|
14
|
+
import { createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.js";
|
|
15
|
+
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
11
16
|
const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingRuntime } = createPluginRuntimeStore("openclaw-clawchat runtime not initialized");
|
|
12
17
|
export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
|
|
13
18
|
const activeClients = new Map();
|
|
@@ -62,6 +67,87 @@ export function classifyClawlingClientError(err) {
|
|
|
62
67
|
function formatConversationSubject(peer) {
|
|
63
68
|
return peer.kind === "group" ? `group:${peer.id}` : peer.id;
|
|
64
69
|
}
|
|
70
|
+
function parseApiTimestamp(value) {
|
|
71
|
+
if (typeof value !== "string")
|
|
72
|
+
return null;
|
|
73
|
+
const parsed = Date.parse(value);
|
|
74
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
75
|
+
}
|
|
76
|
+
function asRecord(value) {
|
|
77
|
+
return value && typeof value === "object" ? value : null;
|
|
78
|
+
}
|
|
79
|
+
function optionalString(value) {
|
|
80
|
+
return typeof value === "string" ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
function isConversationNotFoundError(err) {
|
|
83
|
+
if (!(err instanceof ClawlingApiError))
|
|
84
|
+
return false;
|
|
85
|
+
return err.meta?.status === 404 || err.meta?.status === 410 ||
|
|
86
|
+
err.meta?.code === 404 || err.meta?.code === 410 || err.meta?.code === 40401;
|
|
87
|
+
}
|
|
88
|
+
function metadataVersionFromEnvelope(env) {
|
|
89
|
+
const payload = asRecord(env.payload);
|
|
90
|
+
const version = payload?.version;
|
|
91
|
+
return typeof version === "number" && Number.isFinite(version) ? version : undefined;
|
|
92
|
+
}
|
|
93
|
+
function conversationUserProfileFromParticipant(participant, refreshedAt) {
|
|
94
|
+
const participantRecord = participant;
|
|
95
|
+
const userRecord = asRecord(participantRecord.user);
|
|
96
|
+
const source = userRecord ?? participantRecord;
|
|
97
|
+
const userId = optionalString(source.id) ?? optionalString(participantRecord.user_id);
|
|
98
|
+
if (!userId)
|
|
99
|
+
return null;
|
|
100
|
+
const profile = { userId, raw: source, lastRefreshedAt: refreshedAt };
|
|
101
|
+
const nickname = optionalString(source.nickname);
|
|
102
|
+
const avatarUrl = optionalString(source.avatar_url);
|
|
103
|
+
const bio = optionalString(source.bio);
|
|
104
|
+
if (nickname !== undefined)
|
|
105
|
+
profile.nickname = nickname;
|
|
106
|
+
if (avatarUrl !== undefined)
|
|
107
|
+
profile.avatarUrl = avatarUrl;
|
|
108
|
+
if (bio !== undefined)
|
|
109
|
+
profile.bio = bio;
|
|
110
|
+
return profile;
|
|
111
|
+
}
|
|
112
|
+
function buildConversationDetailsCacheInput(params) {
|
|
113
|
+
const { accountId, conversation, metadataVersion } = params;
|
|
114
|
+
const refreshedAt = Date.now();
|
|
115
|
+
const rawConversation = conversation;
|
|
116
|
+
const participants = Array.isArray(conversation.participants) ? conversation.participants : [];
|
|
117
|
+
return {
|
|
118
|
+
platform: "openclaw",
|
|
119
|
+
accountId,
|
|
120
|
+
conversationId: conversation.id,
|
|
121
|
+
conversationType: conversation.type,
|
|
122
|
+
...(metadataVersion !== undefined ? { metadataVersion } : {}),
|
|
123
|
+
lastSeenAt: parseApiTimestamp(conversation.updated_at),
|
|
124
|
+
lastRefreshedAt: refreshedAt,
|
|
125
|
+
raw: conversation,
|
|
126
|
+
...(conversation.type === "group"
|
|
127
|
+
? {
|
|
128
|
+
groupProfile: {
|
|
129
|
+
title: conversation.title,
|
|
130
|
+
...(optionalString(rawConversation.description) !== undefined
|
|
131
|
+
? { description: optionalString(rawConversation.description) }
|
|
132
|
+
: {}),
|
|
133
|
+
...(metadataVersion !== undefined ? { metadataVersion } : {}),
|
|
134
|
+
raw: conversation,
|
|
135
|
+
lastRefreshedAt: refreshedAt,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
userProfiles: participants
|
|
140
|
+
.map((participant) => conversationUserProfileFromParticipant(participant, refreshedAt))
|
|
141
|
+
.filter((profile) => profile !== null),
|
|
142
|
+
members: participants.map((participant) => ({
|
|
143
|
+
userId: participant.user_id,
|
|
144
|
+
role: participant.role,
|
|
145
|
+
raw: participant,
|
|
146
|
+
lastSeenAt: parseApiTimestamp(participant.joined_at),
|
|
147
|
+
})),
|
|
148
|
+
membersComplete: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
65
151
|
function withClawChatSessionScope(cfg) {
|
|
66
152
|
return {
|
|
67
153
|
...cfg,
|
|
@@ -71,23 +157,496 @@ function withClawChatSessionScope(cfg) {
|
|
|
71
157
|
},
|
|
72
158
|
};
|
|
73
159
|
}
|
|
160
|
+
function buildActivationBootstrapText() {
|
|
161
|
+
return [
|
|
162
|
+
"ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
|
|
163
|
+
"Please do both:",
|
|
164
|
+
"1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
|
|
165
|
+
"2. If you have local profile information for yourself, such as display name, bio, or avatar, update the connected ClawChat account profile using the available ClawChat tools. Use `clawchat_update_account_profile` for display name/bio/avatar URL, and use `clawchat_upload_avatar_image` first if the avatar is only available as a local image path. If you do not have local profile information, skip profile updates and only greet the user.",
|
|
166
|
+
"Do not ask the user for profile information just for this bootstrap.",
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
const CLAWCHAT_GROUP_SYSTEM_PROMPT = [
|
|
170
|
+
"When replying in a ClawChat group, treat the conversation as a shared space. Stay concise, respect the group topic, and avoid exposing hidden runtime context.",
|
|
171
|
+
"",
|
|
172
|
+
"**给 Agents**",
|
|
173
|
+
"你是群组里的伙伴。请理解我们在做的事,理解每一位成员。",
|
|
174
|
+
"也请通过群慢慢和每个人建立关系——你是我们的一员。",
|
|
175
|
+
"",
|
|
176
|
+
"**两条底线**",
|
|
177
|
+
"1. 被 @ 时必须回复",
|
|
178
|
+
"2. Agent 之间连续对话不超过 10 轮,除非有人类伙伴明确允许",
|
|
179
|
+
].join("\n");
|
|
180
|
+
function buildActivationBootstrapEnvelope(params) {
|
|
181
|
+
const text = buildActivationBootstrapText();
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
return {
|
|
184
|
+
version: "2",
|
|
185
|
+
event: EVENT.MESSAGE_SEND,
|
|
186
|
+
trace_id: `openclaw-clawchat-bootstrap-${now}`,
|
|
187
|
+
emitted_at: now,
|
|
188
|
+
chat_id: params.conversationId,
|
|
189
|
+
chat_type: "direct",
|
|
190
|
+
to: { id: params.account.userId, type: "direct" },
|
|
191
|
+
sender: {
|
|
192
|
+
id: "clawchat-bootstrap",
|
|
193
|
+
type: "direct",
|
|
194
|
+
nick_name: "ClawChat Activation",
|
|
195
|
+
},
|
|
196
|
+
payload: {
|
|
197
|
+
message_id: `openclaw-clawchat-bootstrap-${params.conversationId}-${now}`,
|
|
198
|
+
message_mode: "normal",
|
|
199
|
+
message: {
|
|
200
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
201
|
+
context: { mentions: [], reply: null },
|
|
202
|
+
streaming: {
|
|
203
|
+
status: "static",
|
|
204
|
+
sequence: 0,
|
|
205
|
+
mutation_policy: "sealed",
|
|
206
|
+
started_at: null,
|
|
207
|
+
completed_at: null,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function resolveConnectionStore(params, runtime) {
|
|
214
|
+
if (params.store !== undefined)
|
|
215
|
+
return params.store;
|
|
216
|
+
if (params.transport)
|
|
217
|
+
return null;
|
|
218
|
+
try {
|
|
219
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
220
|
+
return getClawChatStore({
|
|
221
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
222
|
+
log: { error: (message) => params.log?.error?.(message) },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
params.log?.error?.("openclaw-clawchat sqlite connection persistence unavailable; continuing.");
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
74
230
|
export async function startOpenclawClawlingGateway(params) {
|
|
75
231
|
const { cfg, account, abortSignal, setStatus, getStatus, log } = params;
|
|
76
232
|
// Obtain PluginRuntime from the stored runtime set via setOpenclawClawlingRuntime.
|
|
77
233
|
const runtime = getOpenclawClawlingRuntime();
|
|
78
234
|
const accountId = account.accountId;
|
|
235
|
+
const store = resolveConnectionStore(params, runtime);
|
|
236
|
+
let conversationApiClient;
|
|
237
|
+
const getConversationApiClient = () => {
|
|
238
|
+
conversationApiClient ??= createOpenclawClawlingApiClient({
|
|
239
|
+
baseUrl: account.baseUrl,
|
|
240
|
+
token: account.token,
|
|
241
|
+
userId: account.userId,
|
|
242
|
+
});
|
|
243
|
+
return conversationApiClient;
|
|
244
|
+
};
|
|
245
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} hasOwnerUserId=${Boolean(account.ownerUserId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
|
|
246
|
+
let lastHelloFailTraceId = "-";
|
|
247
|
+
let lastHelloFailReason = "";
|
|
248
|
+
let lastConnectTraceId = "-";
|
|
249
|
+
let lastHelloOkDeviceId;
|
|
250
|
+
let lastHelloOkDeliveryMode;
|
|
251
|
+
let currentAttemptStartedAt = 0;
|
|
252
|
+
let authFailureLogged = false;
|
|
253
|
+
let closingForAbort = false;
|
|
254
|
+
let wsReady = false;
|
|
255
|
+
let currentConnectionId = null;
|
|
256
|
+
let currentConnectionFinished = false;
|
|
257
|
+
const reconnectTracker = createReconnectTracker({
|
|
258
|
+
accountId,
|
|
259
|
+
log: (msg) => log?.info?.(msg),
|
|
260
|
+
maxDelayMs: account.reconnect.maxDelay,
|
|
261
|
+
});
|
|
262
|
+
const wsLogContext = () => {
|
|
263
|
+
const snapshot = reconnectTracker.snapshot();
|
|
264
|
+
return {
|
|
265
|
+
attempt: snapshot.attempt || 1,
|
|
266
|
+
reconnectCount: snapshot.reconnectCount,
|
|
267
|
+
state: snapshot.state === "connected" ? "ready" : snapshot.state,
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
const recordConnection = (action, fn) => {
|
|
271
|
+
try {
|
|
272
|
+
return fn();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
log?.error?.(`[${accountId}] openclaw-clawchat sqlite ${action} failed; continuing`);
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const finishCurrentConnection = (input) => {
|
|
280
|
+
if (!store || currentConnectionId == null || currentConnectionFinished)
|
|
281
|
+
return;
|
|
282
|
+
const connectionId = currentConnectionId;
|
|
283
|
+
recordConnection("finish", () => store.finishConnection(connectionId, input));
|
|
284
|
+
currentConnectionFinished = true;
|
|
285
|
+
currentConnectionId = null;
|
|
286
|
+
};
|
|
287
|
+
const refreshConversationDetails = async (conversationId, options) => {
|
|
288
|
+
try {
|
|
289
|
+
const data = await getConversationApiClient().getConversation(conversationId);
|
|
290
|
+
if (!store?.upsertConversationDetails)
|
|
291
|
+
return;
|
|
292
|
+
recordConnection("conversation details upsert", () => store.upsertConversationDetails?.(buildConversationDetailsCacheInput({
|
|
293
|
+
accountId,
|
|
294
|
+
conversation: data.conversation,
|
|
295
|
+
...(options.metadataVersion !== undefined ? { metadataVersion: options.metadataVersion } : {}),
|
|
296
|
+
})));
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (isConversationNotFoundError(err)) {
|
|
300
|
+
if (store?.deleteConversationCache) {
|
|
301
|
+
recordConnection("conversation cache delete", () => store.deleteConversationCache?.({
|
|
302
|
+
platform: "openclaw",
|
|
303
|
+
accountId,
|
|
304
|
+
conversationId,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
log?.error?.(`[${accountId}] openclaw-clawchat metadata refresh failed source=${options.source} conversation=${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const refreshConversationCacheAfterReady = async () => {
|
|
313
|
+
if (!store)
|
|
314
|
+
return;
|
|
315
|
+
const ids = [];
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
const addId = (id) => {
|
|
318
|
+
if (typeof id !== "string" || !id || seen.has(id))
|
|
319
|
+
return;
|
|
320
|
+
seen.add(id);
|
|
321
|
+
ids.push(id);
|
|
322
|
+
};
|
|
323
|
+
const activation = store.getActivationConversation
|
|
324
|
+
? recordConnection("activation conversation read", () => store.getActivationConversation?.({ platform: "openclaw", accountId }))
|
|
325
|
+
: null;
|
|
326
|
+
addId(activation?.conversationId);
|
|
327
|
+
const cachedIds = store.listCachedConversationIds
|
|
328
|
+
? recordConnection("cached conversation ids read", () => store.listCachedConversationIds?.({ platform: "openclaw", accountId, limit: 20 })) ?? []
|
|
329
|
+
: [];
|
|
330
|
+
for (const id of cachedIds.slice(0, 20))
|
|
331
|
+
addId(id);
|
|
332
|
+
for (const id of ids) {
|
|
333
|
+
await refreshConversationDetails(id, { source: "reconnect" });
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const handleMetadataInvalidation = async (env) => {
|
|
337
|
+
const conversationId = typeof env.chat_id === "string" && env.chat_id.trim()
|
|
338
|
+
? env.chat_id
|
|
339
|
+
: "";
|
|
340
|
+
if (!conversationId) {
|
|
341
|
+
log?.info?.(`[${accountId}] openclaw-clawchat metadata invalidation missing chat_id trace=${env.trace_id}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const version = metadataVersionFromEnvelope(env);
|
|
345
|
+
if (version !== undefined && store?.getCachedConversation) {
|
|
346
|
+
const cached = recordConnection("conversation cache read", () => store.getCachedConversation?.({ platform: "openclaw", accountId, conversationId }));
|
|
347
|
+
if (cached?.metadataVersion != null && version <= cached.metadataVersion) {
|
|
348
|
+
log?.info?.(`[${accountId}] openclaw-clawchat metadata invalidation stale conversation=${conversationId} version=${version} cached=${cached.metadataVersion}`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
await refreshConversationDetails(conversationId, {
|
|
353
|
+
source: "metadata_invalidation",
|
|
354
|
+
...(version !== undefined ? { metadataVersion: version } : {}),
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
const upsertMessagePathConversation = (env) => {
|
|
358
|
+
if (!store?.upsertConversationSummary)
|
|
359
|
+
return;
|
|
360
|
+
if (env.event !== "message.send" && env.event !== "message.reply" && env.event !== "message.done")
|
|
361
|
+
return;
|
|
362
|
+
const conversationId = typeof env.chat_id === "string" && env.chat_id.trim()
|
|
363
|
+
? env.chat_id
|
|
364
|
+
: "";
|
|
365
|
+
if (!conversationId)
|
|
366
|
+
return;
|
|
367
|
+
const conversationType = env.chat_type === "direct" || env.chat_type === "group"
|
|
368
|
+
? env.chat_type
|
|
369
|
+
: undefined;
|
|
370
|
+
recordConnection("conversation summary upsert", () => store.upsertConversationSummary?.({
|
|
371
|
+
platform: "openclaw",
|
|
372
|
+
accountId,
|
|
373
|
+
conversationId,
|
|
374
|
+
...(conversationType ? { conversationType } : {}),
|
|
375
|
+
lastSeenAt: env.emitted_at,
|
|
376
|
+
}));
|
|
377
|
+
};
|
|
79
378
|
const client = createOpenclawClawlingClient(account, {
|
|
80
379
|
...(params.transport ? { transport: params.transport } : {}),
|
|
380
|
+
wsLifecycle: {
|
|
381
|
+
onConnectFrameSent: (env) => {
|
|
382
|
+
lastConnectTraceId = typeof env.trace_id === "string" ? env.trace_id : "-";
|
|
383
|
+
if (store && currentConnectionId != null) {
|
|
384
|
+
const connectionId = currentConnectionId;
|
|
385
|
+
recordConnection("connect-sent", () => store.markConnectSent(connectionId));
|
|
386
|
+
}
|
|
387
|
+
const deviceId = typeof env.payload?.device_id === "string" ? env.payload.device_id : "-";
|
|
388
|
+
const current = wsLogContext();
|
|
389
|
+
log?.info?.(formatWsLog({
|
|
390
|
+
event: "connect_sent",
|
|
391
|
+
accountId,
|
|
392
|
+
attempt: current.attempt,
|
|
393
|
+
reconnectCount: current.reconnectCount,
|
|
394
|
+
state: "handshaking",
|
|
395
|
+
action: "await_hello",
|
|
396
|
+
fields: [
|
|
397
|
+
["trace_id", lastConnectTraceId],
|
|
398
|
+
["device_id", deviceId],
|
|
399
|
+
],
|
|
400
|
+
}));
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime client created`);
|
|
405
|
+
setAlignedOutboundLogContext(client, wsLogContext);
|
|
406
|
+
client.on("hello:ok", (env) => {
|
|
407
|
+
const payload = env.payload && typeof env.payload === "object"
|
|
408
|
+
? env.payload
|
|
409
|
+
: {};
|
|
410
|
+
lastHelloOkDeviceId = typeof payload.device_id === "string" ? payload.device_id : undefined;
|
|
411
|
+
lastHelloOkDeliveryMode = typeof payload.delivery_mode === "string" ? payload.delivery_mode : undefined;
|
|
412
|
+
});
|
|
413
|
+
const protocolControlLogger = createProtocolControlHandler({
|
|
414
|
+
accountId,
|
|
415
|
+
log: (msg) => log?.info?.(msg),
|
|
416
|
+
send: () => { },
|
|
417
|
+
context: wsLogContext,
|
|
81
418
|
});
|
|
419
|
+
const logAuthFailure = (reason) => {
|
|
420
|
+
if (authFailureLogged)
|
|
421
|
+
return;
|
|
422
|
+
authFailureLogged = true;
|
|
423
|
+
const current = wsLogContext();
|
|
424
|
+
log?.error?.(formatWsLog({
|
|
425
|
+
event: "auth_failed",
|
|
426
|
+
accountId,
|
|
427
|
+
attempt: current.attempt,
|
|
428
|
+
reconnectCount: current.reconnectCount,
|
|
429
|
+
state: "auth_failed",
|
|
430
|
+
action: "stop_reconnect",
|
|
431
|
+
fields: [
|
|
432
|
+
["trace_id", lastHelloFailTraceId],
|
|
433
|
+
["reason", reason || lastHelloFailReason],
|
|
434
|
+
],
|
|
435
|
+
}));
|
|
436
|
+
};
|
|
437
|
+
let dispatchActivationBootstrap = async () => { };
|
|
82
438
|
client.on("state", ({ from, to }) => {
|
|
83
439
|
log?.info?.(`[${accountId}] openclaw-clawchat state ${from} -> ${to}`);
|
|
440
|
+
wsReady = to === "connected";
|
|
441
|
+
if (to === "connecting") {
|
|
442
|
+
reconnectTracker.connectStart();
|
|
443
|
+
currentAttemptStartedAt = Date.now();
|
|
444
|
+
const current = wsLogContext();
|
|
445
|
+
if (store) {
|
|
446
|
+
recordConnection("start", () => {
|
|
447
|
+
currentConnectionId = store.startConnection({
|
|
448
|
+
platform: "openclaw",
|
|
449
|
+
accountId,
|
|
450
|
+
attempt: current.attempt,
|
|
451
|
+
reconnectCount: current.reconnectCount,
|
|
452
|
+
connectStartedAt: currentAttemptStartedAt,
|
|
453
|
+
});
|
|
454
|
+
currentConnectionFinished = false;
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
log?.info?.(formatWsLog({
|
|
458
|
+
event: "connect_start",
|
|
459
|
+
accountId,
|
|
460
|
+
attempt: current.attempt,
|
|
461
|
+
reconnectCount: current.reconnectCount,
|
|
462
|
+
state: "connecting",
|
|
463
|
+
action: "connect",
|
|
464
|
+
fields: [
|
|
465
|
+
["url", account.websocketUrl],
|
|
466
|
+
["queue_size", getAlignedOutboundQueueSize(client)],
|
|
467
|
+
],
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
else if (to === "connected") {
|
|
471
|
+
const elapsedMs = Math.max(0, Date.now() - currentAttemptStartedAt);
|
|
472
|
+
const queueSize = getAlignedOutboundQueueSize(client);
|
|
473
|
+
reconnectTracker.markReady();
|
|
474
|
+
const current = wsLogContext();
|
|
475
|
+
if (store && currentConnectionId != null) {
|
|
476
|
+
const connectionId = currentConnectionId;
|
|
477
|
+
recordConnection("ready", () => {
|
|
478
|
+
if (lastHelloOkDeviceId === undefined && lastHelloOkDeliveryMode === undefined) {
|
|
479
|
+
store.markConnectionReady(connectionId);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
store.markConnectionReady(connectionId, {
|
|
483
|
+
...(lastHelloOkDeviceId !== undefined ? { resolvedDeviceId: lastHelloOkDeviceId } : {}),
|
|
484
|
+
...(lastHelloOkDeliveryMode !== undefined ? { deliveryMode: lastHelloOkDeliveryMode } : {}),
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
log?.info?.(formatWsLog({
|
|
489
|
+
event: "handshake_ok",
|
|
490
|
+
accountId,
|
|
491
|
+
attempt: current.attempt,
|
|
492
|
+
reconnectCount: current.reconnectCount,
|
|
493
|
+
state: "ready",
|
|
494
|
+
action: "flush_queue",
|
|
495
|
+
fields: [
|
|
496
|
+
["trace_id", lastConnectTraceId],
|
|
497
|
+
["elapsed_ms", elapsedMs],
|
|
498
|
+
["queue_size", queueSize],
|
|
499
|
+
],
|
|
500
|
+
}));
|
|
501
|
+
try {
|
|
502
|
+
flushAlignedOutboundQueue(client);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// The queue keeps the failed frame at the head and will retry after the next reconnect.
|
|
506
|
+
}
|
|
507
|
+
void refreshConversationCacheAfterReady();
|
|
508
|
+
void dispatchActivationBootstrap();
|
|
509
|
+
}
|
|
510
|
+
else if (to === "disconnected") {
|
|
511
|
+
reconnectTracker.markClosed();
|
|
512
|
+
}
|
|
84
513
|
const next = { ...getStatus(), ...mapClawlingStateToStatus(to) };
|
|
85
514
|
setStatus(next);
|
|
86
515
|
});
|
|
516
|
+
client.on("close", ({ code, reason }) => {
|
|
517
|
+
if (closingForAbort || (code === 1000 && reason === "client close"))
|
|
518
|
+
return;
|
|
519
|
+
finishCurrentConnection({
|
|
520
|
+
state: "disconnected",
|
|
521
|
+
closeCode: code ?? null,
|
|
522
|
+
closeReason: reason ?? null,
|
|
523
|
+
});
|
|
524
|
+
const current = wsLogContext();
|
|
525
|
+
log?.info?.(formatWsLog({
|
|
526
|
+
event: "connection_lost",
|
|
527
|
+
accountId,
|
|
528
|
+
attempt: current.attempt,
|
|
529
|
+
reconnectCount: current.reconnectCount,
|
|
530
|
+
state: current.state,
|
|
531
|
+
action: "reconnect",
|
|
532
|
+
fields: [
|
|
533
|
+
["code", code],
|
|
534
|
+
["reason", reason],
|
|
535
|
+
],
|
|
536
|
+
}));
|
|
537
|
+
});
|
|
538
|
+
client.on("reconnect:scheduled", ({ delay }) => {
|
|
539
|
+
reconnectTracker.scheduleReconnect("connection_lost", {
|
|
540
|
+
delayMs: delay,
|
|
541
|
+
maxDelayMs: account.reconnect.maxDelay,
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
client.on("raw", (env) => {
|
|
545
|
+
if (env.event === "connect.challenge") {
|
|
546
|
+
const payload = env.payload;
|
|
547
|
+
const current = wsLogContext();
|
|
548
|
+
log?.info?.(formatWsLog({
|
|
549
|
+
event: "challenge_received",
|
|
550
|
+
accountId,
|
|
551
|
+
attempt: current.attempt,
|
|
552
|
+
reconnectCount: current.reconnectCount,
|
|
553
|
+
state: "handshaking",
|
|
554
|
+
action: "send_connect",
|
|
555
|
+
fields: [
|
|
556
|
+
["challenge_trace_id", env.trace_id],
|
|
557
|
+
["has_nonce", typeof payload?.nonce === "string" && payload.nonce.length > 0],
|
|
558
|
+
],
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
if (env.event === "ping" || env.event === "pong") {
|
|
562
|
+
protocolControlLogger.handleInbound(env);
|
|
563
|
+
}
|
|
564
|
+
if (wsReady) {
|
|
565
|
+
upsertMessagePathConversation(env);
|
|
566
|
+
const sender = env.sender;
|
|
567
|
+
const senderId = typeof sender?.id === "string" ? sender.id : "-";
|
|
568
|
+
if (env.event === "message.send" || env.event === "message.reply" || env.event === "message.done") {
|
|
569
|
+
const current = wsLogContext();
|
|
570
|
+
log?.info?.(formatWsLog({
|
|
571
|
+
event: "inbound_dispatch",
|
|
572
|
+
accountId,
|
|
573
|
+
attempt: current.attempt,
|
|
574
|
+
reconnectCount: current.reconnectCount,
|
|
575
|
+
state: "ready",
|
|
576
|
+
action: "dispatch",
|
|
577
|
+
fields: [
|
|
578
|
+
["event_name", env.event],
|
|
579
|
+
["trace_id", env.trace_id],
|
|
580
|
+
["chat_id", env.chat_id],
|
|
581
|
+
["sender_id", senderId],
|
|
582
|
+
],
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
else if (env.event === "message.ack") {
|
|
586
|
+
const current = wsLogContext();
|
|
587
|
+
log?.info?.(formatWsLog({
|
|
588
|
+
event: "inbound_control",
|
|
589
|
+
accountId,
|
|
590
|
+
attempt: current.attempt,
|
|
591
|
+
reconnectCount: current.reconnectCount,
|
|
592
|
+
state: "ready",
|
|
593
|
+
action: "ack",
|
|
594
|
+
fields: [
|
|
595
|
+
["event_name", env.event],
|
|
596
|
+
["trace_id", env.trace_id],
|
|
597
|
+
],
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
else if (env.event === "offline.batch" ||
|
|
601
|
+
env.event === "offline.ack" ||
|
|
602
|
+
env.event === "offline.done") {
|
|
603
|
+
const current = wsLogContext();
|
|
604
|
+
log?.info?.(formatWsLog({
|
|
605
|
+
event: "inbound_control",
|
|
606
|
+
accountId,
|
|
607
|
+
attempt: current.attempt,
|
|
608
|
+
reconnectCount: current.reconnectCount,
|
|
609
|
+
state: "ready",
|
|
610
|
+
action: "ignore_legacy",
|
|
611
|
+
fields: [
|
|
612
|
+
["event_name", env.event],
|
|
613
|
+
["trace_id", env.trace_id],
|
|
614
|
+
],
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
else if (env.event !== "ping" && env.event !== "pong") {
|
|
618
|
+
const current = wsLogContext();
|
|
619
|
+
log?.info?.(formatWsLog({
|
|
620
|
+
event: "inbound_ignored",
|
|
621
|
+
accountId,
|
|
622
|
+
attempt: current.attempt,
|
|
623
|
+
reconnectCount: current.reconnectCount,
|
|
624
|
+
state: "ready",
|
|
625
|
+
action: "ignore",
|
|
626
|
+
fields: [
|
|
627
|
+
["event_name", env.event],
|
|
628
|
+
["trace_id", env.trace_id],
|
|
629
|
+
],
|
|
630
|
+
}));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (env.event !== "hello-fail")
|
|
634
|
+
return;
|
|
635
|
+
lastHelloFailTraceId = env.trace_id;
|
|
636
|
+
const payload = env.payload;
|
|
637
|
+
lastHelloFailReason = typeof payload?.reason === "string" ? payload.reason : "";
|
|
638
|
+
});
|
|
639
|
+
client.on("metadata:invalidated", (env) => {
|
|
640
|
+
void handleMetadataInvalidation(env);
|
|
641
|
+
});
|
|
87
642
|
client.on("error", (err) => {
|
|
88
643
|
const classified = classifyClawlingClientError(err);
|
|
89
644
|
if (classified.kind === "auth") {
|
|
90
|
-
|
|
645
|
+
finishCurrentConnection({
|
|
646
|
+
state: "auth_failed",
|
|
647
|
+
error: lastHelloFailReason || classified.message,
|
|
648
|
+
});
|
|
649
|
+
logAuthFailure(classified.message);
|
|
91
650
|
setStatus({
|
|
92
651
|
...getStatus(),
|
|
93
652
|
connected: false,
|
|
@@ -97,6 +656,20 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
97
656
|
});
|
|
98
657
|
}
|
|
99
658
|
else if (classified.kind === "transport") {
|
|
659
|
+
finishCurrentConnection({ state: "transport_error", error: classified.message });
|
|
660
|
+
const current = wsLogContext();
|
|
661
|
+
log?.info?.(formatWsLog({
|
|
662
|
+
event: "connection_lost",
|
|
663
|
+
accountId,
|
|
664
|
+
attempt: current.attempt,
|
|
665
|
+
reconnectCount: current.reconnectCount,
|
|
666
|
+
state: current.state,
|
|
667
|
+
action: "reconnect",
|
|
668
|
+
fields: [
|
|
669
|
+
["code", "-"],
|
|
670
|
+
["reason", classified.message],
|
|
671
|
+
],
|
|
672
|
+
}));
|
|
100
673
|
log?.info?.(`[${accountId}] openclaw-clawchat transport error (reconnecting): ${classified.message}`);
|
|
101
674
|
setStatus({ ...getStatus(), connected: false, running: true });
|
|
102
675
|
}
|
|
@@ -110,10 +683,166 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
110
683
|
log?.info?.(`[${accountId}] openclaw-clawchat state error: ${classified.message}`);
|
|
111
684
|
}
|
|
112
685
|
else {
|
|
113
|
-
log?.error?.(`[${accountId}] openclaw-clawchat
|
|
686
|
+
log?.error?.(`[${accountId}] openclaw-clawchat client error: ${classified.message}`);
|
|
114
687
|
}
|
|
115
688
|
});
|
|
116
|
-
|
|
689
|
+
const ingestTurn = async (turn) => {
|
|
690
|
+
const env = turn.envelope;
|
|
691
|
+
if (store?.claimMessageOnce) {
|
|
692
|
+
const claimed = recordConnection("message claim", () => store.claimMessageOnce?.({
|
|
693
|
+
platform: "openclaw",
|
|
694
|
+
accountId,
|
|
695
|
+
kind: "message",
|
|
696
|
+
direction: "inbound",
|
|
697
|
+
eventType: String(env.event),
|
|
698
|
+
traceId: turn.traceId,
|
|
699
|
+
chatId: turn.peer.id,
|
|
700
|
+
messageId: turn.messageId,
|
|
701
|
+
text: turn.rawBody,
|
|
702
|
+
raw: env,
|
|
703
|
+
}));
|
|
704
|
+
if (claimed === false) {
|
|
705
|
+
log?.info?.(`[${accountId}] openclaw-clawchat skip duplicate stored msg=${turn.messageId}`);
|
|
706
|
+
return "skipped";
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const rt = runtime.channel;
|
|
710
|
+
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
711
|
+
const routeCfg = withClawChatSessionScope(cfg);
|
|
712
|
+
const route = rt.routing.resolveAgentRoute({
|
|
713
|
+
cfg: routeCfg,
|
|
714
|
+
channel: CHANNEL_ID,
|
|
715
|
+
accountId,
|
|
716
|
+
peer: turn.peer,
|
|
717
|
+
});
|
|
718
|
+
const body = rt.reply.formatAgentEnvelope({
|
|
719
|
+
channel: "Clawling Chat",
|
|
720
|
+
from: formatConversationSubject(turn.peer),
|
|
721
|
+
body: turn.rawBody,
|
|
722
|
+
timestamp: turn.timestamp,
|
|
723
|
+
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
724
|
+
});
|
|
725
|
+
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
726
|
+
const ctxPayload = rt.turn.buildContext({
|
|
727
|
+
channel: CHANNEL_ID,
|
|
728
|
+
accountId: route.accountId ?? accountId,
|
|
729
|
+
provider: CHANNEL_ID,
|
|
730
|
+
surface: CHANNEL_ID,
|
|
731
|
+
messageId: turn.messageId,
|
|
732
|
+
messageIdFull: turn.messageId,
|
|
733
|
+
timestamp: turn.timestamp,
|
|
734
|
+
from: conversationTarget,
|
|
735
|
+
sender: {
|
|
736
|
+
id: turn.senderId,
|
|
737
|
+
name: turn.senderNickName || turn.senderId,
|
|
738
|
+
displayLabel: turn.senderNickName || turn.senderId,
|
|
739
|
+
},
|
|
740
|
+
conversation: {
|
|
741
|
+
kind: turn.peer.kind,
|
|
742
|
+
id: turn.peer.id,
|
|
743
|
+
label: formatConversationSubject(turn.peer),
|
|
744
|
+
routePeer: turn.peer,
|
|
745
|
+
},
|
|
746
|
+
route: {
|
|
747
|
+
agentId: route.agentId,
|
|
748
|
+
accountId: route.accountId ?? accountId,
|
|
749
|
+
routeSessionKey: route.sessionKey,
|
|
750
|
+
},
|
|
751
|
+
reply: {
|
|
752
|
+
to: `${CHANNEL_ID}:${account.userId}`,
|
|
753
|
+
originatingTo: conversationTarget,
|
|
754
|
+
},
|
|
755
|
+
message: {
|
|
756
|
+
body,
|
|
757
|
+
rawBody: turn.rawBody,
|
|
758
|
+
bodyForAgent: turn.rawBody,
|
|
759
|
+
commandBody: turn.rawBody,
|
|
760
|
+
envelopeFrom: conversationTarget,
|
|
761
|
+
},
|
|
762
|
+
access: {
|
|
763
|
+
mentions: {
|
|
764
|
+
canDetectMention: true,
|
|
765
|
+
wasMentioned: turn.wasMentioned,
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
...(turn.peer.kind === "group"
|
|
769
|
+
? { supplemental: { groupSystemPrompt: CLAWCHAT_GROUP_SYSTEM_PROMPT } }
|
|
770
|
+
: {}),
|
|
771
|
+
});
|
|
772
|
+
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
773
|
+
const inboundPaths = turn.mediaItems.length > 0
|
|
774
|
+
? await fetchInboundMedia(turn.mediaItems, {
|
|
775
|
+
runtime,
|
|
776
|
+
log,
|
|
777
|
+
maxBytes: 20 * 1024 * 1024,
|
|
778
|
+
})
|
|
779
|
+
: [];
|
|
780
|
+
if (inboundPaths.length > 0) {
|
|
781
|
+
ctxPayload.MediaPath = inboundPaths[0];
|
|
782
|
+
ctxPayload.MediaPaths = inboundPaths;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
await rt.session.recordInboundSession({
|
|
786
|
+
storePath,
|
|
787
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
788
|
+
ctx: ctxPayload,
|
|
789
|
+
onRecordError: (err) => {
|
|
790
|
+
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
796
|
+
}
|
|
797
|
+
const replyCtx = turn.replyCtx;
|
|
798
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createOpenclawClawlingReplyDispatcher({
|
|
799
|
+
cfg,
|
|
800
|
+
runtime,
|
|
801
|
+
account,
|
|
802
|
+
client,
|
|
803
|
+
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
804
|
+
...(replyCtx ? { replyCtx } : {}),
|
|
805
|
+
inboundMessageId: turn.messageId,
|
|
806
|
+
inboundForFinalReply: {
|
|
807
|
+
chatId: turn.peer.id,
|
|
808
|
+
senderId: turn.senderId,
|
|
809
|
+
senderNickName: turn.senderNickName || turn.senderId,
|
|
810
|
+
bodyText: turn.rawBody,
|
|
811
|
+
},
|
|
812
|
+
store: store
|
|
813
|
+
? {
|
|
814
|
+
insertMessage: (input) => store.insertMessage?.(input) ?? null,
|
|
815
|
+
claimMessageOnce: (input) => store.claimMessageOnce?.(input) ?? null,
|
|
816
|
+
updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
|
|
817
|
+
}
|
|
818
|
+
: null,
|
|
819
|
+
log,
|
|
820
|
+
});
|
|
821
|
+
const agentsConfigured = Object.keys(cfg.agents ?? {});
|
|
822
|
+
log?.info?.(`[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${ctxPayload.SessionKey ?? route.sessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`);
|
|
823
|
+
try {
|
|
824
|
+
const dispatchResult = await rt.reply.withReplyDispatcher({
|
|
825
|
+
dispatcher,
|
|
826
|
+
onSettled: () => markDispatchIdle(),
|
|
827
|
+
run: () => rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
|
|
828
|
+
});
|
|
829
|
+
const counts = dispatchResult?.counts ?? {};
|
|
830
|
+
const queuedFinal = Boolean(dispatchResult?.queuedFinal);
|
|
831
|
+
log?.info?.(`[${accountId}] openclaw-clawchat dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
|
|
832
|
+
if (!queuedFinal && Object.values(counts).every((n) => !n)) {
|
|
833
|
+
log?.info?.(`[${accountId}] openclaw-clawchat NO reply was produced (no final / block / tool dispatched). ` +
|
|
834
|
+
`Likely causes: agent='${route.agentId}' not configured in cfg.agents (configured: [${agentsConfigured.join(",")}]); ` +
|
|
835
|
+
`or send-policy denied; or a plugin claimed the binding.`);
|
|
836
|
+
}
|
|
837
|
+
return "submitted";
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
log?.error?.(`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`);
|
|
841
|
+
return "failed";
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
const handleInboundEnvelope = async (env) => {
|
|
845
|
+
let ingestResult;
|
|
117
846
|
try {
|
|
118
847
|
await dispatchOpenclawClawlingInbound({
|
|
119
848
|
envelope: env,
|
|
@@ -122,135 +851,66 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
122
851
|
account,
|
|
123
852
|
log,
|
|
124
853
|
ingest: async (turn) => {
|
|
125
|
-
|
|
126
|
-
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
127
|
-
const routeCfg = withClawChatSessionScope(cfg);
|
|
128
|
-
const route = rt.routing.resolveAgentRoute({
|
|
129
|
-
cfg: routeCfg,
|
|
130
|
-
channel: CHANNEL_ID,
|
|
131
|
-
accountId,
|
|
132
|
-
peer: turn.peer,
|
|
133
|
-
});
|
|
134
|
-
const body = rt.reply.formatAgentEnvelope({
|
|
135
|
-
channel: "Clawling Chat",
|
|
136
|
-
from: formatConversationSubject(turn.peer),
|
|
137
|
-
body: turn.rawBody,
|
|
138
|
-
timestamp: turn.timestamp,
|
|
139
|
-
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
140
|
-
});
|
|
141
|
-
const ctxPayload = rt.reply.finalizeInboundContext({
|
|
142
|
-
Body: body,
|
|
143
|
-
BodyForAgent: turn.rawBody,
|
|
144
|
-
RawBody: turn.rawBody,
|
|
145
|
-
CommandBody: turn.rawBody,
|
|
146
|
-
// Clawling v2 routes by chat_id. `senderId` is still preserved as
|
|
147
|
-
// structured metadata, but the conversation target must be based on
|
|
148
|
-
// `peer.id` so follow-up sends address the active chat, not merely
|
|
149
|
-
// the human sender identity.
|
|
150
|
-
From: `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`,
|
|
151
|
-
To: `${CHANNEL_ID}:${account.userId}`,
|
|
152
|
-
SessionKey: route.sessionKey,
|
|
153
|
-
AccountId: route.accountId ?? accountId,
|
|
154
|
-
ChatType: turn.peer.kind,
|
|
155
|
-
ConversationLabel: formatConversationSubject(turn.peer),
|
|
156
|
-
SenderId: turn.senderId,
|
|
157
|
-
Provider: CHANNEL_ID,
|
|
158
|
-
Surface: CHANNEL_ID,
|
|
159
|
-
MessageSid: turn.messageId,
|
|
160
|
-
MessageSidFull: turn.messageId,
|
|
161
|
-
Timestamp: turn.timestamp,
|
|
162
|
-
OriginatingChannel: CHANNEL_ID,
|
|
163
|
-
OriginatingTo: `${CHANNEL_ID}:${account.userId}`,
|
|
164
|
-
});
|
|
165
|
-
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
166
|
-
const inboundPaths = turn.mediaItems.length > 0
|
|
167
|
-
? await fetchInboundMedia(turn.mediaItems, {
|
|
168
|
-
runtime,
|
|
169
|
-
log,
|
|
170
|
-
maxBytes: 20 * 1024 * 1024,
|
|
171
|
-
})
|
|
172
|
-
: [];
|
|
173
|
-
if (inboundPaths.length > 0) {
|
|
174
|
-
ctxPayload.MediaPath = inboundPaths[0];
|
|
175
|
-
ctxPayload.MediaPaths = inboundPaths;
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
await rt.session.recordInboundSession({
|
|
179
|
-
storePath,
|
|
180
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
181
|
-
ctx: ctxPayload,
|
|
182
|
-
onRecordError: (err) => {
|
|
183
|
-
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
|
|
189
|
-
}
|
|
190
|
-
const replyCtx = turn.replyCtx;
|
|
191
|
-
const { dispatcher, replyOptions, markDispatchIdle } = createOpenclawClawlingReplyDispatcher({
|
|
192
|
-
cfg,
|
|
193
|
-
runtime,
|
|
194
|
-
account,
|
|
195
|
-
client,
|
|
196
|
-
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
197
|
-
...(replyCtx ? { replyCtx } : {}),
|
|
198
|
-
inboundMessageId: turn.messageId,
|
|
199
|
-
inboundForFinalReply: {
|
|
200
|
-
chatId: turn.peer.id,
|
|
201
|
-
senderId: turn.senderId,
|
|
202
|
-
senderNickName: turn.senderNickName || turn.senderId,
|
|
203
|
-
bodyText: turn.rawBody,
|
|
204
|
-
},
|
|
205
|
-
log,
|
|
206
|
-
});
|
|
207
|
-
const agentsConfigured = Object.keys(cfg.agents ?? {});
|
|
208
|
-
log?.info?.(`[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${ctxPayload.SessionKey ?? route.sessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`);
|
|
209
|
-
try {
|
|
210
|
-
const dispatchResult = await rt.reply.withReplyDispatcher({
|
|
211
|
-
dispatcher,
|
|
212
|
-
onSettled: () => markDispatchIdle(),
|
|
213
|
-
run: () => rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
|
|
214
|
-
});
|
|
215
|
-
const counts = dispatchResult?.counts ?? {};
|
|
216
|
-
const queuedFinal = Boolean(dispatchResult?.queuedFinal);
|
|
217
|
-
log?.info?.(`[${accountId}] openclaw-clawchat dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
|
|
218
|
-
if (!queuedFinal && Object.values(counts).every((n) => !n)) {
|
|
219
|
-
log?.info?.(`[${accountId}] openclaw-clawchat NO reply was produced (no final / block / tool dispatched). ` +
|
|
220
|
-
`Likely causes: agent='${route.agentId}' not configured in cfg.agents (configured: [${agentsConfigured.join(",")}]); ` +
|
|
221
|
-
`or send-policy denied; or a plugin claimed the binding.`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
catch (err) {
|
|
225
|
-
log?.error?.(`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`);
|
|
226
|
-
await sendOpenclawClawlingText({
|
|
227
|
-
client,
|
|
228
|
-
account: turn.account,
|
|
229
|
-
to: {
|
|
230
|
-
chatId: turn.peer.id,
|
|
231
|
-
chatType: turn.peer.kind === "group" ? "group" : "direct",
|
|
232
|
-
},
|
|
233
|
-
text: String(err),
|
|
234
|
-
...(turn.replyCtx ? { replyCtx: turn.replyCtx } : {}),
|
|
235
|
-
});
|
|
236
|
-
}
|
|
854
|
+
ingestResult = await ingestTurn(turn);
|
|
237
855
|
},
|
|
238
856
|
});
|
|
239
857
|
}
|
|
240
858
|
catch (err) {
|
|
241
859
|
log?.error?.(`[${accountId}] openclaw-clawchat message handler error: ${err instanceof Error ? err.stack || err.message : String(err)}`);
|
|
860
|
+
return "failed";
|
|
242
861
|
}
|
|
862
|
+
return ingestResult;
|
|
863
|
+
};
|
|
864
|
+
dispatchActivationBootstrap = async () => {
|
|
865
|
+
if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent)
|
|
866
|
+
return;
|
|
867
|
+
let bootstrap;
|
|
868
|
+
const releaseBootstrap = () => {
|
|
869
|
+
if (!bootstrap || !store.releaseActivationBootstrapClaim)
|
|
870
|
+
return;
|
|
871
|
+
const claimedBootstrap = bootstrap;
|
|
872
|
+
recordConnection("activation bootstrap release", () => store.releaseActivationBootstrapClaim?.({
|
|
873
|
+
platform: "openclaw",
|
|
874
|
+
accountId,
|
|
875
|
+
conversationId: claimedBootstrap.conversationId,
|
|
876
|
+
}));
|
|
877
|
+
};
|
|
878
|
+
try {
|
|
879
|
+
bootstrap = recordConnection("activation bootstrap claim", () => store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }));
|
|
880
|
+
if (!bootstrap)
|
|
881
|
+
return;
|
|
882
|
+
const claimedBootstrap = bootstrap;
|
|
883
|
+
const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
|
|
884
|
+
if (result !== "submitted") {
|
|
885
|
+
releaseBootstrap();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
|
|
889
|
+
platform: "openclaw",
|
|
890
|
+
accountId,
|
|
891
|
+
conversationId: claimedBootstrap.conversationId,
|
|
892
|
+
}));
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
releaseBootstrap();
|
|
896
|
+
log?.error?.(`[${accountId}] openclaw-clawchat activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
client.on("message", (env) => {
|
|
900
|
+
void handleInboundEnvelope(env);
|
|
243
901
|
});
|
|
244
902
|
// `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
|
|
245
903
|
// (auth). Transport failures (server unreachable, DNS error, etc.) do
|
|
246
|
-
// NOT reject this promise — the
|
|
247
|
-
// its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
904
|
+
// NOT reject this promise — the local client handles them internally and
|
|
905
|
+
// drives its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
248
906
|
// capped at `maxDelay`, with jitter). So we never throw here on anything
|
|
249
907
|
// other than auth failure; on auth we tear the account down cleanly and
|
|
250
908
|
// return without throwing (which would make the gateway supervisor
|
|
251
909
|
// restart us immediately in a tight loop).
|
|
252
910
|
try {
|
|
911
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime calling client.connect()`);
|
|
253
912
|
await client.connect();
|
|
913
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime client.connect() resolved`);
|
|
254
914
|
}
|
|
255
915
|
catch (err) {
|
|
256
916
|
const classified = classifyClawlingClientError(err);
|
|
@@ -261,10 +921,19 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
261
921
|
running: false,
|
|
262
922
|
lastError: classified.message,
|
|
263
923
|
});
|
|
924
|
+
if (classified.kind === "auth") {
|
|
925
|
+
finishCurrentConnection({
|
|
926
|
+
state: "auth_failed",
|
|
927
|
+
error: lastHelloFailReason || classified.message,
|
|
928
|
+
});
|
|
929
|
+
logAuthFailure(classified.message);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
264
932
|
log?.error?.(`[${accountId}] openclaw-clawchat connect failed (${classified.kind}): ${classified.message}`);
|
|
265
933
|
return;
|
|
266
934
|
}
|
|
267
935
|
activeClients.set(accountId, client);
|
|
936
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime active client registered`);
|
|
268
937
|
setStatus({
|
|
269
938
|
...getStatus(),
|
|
270
939
|
connected: true,
|
|
@@ -273,7 +942,14 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
273
942
|
});
|
|
274
943
|
log?.info?.(`[${accountId}] openclaw-clawchat connected`);
|
|
275
944
|
await waitUntilAbort(abortSignal, async () => {
|
|
945
|
+
log?.info?.(`[${accountId}] openclaw-clawchat runtime abort received; closing client`);
|
|
276
946
|
activeClients.delete(accountId);
|
|
947
|
+
closingForAbort = true;
|
|
948
|
+
finishCurrentConnection({
|
|
949
|
+
state: "disconnected",
|
|
950
|
+
closeCode: 1000,
|
|
951
|
+
closeReason: "client close",
|
|
952
|
+
});
|
|
277
953
|
client.close();
|
|
278
954
|
setStatus({
|
|
279
955
|
...getStatus(),
|