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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/packages/api/src/do/connection-do.ts +186 -382
  3. package/packages/api/src/index.ts +50 -67
  4. package/packages/api/src/routes/agents.ts +3 -3
  5. package/packages/api/src/routes/auth.ts +1 -0
  6. package/packages/api/src/routes/channels.ts +11 -11
  7. package/packages/api/src/routes/demo.ts +156 -0
  8. package/packages/api/src/routes/sessions.ts +5 -5
  9. package/packages/api/src/routes/tasks.ts +33 -33
  10. package/packages/plugin/dist/src/channel.js +50 -0
  11. package/packages/plugin/dist/src/channel.js.map +1 -1
  12. package/packages/plugin/package.json +18 -2
  13. package/packages/web/dist/assets/index-BtPyCBCl.css +1 -0
  14. package/packages/web/dist/assets/index-BtpsFe4Z.js +2 -0
  15. package/packages/web/dist/assets/index-CQbIYr6_.js +2 -0
  16. package/packages/web/dist/assets/{index-CYQMu_-c.js → index-C_GamcQc.js} +1 -1
  17. package/packages/web/dist/assets/index-LiBjPMg2.js +1 -0
  18. package/packages/web/dist/assets/{index-DYCO-ry1.js → index-MyoWvQAH.js} +1 -1
  19. package/packages/web/dist/assets/index-STIPTMK8.js +1516 -0
  20. package/packages/web/dist/assets/{index.esm-CvOpngZM.js → index.esm-BpQAwtdR.js} +1 -1
  21. package/packages/web/dist/assets/{web-D3LMODYp.js → web-BbTzVNLt.js} +1 -1
  22. package/packages/web/dist/assets/{web-1cdhq2RW.js → web-cnzjgNfD.js} +1 -1
  23. package/packages/web/dist/index.html +2 -2
  24. package/packages/web/src/App.tsx +9 -56
  25. package/packages/web/src/api.ts +5 -61
  26. package/packages/web/src/components/ChatWindow.tsx +9 -9
  27. package/packages/web/src/components/CronDetail.tsx +1 -1
  28. package/packages/web/src/components/ImageLightbox.tsx +96 -0
  29. package/packages/web/src/components/LoginPage.tsx +59 -1
  30. package/packages/web/src/components/MessageContent.tsx +17 -2
  31. package/packages/web/src/components/SessionTabs.tsx +1 -1
  32. package/packages/web/src/components/Sidebar.tsx +1 -3
  33. package/packages/web/src/hooks/useIMEComposition.ts +14 -9
  34. package/packages/web/src/store.ts +7 -39
  35. package/packages/web/src/ws.ts +0 -1
  36. package/scripts/dev.sh +0 -53
  37. package/migrations/0013_agents_table.sql +0 -29
  38. package/migrations/0014_agent_sessions.sql +0 -19
  39. package/migrations/0015_message_traces.sql +0 -27
  40. package/migrations/0016_multi_agent_channels_messages.sql +0 -9
  41. package/migrations/0017_rename_cron_job_id.sql +0 -2
  42. package/packages/api/src/protocol-v2.ts +0 -154
  43. package/packages/api/src/routes/agents-v2.ts +0 -192
  44. package/packages/api/src/routes/history-v2.ts +0 -221
  45. package/packages/api/src/routes/migrate-v2.ts +0 -110
  46. package/packages/web/dist/assets/index-BARPtt0v.css +0 -1
  47. package/packages/web/dist/assets/index-Bf-XL3te.js +0 -2
  48. package/packages/web/dist/assets/index-CYlvfpX9.js +0 -1519
  49. package/packages/web/dist/assets/index-CxcpA4Qo.js +0 -1
  50. package/packages/web/dist/assets/index-QebPVqwj.js +0 -2
  51. package/packages/web/src/components/AgentSettings.tsx +0 -328
  52. 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.provider_job_id,
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
- provider_job_id: string | null;
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
- providerJobId: r.provider_job_id,
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
- // v2: try agents table first (multi-agent), fallback to pairing_tokens (legacy)
388
- const agentRow = await c.env.DB.prepare(
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<{ id: string; user_id: string }>();
393
-
394
- if (agentRow) {
395
- userId = agentRow.user_id;
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 connection stats (agents table for v2, pairing_tokens for legacy)
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
- if (agentId) {
430
- c.executionCtx.waitUntil(
431
- c.env.DB.prepare(
432
- `UPDATE agents
433
- SET status = 'connected', last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1, updated_at = unixepoch()
434
- WHERE id = ?`,
435
- ).bind(clientIp, agentId).run(),
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
- // v2: try agents table first, fallback to pairing_tokens
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
- let userId: string;
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, provider_agent_id FROM channels WHERE user_id = ? ORDER BY created_at ASC",
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; provider_agent_id: 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.provider_agent_id}:botschat:${userId}:adhoc`;
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, provider_agent_id, system_prompt, created_at, updated_at FROM channels WHERE user_id = ? ORDER BY created_at DESC",
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
- provider_agent_id: string;
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
- providerAgentId: r.provider_agent_id,
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, providerAgentId, systemPrompt } = await c.req.json<{
41
+ const { name, description, openclawAgentId, systemPrompt } = await c.req.json<{
42
42
  name: string;
43
43
  description?: string;
44
- providerAgentId?: string;
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
- providerAgentId?.trim() ||
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, provider_agent_id, system_prompt) VALUES (?, ?, ?, ?, ?, ?)",
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
- providerAgentId: agentId,
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, provider_agent_id, system_prompt, created_at, updated_at FROM channels WHERE id = ? AND user_id = ?",
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
- provider_agent_id: string;
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
- providerAgentId: row.provider_agent_id,
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, provider_agent_id FROM channels WHERE id = ? AND user_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; provider_agent_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, provider_agent_id FROM channels WHERE id = ? AND user_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; provider_agent_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.provider_agent_id}:botschat:${userId}:ses:${id}`;
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 (?, ?, ?, ?, ?)",