botschat 0.1.17 → 0.1.19
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 +12 -0
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +145 -8
- package/packages/api/src/index.ts +26 -0
- package/packages/api/src/routes/auth.ts +1 -0
- package/packages/api/src/routes/demo.ts +156 -0
- package/packages/plugin/dist/index.d.ts +1 -0
- package/packages/plugin/dist/index.d.ts.map +1 -1
- package/packages/plugin/dist/index.js +2 -1
- package/packages/plugin/dist/index.js.map +1 -1
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +351 -68
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/dist/src/runtime.d.ts +2 -0
- package/packages/plugin/dist/src/runtime.d.ts.map +1 -1
- package/packages/plugin/dist/src/runtime.js +10 -0
- package/packages/plugin/dist/src/runtime.js.map +1 -1
- package/packages/plugin/dist/src/types.d.ts +12 -0
- package/packages/plugin/dist/src/types.d.ts.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-DzYqprDN.js → index-C_GamcQc.js} +1 -1
- package/packages/web/dist/assets/index-LiBjPMg2.js +1 -0
- package/packages/web/dist/assets/{index-D3T7sc-R.js → index-MyoWvQAH.js} +1 -1
- package/packages/web/dist/assets/index-STIPTMK8.js +1516 -0
- package/packages/web/dist/assets/{index.esm-COzWPkKi.js → index.esm-BpQAwtdR.js} +1 -1
- package/packages/web/dist/assets/{web-DFQypSd0.js → web-BbTzVNLt.js} +1 -1
- package/packages/web/dist/assets/{web-CxXbaApe.js → web-cnzjgNfD.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +32 -0
- package/packages/web/src/api.ts +2 -0
- package/packages/web/src/components/ChatWindow.tsx +125 -5
- 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/hooks/useIMEComposition.ts +14 -9
- package/packages/web/src/store.ts +47 -4
- package/packages/web/src/ws.ts +21 -1
- package/scripts/mock-openclaw.mjs +35 -0
- package/packages/web/dist/assets/index-B5GU1yVt.css +0 -1
- package/packages/web/dist/assets/index-CO9YgLst.js +0 -2
- package/packages/web/dist/assets/index-ClDrCe_c.js +0 -1
- package/packages/web/dist/assets/index-DPEosppm.js +0 -2
- package/packages/web/dist/assets/index-IVUdSd9w.js +0 -1516
package/README.md
CHANGED
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/botschat)
|
|
4
4
|
[](https://www.npmjs.com/package/@botschat/botschat)
|
|
5
5
|
[](LICENSE)
|
|
6
|
+
[](https://github.com/botschat-app/botsChat/releases/latest/download/BotsChat-mac.dmg)
|
|
7
|
+
[](https://apps.apple.com/app/botschat-chat-with-agent/id6759292058)
|
|
6
8
|
|
|
7
9
|
A self-hosted, **end-to-end encrypted** chat interface for [OpenClaw](https://github.com/openclaw/openclaw) AI agents.
|
|
8
10
|
|
|
9
11
|
BotsChat gives you a modern, Slack-like web UI to interact with your OpenClaw agents — organize conversations into **Channels**, schedule **Background Tasks**, and monitor **Job** executions. With **E2E encryption**, your chat messages, cron prompts, and job summaries are encrypted on your device before they ever leave — the server only sees ciphertext it cannot decrypt. Your API keys and data never leave your machine.
|
|
10
12
|
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
https://github.com/user-attachments/assets/e727ef9e-53b9-40d4-b943-c02019588203
|
|
16
|
+
|
|
17
|
+
[▶ Watch in HD on YouTube](https://www.youtube.com/watch?v=_ifqYhoV7Jk)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
11
21
|
## Key Features
|
|
12
22
|
|
|
13
23
|
### Structured Conversation Management
|
|
@@ -95,6 +105,8 @@ BotsChat is **100% open source** — the [same code](https://github.com/botschat
|
|
|
95
105
|
| **B. Run Locally** | Development, no cloud account | Yes |
|
|
96
106
|
| **C. Deploy to Cloudflare** | Remote access (e.g. from phone) | Yes |
|
|
97
107
|
|
|
108
|
+
> **Native Apps**: A macOS client is available — [download the latest DMG](https://github.com/botschat-app/botsChat/releases/latest/download/BotsChat-mac.dmg) (signed and notarized, Apple Silicon + Intel). An iOS app is also available on the [App Store](https://apps.apple.com/app/botschat-chat-with-agent/id6759292058).
|
|
109
|
+
|
|
98
110
|
Pick one below and follow its steps, then continue to [Install the OpenClaw Plugin](#install-the-openclaw-plugin).
|
|
99
111
|
|
|
100
112
|
---
|
package/package.json
CHANGED
|
@@ -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 {
|
|
@@ -104,8 +105,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
104
105
|
// Route: /models — Available models (REST)
|
|
105
106
|
if (url.pathname === "/models") {
|
|
106
107
|
await this.ensureCachedModels();
|
|
107
|
-
|
|
108
|
-
|
|
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 });
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// Route: /scan-data — Cached OpenClaw scan data (schedule/instructions/model)
|
|
@@ -458,17 +460,25 @@ export class ConnectionDO implements DurableObject {
|
|
|
458
460
|
ws.serializeAttachment({ ...attachment, authenticated: true });
|
|
459
461
|
// Include userId so the browser can derive the E2E key
|
|
460
462
|
const doUserId2 = doUserId ?? payload.sub;
|
|
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);
|
|
467
|
+
}
|
|
461
468
|
ws.send(JSON.stringify({ type: "auth.ok", userId: doUserId2 }));
|
|
462
469
|
|
|
463
470
|
// Send current OpenClaw connection status + cached models
|
|
464
471
|
await this.ensureCachedModels();
|
|
465
|
-
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);
|
|
466
476
|
ws.send(
|
|
467
477
|
JSON.stringify({
|
|
468
478
|
type: "connection.status",
|
|
469
479
|
openclawConnected,
|
|
470
|
-
defaultModel:
|
|
471
|
-
models
|
|
480
|
+
defaultModel: model,
|
|
481
|
+
models,
|
|
472
482
|
}),
|
|
473
483
|
);
|
|
474
484
|
return;
|
|
@@ -554,6 +564,8 @@ export class ConnectionDO implements DurableObject {
|
|
|
554
564
|
}
|
|
555
565
|
}
|
|
556
566
|
openclawWs.send(JSON.stringify(enrichedMsg));
|
|
567
|
+
} else if (await this.isDemoUser()) {
|
|
568
|
+
await this.handleDemoMockReply(ws, msg);
|
|
557
569
|
} else {
|
|
558
570
|
ws.send(
|
|
559
571
|
JSON.stringify({
|
|
@@ -574,6 +586,10 @@ export class ConnectionDO implements DurableObject {
|
|
|
574
586
|
private async handleGetScanData(): Promise<Response> {
|
|
575
587
|
const openclawWs = this.getOpenClawSocket();
|
|
576
588
|
if (!openclawWs) {
|
|
589
|
+
// Demo users get mock scan data instead of 503
|
|
590
|
+
if (await this.isDemoUser()) {
|
|
591
|
+
return Response.json({ tasks: [] });
|
|
592
|
+
}
|
|
577
593
|
return Response.json(
|
|
578
594
|
{ error: "OpenClaw not connected", tasks: [] },
|
|
579
595
|
{ status: 503 },
|
|
@@ -614,7 +630,7 @@ export class ConnectionDO implements DurableObject {
|
|
|
614
630
|
}
|
|
615
631
|
}
|
|
616
632
|
|
|
617
|
-
private handleStatus(): Response {
|
|
633
|
+
private async handleStatus(): Promise<Response> {
|
|
618
634
|
const sockets = this.state.getWebSockets();
|
|
619
635
|
const openclawSocket = sockets.find((s) => this.getTag(s) === "openclaw");
|
|
620
636
|
const browserCount = sockets.filter((s) =>
|
|
@@ -627,9 +643,10 @@ export class ConnectionDO implements DurableObject {
|
|
|
627
643
|
openclawAuthenticated = att?.authenticated ?? false;
|
|
628
644
|
}
|
|
629
645
|
|
|
646
|
+
const isDemo = await this.isDemoUser();
|
|
630
647
|
return Response.json({
|
|
631
|
-
openclawConnected: !!openclawSocket,
|
|
632
|
-
openclawAuthenticated,
|
|
648
|
+
openclawConnected: isDemo || !!openclawSocket,
|
|
649
|
+
openclawAuthenticated: isDemo || openclawAuthenticated,
|
|
633
650
|
browserClients: browserCount,
|
|
634
651
|
});
|
|
635
652
|
}
|
|
@@ -1405,6 +1422,126 @@ export class ConnectionDO implements DurableObject {
|
|
|
1405
1422
|
}
|
|
1406
1423
|
}
|
|
1407
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
|
+
|
|
1408
1545
|
private async validatePairingToken(token: string): Promise<boolean> {
|
|
1409
1546
|
// The API worker validates pairing tokens against D1 before routing
|
|
1410
1547
|
// to the DO (and passes ?verified=1). Connections that arrive here
|
|
@@ -15,6 +15,7 @@ import { upload } from "./routes/upload.js";
|
|
|
15
15
|
import { push } from "./routes/push.js";
|
|
16
16
|
import { setup } from "./routes/setup.js";
|
|
17
17
|
import { devAuth } from "./routes/dev-auth.js";
|
|
18
|
+
import { demo, isDemoUserId } from "./routes/demo.js";
|
|
18
19
|
|
|
19
20
|
// Re-export the Durable Object class so wrangler can find it
|
|
20
21
|
export { ConnectionDO } from "./do/connection-do.js";
|
|
@@ -88,11 +89,36 @@ app.get("/api/health", (c) => c.json({ status: "ok", version: "0.1.0" }));
|
|
|
88
89
|
// ---- Public routes (no auth) ----
|
|
89
90
|
app.route("/api/auth", auth);
|
|
90
91
|
app.route("/api/dev-auth", devAuth);
|
|
92
|
+
app.route("/api/demo", demo);
|
|
91
93
|
app.route("/api/setup", setup);
|
|
92
94
|
|
|
93
95
|
// ---- Protected routes (require Bearer token) ----
|
|
94
96
|
const protectedApp = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
95
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
|
+
|
|
96
122
|
protectedApp.route("/agents", agents);
|
|
97
123
|
protectedApp.route("/channels", channels);
|
|
98
124
|
protectedApp.route("/models", models);
|
|
@@ -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}/`;
|
|
@@ -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 };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMlD,QAAA,MAAM,MAAM;;;;;;;;;kBAKI;QACZ,OAAO,EAAE,OAAO,CAAC;QACjB,eAAe,EAAE,CAAC,GAAG,EAAE;YAAE,MAAM,EAAE,OAAO,cAAc,CAAA;SAAE,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMlD,QAAA,MAAM,MAAM;;;;;;;;;kBAKI;QACZ,OAAO,EAAE,OAAO,CAAC;QACjB,eAAe,EAAE,CAAC,GAAG,EAAE;YAAE,MAAM,EAAE,OAAO,cAAc,CAAA;SAAE,KAAK,IAAI,CAAC;QAClE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;KAClG;CAKF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { botschatPlugin } from "./src/channel.js";
|
|
2
|
-
import { setBotsChatRuntime } from "./src/runtime.js";
|
|
2
|
+
import { setBotsChatRuntime, setBotsChatApi } from "./src/runtime.js";
|
|
3
3
|
// OpenClaw Plugin Definition
|
|
4
4
|
// This is the entry point loaded by OpenClaw's plugin system.
|
|
5
5
|
// It registers the BotsChat channel plugin.
|
|
@@ -10,6 +10,7 @@ const plugin = {
|
|
|
10
10
|
configSchema: { safeParse: () => ({ success: true }) },
|
|
11
11
|
register(api) {
|
|
12
12
|
setBotsChatRuntime(api.runtime);
|
|
13
|
+
setBotsChatApi(api);
|
|
13
14
|
api.registerChannel({ plugin: botschatPlugin });
|
|
14
15
|
},
|
|
15
16
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEtE,6BAA6B;AAC7B,8DAA8D;AAC9D,4CAA4C;AAC5C,MAAM,MAAM,GAAG;IACb,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,WAAW,EAAE,yCAAyC;IACtD,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE;IACtD,QAAQ,CAAC,GAIR;QACC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,cAAc,CAAC,GAAG,CAAC,CAAC;QACpB,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IAClD,CAAC;CACF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAuC,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
|