botschat 0.1.12 → 0.1.13

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 (37) hide show
  1. package/README.md +11 -15
  2. package/migrations/0012_push_tokens.sql +11 -0
  3. package/package.json +7 -1
  4. package/packages/api/src/do/connection-do.ts +90 -1
  5. package/packages/api/src/env.ts +2 -0
  6. package/packages/api/src/index.ts +4 -1
  7. package/packages/api/src/routes/auth.ts +39 -6
  8. package/packages/api/src/routes/push.ts +52 -0
  9. package/packages/api/src/utils/fcm.ts +167 -0
  10. package/packages/api/src/utils/firebase.ts +89 -1
  11. package/packages/plugin/package.json +1 -1
  12. package/packages/web/dist/assets/index-B9qN5gs6.js +1 -0
  13. package/packages/web/dist/assets/index-BQNMGVyU.js +2 -0
  14. package/packages/web/dist/assets/{index-CCBhODDo.css → index-Bd_RDcgO.css} +1 -1
  15. package/packages/web/dist/assets/{index-CCFgKLX_.js → index-Civeg2lm.js} +1 -1
  16. package/packages/web/dist/assets/index-Dk33VSnY.js +2 -0
  17. package/packages/web/dist/assets/index-Kr85Nj_-.js +1516 -0
  18. package/packages/web/dist/assets/{index-Dx64BDkP.js → index-lVB82JKU.js} +1 -1
  19. package/packages/web/dist/assets/index.esm-CtMkqqqb.js +599 -0
  20. package/packages/web/dist/assets/{web-DJQW-VLX.js → web-CUXjh_UA.js} +1 -1
  21. package/packages/web/dist/assets/web-vKLTVUul.js +1 -0
  22. package/packages/web/dist/index.html +6 -4
  23. package/packages/web/dist/sw.js +158 -1
  24. package/packages/web/index.html +4 -2
  25. package/packages/web/src/App.tsx +42 -2
  26. package/packages/web/src/api.ts +10 -0
  27. package/packages/web/src/components/AccountSettings.tsx +131 -0
  28. package/packages/web/src/components/DataConsentModal.tsx +249 -0
  29. package/packages/web/src/components/LoginPage.tsx +49 -9
  30. package/packages/web/src/firebase.ts +89 -2
  31. package/packages/web/src/foreground.ts +51 -0
  32. package/packages/web/src/main.tsx +2 -1
  33. package/packages/web/src/push.ts +205 -0
  34. package/scripts/dev.sh +139 -13
  35. package/scripts/mock-openclaw.mjs +382 -0
  36. package/packages/web/dist/assets/index-D8mBAwjS.js +0 -1516
  37. package/packages/web/dist/assets/index-E-nzPZl8.js +0 -2
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mock OpenClaw — a lightweight WebSocket client that simulates an OpenClaw
4
+ * plugin for local BotsChat development. No OpenClaw dependency required.
5
+ *
6
+ * Usage:
7
+ * node scripts/mock-openclaw.mjs --token bc_pat_xxx
8
+ * node scripts/mock-openclaw.mjs --token bc_pat_xxx --delay 500 --stream
9
+ *
10
+ * Options:
11
+ * --token <pat> Pairing token (required)
12
+ * --url <url> Server URL (default: http://localhost:8787)
13
+ * --agents <list> Comma-separated agent IDs (default: main)
14
+ * --delay <ms> Reply delay in ms (default: 300)
15
+ * --stream Enable streaming replies (chunk by chunk)
16
+ * --model <name> Default model name (default: mock/echo-1.0)
17
+ */
18
+
19
+ import { randomUUID } from "node:crypto";
20
+ import { parseArgs } from "node:util";
21
+
22
+ // ── CLI args ─────────────────────────────────────────────────────────
23
+
24
+ const { values: args } = parseArgs({
25
+ options: {
26
+ token: { type: "string" },
27
+ url: { type: "string", default: "http://localhost:8787" },
28
+ agents: { type: "string", default: "main" },
29
+ delay: { type: "string", default: "300" },
30
+ stream: { type: "boolean", default: false },
31
+ model: { type: "string", default: "mock/echo-1.0" },
32
+ help: { type: "boolean", short: "h", default: false },
33
+ },
34
+ strict: true,
35
+ });
36
+
37
+ if (args.help || !args.token) {
38
+ console.log(`Mock OpenClaw — simulate an OpenClaw plugin for local testing
39
+
40
+ Usage:
41
+ node scripts/mock-openclaw.mjs --token <pairing-token> [options]
42
+
43
+ Options:
44
+ --token <pat> Pairing token (required)
45
+ --url <url> Server URL (default: http://localhost:8787)
46
+ --agents <list> Comma-separated agent IDs (default: main)
47
+ --delay <ms> Reply delay in ms (default: 300)
48
+ --stream Enable streaming replies
49
+ --model <name> Default model name (default: mock/echo-1.0)
50
+ -h, --help Show this help`);
51
+ process.exit(args.help ? 0 : 1);
52
+ }
53
+
54
+ const TOKEN = args.token;
55
+ const SERVER_URL = args.url;
56
+ const AGENTS = args.agents.split(",").map((s) => s.trim());
57
+ const DELAY_MS = parseInt(args.delay, 10);
58
+ const STREAMING = args.stream;
59
+ const MODEL = args.model;
60
+
61
+ // ── Colours ──────────────────────────────────────────────────────────
62
+
63
+ const c = {
64
+ reset: "\x1b[0m",
65
+ dim: "\x1b[2m",
66
+ cyan: "\x1b[36m",
67
+ green: "\x1b[32m",
68
+ yellow:"\x1b[33m",
69
+ red: "\x1b[31m",
70
+ magenta:"\x1b[35m",
71
+ };
72
+
73
+ function log(icon, msg) {
74
+ const ts = new Date().toISOString().slice(11, 23);
75
+ console.log(`${c.dim}${ts}${c.reset} ${icon} ${msg}`);
76
+ }
77
+ const logInfo = (msg) => log(`${c.cyan}▸${c.reset}`, msg);
78
+ const logOk = (msg) => log(`${c.green}✔${c.reset}`, msg);
79
+ const logWarn = (msg) => log(`${c.yellow}▲${c.reset}`, msg);
80
+ const logErr = (msg) => log(`${c.red}✖${c.reset}`, msg);
81
+ const logRecv = (msg) => log(`${c.magenta}◂${c.reset}`, msg);
82
+ const logSend = (msg) => log(`${c.cyan}▸${c.reset}`, msg);
83
+
84
+ // ── Mock models ──────────────────────────────────────────────────────
85
+
86
+ const MOCK_MODELS = [
87
+ { id: "mock/echo-1.0", name: "Echo 1.0", provider: "mock" },
88
+ { id: "mock/echo-streaming", name: "Echo Streaming", provider: "mock" },
89
+ { id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
90
+ { id: "openai/gpt-4o", name: "GPT-4o", provider: "openai" },
91
+ ];
92
+
93
+ // ── WebSocket connection ─────────────────────────────────────────────
94
+
95
+ const MIN_BACKOFF = 1_000;
96
+ const MAX_BACKOFF = 30_000;
97
+ let backoff = MIN_BACKOFF;
98
+ let ws = null;
99
+ let pingTimer = null;
100
+ let intentionalClose = false;
101
+ let userId = null;
102
+
103
+ function buildWsUrl() {
104
+ let host = SERVER_URL.replace(/^https?:\/\//, "");
105
+ const isPlainHttp = SERVER_URL.startsWith("http://");
106
+ const scheme = isPlainHttp ? "ws" : "wss";
107
+ return `${scheme}://${host}/api/gateway/mock?token=${encodeURIComponent(TOKEN)}`;
108
+ }
109
+
110
+ function connect() {
111
+ const url = buildWsUrl();
112
+ logInfo(`Connecting to ${url.replace(/token=.*/, "token=***")}`);
113
+
114
+ ws = new WebSocket(url);
115
+
116
+ ws.addEventListener("open", () => {
117
+ logInfo("Connected, sending auth…");
118
+ send({ type: "auth", token: TOKEN, agents: AGENTS, model: MODEL });
119
+ });
120
+
121
+ ws.addEventListener("message", (event) => {
122
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
123
+ let msg;
124
+ try {
125
+ msg = JSON.parse(data);
126
+ } catch {
127
+ logErr(`Bad JSON: ${data.slice(0, 100)}`);
128
+ return;
129
+ }
130
+ handleMessage(msg);
131
+ });
132
+
133
+ ws.addEventListener("close", (event) => {
134
+ logWarn(`Disconnected: code=${event.code} reason=${event.reason || "?"}`);
135
+ stopPing();
136
+ if (!intentionalClose) scheduleReconnect();
137
+ });
138
+
139
+ ws.addEventListener("error", (event) => {
140
+ logErr(`WebSocket error: ${event.message || "unknown"}`);
141
+ });
142
+ }
143
+
144
+ function send(msg) {
145
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
146
+ ws.send(JSON.stringify(msg));
147
+ }
148
+
149
+ function scheduleReconnect() {
150
+ logInfo(`Reconnecting in ${backoff}ms…`);
151
+ setTimeout(() => {
152
+ backoff = Math.min(backoff * 2, MAX_BACKOFF);
153
+ connect();
154
+ }, backoff);
155
+ }
156
+
157
+ function startPing() {
158
+ stopPing();
159
+ pingTimer = setInterval(() => {
160
+ send({ type: "status", connected: true, agents: AGENTS, model: MODEL });
161
+ }, 25_000);
162
+ }
163
+
164
+ function stopPing() {
165
+ if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
166
+ }
167
+
168
+ // ── Message handlers ─────────────────────────────────────────────────
169
+
170
+ function handleMessage(msg) {
171
+ switch (msg.type) {
172
+ case "auth.ok":
173
+ userId = msg.userId;
174
+ backoff = MIN_BACKOFF;
175
+ logOk(`Authenticated (userId=${userId})`);
176
+ startPing();
177
+ break;
178
+
179
+ case "auth.fail":
180
+ logErr(`Auth failed: ${msg.reason}`);
181
+ intentionalClose = true;
182
+ ws?.close(4001, "auth failed");
183
+ break;
184
+
185
+ case "ping":
186
+ send({ type: "pong" });
187
+ break;
188
+
189
+ case "user.message":
190
+ logRecv(`[user.message] sessionKey=${msg.sessionKey} text="${truncate(msg.text, 80)}"`);
191
+ handleUserMessage(msg);
192
+ break;
193
+
194
+ case "user.media":
195
+ logRecv(`[user.media] sessionKey=${msg.sessionKey} url=${msg.mediaUrl}`);
196
+ setTimeout(() => {
197
+ send({
198
+ type: "agent.text",
199
+ sessionKey: msg.sessionKey,
200
+ text: `📎 Received media: ${msg.mediaUrl}`,
201
+ messageId: randomUUID(),
202
+ });
203
+ logSend("[agent.text] media acknowledgement");
204
+ }, DELAY_MS);
205
+ break;
206
+
207
+ case "user.command":
208
+ logRecv(`[user.command] command=${msg.command} args=${msg.args || ""}`);
209
+ setTimeout(() => {
210
+ send({
211
+ type: "agent.text",
212
+ sessionKey: msg.sessionKey,
213
+ text: `Command received: /${msg.command} ${msg.args || ""}`.trim(),
214
+ messageId: randomUUID(),
215
+ });
216
+ }, DELAY_MS);
217
+ break;
218
+
219
+ case "user.action":
220
+ logRecv(`[user.action] action=${msg.action} params=${JSON.stringify(msg.params)}`);
221
+ setTimeout(() => {
222
+ send({
223
+ type: "agent.text",
224
+ sessionKey: msg.sessionKey,
225
+ text: `Action received: ${msg.action}`,
226
+ messageId: randomUUID(),
227
+ });
228
+ }, DELAY_MS);
229
+ break;
230
+
231
+ case "task.scan.request":
232
+ logRecv("[task.scan.request]");
233
+ send({ type: "task.scan.result", tasks: [] });
234
+ logSend("[task.scan.result] empty tasks");
235
+ break;
236
+
237
+ case "models.request":
238
+ logRecv("[models.request]");
239
+ send({ type: "models.list", models: MOCK_MODELS });
240
+ logSend(`[models.list] ${MOCK_MODELS.length} models`);
241
+ break;
242
+
243
+ case "task.schedule":
244
+ logRecv(`[task.schedule] cronJobId=${msg.cronJobId} schedule=${msg.schedule}`);
245
+ send({
246
+ type: "task.schedule.ack",
247
+ cronJobId: msg.cronJobId || `mock_cron_${Date.now()}`,
248
+ taskId: msg.taskId,
249
+ ok: true,
250
+ });
251
+ logSend("[task.schedule.ack] ok");
252
+ break;
253
+
254
+ case "task.delete":
255
+ logRecv(`[task.delete] cronJobId=${msg.cronJobId}`);
256
+ break;
257
+
258
+ case "task.run":
259
+ logRecv(`[task.run] cronJobId=${msg.cronJobId}`);
260
+ handleTaskRun(msg);
261
+ break;
262
+
263
+ case "settings.defaultModel":
264
+ logRecv(`[settings.defaultModel] model=${msg.defaultModel}`);
265
+ send({ type: "defaultModel.updated", model: msg.defaultModel });
266
+ logSend(`[defaultModel.updated] ${msg.defaultModel}`);
267
+ break;
268
+
269
+ default:
270
+ logWarn(`Unhandled message type: ${msg.type}`);
271
+ }
272
+ }
273
+
274
+ // ── User message reply ───────────────────────────────────────────────
275
+
276
+ async function handleUserMessage(msg) {
277
+ const replyText = `Mock reply: ${msg.text}`;
278
+
279
+ await sleep(DELAY_MS);
280
+
281
+ if (STREAMING) {
282
+ const runId = randomUUID();
283
+ send({ type: "agent.stream.start", sessionKey: msg.sessionKey, runId });
284
+
285
+ const words = replyText.split(" ");
286
+ for (let i = 0; i < words.length; i++) {
287
+ await sleep(50);
288
+ const chunk = (i === 0 ? "" : " ") + words[i];
289
+ send({ type: "agent.stream.chunk", sessionKey: msg.sessionKey, runId, text: chunk });
290
+ }
291
+
292
+ send({ type: "agent.stream.end", sessionKey: msg.sessionKey, runId });
293
+ logSend(`[agent.stream] ${words.length} chunks`);
294
+
295
+ send({
296
+ type: "agent.text",
297
+ sessionKey: msg.sessionKey,
298
+ text: replyText,
299
+ messageId: randomUUID(),
300
+ });
301
+ } else {
302
+ send({
303
+ type: "agent.text",
304
+ sessionKey: msg.sessionKey,
305
+ text: replyText,
306
+ messageId: randomUUID(),
307
+ });
308
+ logSend(`[agent.text] "${truncate(replyText, 60)}"`);
309
+ }
310
+ }
311
+
312
+ // ── Task run simulation ──────────────────────────────────────────────
313
+
314
+ async function handleTaskRun(msg) {
315
+ const jobId = `mock_job_${Date.now()}`;
316
+ const sessionKey = `agent:${msg.agentId || "main"}:botschat:${userId}:task:mock`;
317
+ const startedAt = Math.floor(Date.now() / 1000);
318
+
319
+ send({
320
+ type: "job.update",
321
+ cronJobId: msg.cronJobId,
322
+ jobId,
323
+ sessionKey,
324
+ status: "running",
325
+ startedAt,
326
+ });
327
+ logSend(`[job.update] running jobId=${jobId}`);
328
+
329
+ send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job started…\n" });
330
+ await sleep(1000);
331
+ send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job processing…\n" });
332
+ await sleep(1000);
333
+ send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job complete.\n" });
334
+
335
+ const finishedAt = Math.floor(Date.now() / 1000);
336
+ send({
337
+ type: "job.update",
338
+ cronJobId: msg.cronJobId,
339
+ jobId,
340
+ sessionKey,
341
+ status: "ok",
342
+ summary: "Mock task executed successfully",
343
+ startedAt,
344
+ finishedAt,
345
+ durationMs: (finishedAt - startedAt) * 1000,
346
+ });
347
+ logSend(`[job.update] ok (${finishedAt - startedAt}s)`);
348
+ }
349
+
350
+ // ── Utilities ────────────────────────────────────────────────────────
351
+
352
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
353
+ function truncate(s, n) { return s && s.length > n ? s.slice(0, n) + "…" : s; }
354
+
355
+ // ── Graceful shutdown ────────────────────────────────────────────────
356
+
357
+ function shutdown() {
358
+ logInfo("Shutting down…");
359
+ intentionalClose = true;
360
+ stopPing();
361
+ ws?.close(1000, "shutdown");
362
+ process.exit(0);
363
+ }
364
+ process.on("SIGINT", shutdown);
365
+ process.on("SIGTERM", shutdown);
366
+
367
+ // ── Start ────────────────────────────────────────────────────────────
368
+
369
+ console.log(`
370
+ ${c.cyan}╭──────────────────────────────────────╮
371
+ │ Mock OpenClaw v1.0 │
372
+ │ Local testing without deployment │
373
+ ╰──────────────────────────────────────╯${c.reset}
374
+ Server: ${SERVER_URL}
375
+ Token: ${TOKEN.slice(0, 12)}***
376
+ Agents: ${AGENTS.join(", ")}
377
+ Model: ${MODEL}
378
+ Delay: ${DELAY_MS}ms
379
+ Streaming: ${STREAMING}
380
+ `);
381
+
382
+ connect();