botschat 0.1.20 → 0.1.22
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/README.md +68 -1
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +186 -382
- package/packages/api/src/index.ts +50 -67
- package/packages/api/src/routes/agents.ts +3 -3
- package/packages/api/src/routes/auth.ts +1 -0
- package/packages/api/src/routes/channels.ts +11 -11
- package/packages/api/src/routes/demo.ts +156 -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.js +50 -0
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/package.json +23 -4
- package/packages/web/dist/assets/index-BaRi2ZPe.js +1517 -0
- package/packages/web/dist/assets/index-Be-wHtaM.js +1 -0
- package/packages/web/dist/assets/index-BtPyCBCl.css +1 -0
- package/packages/web/dist/assets/index-C8XPcaR1.js +2 -0
- package/packages/web/dist/assets/index-CyXci1aU.js +2 -0
- package/packages/web/dist/assets/{index-DYCO-ry1.js → index-DVvunB1I.js} +1 -1
- package/packages/web/dist/assets/{index-CYQMu_-c.js → index-SvEo7uGi.js} +1 -1
- package/packages/web/dist/assets/{index.esm-CvOpngZM.js → index.esm-DAPLRyFT.js} +1 -1
- package/packages/web/dist/assets/{web-D3LMODYp.js → web-C6s08DDW.js} +1 -1
- package/packages/web/dist/assets/{web-1cdhq2RW.js → web-K88lR3JA.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +44 -57
- package/packages/web/src/api.ts +5 -61
- package/packages/web/src/components/ChatWindow.tsx +9 -9
- package/packages/web/src/components/CronDetail.tsx +1 -1
- package/packages/web/src/components/ImageLightbox.tsx +96 -0
- package/packages/web/src/components/LoginPage.tsx +78 -1
- package/packages/web/src/components/MessageContent.tsx +17 -2
- package/packages/web/src/components/SessionTabs.tsx +1 -1
- package/packages/web/src/components/Sidebar.tsx +1 -3
- package/packages/web/src/hooks/useIMEComposition.ts +14 -9
- package/packages/web/src/store.ts +7 -39
- package/packages/web/src/ws.ts +0 -1
- package/scripts/dev.sh +0 -53
- package/migrations/0013_agents_table.sql +0 -29
- package/migrations/0014_agent_sessions.sql +0 -19
- package/migrations/0015_message_traces.sql +0 -27
- package/migrations/0016_multi_agent_channels_messages.sql +0 -9
- package/migrations/0017_rename_cron_job_id.sql +0 -2
- package/packages/api/src/protocol-v2.ts +0 -154
- package/packages/api/src/routes/agents-v2.ts +0 -192
- package/packages/api/src/routes/history-v2.ts +0 -221
- package/packages/api/src/routes/migrate-v2.ts +0 -110
- package/packages/web/dist/assets/index-BARPtt0v.css +0 -1
- package/packages/web/dist/assets/index-Bf-XL3te.js +0 -2
- package/packages/web/dist/assets/index-CYlvfpX9.js +0 -1519
- package/packages/web/dist/assets/index-CxcpA4Qo.js +0 -1
- package/packages/web/dist/assets/index-QebPVqwj.js +0 -2
- package/packages/web/src/components/AgentSettings.tsx +0 -328
- package/scripts/mock-openclaw-v2.mjs +0 -486
|
@@ -4,6 +4,7 @@ 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";
|
|
7
8
|
|
|
8
9
|
/** Presence info stored in browser WebSocket attachments (survives DO hibernation). */
|
|
9
10
|
interface BrowserAttachment {
|
|
@@ -29,8 +30,7 @@ const DC_GRACE_MS = 30_000; // 30 s after WebSocket disconnect
|
|
|
29
30
|
* - Use WebSocket Hibernation API so idle users cost zero compute
|
|
30
31
|
*
|
|
31
32
|
* Connection tagging (via serializeAttachment / deserializeAttachment):
|
|
32
|
-
* - "
|
|
33
|
-
* - "openclaw" = legacy WebSocket from the OpenClaw plugin (backward compat)
|
|
33
|
+
* - "openclaw" = the WebSocket from the OpenClaw plugin
|
|
34
34
|
* - "browser:<sessionId>" = a browser client WebSocket
|
|
35
35
|
*/
|
|
36
36
|
export class ConnectionDO implements DurableObject {
|
|
@@ -54,9 +54,6 @@ 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
|
-
|
|
60
57
|
constructor(state: DurableObjectState, env: Env) {
|
|
61
58
|
this.state = state;
|
|
62
59
|
this.env = env;
|
|
@@ -92,8 +89,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
92
89
|
}
|
|
93
90
|
}
|
|
94
91
|
const preVerified = url.searchParams.get("verified") === "1";
|
|
95
|
-
|
|
96
|
-
return this.handleOpenClawConnect(request, preVerified, agentId);
|
|
92
|
+
return this.handleOpenClawConnect(request, preVerified);
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
// Route: /client/:sessionId — Browser client connects here
|
|
@@ -109,8 +105,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
109
105
|
// Route: /models — Available models (REST)
|
|
110
106
|
if (url.pathname === "/models") {
|
|
111
107
|
await this.ensureCachedModels();
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
const models = this.cachedModels.length ? this.cachedModels : (await this.isDemoUser() ? ConnectionDO.DEMO_MODELS : []);
|
|
109
|
+
console.log(`[DO] GET /models — returning ${models.length} models`);
|
|
110
|
+
return Response.json({ models });
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
// Route: /scan-data — Cached OpenClaw scan data (schedule/instructions/model)
|
|
@@ -146,7 +143,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
146
143
|
return;
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
if (tag === "openclaw"
|
|
146
|
+
if (tag === "openclaw") {
|
|
150
147
|
await this.handleOpenClawMessage(ws, parsed);
|
|
151
148
|
} else if (tag?.startsWith("browser:")) {
|
|
152
149
|
await this.handleBrowserMessage(ws, parsed);
|
|
@@ -167,7 +164,8 @@ export class ConnectionDO implements DurableObject {
|
|
|
167
164
|
/** Called when a WebSocket is closed. */
|
|
168
165
|
async webSocketClose(ws: WebSocket, code: number, _reason: string, _wasClean: boolean): Promise<void> {
|
|
169
166
|
const tag = this.getTag(ws);
|
|
170
|
-
if (tag === "openclaw"
|
|
167
|
+
if (tag === "openclaw") {
|
|
168
|
+
// OpenClaw disconnected — notify all browser clients
|
|
171
169
|
this.broadcastToBrowsers(
|
|
172
170
|
JSON.stringify({ type: "openclaw.disconnected" }),
|
|
173
171
|
);
|
|
@@ -190,7 +188,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
190
188
|
|
|
191
189
|
// ---- Connection handlers ----
|
|
192
190
|
|
|
193
|
-
private handleOpenClawConnect(request: Request, preVerified = false
|
|
191
|
+
private handleOpenClawConnect(request: Request, preVerified = false): Response {
|
|
194
192
|
if (request.headers.get("Upgrade") !== "websocket") {
|
|
195
193
|
return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
196
194
|
}
|
|
@@ -206,14 +204,12 @@ export class ConnectionDO implements DurableObject {
|
|
|
206
204
|
}
|
|
207
205
|
this.lastOpenClawAcceptedAt = now;
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Safety valve: if stale sockets with the same tag accumulated (e.g. from
|
|
207
|
+
// Safety valve: if stale openclaw sockets accumulated (e.g. from
|
|
212
208
|
// rapid reconnects that authenticated but then lost their edge
|
|
213
209
|
// connection), close them all before accepting a new one.
|
|
214
|
-
const existing = this.state.getWebSockets(
|
|
210
|
+
const existing = this.state.getWebSockets("openclaw");
|
|
215
211
|
if (existing.length > 3) {
|
|
216
|
-
console.warn(`[DO] Safety valve: ${existing.length}
|
|
212
|
+
console.warn(`[DO] Safety valve: ${existing.length} openclaw sockets, closing all`);
|
|
217
213
|
for (const s of existing) {
|
|
218
214
|
try { s.close(4009, "replaced"); } catch { /* dead */ }
|
|
219
215
|
}
|
|
@@ -222,12 +218,12 @@ export class ConnectionDO implements DurableObject {
|
|
|
222
218
|
const pair = new WebSocketPair();
|
|
223
219
|
const [client, server] = [pair[0], pair[1]];
|
|
224
220
|
|
|
225
|
-
this.state.acceptWebSocket(server, [
|
|
221
|
+
this.state.acceptWebSocket(server, ["openclaw"]);
|
|
226
222
|
|
|
227
223
|
// If the API worker already verified the token against D1, mark as
|
|
228
224
|
// pre-verified. The plugin still sends an auth message, which we'll
|
|
229
225
|
// fast-track through without re-validating the token.
|
|
230
|
-
server.serializeAttachment({ authenticated: false, tag:
|
|
226
|
+
server.serializeAttachment({ authenticated: false, tag: "openclaw", preVerified });
|
|
231
227
|
|
|
232
228
|
return new Response(null, { status: 101, webSocket: client });
|
|
233
229
|
}
|
|
@@ -276,12 +272,11 @@ export class ConnectionDO implements DurableObject {
|
|
|
276
272
|
const isValid = attachment?.preVerified || await this.validatePairingToken(token);
|
|
277
273
|
|
|
278
274
|
if (isValid) {
|
|
279
|
-
// Close other sockets
|
|
275
|
+
// Close ALL other openclaw sockets. Use custom code 4009 so
|
|
280
276
|
// well-behaved plugins know they were replaced (not a crash)
|
|
281
277
|
// and should NOT reconnect. The Worker-level rate limit (10s)
|
|
282
278
|
// prevents the resulting close event from flooding the DO.
|
|
283
|
-
const
|
|
284
|
-
const existingSockets = this.state.getWebSockets(wsTag);
|
|
279
|
+
const existingSockets = this.state.getWebSockets("openclaw");
|
|
285
280
|
let closedCount = 0;
|
|
286
281
|
for (const oldWs of existingSockets) {
|
|
287
282
|
if (oldWs !== ws) {
|
|
@@ -323,15 +318,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
323
318
|
return;
|
|
324
319
|
}
|
|
325
320
|
|
|
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
|
-
|
|
335
321
|
// Persist agent messages to D1 (skip transient stream events)
|
|
336
322
|
if (msg.type === "agent.text" || msg.type === "agent.media" || msg.type === "agent.a2ui") {
|
|
337
323
|
console.log("[DO] Agent outbound:", JSON.stringify({
|
|
@@ -357,9 +343,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
357
343
|
// Bitmask: bit 0 = text encrypted, bit 1 = media encrypted
|
|
358
344
|
const encryptedBits = (msg.encrypted ? 1 : 0) | (msg.mediaEncrypted ? 2 : 0);
|
|
359
345
|
|
|
360
|
-
// Extract agentId from the sending WebSocket's attachment
|
|
361
|
-
const wsAtt = ws.deserializeAttachment() as { agentId?: string } | null;
|
|
362
|
-
|
|
363
346
|
await this.persistMessage({
|
|
364
347
|
id: msg.messageId as string | undefined,
|
|
365
348
|
sender: "agent",
|
|
@@ -369,7 +352,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
369
352
|
mediaUrl: persistedMediaUrl,
|
|
370
353
|
a2ui: msg.jsonl as string | undefined,
|
|
371
354
|
encrypted: encryptedBits,
|
|
372
|
-
senderAgentId: (msg.agentId as string | undefined) ?? wsAtt?.agentId ?? undefined,
|
|
373
355
|
});
|
|
374
356
|
}
|
|
375
357
|
|
|
@@ -383,7 +365,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
383
365
|
if (msg.type === "task.schedule.ack" && msg.ok && msg.taskId && msg.cronJobId) {
|
|
384
366
|
try {
|
|
385
367
|
await this.env.DB.prepare(
|
|
386
|
-
"UPDATE tasks SET
|
|
368
|
+
"UPDATE tasks SET openclaw_cron_job_id = ? WHERE id = ?",
|
|
387
369
|
).bind(msg.cronJobId, msg.taskId).run();
|
|
388
370
|
console.log(`[DO] Updated task ${msg.taskId} with cronJobId ${msg.cronJobId}`);
|
|
389
371
|
} catch (err) {
|
|
@@ -424,44 +406,11 @@ export class ConnectionDO implements DurableObject {
|
|
|
424
406
|
await this.handleJobUpdate(msg);
|
|
425
407
|
}
|
|
426
408
|
|
|
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
|
-
|
|
439
409
|
// Forward all messages to browser clients (strip notifyPreview — plaintext
|
|
440
410
|
// must not be relayed to browser WebSockets; browsers decrypt locally)
|
|
441
411
|
if (msg.type === "agent.text") {
|
|
442
412
|
console.log(`[DO] Forwarding agent.text to browsers: encrypted=${msg.encrypted}, messageId=${msg.messageId}, textLen=${typeof msg.text === "string" ? msg.text.length : "?"}`);
|
|
443
413
|
}
|
|
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
|
-
|
|
465
414
|
const { notifyPreview: _stripped, ...msgForBrowser } = msg;
|
|
466
415
|
this.broadcastToBrowsers(JSON.stringify(msgForBrowser));
|
|
467
416
|
|
|
@@ -509,40 +458,27 @@ export class ConnectionDO implements DurableObject {
|
|
|
509
458
|
}
|
|
510
459
|
|
|
511
460
|
ws.serializeAttachment({ ...attachment, authenticated: true });
|
|
461
|
+
// Include userId so the browser can derive the E2E key
|
|
512
462
|
const doUserId2 = doUserId ?? payload.sub;
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
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);
|
|
463
|
+
// Persist userId to storage so isDemoUser() and other helpers work
|
|
464
|
+
// even when no OpenClaw plugin has connected (e.g. demo mode).
|
|
465
|
+
if (!doUserId) {
|
|
466
|
+
await this.state.storage.put("userId", doUserId2);
|
|
532
467
|
}
|
|
468
|
+
ws.send(JSON.stringify({ type: "auth.ok", userId: doUserId2 }));
|
|
533
469
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
// Send current connection status + cached models
|
|
470
|
+
// Send current OpenClaw connection status + cached models
|
|
537
471
|
await this.ensureCachedModels();
|
|
538
|
-
const
|
|
472
|
+
const isDemo = isDemoUserId(doUserId2);
|
|
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);
|
|
539
476
|
ws.send(
|
|
540
477
|
JSON.stringify({
|
|
541
478
|
type: "connection.status",
|
|
542
479
|
openclawConnected,
|
|
543
|
-
defaultModel:
|
|
544
|
-
models
|
|
545
|
-
connectedAgents: availableAgents.filter((a) => a.status === "connected"),
|
|
480
|
+
defaultModel: model,
|
|
481
|
+
models,
|
|
546
482
|
}),
|
|
547
483
|
);
|
|
548
484
|
return;
|
|
@@ -585,7 +521,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
585
521
|
type: msg.type,
|
|
586
522
|
sessionKey: msg.sessionKey,
|
|
587
523
|
messageId: msg.messageId,
|
|
588
|
-
targetAgentId: msg.targetAgentId,
|
|
589
524
|
hasMedia: !!msg.mediaUrl,
|
|
590
525
|
}));
|
|
591
526
|
await this.persistMessage({
|
|
@@ -595,34 +530,14 @@ export class ConnectionDO implements DurableObject {
|
|
|
595
530
|
text: (msg.text ?? "") as string,
|
|
596
531
|
mediaUrl: msg.mediaUrl as string | undefined,
|
|
597
532
|
encrypted: msg.encrypted ? 1 : 0,
|
|
598
|
-
targetAgentId: msg.targetAgentId as string | undefined,
|
|
599
533
|
});
|
|
600
534
|
}
|
|
601
535
|
|
|
602
|
-
//
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
536
|
+
// Forward user messages to OpenClaw
|
|
537
|
+
const openclawWs = this.getOpenClawSocket();
|
|
538
|
+
if (openclawWs) {
|
|
539
|
+
// If this is a thread message, look up the parent message and attach it
|
|
540
|
+
// so the plugin can inject the thread-origin context into the AI conversation.
|
|
626
541
|
const sessionKey = msg.sessionKey as string | undefined;
|
|
627
542
|
const threadMatch = sessionKey?.match(/:thread:(.+)$/);
|
|
628
543
|
let enrichedMsg = msg;
|
|
@@ -642,17 +557,20 @@ export class ConnectionDO implements DurableObject {
|
|
|
642
557
|
parentSender: parentRow.sender as string,
|
|
643
558
|
parentEncrypted: (parentRow.encrypted ?? 0) as number,
|
|
644
559
|
};
|
|
560
|
+
console.log(`[DO] Attached parent message for thread: parentId=${parentId}, sender=${parentRow.sender}, encrypted=${parentRow.encrypted}`);
|
|
645
561
|
}
|
|
646
562
|
} catch (err) {
|
|
647
|
-
console.error(`[DO] Failed to fetch parent message for thread:`, err);
|
|
563
|
+
console.error(`[DO] Failed to fetch parent message for thread ${parentId}:`, err);
|
|
648
564
|
}
|
|
649
565
|
}
|
|
650
|
-
|
|
566
|
+
openclawWs.send(JSON.stringify(enrichedMsg));
|
|
567
|
+
} else if (await this.isDemoUser()) {
|
|
568
|
+
await this.handleDemoMockReply(ws, msg);
|
|
651
569
|
} else {
|
|
652
570
|
ws.send(
|
|
653
571
|
JSON.stringify({
|
|
654
572
|
type: "error",
|
|
655
|
-
message: "
|
|
573
|
+
message: "OpenClaw is not connected. Please check your OpenClaw instance.",
|
|
656
574
|
}),
|
|
657
575
|
);
|
|
658
576
|
}
|
|
@@ -668,6 +586,10 @@ export class ConnectionDO implements DurableObject {
|
|
|
668
586
|
private async handleGetScanData(): Promise<Response> {
|
|
669
587
|
const openclawWs = this.getOpenClawSocket();
|
|
670
588
|
if (!openclawWs) {
|
|
589
|
+
// Demo users get mock scan data instead of 503
|
|
590
|
+
if (await this.isDemoUser()) {
|
|
591
|
+
return Response.json({ tasks: [] });
|
|
592
|
+
}
|
|
671
593
|
return Response.json(
|
|
672
594
|
{ error: "OpenClaw not connected", tasks: [] },
|
|
673
595
|
{ status: 503 },
|
|
@@ -708,12 +630,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
708
630
|
}
|
|
709
631
|
}
|
|
710
632
|
|
|
711
|
-
private handleStatus(): Response {
|
|
633
|
+
private async handleStatus(): Promise<Response> {
|
|
712
634
|
const sockets = this.state.getWebSockets();
|
|
713
|
-
const openclawSocket = sockets.find((s) =>
|
|
714
|
-
const t = this.getTag(s);
|
|
715
|
-
return t === "openclaw" || t?.startsWith("agent:");
|
|
716
|
-
});
|
|
635
|
+
const openclawSocket = sockets.find((s) => this.getTag(s) === "openclaw");
|
|
717
636
|
const browserCount = sockets.filter((s) =>
|
|
718
637
|
this.getTag(s)?.startsWith("browser:"),
|
|
719
638
|
).length;
|
|
@@ -724,9 +643,10 @@ export class ConnectionDO implements DurableObject {
|
|
|
724
643
|
openclawAuthenticated = att?.authenticated ?? false;
|
|
725
644
|
}
|
|
726
645
|
|
|
646
|
+
const isDemo = await this.isDemoUser();
|
|
727
647
|
return Response.json({
|
|
728
|
-
openclawConnected: !!openclawSocket,
|
|
729
|
-
openclawAuthenticated,
|
|
648
|
+
openclawConnected: isDemo || !!openclawSocket,
|
|
649
|
+
openclawAuthenticated: isDemo || openclawAuthenticated,
|
|
730
650
|
browserClients: browserCount,
|
|
731
651
|
});
|
|
732
652
|
}
|
|
@@ -784,9 +704,11 @@ export class ConnectionDO implements DurableObject {
|
|
|
784
704
|
return att?.tag ?? null;
|
|
785
705
|
}
|
|
786
706
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
707
|
+
private getOpenClawSocket(): WebSocket | null {
|
|
708
|
+
const sockets = this.state.getWebSockets("openclaw");
|
|
709
|
+
// Return the LAST (newest) authenticated OpenClaw socket.
|
|
710
|
+
// After a reconnection there may briefly be multiple sockets
|
|
711
|
+
// before the stale cleanup in handleOpenClawMessage runs.
|
|
790
712
|
let newest: WebSocket | null = null;
|
|
791
713
|
for (const s of sockets) {
|
|
792
714
|
const att = s.deserializeAttachment() as { authenticated: boolean } | null;
|
|
@@ -795,147 +717,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
795
717
|
return newest ?? sockets[sockets.length - 1] ?? null;
|
|
796
718
|
}
|
|
797
719
|
|
|
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
|
-
|
|
939
720
|
private broadcastToBrowsers(message: string): void {
|
|
940
721
|
const sockets = this.state.getWebSockets();
|
|
941
722
|
for (const s of sockets) {
|
|
@@ -1264,8 +1045,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
1264
1045
|
mediaUrl?: string;
|
|
1265
1046
|
a2ui?: string;
|
|
1266
1047
|
encrypted?: number;
|
|
1267
|
-
senderAgentId?: string;
|
|
1268
|
-
targetAgentId?: string;
|
|
1269
1048
|
}): Promise<void> {
|
|
1270
1049
|
try {
|
|
1271
1050
|
const userId = (await this.state.storage.get<string>("userId")) ?? "unknown";
|
|
@@ -1280,10 +1059,10 @@ export class ConnectionDO implements DurableObject {
|
|
|
1280
1059
|
}
|
|
1281
1060
|
|
|
1282
1061
|
await this.env.DB.prepare(
|
|
1283
|
-
`INSERT INTO messages (id, user_id, session_key, thread_id, sender, text, media_url, a2ui, encrypted
|
|
1284
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
1062
|
+
`INSERT INTO messages (id, user_id, session_key, thread_id, sender, text, media_url, a2ui, encrypted)
|
|
1063
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1285
1064
|
)
|
|
1286
|
-
.bind(id, userId, opts.sessionKey, threadId ?? null, opts.sender, opts.text, opts.mediaUrl ?? null, opts.a2ui ?? null, encrypted
|
|
1065
|
+
.bind(id, userId, opts.sessionKey, threadId ?? null, opts.sender, opts.text, opts.mediaUrl ?? null, opts.a2ui ?? null, encrypted)
|
|
1287
1066
|
.run();
|
|
1288
1067
|
} catch (err) {
|
|
1289
1068
|
console.error("Failed to persist message:", err);
|
|
@@ -1445,7 +1224,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1445
1224
|
|
|
1446
1225
|
// Check if a matching task already exists
|
|
1447
1226
|
const existingTask = await this.env.DB.prepare(
|
|
1448
|
-
"SELECT id, session_key FROM tasks WHERE
|
|
1227
|
+
"SELECT id, session_key FROM tasks WHERE openclaw_cron_job_id = ?",
|
|
1449
1228
|
)
|
|
1450
1229
|
.bind(t.cronJobId)
|
|
1451
1230
|
.first<{ id: string; session_key: string }>();
|
|
@@ -1464,7 +1243,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1464
1243
|
t.cronJobId,
|
|
1465
1244
|
];
|
|
1466
1245
|
await this.env.DB.prepare(
|
|
1467
|
-
`UPDATE tasks SET ${updateParts.join(", ")} WHERE
|
|
1246
|
+
`UPDATE tasks SET ${updateParts.join(", ")} WHERE openclaw_cron_job_id = ?`,
|
|
1468
1247
|
)
|
|
1469
1248
|
.bind(...updateVals)
|
|
1470
1249
|
.run();
|
|
@@ -1483,7 +1262,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1483
1262
|
// D1 only stores basic task metadata — schedule/instructions/model
|
|
1484
1263
|
// belong to OpenClaw and are delivered via task.scan.result WebSocket.
|
|
1485
1264
|
await this.env.DB.prepare(
|
|
1486
|
-
`INSERT INTO tasks (id, channel_id, name, kind,
|
|
1265
|
+
`INSERT INTO tasks (id, channel_id, name, kind, openclaw_cron_job_id, session_key, enabled)
|
|
1487
1266
|
VALUES (?, ?, ?, 'background', ?, ?, ?)`,
|
|
1488
1267
|
)
|
|
1489
1268
|
.bind(taskId, defaultChannelId, taskName, t.cronJobId, sessionKey, t.enabled ? 1 : 0)
|
|
@@ -1494,7 +1273,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1494
1273
|
|
|
1495
1274
|
// Resolve the task record for job persistence (may have just been created)
|
|
1496
1275
|
const taskRecord = await this.env.DB.prepare(
|
|
1497
|
-
"SELECT id, session_key FROM tasks WHERE
|
|
1276
|
+
"SELECT id, session_key FROM tasks WHERE openclaw_cron_job_id = ?",
|
|
1498
1277
|
)
|
|
1499
1278
|
.bind(t.cronJobId)
|
|
1500
1279
|
.first<{ id: string; session_key: string }>();
|
|
@@ -1558,7 +1337,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1558
1337
|
private async ensureDefaultChannel(userId: string): Promise<string> {
|
|
1559
1338
|
// Check if a default channel already exists
|
|
1560
1339
|
const existing = await this.env.DB.prepare(
|
|
1561
|
-
"SELECT id FROM channels WHERE user_id = ? AND
|
|
1340
|
+
"SELECT id FROM channels WHERE user_id = ? AND openclaw_agent_id = 'main' ORDER BY created_at ASC LIMIT 1",
|
|
1562
1341
|
)
|
|
1563
1342
|
.bind(userId)
|
|
1564
1343
|
.first<{ id: string }>();
|
|
@@ -1568,7 +1347,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1568
1347
|
// Create the default channel
|
|
1569
1348
|
const channelId = this.generateId("ch_");
|
|
1570
1349
|
await this.env.DB.prepare(
|
|
1571
|
-
"INSERT INTO channels (id, user_id, name, description,
|
|
1350
|
+
"INSERT INTO channels (id, user_id, name, description, openclaw_agent_id, system_prompt) VALUES (?, ?, 'Default', 'Auto-created channel for imported background tasks', 'main', '')",
|
|
1572
1351
|
)
|
|
1573
1352
|
.bind(channelId, userId)
|
|
1574
1353
|
.run();
|
|
@@ -1586,101 +1365,6 @@ export class ConnectionDO implements DurableObject {
|
|
|
1586
1365
|
return channelId;
|
|
1587
1366
|
}
|
|
1588
1367
|
|
|
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
|
-
|
|
1684
1368
|
/** Generate a short random ID (URL-safe) using CSPRNG (bias-free). */
|
|
1685
1369
|
private generateId(prefix = ""): string {
|
|
1686
1370
|
return generateIdUtil(prefix);
|
|
@@ -1704,7 +1388,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
1704
1388
|
|
|
1705
1389
|
// Find the task by cronJobId
|
|
1706
1390
|
const task = await this.env.DB.prepare(
|
|
1707
|
-
"SELECT id FROM tasks WHERE
|
|
1391
|
+
"SELECT id FROM tasks WHERE openclaw_cron_job_id = ?",
|
|
1708
1392
|
)
|
|
1709
1393
|
.bind(cronJobId)
|
|
1710
1394
|
.first<{ id: string }>();
|
|
@@ -1738,6 +1422,126 @@ export class ConnectionDO implements DurableObject {
|
|
|
1738
1422
|
}
|
|
1739
1423
|
}
|
|
1740
1424
|
|
|
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
|
+
|
|
1741
1545
|
private async validatePairingToken(token: string): Promise<boolean> {
|
|
1742
1546
|
// The API worker validates pairing tokens against D1 before routing
|
|
1743
1547
|
// to the DO (and passes ?verified=1). Connections that arrive here
|