botschat 0.1.20 → 0.1.21
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/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 +18 -2
- package/packages/web/dist/assets/index-BtPyCBCl.css +1 -0
- package/packages/web/dist/assets/index-BtpsFe4Z.js +2 -0
- package/packages/web/dist/assets/index-CQbIYr6_.js +2 -0
- package/packages/web/dist/assets/{index-CYQMu_-c.js → index-C_GamcQc.js} +1 -1
- package/packages/web/dist/assets/index-LiBjPMg2.js +1 -0
- package/packages/web/dist/assets/{index-DYCO-ry1.js → index-MyoWvQAH.js} +1 -1
- package/packages/web/dist/assets/index-STIPTMK8.js +1516 -0
- package/packages/web/dist/assets/{index.esm-CvOpngZM.js → index.esm-BpQAwtdR.js} +1 -1
- package/packages/web/dist/assets/{web-D3LMODYp.js → web-BbTzVNLt.js} +1 -1
- package/packages/web/dist/assets/{web-1cdhq2RW.js → web-cnzjgNfD.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +9 -56
- 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 +59 -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
|
@@ -5,9 +5,6 @@ import { authMiddleware, verifyToken, getJwtSecret, verifyMediaSignature, signMe
|
|
|
5
5
|
import { randomUUID } from "./utils/uuid.js";
|
|
6
6
|
import { auth } from "./routes/auth.js";
|
|
7
7
|
import { agents } from "./routes/agents.js";
|
|
8
|
-
import { agentsV2 } from "./routes/agents-v2.js";
|
|
9
|
-
import { migrateV2 } from "./routes/migrate-v2.js";
|
|
10
|
-
import { historyV2 } from "./routes/history-v2.js";
|
|
11
8
|
import { channels } from "./routes/channels.js";
|
|
12
9
|
import { tasks } from "./routes/tasks.js";
|
|
13
10
|
import { jobs } from "./routes/jobs.js";
|
|
@@ -18,6 +15,7 @@ import { upload } from "./routes/upload.js";
|
|
|
18
15
|
import { push } from "./routes/push.js";
|
|
19
16
|
import { setup } from "./routes/setup.js";
|
|
20
17
|
import { devAuth } from "./routes/dev-auth.js";
|
|
18
|
+
import { demo, isDemoUserId } from "./routes/demo.js";
|
|
21
19
|
|
|
22
20
|
// Re-export the Durable Object class so wrangler can find it
|
|
23
21
|
export { ConnectionDO } from "./do/connection-do.js";
|
|
@@ -91,15 +89,37 @@ app.get("/api/health", (c) => c.json({ status: "ok", version: "0.1.0" }));
|
|
|
91
89
|
// ---- Public routes (no auth) ----
|
|
92
90
|
app.route("/api/auth", auth);
|
|
93
91
|
app.route("/api/dev-auth", devAuth);
|
|
92
|
+
app.route("/api/demo", demo);
|
|
94
93
|
app.route("/api/setup", setup);
|
|
95
94
|
|
|
96
95
|
// ---- Protected routes (require Bearer token) ----
|
|
97
96
|
const protectedApp = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
98
97
|
protectedApp.use("/*", authMiddleware());
|
|
98
|
+
|
|
99
|
+
// Block sensitive operations for the demo user
|
|
100
|
+
const DEMO_BLOCKED_ROUTES: Array<{ method: string; pattern: RegExp }> = [
|
|
101
|
+
{ method: "POST", pattern: /^\/pairing-tokens/ },
|
|
102
|
+
{ method: "POST", pattern: /^\/upload/ },
|
|
103
|
+
{ method: "DELETE", pattern: /^\/$/ }, // DELETE /api (account deletion goes via /api/auth/account)
|
|
104
|
+
];
|
|
105
|
+
protectedApp.use("/*", async (c, next) => {
|
|
106
|
+
const userId = c.get("userId");
|
|
107
|
+
if (!isDemoUserId(userId)) return next();
|
|
108
|
+
const method = c.req.method;
|
|
109
|
+
const path = c.req.path.replace(/^\/api/, "");
|
|
110
|
+
// Block pairing tokens, file upload, and any DELETE (channels, tasks, sessions, account)
|
|
111
|
+
if (method === "DELETE") {
|
|
112
|
+
return c.json({ error: "Demo account cannot perform this action" }, 403);
|
|
113
|
+
}
|
|
114
|
+
for (const r of DEMO_BLOCKED_ROUTES) {
|
|
115
|
+
if (method === r.method && r.pattern.test(path)) {
|
|
116
|
+
return c.json({ error: "Demo account cannot perform this action" }, 403);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return next();
|
|
120
|
+
});
|
|
121
|
+
|
|
99
122
|
protectedApp.route("/agents", agents);
|
|
100
|
-
protectedApp.route("/v2/agents", agentsV2);
|
|
101
|
-
protectedApp.route("/v2/migrate", migrateV2);
|
|
102
|
-
protectedApp.route("/v2/messages", historyV2);
|
|
103
123
|
protectedApp.route("/channels", channels);
|
|
104
124
|
protectedApp.route("/models", models);
|
|
105
125
|
protectedApp.get("/me", async (c) => {
|
|
@@ -215,7 +235,7 @@ protectedApp.get("/tasks", async (c) => {
|
|
|
215
235
|
const kind = c.req.query("kind") ?? "background";
|
|
216
236
|
|
|
217
237
|
const { results } = await c.env.DB.prepare(
|
|
218
|
-
`SELECT t.id, t.channel_id, t.name, t.kind, t.
|
|
238
|
+
`SELECT t.id, t.channel_id, t.name, t.kind, t.openclaw_cron_job_id,
|
|
219
239
|
t.session_key, t.enabled, t.created_at, t.updated_at
|
|
220
240
|
FROM tasks t
|
|
221
241
|
JOIN channels ch ON t.channel_id = ch.id
|
|
@@ -228,7 +248,7 @@ protectedApp.get("/tasks", async (c) => {
|
|
|
228
248
|
channel_id: string;
|
|
229
249
|
name: string;
|
|
230
250
|
kind: string;
|
|
231
|
-
|
|
251
|
+
openclaw_cron_job_id: string | null;
|
|
232
252
|
session_key: string | null;
|
|
233
253
|
enabled: number;
|
|
234
254
|
created_at: number;
|
|
@@ -241,7 +261,7 @@ protectedApp.get("/tasks", async (c) => {
|
|
|
241
261
|
channelId: r.channel_id,
|
|
242
262
|
name: r.name,
|
|
243
263
|
kind: r.kind,
|
|
244
|
-
|
|
264
|
+
openclawCronJobId: r.openclaw_cron_job_id,
|
|
245
265
|
sessionKey: r.session_key,
|
|
246
266
|
enabled: !!r.enabled,
|
|
247
267
|
createdAt: r.created_at,
|
|
@@ -372,8 +392,6 @@ async function verifyUserAccess(c: { req: { header: (n: string) => string | unde
|
|
|
372
392
|
app.all("/api/gateway/:connId", async (c) => {
|
|
373
393
|
let userId = c.req.param("connId");
|
|
374
394
|
|
|
375
|
-
let agentId: string | null = null;
|
|
376
|
-
|
|
377
395
|
if (!userId.startsWith("u_")) {
|
|
378
396
|
const token =
|
|
379
397
|
c.req.query("token") ??
|
|
@@ -384,28 +402,16 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
384
402
|
return c.json({ error: "Token required for gateway connection" }, 401);
|
|
385
403
|
}
|
|
386
404
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"SELECT id, user_id FROM agents WHERE pairing_token = ?",
|
|
405
|
+
const row = await c.env.DB.prepare(
|
|
406
|
+
"SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
|
|
390
407
|
)
|
|
391
408
|
.bind(token)
|
|
392
|
-
.first<{
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
agentId = agentRow.id;
|
|
397
|
-
} else {
|
|
398
|
-
const row = await c.env.DB.prepare(
|
|
399
|
-
"SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
|
|
400
|
-
)
|
|
401
|
-
.bind(token)
|
|
402
|
-
.first<{ user_id: string }>();
|
|
403
|
-
|
|
404
|
-
if (!row) {
|
|
405
|
-
return c.json({ error: "Invalid pairing token" }, 401);
|
|
406
|
-
}
|
|
407
|
-
userId = row.user_id;
|
|
409
|
+
.first<{ user_id: string }>();
|
|
410
|
+
|
|
411
|
+
if (!row) {
|
|
412
|
+
return c.json({ error: "Invalid pairing token" }, 401);
|
|
408
413
|
}
|
|
414
|
+
userId = row.user_id;
|
|
409
415
|
}
|
|
410
416
|
|
|
411
417
|
// --- Worker-level rate limit (Cache API) ---
|
|
@@ -422,27 +428,17 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
422
428
|
});
|
|
423
429
|
}
|
|
424
430
|
|
|
425
|
-
// Audit: update
|
|
431
|
+
// Audit: update pairing token stats (only when not rate-limited)
|
|
426
432
|
const token = c.req.query("token") ?? c.req.header("X-Pairing-Token");
|
|
427
433
|
if (token) {
|
|
428
434
|
const clientIp = c.req.header("CF-Connecting-IP") ?? c.req.header("X-Forwarded-For") ?? "unknown";
|
|
429
|
-
|
|
430
|
-
c.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
);
|
|
437
|
-
} else {
|
|
438
|
-
c.executionCtx.waitUntil(
|
|
439
|
-
c.env.DB.prepare(
|
|
440
|
-
`UPDATE pairing_tokens
|
|
441
|
-
SET last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1
|
|
442
|
-
WHERE token = ?`,
|
|
443
|
-
).bind(clientIp, token).run(),
|
|
444
|
-
);
|
|
445
|
-
}
|
|
435
|
+
c.executionCtx.waitUntil(
|
|
436
|
+
c.env.DB.prepare(
|
|
437
|
+
`UPDATE pairing_tokens
|
|
438
|
+
SET last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1
|
|
439
|
+
WHERE token = ?`,
|
|
440
|
+
).bind(clientIp, token).run(),
|
|
441
|
+
);
|
|
446
442
|
}
|
|
447
443
|
|
|
448
444
|
const doId = c.env.CONNECTION_DO.idFromName(userId);
|
|
@@ -450,7 +446,6 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
450
446
|
const url = new URL(c.req.url);
|
|
451
447
|
url.pathname = `/gateway/${userId}`;
|
|
452
448
|
url.searchParams.set("verified", "1");
|
|
453
|
-
if (agentId) url.searchParams.set("agentId", agentId);
|
|
454
449
|
const doResp = await stub.fetch(new Request(url.toString(), c.req.raw));
|
|
455
450
|
|
|
456
451
|
// Cache the rate limit after the DO responds (success or rate-limited).
|
|
@@ -523,28 +518,16 @@ app.post("/api/plugin-upload", async (c) => {
|
|
|
523
518
|
if (!token) {
|
|
524
519
|
return c.json({ error: "Missing X-Pairing-Token header" }, 401);
|
|
525
520
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const agentRow = await c.env.DB.prepare(
|
|
529
|
-
"SELECT user_id FROM agents WHERE pairing_token = ?",
|
|
521
|
+
const row = await c.env.DB.prepare(
|
|
522
|
+
"SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
|
|
530
523
|
)
|
|
531
524
|
.bind(token)
|
|
532
525
|
.first<{ user_id: string }>();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (agentRow) {
|
|
536
|
-
userId = agentRow.user_id;
|
|
537
|
-
} else {
|
|
538
|
-
const row = await c.env.DB.prepare(
|
|
539
|
-
"SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
|
|
540
|
-
)
|
|
541
|
-
.bind(token)
|
|
542
|
-
.first<{ user_id: string }>();
|
|
543
|
-
if (!row) {
|
|
544
|
-
return c.json({ error: "Invalid pairing token" }, 401);
|
|
545
|
-
}
|
|
546
|
-
userId = row.user_id;
|
|
526
|
+
if (!row) {
|
|
527
|
+
return c.json({ error: "Invalid pairing token" }, 401);
|
|
547
528
|
}
|
|
529
|
+
|
|
530
|
+
const userId = row.user_id;
|
|
548
531
|
const contentType = c.req.header("Content-Type") ?? "";
|
|
549
532
|
if (!contentType.includes("multipart/form-data")) {
|
|
550
533
|
return c.json({ error: "Expected multipart/form-data" }, 400);
|
|
@@ -23,10 +23,10 @@ agents.get("/", async (c) => {
|
|
|
23
23
|
|
|
24
24
|
// 1. Channel-based agents (each channel = one agent, default session = first adhoc task)
|
|
25
25
|
const { results: channels } = await c.env.DB.prepare(
|
|
26
|
-
"SELECT id, name,
|
|
26
|
+
"SELECT id, name, openclaw_agent_id FROM channels WHERE user_id = ? ORDER BY created_at ASC",
|
|
27
27
|
)
|
|
28
28
|
.bind(userId)
|
|
29
|
-
.all<{ id: string; name: string;
|
|
29
|
+
.all<{ id: string; name: string; openclaw_agent_id: string }>();
|
|
30
30
|
|
|
31
31
|
// Find the "General" channel to associate with the default agent (for session support).
|
|
32
32
|
// If the user has created a "General" channel (auto-created on first session "+"),
|
|
@@ -52,7 +52,7 @@ agents.get("/", async (c) => {
|
|
|
52
52
|
.first<{ session_key: string }>();
|
|
53
53
|
|
|
54
54
|
const sessionKey =
|
|
55
|
-
task?.session_key ?? `agent:${ch.
|
|
55
|
+
task?.session_key ?? `agent:${ch.openclaw_agent_id}:botschat:${userId}:adhoc`;
|
|
56
56
|
list.push({
|
|
57
57
|
id: ch.id,
|
|
58
58
|
name: ch.name,
|
|
@@ -357,6 +357,7 @@ auth.get("/me", async (c) => {
|
|
|
357
357
|
auth.delete("/account", async (c) => {
|
|
358
358
|
const userId = c.get("userId" as never) as string;
|
|
359
359
|
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
360
|
+
if (userId.startsWith("demo_")) return c.json({ error: "Demo account cannot be deleted" }, 403);
|
|
360
361
|
|
|
361
362
|
// Delete all user media from R2
|
|
362
363
|
const prefix = `${userId}/`;
|
|
@@ -9,14 +9,14 @@ channels.get("/", async (c) => {
|
|
|
9
9
|
const userId = c.get("userId");
|
|
10
10
|
|
|
11
11
|
const { results } = await c.env.DB.prepare(
|
|
12
|
-
"SELECT id, name, description,
|
|
12
|
+
"SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM channels WHERE user_id = ? ORDER BY created_at DESC",
|
|
13
13
|
)
|
|
14
14
|
.bind(userId)
|
|
15
15
|
.all<{
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
18
18
|
description: string;
|
|
19
|
-
|
|
19
|
+
openclaw_agent_id: string;
|
|
20
20
|
system_prompt: string;
|
|
21
21
|
created_at: number;
|
|
22
22
|
updated_at: number;
|
|
@@ -27,7 +27,7 @@ channels.get("/", async (c) => {
|
|
|
27
27
|
id: r.id,
|
|
28
28
|
name: r.name,
|
|
29
29
|
description: r.description,
|
|
30
|
-
|
|
30
|
+
openclawAgentId: r.openclaw_agent_id,
|
|
31
31
|
systemPrompt: r.system_prompt,
|
|
32
32
|
createdAt: r.created_at,
|
|
33
33
|
updatedAt: r.updated_at,
|
|
@@ -38,10 +38,10 @@ channels.get("/", async (c) => {
|
|
|
38
38
|
/** POST /api/channels — create a new channel */
|
|
39
39
|
channels.post("/", async (c) => {
|
|
40
40
|
const userId = c.get("userId");
|
|
41
|
-
const { name, description,
|
|
41
|
+
const { name, description, openclawAgentId, systemPrompt } = await c.req.json<{
|
|
42
42
|
name: string;
|
|
43
43
|
description?: string;
|
|
44
|
-
|
|
44
|
+
openclawAgentId?: string;
|
|
45
45
|
systemPrompt?: string;
|
|
46
46
|
}>();
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ channels.post("/", async (c) => {
|
|
|
52
52
|
const id = generateId("ch_");
|
|
53
53
|
// Default agent ID derived from channel name (slug)
|
|
54
54
|
const agentId =
|
|
55
|
-
|
|
55
|
+
openclawAgentId?.trim() ||
|
|
56
56
|
name
|
|
57
57
|
.trim()
|
|
58
58
|
.toLowerCase()
|
|
@@ -60,7 +60,7 @@ channels.post("/", async (c) => {
|
|
|
60
60
|
.replace(/^-|-$/g, "");
|
|
61
61
|
|
|
62
62
|
await c.env.DB.prepare(
|
|
63
|
-
"INSERT INTO channels (id, user_id, name, description,
|
|
63
|
+
"INSERT INTO channels (id, user_id, name, description, openclaw_agent_id, system_prompt) VALUES (?, ?, ?, ?, ?, ?)",
|
|
64
64
|
)
|
|
65
65
|
.bind(id, userId, name.trim(), description?.trim() ?? "", agentId, systemPrompt?.trim() ?? "")
|
|
66
66
|
.run();
|
|
@@ -88,7 +88,7 @@ channels.post("/", async (c) => {
|
|
|
88
88
|
id,
|
|
89
89
|
name: name.trim(),
|
|
90
90
|
description: description?.trim() ?? "",
|
|
91
|
-
|
|
91
|
+
openclawAgentId: agentId,
|
|
92
92
|
systemPrompt: systemPrompt?.trim() ?? "",
|
|
93
93
|
},
|
|
94
94
|
201,
|
|
@@ -101,14 +101,14 @@ channels.get("/:id", async (c) => {
|
|
|
101
101
|
const channelId = c.req.param("id");
|
|
102
102
|
|
|
103
103
|
const row = await c.env.DB.prepare(
|
|
104
|
-
"SELECT id, name, description,
|
|
104
|
+
"SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM channels WHERE id = ? AND user_id = ?",
|
|
105
105
|
)
|
|
106
106
|
.bind(channelId, userId)
|
|
107
107
|
.first<{
|
|
108
108
|
id: string;
|
|
109
109
|
name: string;
|
|
110
110
|
description: string;
|
|
111
|
-
|
|
111
|
+
openclaw_agent_id: string;
|
|
112
112
|
system_prompt: string;
|
|
113
113
|
created_at: number;
|
|
114
114
|
updated_at: number;
|
|
@@ -120,7 +120,7 @@ channels.get("/:id", async (c) => {
|
|
|
120
120
|
id: row.id,
|
|
121
121
|
name: row.name,
|
|
122
122
|
description: row.description,
|
|
123
|
-
|
|
123
|
+
openclawAgentId: row.openclaw_agent_id,
|
|
124
124
|
systemPrompt: row.system_prompt,
|
|
125
125
|
createdAt: row.created_at,
|
|
126
126
|
updatedAt: row.updated_at,
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
import { createToken, createRefreshToken, getJwtSecret } from "../utils/auth.js";
|
|
4
|
+
import { generateId } from "../utils/id.js";
|
|
5
|
+
import { randomUUID } from "../utils/uuid.js";
|
|
6
|
+
|
|
7
|
+
const DEMO_USER_PREFIX = "demo_";
|
|
8
|
+
const DEMO_DISPLAY_NAME = "Demo User";
|
|
9
|
+
const DEMO_TTL_SECONDS = 24 * 3600; // 24 hours
|
|
10
|
+
|
|
11
|
+
function isDemoUserId(userId: string): boolean {
|
|
12
|
+
return userId.startsWith(DEMO_USER_PREFIX);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const demo = new Hono<{ Bindings: Env }>();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* POST /api/demo/login — public endpoint for Google Play reviewers and demos.
|
|
19
|
+
* Each call creates a fresh isolated demo user with seeded data.
|
|
20
|
+
* Old demo users (>24h) are cleaned up in the background.
|
|
21
|
+
* Rate limited: 1 request per 5 seconds per IP (via Cache API).
|
|
22
|
+
*/
|
|
23
|
+
demo.post("/login", async (c) => {
|
|
24
|
+
// Rate limit by IP
|
|
25
|
+
const ip = c.req.header("CF-Connecting-IP") ?? c.req.header("X-Forwarded-For") ?? "unknown";
|
|
26
|
+
const cache = caches.default;
|
|
27
|
+
const rateCacheUrl = `https://rate.internal/demo-login/${ip}`;
|
|
28
|
+
const rateCacheReq = new Request(rateCacheUrl);
|
|
29
|
+
const cached = await cache.match(rateCacheReq);
|
|
30
|
+
if (cached) {
|
|
31
|
+
return c.json({ error: "Too many requests, try again later" }, 429);
|
|
32
|
+
}
|
|
33
|
+
c.executionCtx.waitUntil(
|
|
34
|
+
cache.put(rateCacheReq, new Response(null, {
|
|
35
|
+
headers: { "Cache-Control": "public, max-age=5" },
|
|
36
|
+
})),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const jwtSecret = getJwtSecret(c.env);
|
|
40
|
+
const userId = DEMO_USER_PREFIX + generateId("").slice(0, 12);
|
|
41
|
+
const email = `${userId}@demo.botschat.app`;
|
|
42
|
+
|
|
43
|
+
await c.env.DB.prepare(
|
|
44
|
+
"INSERT INTO users (id, email, password_hash, display_name) VALUES (?, ?, '', ?)",
|
|
45
|
+
).bind(userId, email, DEMO_DISPLAY_NAME).run();
|
|
46
|
+
|
|
47
|
+
await seedDemoData(c.env.DB, userId);
|
|
48
|
+
|
|
49
|
+
// Clean up expired demo users in the background (non-blocking)
|
|
50
|
+
c.executionCtx.waitUntil(cleanupOldDemoUsers(c.env.DB));
|
|
51
|
+
|
|
52
|
+
const token = await createToken(userId, jwtSecret);
|
|
53
|
+
const refreshToken = await createRefreshToken(userId, jwtSecret);
|
|
54
|
+
|
|
55
|
+
return c.json({
|
|
56
|
+
id: userId,
|
|
57
|
+
email,
|
|
58
|
+
displayName: DEMO_DISPLAY_NAME,
|
|
59
|
+
token,
|
|
60
|
+
refreshToken,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async function cleanupOldDemoUsers(db: D1Database) {
|
|
65
|
+
try {
|
|
66
|
+
const cutoff = Math.floor(Date.now() / 1000) - DEMO_TTL_SECONDS;
|
|
67
|
+
const { results } = await db.prepare(
|
|
68
|
+
"SELECT id FROM users WHERE id LIKE 'demo_%' AND created_at < ? LIMIT 20",
|
|
69
|
+
).bind(cutoff).all<{ id: string }>();
|
|
70
|
+
|
|
71
|
+
for (const row of results ?? []) {
|
|
72
|
+
await db.batch([
|
|
73
|
+
db.prepare("DELETE FROM messages WHERE user_id = ?").bind(row.id),
|
|
74
|
+
db.prepare("DELETE FROM jobs WHERE user_id = ?").bind(row.id),
|
|
75
|
+
db.prepare("DELETE FROM tasks WHERE channel_id IN (SELECT id FROM channels WHERE user_id = ?)").bind(row.id),
|
|
76
|
+
db.prepare("DELETE FROM sessions WHERE user_id = ?").bind(row.id),
|
|
77
|
+
db.prepare("DELETE FROM channels WHERE user_id = ?").bind(row.id),
|
|
78
|
+
db.prepare("DELETE FROM pairing_tokens WHERE user_id = ?").bind(row.id),
|
|
79
|
+
db.prepare("DELETE FROM push_tokens WHERE user_id = ?").bind(row.id),
|
|
80
|
+
db.prepare("DELETE FROM users WHERE id = ?").bind(row.id),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
if ((results?.length ?? 0) > 0) {
|
|
84
|
+
console.log(`[demo] Cleaned up ${results!.length} expired demo users`);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error("[demo] Cleanup failed:", err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function seedDemoData(db: D1Database, userId: string) {
|
|
92
|
+
const now = Math.floor(Date.now() / 1000);
|
|
93
|
+
|
|
94
|
+
const ch1 = generateId("ch_");
|
|
95
|
+
const ch2 = generateId("ch_");
|
|
96
|
+
|
|
97
|
+
await db.batch([
|
|
98
|
+
db.prepare(
|
|
99
|
+
"INSERT INTO channels (id, user_id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at) VALUES (?, ?, ?, ?, 'main', '', ?, ?)",
|
|
100
|
+
).bind(ch1, userId, "General", "Default chat channel", now, now),
|
|
101
|
+
db.prepare(
|
|
102
|
+
"INSERT INTO channels (id, user_id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at) VALUES (?, ?, ?, ?, 'main', '', ?, ?)",
|
|
103
|
+
).bind(ch2, userId, "Tasks Demo", "Background task demo channel", now, now),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const adhocTask1 = generateId("tsk_");
|
|
107
|
+
const adhocTask2 = generateId("tsk_");
|
|
108
|
+
const ses1 = generateId("ses_");
|
|
109
|
+
const ses2 = generateId("ses_");
|
|
110
|
+
const sessionKey1 = `agent:main:botschat:${userId}:adhoc`;
|
|
111
|
+
const sessionKey2 = `agent:main:botschat:${userId}:adhoc:${ch2}`;
|
|
112
|
+
|
|
113
|
+
await db.batch([
|
|
114
|
+
db.prepare(
|
|
115
|
+
"INSERT INTO tasks (id, channel_id, name, kind, session_key, enabled, created_at, updated_at) VALUES (?, ?, 'Ad Hoc Chat', 'adhoc', ?, 1, ?, ?)",
|
|
116
|
+
).bind(adhocTask1, ch1, sessionKey1, now, now),
|
|
117
|
+
db.prepare(
|
|
118
|
+
"INSERT INTO tasks (id, channel_id, name, kind, session_key, enabled, created_at, updated_at) VALUES (?, ?, 'Ad Hoc Chat', 'adhoc', ?, 1, ?, ?)",
|
|
119
|
+
).bind(adhocTask2, ch2, sessionKey2, now, now),
|
|
120
|
+
db.prepare(
|
|
121
|
+
"INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, 'Session 1', ?)",
|
|
122
|
+
).bind(ses1, ch1, userId, sessionKey1),
|
|
123
|
+
db.prepare(
|
|
124
|
+
"INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, 'Session 1', ?)",
|
|
125
|
+
).bind(ses2, ch2, userId, sessionKey2),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const bgTask = generateId("tsk_");
|
|
129
|
+
const bgCronId = randomUUID();
|
|
130
|
+
const bgSessionKey = `agent:main:botschat:${userId}:task:${bgTask}`;
|
|
131
|
+
|
|
132
|
+
await db.prepare(
|
|
133
|
+
"INSERT INTO tasks (id, channel_id, name, kind, openclaw_cron_job_id, session_key, enabled, created_at, updated_at) VALUES (?, ?, ?, 'background', ?, ?, 1, ?, ?)",
|
|
134
|
+
).bind(bgTask, ch2, "Daily Summary", bgCronId, bgSessionKey, now, now).run();
|
|
135
|
+
|
|
136
|
+
const jobId = `job_demo_${Date.now()}`;
|
|
137
|
+
await db.prepare(
|
|
138
|
+
"INSERT INTO jobs (id, task_id, user_id, session_key, status, started_at, finished_at, duration_ms, summary, created_at) VALUES (?, ?, ?, ?, 'ok', ?, ?, 2300, 'Daily summary generated successfully.', ?)",
|
|
139
|
+
).bind(jobId, bgTask, userId, bgSessionKey, now - 3600, now - 3600 + 2, now).run();
|
|
140
|
+
|
|
141
|
+
const msgs = [
|
|
142
|
+
{ sender: "user", text: "Hello! What can you do?" },
|
|
143
|
+
{ sender: "agent", text: "Hi there! I'm your AI assistant powered by OpenClaw. I can help with:\n\n- **Chat**: Ask me anything — coding, writing, research, brainstorming\n- **Scheduled Tasks**: Set up recurring background jobs (e.g. daily summaries, monitoring)\n- **Multi-channel**: Organize conversations into separate channels\n- **E2E Encryption**: All messages can be end-to-end encrypted\n\nTry typing a message below!" },
|
|
144
|
+
{ sender: "user", text: "Can you summarize a webpage for me?" },
|
|
145
|
+
{ sender: "agent", text: "Absolutely! Just paste the URL and I'll summarize it for you. I can also extract key points, translate content, or answer questions about the page.\n\nFor example, you could say:\n> Summarize https://example.com/article\n\nNote: In this demo, I'll echo your messages back. Connect a real OpenClaw instance for full AI capabilities." },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const msgBatch = msgs.map((m, i) =>
|
|
149
|
+
db.prepare(
|
|
150
|
+
"INSERT INTO messages (id, user_id, session_key, sender, text, encrypted, created_at) VALUES (?, ?, ?, ?, ?, 0, ?)",
|
|
151
|
+
).bind(randomUUID(), userId, sessionKey1, m.sender, m.text, now - 300 + i * 30),
|
|
152
|
+
);
|
|
153
|
+
await db.batch(msgBatch);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { demo, isDemoUserId };
|
|
@@ -11,10 +11,10 @@ sessions.get("/", async (c) => {
|
|
|
11
11
|
|
|
12
12
|
// Verify channel ownership
|
|
13
13
|
const channel = await c.env.DB.prepare(
|
|
14
|
-
"SELECT id,
|
|
14
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
15
15
|
)
|
|
16
16
|
.bind(channelId, userId)
|
|
17
|
-
.first<{ id: string;
|
|
17
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
18
18
|
|
|
19
19
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
20
20
|
|
|
@@ -48,10 +48,10 @@ sessions.post("/", async (c) => {
|
|
|
48
48
|
|
|
49
49
|
// Verify channel ownership
|
|
50
50
|
const channel = await c.env.DB.prepare(
|
|
51
|
-
"SELECT id,
|
|
51
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
52
52
|
)
|
|
53
53
|
.bind(channelId, userId)
|
|
54
|
-
.first<{ id: string;
|
|
54
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
55
55
|
|
|
56
56
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
57
57
|
|
|
@@ -67,7 +67,7 @@ sessions.post("/", async (c) => {
|
|
|
67
67
|
|
|
68
68
|
const sessionName = name?.trim() || `Session ${count + 1}`;
|
|
69
69
|
const id = generateId("ses_");
|
|
70
|
-
const sessionKey = `agent:${channel.
|
|
70
|
+
const sessionKey = `agent:${channel.openclaw_agent_id}:botschat:${userId}:ses:${id}`;
|
|
71
71
|
|
|
72
72
|
await c.env.DB.prepare(
|
|
73
73
|
"INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, ?, ?)",
|