botschat 0.1.19 → 0.1.20
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/migrations/0013_agents_table.sql +29 -0
- package/migrations/0014_agent_sessions.sql +19 -0
- package/migrations/0015_message_traces.sql +27 -0
- package/migrations/0016_multi_agent_channels_messages.sql +9 -0
- package/migrations/0017_rename_cron_job_id.sql +2 -0
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +382 -186
- package/packages/api/src/index.ts +67 -50
- package/packages/api/src/protocol-v2.ts +154 -0
- package/packages/api/src/routes/agents-v2.ts +192 -0
- package/packages/api/src/routes/agents.ts +3 -3
- package/packages/api/src/routes/auth.ts +0 -1
- package/packages/api/src/routes/channels.ts +11 -11
- package/packages/api/src/routes/history-v2.ts +221 -0
- package/packages/api/src/routes/migrate-v2.ts +110 -0
- package/packages/api/src/routes/sessions.ts +5 -5
- package/packages/api/src/routes/tasks.ts +33 -33
- package/packages/plugin/dist/src/channel.d.ts +10 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +10 -51
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/dist/src/types.d.ts +13 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -1
- package/packages/plugin/package.json +2 -18
- package/packages/web/dist/assets/index-BARPtt0v.css +1 -0
- package/packages/web/dist/assets/index-Bf-XL3te.js +2 -0
- package/packages/web/dist/assets/{index-C_GamcQc.js → index-CYQMu_-c.js} +1 -1
- package/packages/web/dist/assets/index-CYlvfpX9.js +1519 -0
- package/packages/web/dist/assets/index-CxcpA4Qo.js +1 -0
- package/packages/web/dist/assets/{index-MyoWvQAH.js → index-DYCO-ry1.js} +1 -1
- package/packages/web/dist/assets/index-QebPVqwj.js +2 -0
- package/packages/web/dist/assets/{index.esm-BpQAwtdR.js → index.esm-CvOpngZM.js} +1 -1
- package/packages/web/dist/assets/{web-cnzjgNfD.js → web-1cdhq2RW.js} +1 -1
- package/packages/web/dist/assets/{web-BbTzVNLt.js → web-D3LMODYp.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +56 -9
- package/packages/web/src/api.ts +61 -5
- package/packages/web/src/components/AgentSettings.tsx +328 -0
- package/packages/web/src/components/ChatWindow.tsx +9 -9
- package/packages/web/src/components/CronDetail.tsx +1 -1
- package/packages/web/src/components/LoginPage.tsx +1 -59
- package/packages/web/src/components/MessageContent.tsx +2 -17
- package/packages/web/src/components/SessionTabs.tsx +1 -1
- package/packages/web/src/components/Sidebar.tsx +3 -1
- package/packages/web/src/hooks/useIMEComposition.ts +9 -14
- package/packages/web/src/store.ts +39 -7
- package/packages/web/src/ws.ts +1 -0
- package/scripts/dev.sh +53 -0
- package/scripts/mock-openclaw-v2.mjs +486 -0
- package/packages/api/src/routes/demo.ts +0 -156
- package/packages/web/dist/assets/index-BtPyCBCl.css +0 -1
- package/packages/web/dist/assets/index-BtpsFe4Z.js +0 -2
- package/packages/web/dist/assets/index-CQbIYr6_.js +0 -2
- package/packages/web/dist/assets/index-LiBjPMg2.js +0 -1
- package/packages/web/dist/assets/index-STIPTMK8.js +0 -1516
- package/packages/web/src/components/ImageLightbox.tsx +0 -96
|
@@ -4,7 +4,6 @@ import { getFcmAccessToken, sendPushNotification } from "../utils/fcm.js";
|
|
|
4
4
|
import { sendApnsNotification, type ApnsConfig } from "../utils/apns.js";
|
|
5
5
|
import { generateId as generateIdUtil } from "../utils/id.js";
|
|
6
6
|
import { randomUUID } from "../utils/uuid.js";
|
|
7
|
-
import { isDemoUserId } from "../routes/demo.js";
|
|
8
7
|
|
|
9
8
|
/** Presence info stored in browser WebSocket attachments (survives DO hibernation). */
|
|
10
9
|
interface BrowserAttachment {
|
|
@@ -30,7 +29,8 @@ const DC_GRACE_MS = 30_000; // 30 s after WebSocket disconnect
|
|
|
30
29
|
* - Use WebSocket Hibernation API so idle users cost zero compute
|
|
31
30
|
*
|
|
32
31
|
* Connection tagging (via serializeAttachment / deserializeAttachment):
|
|
33
|
-
* - "
|
|
32
|
+
* - "agent:<agentId>" = WebSocket from an agent plugin (v2, per-agent)
|
|
33
|
+
* - "openclaw" = legacy WebSocket from the OpenClaw plugin (backward compat)
|
|
34
34
|
* - "browser:<sessionId>" = a browser client WebSocket
|
|
35
35
|
*/
|
|
36
36
|
export class ConnectionDO implements DurableObject {
|
|
@@ -54,6 +54,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
54
54
|
/** Timestamp of last accepted OpenClaw WebSocket (in-memory, no storage write). */
|
|
55
55
|
private lastOpenClawAcceptedAt = 0;
|
|
56
56
|
|
|
57
|
+
/** Pending agent.request delegations — maps requestId to originator info. */
|
|
58
|
+
private pendingRequests = new Map<string, { fromAgentId: string; sessionKey: string; depth: number; createdAt: number }>();
|
|
59
|
+
|
|
57
60
|
constructor(state: DurableObjectState, env: Env) {
|
|
58
61
|
this.state = state;
|
|
59
62
|
this.env = env;
|
|
@@ -89,7 +92,8 @@ export class ConnectionDO implements DurableObject {
|
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
const preVerified = url.searchParams.get("verified") === "1";
|
|
92
|
-
|
|
95
|
+
const agentId = url.searchParams.get("agentId");
|
|
96
|
+
return this.handleOpenClawConnect(request, preVerified, agentId);
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// Route: /client/:sessionId — Browser client connects here
|
|
@@ -105,9 +109,8 @@ export class ConnectionDO implements DurableObject {
|
|
|
105
109
|
// Route: /models — Available models (REST)
|
|
106
110
|
if (url.pathname === "/models") {
|
|
107
111
|
await this.ensureCachedModels();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return Response.json({ models });
|
|
112
|
+
console.log(`[DO] GET /models — returning ${this.cachedModels.length} models`);
|
|
113
|
+
return Response.json({ models: this.cachedModels });
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
// Route: /scan-data — Cached OpenClaw scan data (schedule/instructions/model)
|
|
@@ -143,7 +146,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
143
146
|
return;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
if (tag === "openclaw") {
|
|
149
|
+
if (tag === "openclaw" || tag?.startsWith("agent:")) {
|
|
147
150
|
await this.handleOpenClawMessage(ws, parsed);
|
|
148
151
|
} else if (tag?.startsWith("browser:")) {
|
|
149
152
|
await this.handleBrowserMessage(ws, parsed);
|
|
@@ -164,8 +167,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
164
167
|
/** Called when a WebSocket is closed. */
|
|
165
168
|
async webSocketClose(ws: WebSocket, code: number, _reason: string, _wasClean: boolean): Promise<void> {
|
|
166
169
|
const tag = this.getTag(ws);
|
|
167
|
-
if (tag === "openclaw") {
|
|
168
|
-
// OpenClaw disconnected — notify all browser clients
|
|
170
|
+
if (tag === "openclaw" || tag?.startsWith("agent:")) {
|
|
169
171
|
this.broadcastToBrowsers(
|
|
170
172
|
JSON.stringify({ type: "openclaw.disconnected" }),
|
|
171
173
|
);
|
|
@@ -188,7 +190,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
188
190
|
|
|
189
191
|
// ---- Connection handlers ----
|
|
190
192
|
|
|
191
|
-
private handleOpenClawConnect(request: Request, preVerified = false): Response {
|
|
193
|
+
private handleOpenClawConnect(request: Request, preVerified = false, agentId?: string | null): Response {
|
|
192
194
|
if (request.headers.get("Upgrade") !== "websocket") {
|
|
193
195
|
return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
194
196
|
}
|
|
@@ -204,12 +206,14 @@ export class ConnectionDO implements DurableObject {
|
|
|
204
206
|
}
|
|
205
207
|
this.lastOpenClawAcceptedAt = now;
|
|
206
208
|
|
|
207
|
-
|
|
209
|
+
const wsTag = agentId ? "agent:" + agentId : "openclaw";
|
|
210
|
+
|
|
211
|
+
// Safety valve: if stale sockets with the same tag accumulated (e.g. from
|
|
208
212
|
// rapid reconnects that authenticated but then lost their edge
|
|
209
213
|
// connection), close them all before accepting a new one.
|
|
210
|
-
const existing = this.state.getWebSockets(
|
|
214
|
+
const existing = this.state.getWebSockets(wsTag);
|
|
211
215
|
if (existing.length > 3) {
|
|
212
|
-
console.warn(`[DO] Safety valve: ${existing.length}
|
|
216
|
+
console.warn(`[DO] Safety valve: ${existing.length} ${wsTag} sockets, closing all`);
|
|
213
217
|
for (const s of existing) {
|
|
214
218
|
try { s.close(4009, "replaced"); } catch { /* dead */ }
|
|
215
219
|
}
|
|
@@ -218,12 +222,12 @@ export class ConnectionDO implements DurableObject {
|
|
|
218
222
|
const pair = new WebSocketPair();
|
|
219
223
|
const [client, server] = [pair[0], pair[1]];
|
|
220
224
|
|
|
221
|
-
this.state.acceptWebSocket(server, [
|
|
225
|
+
this.state.acceptWebSocket(server, [wsTag]);
|
|
222
226
|
|
|
223
227
|
// If the API worker already verified the token against D1, mark as
|
|
224
228
|
// pre-verified. The plugin still sends an auth message, which we'll
|
|
225
229
|
// fast-track through without re-validating the token.
|
|
226
|
-
server.serializeAttachment({ authenticated: false, tag:
|
|
230
|
+
server.serializeAttachment({ authenticated: false, tag: wsTag, agentId: agentId ?? null, preVerified });
|
|
227
231
|
|
|
228
232
|
return new Response(null, { status: 101, webSocket: client });
|
|
229
233
|
}
|
|
@@ -272,11 +276,12 @@ export class ConnectionDO implements DurableObject {
|
|
|
272
276
|
const isValid = attachment?.preVerified || await this.validatePairingToken(token);
|
|
273
277
|
|
|
274
278
|
if (isValid) {
|
|
275
|
-
// Close
|
|
279
|
+
// Close other sockets with the SAME tag. Use custom code 4009 so
|
|
276
280
|
// well-behaved plugins know they were replaced (not a crash)
|
|
277
281
|
// and should NOT reconnect. The Worker-level rate limit (10s)
|
|
278
282
|
// prevents the resulting close event from flooding the DO.
|
|
279
|
-
const
|
|
283
|
+
const wsTag = attachment?.tag ?? "openclaw";
|
|
284
|
+
const existingSockets = this.state.getWebSockets(wsTag);
|
|
280
285
|
let closedCount = 0;
|
|
281
286
|
for (const oldWs of existingSockets) {
|
|
282
287
|
if (oldWs !== ws) {
|
|
@@ -318,6 +323,15 @@ export class ConnectionDO implements DurableObject {
|
|
|
318
323
|
return;
|
|
319
324
|
}
|
|
320
325
|
|
|
326
|
+
// Resolve @channelName session keys to real adhoc task session keys
|
|
327
|
+
if (
|
|
328
|
+
(msg.type === "agent.text" || msg.type === "agent.media" || msg.type === "agent.a2ui" ||
|
|
329
|
+
msg.type === "agent.stream.start" || msg.type === "agent.stream.chunk" || msg.type === "agent.stream.end") &&
|
|
330
|
+
typeof msg.sessionKey === "string" && msg.sessionKey.startsWith("@")
|
|
331
|
+
) {
|
|
332
|
+
msg.sessionKey = await this.resolveChannelSessionKey(msg.sessionKey);
|
|
333
|
+
}
|
|
334
|
+
|
|
321
335
|
// Persist agent messages to D1 (skip transient stream events)
|
|
322
336
|
if (msg.type === "agent.text" || msg.type === "agent.media" || msg.type === "agent.a2ui") {
|
|
323
337
|
console.log("[DO] Agent outbound:", JSON.stringify({
|
|
@@ -343,6 +357,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
343
357
|
// Bitmask: bit 0 = text encrypted, bit 1 = media encrypted
|
|
344
358
|
const encryptedBits = (msg.encrypted ? 1 : 0) | (msg.mediaEncrypted ? 2 : 0);
|
|
345
359
|
|
|
360
|
+
// Extract agentId from the sending WebSocket's attachment
|
|
361
|
+
const wsAtt = ws.deserializeAttachment() as { agentId?: string } | null;
|
|
362
|
+
|
|
346
363
|
await this.persistMessage({
|
|
347
364
|
id: msg.messageId as string | undefined,
|
|
348
365
|
sender: "agent",
|
|
@@ -352,6 +369,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
352
369
|
mediaUrl: persistedMediaUrl,
|
|
353
370
|
a2ui: msg.jsonl as string | undefined,
|
|
354
371
|
encrypted: encryptedBits,
|
|
372
|
+
senderAgentId: (msg.agentId as string | undefined) ?? wsAtt?.agentId ?? undefined,
|
|
355
373
|
});
|
|
356
374
|
}
|
|
357
375
|
|
|
@@ -365,7 +383,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
365
383
|
if (msg.type === "task.schedule.ack" && msg.ok && msg.taskId && msg.cronJobId) {
|
|
366
384
|
try {
|
|
367
385
|
await this.env.DB.prepare(
|
|
368
|
-
"UPDATE tasks SET
|
|
386
|
+
"UPDATE tasks SET provider_job_id = ? WHERE id = ?",
|
|
369
387
|
).bind(msg.cronJobId, msg.taskId).run();
|
|
370
388
|
console.log(`[DO] Updated task ${msg.taskId} with cronJobId ${msg.cronJobId}`);
|
|
371
389
|
} catch (err) {
|
|
@@ -406,11 +424,44 @@ export class ConnectionDO implements DurableObject {
|
|
|
406
424
|
await this.handleJobUpdate(msg);
|
|
407
425
|
}
|
|
408
426
|
|
|
427
|
+
// Handle agent.request — Agent-to-Agent delegation
|
|
428
|
+
if (msg.type === "agent.request") {
|
|
429
|
+
await this.handleAgentRequest(ws, msg);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Handle agent.trace — persist verbose execution traces (lv2/lv3)
|
|
434
|
+
if (msg.type === "agent.trace") {
|
|
435
|
+
await this.handleAgentTrace(msg);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
409
439
|
// Forward all messages to browser clients (strip notifyPreview — plaintext
|
|
410
440
|
// must not be relayed to browser WebSockets; browsers decrypt locally)
|
|
411
441
|
if (msg.type === "agent.text") {
|
|
412
442
|
console.log(`[DO] Forwarding agent.text to browsers: encrypted=${msg.encrypted}, messageId=${msg.messageId}, textLen=${typeof msg.text === "string" ? msg.text.length : "?"}`);
|
|
413
443
|
}
|
|
444
|
+
|
|
445
|
+
const wsAttForReply = ws.deserializeAttachment() as { agentId?: string } | null;
|
|
446
|
+
|
|
447
|
+
// Check if this agent.text is a response to a pending agent.request
|
|
448
|
+
if ((msg.type === "agent.text" || msg.type === "agent.stream.end") && msg.requestId) {
|
|
449
|
+
const pending = this.pendingRequests.get(msg.requestId as string);
|
|
450
|
+
if (pending) {
|
|
451
|
+
const originSocket = this.getAgentSocket(pending.fromAgentId);
|
|
452
|
+
if (originSocket) {
|
|
453
|
+
originSocket.send(JSON.stringify({
|
|
454
|
+
type: "agent.response",
|
|
455
|
+
requestId: msg.requestId,
|
|
456
|
+
fromAgentId: (msg.agentId as string) ?? wsAttForReply?.agentId ?? null,
|
|
457
|
+
text: (msg.text ?? "") as string,
|
|
458
|
+
sessionKey: (msg.sessionKey ?? "") as string,
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
this.pendingRequests.delete(msg.requestId as string);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
414
465
|
const { notifyPreview: _stripped, ...msgForBrowser } = msg;
|
|
415
466
|
this.broadcastToBrowsers(JSON.stringify(msgForBrowser));
|
|
416
467
|
|
|
@@ -458,27 +509,40 @@ export class ConnectionDO implements DurableObject {
|
|
|
458
509
|
}
|
|
459
510
|
|
|
460
511
|
ws.serializeAttachment({ ...attachment, authenticated: true });
|
|
461
|
-
// Include userId so the browser can derive the E2E key
|
|
462
512
|
const doUserId2 = doUserId ?? payload.sub;
|
|
463
|
-
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
513
|
+
|
|
514
|
+
// Fetch available agents from D1 for the auth.ok response
|
|
515
|
+
let availableAgents: Array<{ id: string; name: string; type: string; role: string; capabilities: string[]; status: string }> = [];
|
|
516
|
+
try {
|
|
517
|
+
const { results } = await this.env.DB.prepare(
|
|
518
|
+
"SELECT id, name, type, role, capabilities, status FROM agents WHERE user_id = ? ORDER BY created_at ASC",
|
|
519
|
+
)
|
|
520
|
+
.bind(doUserId2)
|
|
521
|
+
.all<{ id: string; name: string; type: string; role: string; capabilities: string; status: string }>();
|
|
522
|
+
availableAgents = (results ?? []).map((r) => ({
|
|
523
|
+
id: r.id,
|
|
524
|
+
name: r.name,
|
|
525
|
+
type: r.type,
|
|
526
|
+
role: r.role,
|
|
527
|
+
capabilities: JSON.parse(r.capabilities || "[]"),
|
|
528
|
+
status: r.status,
|
|
529
|
+
}));
|
|
530
|
+
} catch (err) {
|
|
531
|
+
console.error("[DO] Failed to fetch agents for auth.ok:", err);
|
|
467
532
|
}
|
|
468
|
-
ws.send(JSON.stringify({ type: "auth.ok", userId: doUserId2 }));
|
|
469
533
|
|
|
470
|
-
|
|
534
|
+
ws.send(JSON.stringify({ type: "auth.ok", userId: doUserId2, availableAgents }));
|
|
535
|
+
|
|
536
|
+
// Send current connection status + cached models
|
|
471
537
|
await this.ensureCachedModels();
|
|
472
|
-
const
|
|
473
|
-
const openclawConnected = isDemo || this.getOpenClawSocket() !== null;
|
|
474
|
-
const models = this.cachedModels.length ? this.cachedModels : (isDemo ? ConnectionDO.DEMO_MODELS : []);
|
|
475
|
-
const model = this.defaultModel || (isDemo ? "mock/echo-1.0" : null);
|
|
538
|
+
const openclawConnected = this.getOpenClawSocket() !== null;
|
|
476
539
|
ws.send(
|
|
477
540
|
JSON.stringify({
|
|
478
541
|
type: "connection.status",
|
|
479
542
|
openclawConnected,
|
|
480
|
-
defaultModel:
|
|
481
|
-
models,
|
|
543
|
+
defaultModel: this.defaultModel,
|
|
544
|
+
models: this.cachedModels,
|
|
545
|
+
connectedAgents: availableAgents.filter((a) => a.status === "connected"),
|
|
482
546
|
}),
|
|
483
547
|
);
|
|
484
548
|
return;
|
|
@@ -521,6 +585,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
521
585
|
type: msg.type,
|
|
522
586
|
sessionKey: msg.sessionKey,
|
|
523
587
|
messageId: msg.messageId,
|
|
588
|
+
targetAgentId: msg.targetAgentId,
|
|
524
589
|
hasMedia: !!msg.mediaUrl,
|
|
525
590
|
}));
|
|
526
591
|
await this.persistMessage({
|
|
@@ -530,14 +595,34 @@ export class ConnectionDO implements DurableObject {
|
|
|
530
595
|
text: (msg.text ?? "") as string,
|
|
531
596
|
mediaUrl: msg.mediaUrl as string | undefined,
|
|
532
597
|
encrypted: msg.encrypted ? 1 : 0,
|
|
598
|
+
targetAgentId: msg.targetAgentId as string | undefined,
|
|
533
599
|
});
|
|
534
600
|
}
|
|
535
601
|
|
|
536
|
-
//
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
602
|
+
// Route user message to the target agent
|
|
603
|
+
const targetAgentId = msg.targetAgentId as string | undefined;
|
|
604
|
+
let targetWs: WebSocket | null = null;
|
|
605
|
+
|
|
606
|
+
if (targetAgentId) {
|
|
607
|
+
targetWs = this.getAgentSocket(targetAgentId);
|
|
608
|
+
}
|
|
609
|
+
if (!targetWs) {
|
|
610
|
+
// No explicit target or target not found — try channel's default agent
|
|
611
|
+
const sessionKey = msg.sessionKey as string | undefined;
|
|
612
|
+
if (sessionKey) {
|
|
613
|
+
const channelDefault = await this.resolveDefaultAgentId(sessionKey);
|
|
614
|
+
if (channelDefault) {
|
|
615
|
+
targetWs = this.getAgentSocket(channelDefault);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (!targetWs) {
|
|
620
|
+
// Final fallback: any connected agent (backward compat)
|
|
621
|
+
targetWs = this.getAnyAgentSocket();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (targetWs) {
|
|
625
|
+
// Enrich thread messages with parent context
|
|
541
626
|
const sessionKey = msg.sessionKey as string | undefined;
|
|
542
627
|
const threadMatch = sessionKey?.match(/:thread:(.+)$/);
|
|
543
628
|
let enrichedMsg = msg;
|
|
@@ -557,20 +642,17 @@ export class ConnectionDO implements DurableObject {
|
|
|
557
642
|
parentSender: parentRow.sender as string,
|
|
558
643
|
parentEncrypted: (parentRow.encrypted ?? 0) as number,
|
|
559
644
|
};
|
|
560
|
-
console.log(`[DO] Attached parent message for thread: parentId=${parentId}, sender=${parentRow.sender}, encrypted=${parentRow.encrypted}`);
|
|
561
645
|
}
|
|
562
646
|
} catch (err) {
|
|
563
|
-
console.error(`[DO] Failed to fetch parent message for thread
|
|
647
|
+
console.error(`[DO] Failed to fetch parent message for thread:`, err);
|
|
564
648
|
}
|
|
565
649
|
}
|
|
566
|
-
|
|
567
|
-
} else if (await this.isDemoUser()) {
|
|
568
|
-
await this.handleDemoMockReply(ws, msg);
|
|
650
|
+
targetWs.send(JSON.stringify(enrichedMsg));
|
|
569
651
|
} else {
|
|
570
652
|
ws.send(
|
|
571
653
|
JSON.stringify({
|
|
572
654
|
type: "error",
|
|
573
|
-
message: "
|
|
655
|
+
message: "No agent is currently connected. Please check your agent instances.",
|
|
574
656
|
}),
|
|
575
657
|
);
|
|
576
658
|
}
|
|
@@ -586,10 +668,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
586
668
|
private async handleGetScanData(): Promise<Response> {
|
|
587
669
|
const openclawWs = this.getOpenClawSocket();
|
|
588
670
|
if (!openclawWs) {
|
|
589
|
-
// Demo users get mock scan data instead of 503
|
|
590
|
-
if (await this.isDemoUser()) {
|
|
591
|
-
return Response.json({ tasks: [] });
|
|
592
|
-
}
|
|
593
671
|
return Response.json(
|
|
594
672
|
{ error: "OpenClaw not connected", tasks: [] },
|
|
595
673
|
{ status: 503 },
|
|
@@ -630,9 +708,12 @@ export class ConnectionDO implements DurableObject {
|
|
|
630
708
|
}
|
|
631
709
|
}
|
|
632
710
|
|
|
633
|
-
private
|
|
711
|
+
private handleStatus(): Response {
|
|
634
712
|
const sockets = this.state.getWebSockets();
|
|
635
|
-
const openclawSocket = sockets.find((s) =>
|
|
713
|
+
const openclawSocket = sockets.find((s) => {
|
|
714
|
+
const t = this.getTag(s);
|
|
715
|
+
return t === "openclaw" || t?.startsWith("agent:");
|
|
716
|
+
});
|
|
636
717
|
const browserCount = sockets.filter((s) =>
|
|
637
718
|
this.getTag(s)?.startsWith("browser:"),
|
|
638
719
|
).length;
|
|
@@ -643,10 +724,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
643
724
|
openclawAuthenticated = att?.authenticated ?? false;
|
|
644
725
|
}
|
|
645
726
|
|
|
646
|
-
const isDemo = await this.isDemoUser();
|
|
647
727
|
return Response.json({
|
|
648
|
-
openclawConnected:
|
|
649
|
-
openclawAuthenticated
|
|
728
|
+
openclawConnected: !!openclawSocket,
|
|
729
|
+
openclawAuthenticated,
|
|
650
730
|
browserClients: browserCount,
|
|
651
731
|
});
|
|
652
732
|
}
|
|
@@ -704,11 +784,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
704
784
|
return att?.tag ?? null;
|
|
705
785
|
}
|
|
706
786
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
// After a reconnection there may briefly be multiple sockets
|
|
711
|
-
// before the stale cleanup in handleOpenClawMessage runs.
|
|
787
|
+
/** Get a specific agent's socket by agentId. */
|
|
788
|
+
private getAgentSocket(agentId: string): WebSocket | null {
|
|
789
|
+
const sockets = this.state.getWebSockets("agent:" + agentId);
|
|
712
790
|
let newest: WebSocket | null = null;
|
|
713
791
|
for (const s of sockets) {
|
|
714
792
|
const att = s.deserializeAttachment() as { authenticated: boolean } | null;
|
|
@@ -717,6 +795,147 @@ export class ConnectionDO implements DurableObject {
|
|
|
717
795
|
return newest ?? sockets[sockets.length - 1] ?? null;
|
|
718
796
|
}
|
|
719
797
|
|
|
798
|
+
/** Get any connected agent socket (new agent:xxx or legacy openclaw). */
|
|
799
|
+
private getAnyAgentSocket(): WebSocket | null {
|
|
800
|
+
const allSockets = this.state.getWebSockets();
|
|
801
|
+
for (const s of allSockets) {
|
|
802
|
+
const att = s.deserializeAttachment() as { authenticated: boolean; tag?: string } | null;
|
|
803
|
+
if (att?.authenticated && att?.tag?.startsWith("agent:")) return s;
|
|
804
|
+
}
|
|
805
|
+
const legacySockets = this.state.getWebSockets("openclaw");
|
|
806
|
+
for (const s of legacySockets) {
|
|
807
|
+
const att = s.deserializeAttachment() as { authenticated: boolean } | null;
|
|
808
|
+
if (att?.authenticated) return s;
|
|
809
|
+
}
|
|
810
|
+
return legacySockets[legacySockets.length - 1] ?? null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private getOpenClawSocket(): WebSocket | null {
|
|
814
|
+
return this.getAnyAgentSocket();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ---- Agent-to-Agent delegation ----
|
|
818
|
+
|
|
819
|
+
private async handleAgentRequest(ws: WebSocket, msg: Record<string, unknown>): Promise<void> {
|
|
820
|
+
const MAX_DEPTH = 5;
|
|
821
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
822
|
+
const depth = (msg.depth as number) ?? 0;
|
|
823
|
+
const requestId = msg.requestId as string;
|
|
824
|
+
const targetAgentId = msg.targetAgentId as string;
|
|
825
|
+
const fromAtt = ws.deserializeAttachment() as { agentId?: string } | null;
|
|
826
|
+
const fromAgentId = (msg.agentId as string) ?? fromAtt?.agentId ?? "unknown";
|
|
827
|
+
|
|
828
|
+
if (depth >= MAX_DEPTH) {
|
|
829
|
+
const originSocket = this.getAgentSocket(fromAgentId);
|
|
830
|
+
if (originSocket) {
|
|
831
|
+
originSocket.send(JSON.stringify({
|
|
832
|
+
type: "agent.response",
|
|
833
|
+
requestId,
|
|
834
|
+
fromAgentId: targetAgentId,
|
|
835
|
+
error: "Maximum delegation depth exceeded",
|
|
836
|
+
sessionKey: msg.sessionKey,
|
|
837
|
+
}));
|
|
838
|
+
}
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const targetSocket = this.getAgentSocket(targetAgentId);
|
|
843
|
+
if (!targetSocket) {
|
|
844
|
+
// Target agent offline — notify originator and broadcast system message
|
|
845
|
+
const originSocket = this.getAgentSocket(fromAgentId);
|
|
846
|
+
if (originSocket) {
|
|
847
|
+
originSocket.send(JSON.stringify({
|
|
848
|
+
type: "agent.response",
|
|
849
|
+
requestId,
|
|
850
|
+
fromAgentId: targetAgentId,
|
|
851
|
+
error: `Agent ${targetAgentId} is currently offline`,
|
|
852
|
+
sessionKey: msg.sessionKey,
|
|
853
|
+
}));
|
|
854
|
+
}
|
|
855
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
856
|
+
type: "system.message",
|
|
857
|
+
text: `Agent delegation failed: target agent is offline`,
|
|
858
|
+
sessionKey: msg.sessionKey,
|
|
859
|
+
}));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Track the pending request for response routing
|
|
864
|
+
this.pendingRequests.set(requestId, {
|
|
865
|
+
fromAgentId,
|
|
866
|
+
sessionKey: msg.sessionKey as string,
|
|
867
|
+
depth,
|
|
868
|
+
createdAt: Date.now(),
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// Clean up expired pending requests
|
|
872
|
+
const now = Date.now();
|
|
873
|
+
for (const [rid, pr] of this.pendingRequests) {
|
|
874
|
+
if (now - pr.createdAt > TIMEOUT_MS) {
|
|
875
|
+
this.pendingRequests.delete(rid);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Persist the delegation message to D1 (visible as agent-to-agent in chat)
|
|
880
|
+
await this.persistMessage({
|
|
881
|
+
sender: "agent",
|
|
882
|
+
sessionKey: msg.sessionKey as string,
|
|
883
|
+
text: (msg.text ?? "") as string,
|
|
884
|
+
senderAgentId: fromAgentId,
|
|
885
|
+
targetAgentId,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Forward to target agent as a user.message (agents don't need to know it's a delegation)
|
|
889
|
+
targetSocket.send(JSON.stringify({
|
|
890
|
+
type: "user.message",
|
|
891
|
+
sessionKey: msg.sessionKey,
|
|
892
|
+
text: msg.text,
|
|
893
|
+
userId: fromAgentId,
|
|
894
|
+
messageId: requestId,
|
|
895
|
+
targetAgentId,
|
|
896
|
+
context: msg.context,
|
|
897
|
+
depth: depth + 1,
|
|
898
|
+
}));
|
|
899
|
+
|
|
900
|
+
// Also broadcast to browser so user can see the delegation
|
|
901
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
902
|
+
type: "agent.text",
|
|
903
|
+
agentId: fromAgentId,
|
|
904
|
+
sessionKey: msg.sessionKey,
|
|
905
|
+
text: msg.text,
|
|
906
|
+
targetAgentId,
|
|
907
|
+
isDelegation: true,
|
|
908
|
+
}));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ---- Verbose trace persistence ----
|
|
912
|
+
|
|
913
|
+
private async handleAgentTrace(msg: Record<string, unknown>): Promise<void> {
|
|
914
|
+
try {
|
|
915
|
+
const userId = (await this.state.storage.get<string>("userId")) ?? "unknown";
|
|
916
|
+
const id = generateIdUtil("mt_");
|
|
917
|
+
await this.env.DB.prepare(
|
|
918
|
+
`INSERT INTO message_traces (id, message_id, user_id, session_key, agent_id, verbose_level, trace_type, content, metadata_json, encrypted)
|
|
919
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
920
|
+
)
|
|
921
|
+
.bind(
|
|
922
|
+
id,
|
|
923
|
+
(msg.messageId ?? "") as string,
|
|
924
|
+
userId,
|
|
925
|
+
(msg.sessionKey ?? "") as string,
|
|
926
|
+
(msg.agentId ?? "") as string,
|
|
927
|
+
(msg.verboseLevel ?? 2) as number,
|
|
928
|
+
(msg.traceType ?? "thinking") as string,
|
|
929
|
+
(msg.content ?? "") as string,
|
|
930
|
+
JSON.stringify(msg.metadata ?? {}),
|
|
931
|
+
msg.encrypted ? 1 : 0,
|
|
932
|
+
)
|
|
933
|
+
.run();
|
|
934
|
+
} catch (err) {
|
|
935
|
+
console.error("[DO] Failed to persist agent trace:", err);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
720
939
|
private broadcastToBrowsers(message: string): void {
|
|
721
940
|
const sockets = this.state.getWebSockets();
|
|
722
941
|
for (const s of sockets) {
|
|
@@ -1045,6 +1264,8 @@ export class ConnectionDO implements DurableObject {
|
|
|
1045
1264
|
mediaUrl?: string;
|
|
1046
1265
|
a2ui?: string;
|
|
1047
1266
|
encrypted?: number;
|
|
1267
|
+
senderAgentId?: string;
|
|
1268
|
+
targetAgentId?: string;
|
|
1048
1269
|
}): Promise<void> {
|
|
1049
1270
|
try {
|
|
1050
1271
|
const userId = (await this.state.storage.get<string>("userId")) ?? "unknown";
|
|
@@ -1059,10 +1280,10 @@ export class ConnectionDO implements DurableObject {
|
|
|
1059
1280
|
}
|
|
1060
1281
|
|
|
1061
1282
|
await this.env.DB.prepare(
|
|
1062
|
-
`INSERT INTO messages (id, user_id, session_key, thread_id, sender, text, media_url, a2ui, encrypted)
|
|
1063
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1283
|
+
`INSERT INTO messages (id, user_id, session_key, thread_id, sender, text, media_url, a2ui, encrypted, sender_agent_id, target_agent_id)
|
|
1284
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1064
1285
|
)
|
|
1065
|
-
.bind(id, userId, opts.sessionKey, threadId ?? null, opts.sender, opts.text, opts.mediaUrl ?? null, opts.a2ui ?? null, encrypted)
|
|
1286
|
+
.bind(id, userId, opts.sessionKey, threadId ?? null, opts.sender, opts.text, opts.mediaUrl ?? null, opts.a2ui ?? null, encrypted, opts.senderAgentId ?? null, opts.targetAgentId ?? null)
|
|
1066
1287
|
.run();
|
|
1067
1288
|
} catch (err) {
|
|
1068
1289
|
console.error("Failed to persist message:", err);
|
|
@@ -1224,7 +1445,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1224
1445
|
|
|
1225
1446
|
// Check if a matching task already exists
|
|
1226
1447
|
const existingTask = await this.env.DB.prepare(
|
|
1227
|
-
"SELECT id, session_key FROM tasks WHERE
|
|
1448
|
+
"SELECT id, session_key FROM tasks WHERE provider_job_id = ?",
|
|
1228
1449
|
)
|
|
1229
1450
|
.bind(t.cronJobId)
|
|
1230
1451
|
.first<{ id: string; session_key: string }>();
|
|
@@ -1243,7 +1464,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1243
1464
|
t.cronJobId,
|
|
1244
1465
|
];
|
|
1245
1466
|
await this.env.DB.prepare(
|
|
1246
|
-
`UPDATE tasks SET ${updateParts.join(", ")} WHERE
|
|
1467
|
+
`UPDATE tasks SET ${updateParts.join(", ")} WHERE provider_job_id = ?`,
|
|
1247
1468
|
)
|
|
1248
1469
|
.bind(...updateVals)
|
|
1249
1470
|
.run();
|
|
@@ -1262,7 +1483,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1262
1483
|
// D1 only stores basic task metadata — schedule/instructions/model
|
|
1263
1484
|
// belong to OpenClaw and are delivered via task.scan.result WebSocket.
|
|
1264
1485
|
await this.env.DB.prepare(
|
|
1265
|
-
`INSERT INTO tasks (id, channel_id, name, kind,
|
|
1486
|
+
`INSERT INTO tasks (id, channel_id, name, kind, provider_job_id, session_key, enabled)
|
|
1266
1487
|
VALUES (?, ?, ?, 'background', ?, ?, ?)`,
|
|
1267
1488
|
)
|
|
1268
1489
|
.bind(taskId, defaultChannelId, taskName, t.cronJobId, sessionKey, t.enabled ? 1 : 0)
|
|
@@ -1273,7 +1494,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1273
1494
|
|
|
1274
1495
|
// Resolve the task record for job persistence (may have just been created)
|
|
1275
1496
|
const taskRecord = await this.env.DB.prepare(
|
|
1276
|
-
"SELECT id, session_key FROM tasks WHERE
|
|
1497
|
+
"SELECT id, session_key FROM tasks WHERE provider_job_id = ?",
|
|
1277
1498
|
)
|
|
1278
1499
|
.bind(t.cronJobId)
|
|
1279
1500
|
.first<{ id: string; session_key: string }>();
|
|
@@ -1337,7 +1558,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1337
1558
|
private async ensureDefaultChannel(userId: string): Promise<string> {
|
|
1338
1559
|
// Check if a default channel already exists
|
|
1339
1560
|
const existing = await this.env.DB.prepare(
|
|
1340
|
-
"SELECT id FROM channels WHERE user_id = ? AND
|
|
1561
|
+
"SELECT id FROM channels WHERE user_id = ? AND provider_agent_id = 'main' ORDER BY created_at ASC LIMIT 1",
|
|
1341
1562
|
)
|
|
1342
1563
|
.bind(userId)
|
|
1343
1564
|
.first<{ id: string }>();
|
|
@@ -1347,7 +1568,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1347
1568
|
// Create the default channel
|
|
1348
1569
|
const channelId = this.generateId("ch_");
|
|
1349
1570
|
await this.env.DB.prepare(
|
|
1350
|
-
"INSERT INTO channels (id, user_id, name, description,
|
|
1571
|
+
"INSERT INTO channels (id, user_id, name, description, provider_agent_id, system_prompt) VALUES (?, ?, 'Default', 'Auto-created channel for imported background tasks', 'main', '')",
|
|
1351
1572
|
)
|
|
1352
1573
|
.bind(channelId, userId)
|
|
1353
1574
|
.run();
|
|
@@ -1365,6 +1586,101 @@ export class ConnectionDO implements DurableObject {
|
|
|
1365
1586
|
return channelId;
|
|
1366
1587
|
}
|
|
1367
1588
|
|
|
1589
|
+
/**
|
|
1590
|
+
* Resolve a "@channelName" session key to a real adhoc task session key.
|
|
1591
|
+
* Resolve the default agent ID for a given session key.
|
|
1592
|
+
* Looks up the channel from the session key pattern and returns its default_agent_id.
|
|
1593
|
+
*/
|
|
1594
|
+
private async resolveDefaultAgentId(sessionKey: string): Promise<string | null> {
|
|
1595
|
+
try {
|
|
1596
|
+
const userId = await this.state.storage.get<string>("userId");
|
|
1597
|
+
if (!userId) return null;
|
|
1598
|
+
|
|
1599
|
+
// Try to find channel by matching session_key in sessions or tasks table
|
|
1600
|
+
const session = await this.env.DB.prepare(
|
|
1601
|
+
"SELECT channel_id FROM sessions WHERE session_key = ? AND user_id = ? LIMIT 1",
|
|
1602
|
+
)
|
|
1603
|
+
.bind(sessionKey.replace(/:thread:.+$/, ""), userId)
|
|
1604
|
+
.first<{ channel_id: string }>();
|
|
1605
|
+
|
|
1606
|
+
const channelId = session?.channel_id;
|
|
1607
|
+
if (!channelId) return null;
|
|
1608
|
+
|
|
1609
|
+
const channel = await this.env.DB.prepare(
|
|
1610
|
+
"SELECT default_agent_id FROM channels WHERE id = ? LIMIT 1",
|
|
1611
|
+
)
|
|
1612
|
+
.bind(channelId)
|
|
1613
|
+
.first<{ default_agent_id: string | null }>();
|
|
1614
|
+
|
|
1615
|
+
return channel?.default_agent_id ?? null;
|
|
1616
|
+
} catch {
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* - "@default" → first channel (by created_at ASC)
|
|
1623
|
+
* - "@SomeName" → channel matched by name (case-insensitive)
|
|
1624
|
+
* Returns the original sessionKey if it doesn't start with "@".
|
|
1625
|
+
*/
|
|
1626
|
+
private async resolveChannelSessionKey(rawSessionKey: string): Promise<string> {
|
|
1627
|
+
if (!rawSessionKey.startsWith("@")) return rawSessionKey;
|
|
1628
|
+
|
|
1629
|
+
const userId = await this.state.storage.get<string>("userId");
|
|
1630
|
+
if (!userId) {
|
|
1631
|
+
console.error("[DO] resolveChannelSessionKey: no userId in storage");
|
|
1632
|
+
return rawSessionKey;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const isDefault = rawSessionKey.toLowerCase() === "@default";
|
|
1636
|
+
const channelName = isDefault ? null : rawSessionKey.slice(1);
|
|
1637
|
+
|
|
1638
|
+
let channel: { id: string; provider_agent_id: string } | null = null;
|
|
1639
|
+
|
|
1640
|
+
if (channelName) {
|
|
1641
|
+
channel = await this.env.DB.prepare(
|
|
1642
|
+
"SELECT id, provider_agent_id FROM channels WHERE user_id = ? AND LOWER(name) = LOWER(?) LIMIT 1",
|
|
1643
|
+
)
|
|
1644
|
+
.bind(userId, channelName)
|
|
1645
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (!channel) {
|
|
1649
|
+
channel = await this.env.DB.prepare(
|
|
1650
|
+
"SELECT id, provider_agent_id FROM channels WHERE user_id = ? ORDER BY created_at ASC LIMIT 1",
|
|
1651
|
+
)
|
|
1652
|
+
.bind(userId)
|
|
1653
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (!channel) {
|
|
1657
|
+
const channelId = await this.ensureDefaultChannel(userId);
|
|
1658
|
+
channel = { id: channelId, provider_agent_id: "main" };
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const task = await this.env.DB.prepare(
|
|
1662
|
+
"SELECT session_key FROM tasks WHERE channel_id = ? AND kind = 'adhoc' LIMIT 1",
|
|
1663
|
+
)
|
|
1664
|
+
.bind(channel.id)
|
|
1665
|
+
.first<{ session_key: string }>();
|
|
1666
|
+
|
|
1667
|
+
if (task?.session_key) {
|
|
1668
|
+
console.log(`[DO] Resolved ${rawSessionKey} → ${task.session_key}`);
|
|
1669
|
+
return task.session_key;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const taskId = this.generateId("tsk_");
|
|
1673
|
+
const agentId = channel.provider_agent_id || "main";
|
|
1674
|
+
const sessionKey = `agent:${agentId}:botschat:${userId}:adhoc`;
|
|
1675
|
+
await this.env.DB.prepare(
|
|
1676
|
+
"INSERT INTO tasks (id, channel_id, name, kind, session_key) VALUES (?, ?, 'Ad Hoc Chat', 'adhoc', ?)",
|
|
1677
|
+
)
|
|
1678
|
+
.bind(taskId, channel.id, sessionKey)
|
|
1679
|
+
.run();
|
|
1680
|
+
console.log(`[DO] Created adhoc task for channel ${channel.id}, resolved ${rawSessionKey} → ${sessionKey}`);
|
|
1681
|
+
return sessionKey;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1368
1684
|
/** Generate a short random ID (URL-safe) using CSPRNG (bias-free). */
|
|
1369
1685
|
private generateId(prefix = ""): string {
|
|
1370
1686
|
return generateIdUtil(prefix);
|
|
@@ -1388,7 +1704,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1388
1704
|
|
|
1389
1705
|
// Find the task by cronJobId
|
|
1390
1706
|
const task = await this.env.DB.prepare(
|
|
1391
|
-
"SELECT id FROM tasks WHERE
|
|
1707
|
+
"SELECT id FROM tasks WHERE provider_job_id = ?",
|
|
1392
1708
|
)
|
|
1393
1709
|
.bind(cronJobId)
|
|
1394
1710
|
.first<{ id: string }>();
|
|
@@ -1422,126 +1738,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
1422
1738
|
}
|
|
1423
1739
|
}
|
|
1424
1740
|
|
|
1425
|
-
// ---- Demo mode (built-in mock OpenClaw) ----
|
|
1426
|
-
|
|
1427
|
-
private static readonly DEMO_MODELS = [
|
|
1428
|
-
{ id: "mock/echo-1.0", name: "Echo 1.0", provider: "mock" },
|
|
1429
|
-
{ id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
|
|
1430
|
-
{ id: "openai/gpt-4o", name: "GPT-4o", provider: "openai" },
|
|
1431
|
-
];
|
|
1432
|
-
|
|
1433
|
-
private async isDemoUser(): Promise<boolean> {
|
|
1434
|
-
const userId = await this.state.storage.get<string>("userId");
|
|
1435
|
-
return !!userId && isDemoUserId(userId);
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
/**
|
|
1439
|
-
* Handle a browser message when no OpenClaw plugin is connected and the
|
|
1440
|
-
* user is a demo account. Simulates the mock OpenClaw responses inline.
|
|
1441
|
-
*/
|
|
1442
|
-
private async handleDemoMockReply(
|
|
1443
|
-
ws: WebSocket,
|
|
1444
|
-
msg: Record<string, unknown>,
|
|
1445
|
-
): Promise<void> {
|
|
1446
|
-
const sessionKey = msg.sessionKey as string;
|
|
1447
|
-
|
|
1448
|
-
if (msg.type === "user.message") {
|
|
1449
|
-
const userText = (msg.text as string) ?? "";
|
|
1450
|
-
const replyText = `Mock reply: ${userText}`;
|
|
1451
|
-
const replyId = randomUUID();
|
|
1452
|
-
|
|
1453
|
-
// Small delay to feel natural
|
|
1454
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
1455
|
-
|
|
1456
|
-
const reply = {
|
|
1457
|
-
type: "agent.text",
|
|
1458
|
-
sessionKey,
|
|
1459
|
-
text: replyText,
|
|
1460
|
-
messageId: replyId,
|
|
1461
|
-
};
|
|
1462
|
-
|
|
1463
|
-
await this.persistMessage({
|
|
1464
|
-
id: replyId,
|
|
1465
|
-
sender: "agent",
|
|
1466
|
-
sessionKey,
|
|
1467
|
-
text: replyText,
|
|
1468
|
-
encrypted: 0,
|
|
1469
|
-
});
|
|
1470
|
-
|
|
1471
|
-
this.broadcastToBrowsers(JSON.stringify(reply));
|
|
1472
|
-
} else if (msg.type === "user.media") {
|
|
1473
|
-
const replyId = randomUUID();
|
|
1474
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
1475
|
-
const reply = {
|
|
1476
|
-
type: "agent.text",
|
|
1477
|
-
sessionKey,
|
|
1478
|
-
text: `📎 Received media: ${msg.mediaUrl}`,
|
|
1479
|
-
messageId: replyId,
|
|
1480
|
-
};
|
|
1481
|
-
await this.persistMessage({ id: replyId, sender: "agent", sessionKey, text: reply.text, encrypted: 0 });
|
|
1482
|
-
this.broadcastToBrowsers(JSON.stringify(reply));
|
|
1483
|
-
} else if (msg.type === "user.command" || msg.type === "user.action") {
|
|
1484
|
-
const replyId = randomUUID();
|
|
1485
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
1486
|
-
const text = msg.type === "user.command"
|
|
1487
|
-
? `Command received: /${msg.command} ${msg.args || ""}`.trim()
|
|
1488
|
-
: `Action received: ${msg.action}`;
|
|
1489
|
-
const reply = { type: "agent.text", sessionKey, text, messageId: replyId };
|
|
1490
|
-
await this.persistMessage({ id: replyId, sender: "agent", sessionKey, text, encrypted: 0 });
|
|
1491
|
-
this.broadcastToBrowsers(JSON.stringify(reply));
|
|
1492
|
-
} else if (msg.type === "task.schedule") {
|
|
1493
|
-
const ack = {
|
|
1494
|
-
type: "task.schedule.ack",
|
|
1495
|
-
cronJobId: (msg.cronJobId as string) || `mock_cron_${Date.now()}`,
|
|
1496
|
-
taskId: msg.taskId,
|
|
1497
|
-
ok: true,
|
|
1498
|
-
};
|
|
1499
|
-
this.broadcastToBrowsers(JSON.stringify(ack));
|
|
1500
|
-
} else if (msg.type === "task.run") {
|
|
1501
|
-
const jobId = `mock_job_${Date.now()}`;
|
|
1502
|
-
const startedAt = Math.floor(Date.now() / 1000);
|
|
1503
|
-
this.broadcastToBrowsers(JSON.stringify({
|
|
1504
|
-
type: "job.update",
|
|
1505
|
-
cronJobId: msg.cronJobId,
|
|
1506
|
-
jobId,
|
|
1507
|
-
sessionKey: sessionKey ?? "",
|
|
1508
|
-
status: "running",
|
|
1509
|
-
startedAt,
|
|
1510
|
-
}));
|
|
1511
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
1512
|
-
const finishedAt = Math.floor(Date.now() / 1000);
|
|
1513
|
-
const update = {
|
|
1514
|
-
type: "job.update",
|
|
1515
|
-
cronJobId: msg.cronJobId,
|
|
1516
|
-
jobId,
|
|
1517
|
-
sessionKey: sessionKey ?? "",
|
|
1518
|
-
status: "ok",
|
|
1519
|
-
summary: "Mock task executed successfully",
|
|
1520
|
-
startedAt,
|
|
1521
|
-
finishedAt,
|
|
1522
|
-
durationMs: (finishedAt - startedAt) * 1000,
|
|
1523
|
-
};
|
|
1524
|
-
await this.handleJobUpdate(update);
|
|
1525
|
-
this.broadcastToBrowsers(JSON.stringify(update));
|
|
1526
|
-
} else if (msg.type === "settings.defaultModel") {
|
|
1527
|
-
const model = (msg.defaultModel as string) ?? "";
|
|
1528
|
-
if (model) {
|
|
1529
|
-
this.defaultModel = model;
|
|
1530
|
-
await this.state.storage.put("defaultModel", model);
|
|
1531
|
-
}
|
|
1532
|
-
this.broadcastToBrowsers(JSON.stringify({
|
|
1533
|
-
type: "defaultModel.updated",
|
|
1534
|
-
model,
|
|
1535
|
-
}));
|
|
1536
|
-
this.broadcastToBrowsers(JSON.stringify({
|
|
1537
|
-
type: "connection.status",
|
|
1538
|
-
openclawConnected: true,
|
|
1539
|
-
defaultModel: this.defaultModel,
|
|
1540
|
-
models: this.cachedModels.length ? this.cachedModels : ConnectionDO.DEMO_MODELS,
|
|
1541
|
-
}));
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
1741
|
private async validatePairingToken(token: string): Promise<boolean> {
|
|
1546
1742
|
// The API worker validates pairing tokens against D1 before routing
|
|
1547
1743
|
// to the DO (and passes ?verified=1). Connections that arrive here
|