botschat 0.1.18 → 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.
Files changed (46) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/packages/api/src/do/connection-do.ts +145 -8
  4. package/packages/api/src/index.ts +26 -0
  5. package/packages/api/src/routes/auth.ts +1 -0
  6. package/packages/api/src/routes/demo.ts +156 -0
  7. package/packages/plugin/dist/index.d.ts +1 -0
  8. package/packages/plugin/dist/index.d.ts.map +1 -1
  9. package/packages/plugin/dist/index.js +2 -1
  10. package/packages/plugin/dist/index.js.map +1 -1
  11. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  12. package/packages/plugin/dist/src/channel.js +351 -68
  13. package/packages/plugin/dist/src/channel.js.map +1 -1
  14. package/packages/plugin/dist/src/runtime.d.ts +2 -0
  15. package/packages/plugin/dist/src/runtime.d.ts.map +1 -1
  16. package/packages/plugin/dist/src/runtime.js +10 -0
  17. package/packages/plugin/dist/src/runtime.js.map +1 -1
  18. package/packages/plugin/dist/src/types.d.ts +12 -0
  19. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  20. package/packages/plugin/package.json +18 -2
  21. package/packages/web/dist/assets/index-BtPyCBCl.css +1 -0
  22. package/packages/web/dist/assets/index-BtpsFe4Z.js +2 -0
  23. package/packages/web/dist/assets/index-CQbIYr6_.js +2 -0
  24. package/packages/web/dist/assets/{index-DzYqprDN.js → index-C_GamcQc.js} +1 -1
  25. package/packages/web/dist/assets/index-LiBjPMg2.js +1 -0
  26. package/packages/web/dist/assets/{index-D3T7sc-R.js → index-MyoWvQAH.js} +1 -1
  27. package/packages/web/dist/assets/index-STIPTMK8.js +1516 -0
  28. package/packages/web/dist/assets/{index.esm-COzWPkKi.js → index.esm-BpQAwtdR.js} +1 -1
  29. package/packages/web/dist/assets/{web-DFQypSd0.js → web-BbTzVNLt.js} +1 -1
  30. package/packages/web/dist/assets/{web-CxXbaApe.js → web-cnzjgNfD.js} +1 -1
  31. package/packages/web/dist/index.html +2 -2
  32. package/packages/web/src/App.tsx +32 -0
  33. package/packages/web/src/api.ts +2 -0
  34. package/packages/web/src/components/ChatWindow.tsx +125 -5
  35. package/packages/web/src/components/ImageLightbox.tsx +96 -0
  36. package/packages/web/src/components/LoginPage.tsx +59 -1
  37. package/packages/web/src/components/MessageContent.tsx +17 -2
  38. package/packages/web/src/hooks/useIMEComposition.ts +14 -9
  39. package/packages/web/src/store.ts +47 -4
  40. package/packages/web/src/ws.ts +21 -1
  41. package/scripts/mock-openclaw.mjs +35 -0
  42. package/packages/web/dist/assets/index-B5GU1yVt.css +0 -1
  43. package/packages/web/dist/assets/index-CO9YgLst.js +0 -2
  44. package/packages/web/dist/assets/index-ClDrCe_c.js +0 -1
  45. package/packages/web/dist/assets/index-DPEosppm.js +0 -2
  46. package/packages/web/dist/assets/index-IVUdSd9w.js +0 -1516
@@ -1,8 +1,49 @@
1
1
  import { deleteBotsChatAccount, listBotsChatAccountIds, resolveBotsChatAccount, resolveDefaultBotsChatAccountId, setBotsChatAccountEnabled, } from "./accounts.js";
2
- import { getBotsChatRuntime } from "./runtime.js";
2
+ import { getBotsChatRuntime, getBotsChatApi } from "./runtime.js";
3
3
  import { BotsChatCloudClient } from "./ws-client.js";
4
4
  import crypto from "crypto";
5
5
  import { encryptText, encryptBytes, decryptText, decryptBytes, toBase64, fromBase64 } from "./e2e-crypto.js";
6
+ import fs from "fs";
7
+ import path from "path";
8
+ const MIME_BY_EXT = {
9
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
10
+ ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
11
+ ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".wav": "audio/wav",
12
+ ".pdf": "application/pdf",
13
+ };
14
+ async function readMedia(urlOrPath) {
15
+ if (urlOrPath.startsWith("http://") || urlOrPath.startsWith("https://")) {
16
+ const resp = await fetch(urlOrPath, { signal: AbortSignal.timeout(15_000) });
17
+ if (!resp.ok)
18
+ return null;
19
+ return {
20
+ bytes: new Uint8Array(await resp.arrayBuffer()),
21
+ contentType: resp.headers.get("Content-Type") ?? "application/octet-stream",
22
+ };
23
+ }
24
+ const filePath = urlOrPath.startsWith("file://") ? urlOrPath.slice(7) : urlOrPath;
25
+ try {
26
+ const buf = await fs.promises.readFile(filePath);
27
+ const ext = path.extname(filePath).toLowerCase();
28
+ return { bytes: new Uint8Array(buf), contentType: MIME_BY_EXT[ext] ?? "application/octet-stream" };
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ async function encryptForStream(client, text) {
35
+ const id = crypto.randomUUID();
36
+ if (client.e2eKey) {
37
+ try {
38
+ const ct = await encryptText(client.e2eKey, text, id);
39
+ return { text: toBase64(ct), encrypted: true, id };
40
+ }
41
+ catch {
42
+ return { text, encrypted: false, id };
43
+ }
44
+ }
45
+ return { text, encrypted: false, id };
46
+ }
6
47
  // ---------------------------------------------------------------------------
7
48
  // A2UI message-tool hints — injected via agentPrompt.messageToolHints so
8
49
  // the agent knows it can output interactive UI components. These strings
@@ -40,6 +81,87 @@ function readAgentModel(_agentId) {
40
81
  // Connection registry — maps accountId → live WSS client
41
82
  // ---------------------------------------------------------------------------
42
83
  const cloudClients = new Map();
84
+ const lastSessionKeys = new Map();
85
+ function isValidSessionKey(target) {
86
+ const t = target.trim();
87
+ return t.startsWith("agent:") || t.startsWith("botschat:") || /^(ses_|u_)/.test(t);
88
+ }
89
+ function resolveTarget(target, accountId) {
90
+ if (isValidSessionKey(target))
91
+ return target;
92
+ const fallback = lastSessionKeys.get(accountId);
93
+ if (fallback) {
94
+ console.log(`[botschat] resolveTarget: "${target.slice(0, 20)}…" → fallback to lastSessionKey "${fallback}"`);
95
+ return fallback;
96
+ }
97
+ return target;
98
+ }
99
+ function findClientForSession(_sessionKey) {
100
+ for (const client of cloudClients.values()) {
101
+ if (client.connected)
102
+ return client;
103
+ }
104
+ return null;
105
+ }
106
+ let hooksRegistered = false;
107
+ function registerActivityHooks() {
108
+ if (hooksRegistered)
109
+ return;
110
+ const api = getBotsChatApi();
111
+ if (!api?.registerHook) {
112
+ console.error("[botschat] registerActivityHooks: api.registerHook not available", { hasApi: !!api, keys: api ? Object.keys(api).slice(0, 10) : [] });
113
+ return;
114
+ }
115
+ hooksRegistered = true;
116
+ console.log("[botschat] Registering before_tool_call / after_tool_call hooks");
117
+ api.registerHook("before_tool_call", async (event, ctx) => {
118
+ console.log(`[botschat][hook] before_tool_call: tool=${event.toolName} sessionKey=${ctx.sessionKey ?? "none"}`);
119
+ if (!ctx.sessionKey)
120
+ return;
121
+ const client = findClientForSession(ctx.sessionKey);
122
+ if (!client?.connected)
123
+ return;
124
+ client.send({
125
+ type: "agent.activity",
126
+ sessionKey: ctx.sessionKey,
127
+ runId: "",
128
+ kind: "tool_start",
129
+ toolName: event.toolName,
130
+ });
131
+ });
132
+ api.registerHook("after_tool_call", async (event, ctx) => {
133
+ console.log(`[botschat][hook] after_tool_call: tool=${event.toolName} sessionKey=${ctx.sessionKey ?? "none"} durationMs=${event.durationMs ?? "?"}`);
134
+ if (!ctx.sessionKey)
135
+ return;
136
+ const client = findClientForSession(ctx.sessionKey);
137
+ if (!client?.connected)
138
+ return;
139
+ let text;
140
+ if (typeof event.result === "string" && event.result) {
141
+ text = event.result.length > 500 ? event.result.slice(0, 500) + "…" : event.result;
142
+ }
143
+ else if (event.error) {
144
+ text = `Error: ${event.error}`;
145
+ }
146
+ const msg = {
147
+ type: "agent.activity",
148
+ sessionKey: ctx.sessionKey,
149
+ runId: "",
150
+ kind: "tool_end",
151
+ toolName: event.toolName,
152
+ durationMs: event.durationMs,
153
+ };
154
+ if (text) {
155
+ const enc = await encryptForStream(client, text);
156
+ msg.text = enc.text;
157
+ if (enc.encrypted) {
158
+ msg.encrypted = true;
159
+ msg.activityId = enc.id;
160
+ }
161
+ }
162
+ client.send(msg);
163
+ });
164
+ }
43
165
  /** Maps accountId → cloudUrl so handleCloudMessage can resolve relative URLs */
44
166
  const cloudUrls = new Map();
45
167
  /** Maps accountId → pairingToken for plugin HTTP uploads */
@@ -81,6 +203,8 @@ export const botschatPlugin = {
81
203
  return true;
82
204
  if (/^(ses_|u_)/.test(t))
83
205
  return true;
206
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t))
207
+ return true;
84
208
  return false;
85
209
  },
86
210
  },
@@ -105,14 +229,16 @@ export const botschatPlugin = {
105
229
  outbound: {
106
230
  deliveryMode: "direct",
107
231
  sendText: async (ctx) => {
108
- const client = getCloudClient(ctx.accountId ?? "default");
232
+ const accountId = ctx.accountId ?? "default";
233
+ const client = getCloudClient(accountId);
109
234
  if (!client?.connected) {
110
235
  return { ok: false, error: new Error("Not connected to BotsChat cloud") };
111
236
  }
237
+ const to = resolveTarget(ctx.to, accountId);
112
238
  const messageId = crypto.randomUUID();
113
239
  let text = ctx.text;
114
240
  let encrypted = false;
115
- console.log(`[botschat][sendText] e2eKey=${!!client.e2eKey}, textLen=${text.length}`);
241
+ console.log(`[botschat][sendText] e2eKey=${!!client.e2eKey}, textLen=${text.length}, to=${to}`);
116
242
  if (client.e2eKey) {
117
243
  try {
118
244
  // Encrypt text using messageId as contextId
@@ -131,7 +257,7 @@ export const botschatPlugin = {
131
257
  : undefined;
132
258
  client.send({
133
259
  type: "agent.text",
134
- sessionKey: ctx.to,
260
+ sessionKey: to,
135
261
  text,
136
262
  replyToId: ctx.replyToId ?? undefined,
137
263
  threadId: ctx.threadId?.toString(),
@@ -148,6 +274,7 @@ export const botschatPlugin = {
148
274
  if (!client?.connected) {
149
275
  return { ok: false, error: new Error("Not connected to BotsChat cloud") };
150
276
  }
277
+ const to = resolveTarget(ctx.to, accountId);
151
278
  const messageId = crypto.randomUUID();
152
279
  let text = ctx.text;
153
280
  let encrypted = false;
@@ -163,21 +290,23 @@ export const botschatPlugin = {
163
290
  }
164
291
  }
165
292
  let finalMediaUrl = ctx.mediaUrl;
166
- if (client.e2eKey && ctx.mediaUrl && !ctx.mediaUrl.startsWith("/api/media/")) {
293
+ if (ctx.mediaUrl && !ctx.mediaUrl.startsWith("/api/media/")) {
167
294
  try {
168
295
  const baseUrl = cloudUrls.get(accountId);
169
296
  const token = pairingTokens.get(accountId);
170
297
  if (baseUrl && token) {
171
- const resp = await fetch(ctx.mediaUrl, { signal: AbortSignal.timeout(15_000) });
172
- if (resp.ok) {
173
- const rawBytes = new Uint8Array(await resp.arrayBuffer());
174
- const encBytes = await encryptBytes(client.e2eKey, rawBytes, `${messageId}:media`);
175
- const contentType = resp.headers.get("Content-Type") ?? "application/octet-stream";
298
+ const media = await readMedia(ctx.mediaUrl);
299
+ if (media) {
300
+ let uploadBytes = media.bytes;
301
+ if (client.e2eKey) {
302
+ uploadBytes = await encryptBytes(client.e2eKey, media.bytes, `${messageId}:media`);
303
+ mediaEncrypted = true;
304
+ }
176
305
  const extMap = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
177
- const ext = extMap[contentType] ?? (contentType.startsWith("image/") ? "png" : "bin");
306
+ const ext = extMap[media.contentType] ?? (media.contentType.startsWith("image/") ? "png" : "bin");
178
307
  const formData = new FormData();
179
- const blob = new Blob([encBytes], { type: contentType });
180
- formData.append("file", blob, `encrypted.${ext}`);
308
+ const blob = new Blob([uploadBytes], { type: media.contentType });
309
+ formData.append("file", blob, `${mediaEncrypted ? "encrypted" : "media"}.${ext}`);
181
310
  const uploadUrl = `${baseUrl.replace(/\/$/, "")}/api/plugin-upload`;
182
311
  const uploadResp = await fetch(uploadUrl, {
183
312
  method: "POST",
@@ -188,20 +317,19 @@ export const botschatPlugin = {
188
317
  if (uploadResp.ok) {
189
318
  const result = await uploadResp.json();
190
319
  finalMediaUrl = result.url;
191
- mediaEncrypted = true;
192
- console.log(`[botschat][sendMedia] E2E encrypted media uploaded (${rawBytes.length} → ${encBytes.length} bytes)`);
320
+ console.log(`[botschat][sendMedia] media uploaded to R2 (${media.bytes.length} bytes, e2e=${mediaEncrypted})`);
193
321
  }
194
322
  else {
195
323
  console.error(`[botschat][sendMedia] Plugin upload failed: HTTP ${uploadResp.status}`);
196
324
  }
197
325
  }
198
326
  else {
199
- console.error(`[botschat][sendMedia] Failed to download media: HTTP ${resp.status}`);
327
+ console.error(`[botschat][sendMedia] Failed to read media: ${ctx.mediaUrl}`);
200
328
  }
201
329
  }
202
330
  }
203
331
  catch (err) {
204
- console.error(`[botschat][sendMedia] E2E media encryption failed, sending unencrypted:`, err);
332
+ console.error(`[botschat][sendMedia] media upload failed, sending raw:`, err);
205
333
  }
206
334
  }
207
335
  const notifyPreview = (encrypted && client.notifyPreview && ctx.text)
@@ -210,7 +338,7 @@ export const botschatPlugin = {
210
338
  if (finalMediaUrl) {
211
339
  client.send({
212
340
  type: "agent.media",
213
- sessionKey: ctx.to,
341
+ sessionKey: to,
214
342
  mediaUrl: finalMediaUrl,
215
343
  caption: text || undefined,
216
344
  messageId,
@@ -222,7 +350,7 @@ export const botschatPlugin = {
222
350
  else {
223
351
  client.send({
224
352
  type: "agent.text",
225
- sessionKey: ctx.to,
353
+ sessionKey: to,
226
354
  text: text,
227
355
  messageId,
228
356
  encrypted,
@@ -283,6 +411,7 @@ export const botschatPlugin = {
283
411
  cloudUrls.set(accountId, account.cloudUrl);
284
412
  pairingTokens.set(accountId, account.pairingToken);
285
413
  client.connect();
414
+ registerActivityHooks();
286
415
  ctx.abortSignal.addEventListener("abort", () => {
287
416
  client.disconnect();
288
417
  cloudClients.delete(accountId);
@@ -401,6 +530,7 @@ export const botschatPlugin = {
401
530
  async function handleCloudMessage(msg, ctx) {
402
531
  switch (msg.type) {
403
532
  case "user.message": {
533
+ lastSessionKeys.set(ctx.accountId, msg.sessionKey);
404
534
  let text = msg.text;
405
535
  // Decrypt if needed
406
536
  const client = getCloudClient(ctx.accountId);
@@ -536,51 +666,144 @@ async function handleCloudMessage(msg, ctx) {
536
666
  };
537
667
  // Finalize the context (normalizes fields, resolves agent route)
538
668
  const finalizedCtx = runtime.channel.reply.finalizeInboundContext(msgCtx);
669
+ // Record session metadata and update delivery route so that cron
670
+ // delivery with channel:"botschat" can resolve the target.
671
+ // Without this, lastChannel is never set to "botschat" in the
672
+ // session store, causing delivery resolution to fail.
673
+ if (runtime.channel.session?.recordInboundSession) {
674
+ try {
675
+ const storePath = runtime.channel.session.resolveStorePath(cfg);
676
+ await runtime.channel.session.recordInboundSession({
677
+ storePath,
678
+ sessionKey: finalizedCtx.SessionKey ?? msg.sessionKey,
679
+ ctx: finalizedCtx,
680
+ updateLastRoute: {
681
+ sessionKey: finalizedCtx.SessionKey ?? msg.sessionKey,
682
+ channel: "botschat",
683
+ to: msg.sessionKey,
684
+ accountId: ctx.accountId ?? "default",
685
+ ...(threadId ? { threadId } : {}),
686
+ },
687
+ onRecordError: (err) => {
688
+ console.error("[botschat] failed updating session meta:", err);
689
+ },
690
+ });
691
+ }
692
+ catch (err) {
693
+ console.error("[botschat] recordInboundSession error:", err);
694
+ }
695
+ }
539
696
  // Create a reply dispatcher that sends responses back through the cloud WSS
540
697
  // NOTE: reuses `client` from line ~424 (same block scope, same value)
541
698
  console.log(`[botschat] client for accountId=${ctx.accountId}: connected=${client?.connected}`);
542
699
  const deliver = async (payload) => {
543
- console.log(`[botschat][deliver] called, connected=${client?.connected}, hasKey=${!!client?.e2eKey}, textLen=${(payload.text || "").length}`);
700
+ const mediaList = payload.mediaUrls?.length
701
+ ? payload.mediaUrls
702
+ : payload.mediaUrl
703
+ ? [payload.mediaUrl]
704
+ : [];
705
+ console.log(`[botschat][deliver] called, connected=${client?.connected}, hasKey=${!!client?.e2eKey}, textLen=${(payload.text || "").length}, mediaCount=${mediaList.length}`);
544
706
  if (!client?.connected) {
545
707
  console.log("[botschat][deliver] SKIP - not connected");
546
708
  return;
547
709
  }
548
- const messageId = crypto.randomUUID();
549
- let text = payload.text ?? "";
550
- let caption = payload.text ?? "";
551
- let encrypted = false;
552
- if (client.e2eKey && text) {
553
- try {
554
- const ct = await encryptText(client.e2eKey, text, messageId);
555
- text = toBase64(ct);
556
- caption = text;
557
- encrypted = true;
558
- console.log(`[botschat][deliver] encrypted OK: msgId=${messageId}, ctLen=${text.length}, encrypted=${encrypted}`);
559
- }
560
- catch (err) {
561
- console.error("[botschat][deliver] E2E encrypt failed:", err);
710
+ if (mediaList.length > 0) {
711
+ const accountId = ctx.accountId ?? "default";
712
+ const baseUrl = cloudUrls.get(accountId);
713
+ const token = pairingTokens.get(accountId);
714
+ let first = true;
715
+ for (const rawMediaUrl of mediaList) {
716
+ const messageId = crypto.randomUUID();
717
+ const rawCaption = first ? (payload.text ?? "") : "";
718
+ first = false;
719
+ let caption = rawCaption;
720
+ let encrypted = false;
721
+ if (client.e2eKey && caption) {
722
+ try {
723
+ const ct = await encryptText(client.e2eKey, caption, messageId);
724
+ caption = toBase64(ct);
725
+ encrypted = true;
726
+ }
727
+ catch (err) {
728
+ console.error("[botschat][deliver] E2E encrypt caption failed:", err);
729
+ }
730
+ }
731
+ let finalMediaUrl = rawMediaUrl;
732
+ let mediaEncrypted = false;
733
+ if (!rawMediaUrl.startsWith("/api/media/") && baseUrl && token) {
734
+ try {
735
+ const media = await readMedia(rawMediaUrl);
736
+ if (media) {
737
+ let uploadBytes = media.bytes;
738
+ if (client.e2eKey) {
739
+ uploadBytes = await encryptBytes(client.e2eKey, media.bytes, `${messageId}:media`);
740
+ mediaEncrypted = true;
741
+ }
742
+ const extMap = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
743
+ const ext = extMap[media.contentType] ?? (media.contentType.startsWith("image/") ? "png" : "bin");
744
+ const formData = new FormData();
745
+ const blob = new Blob([uploadBytes], { type: media.contentType });
746
+ formData.append("file", blob, `${mediaEncrypted ? "encrypted" : "media"}.${ext}`);
747
+ const uploadUrl = `${baseUrl.replace(/\/$/, "")}/api/plugin-upload`;
748
+ const uploadResp = await fetch(uploadUrl, {
749
+ method: "POST",
750
+ headers: { "X-Pairing-Token": token },
751
+ body: formData,
752
+ signal: AbortSignal.timeout(30_000),
753
+ });
754
+ if (uploadResp.ok) {
755
+ const result = await uploadResp.json();
756
+ finalMediaUrl = result.url;
757
+ console.log(`[botschat][deliver] media uploaded to R2: ${rawMediaUrl.slice(0, 80)} → ${finalMediaUrl} (${media.bytes.length} bytes, e2e=${mediaEncrypted})`);
758
+ }
759
+ else {
760
+ console.error(`[botschat][deliver] plugin-upload failed: HTTP ${uploadResp.status}`);
761
+ }
762
+ }
763
+ else {
764
+ console.error(`[botschat][deliver] failed to read media: ${rawMediaUrl.slice(0, 120)}`);
765
+ }
766
+ }
767
+ catch (err) {
768
+ console.error("[botschat][deliver] media upload failed, sending raw URL:", err);
769
+ }
770
+ }
771
+ const notifyPreviewText = (encrypted && client.notifyPreview && rawCaption)
772
+ ? (rawCaption.length > 100 ? rawCaption.slice(0, 100) + "…" : rawCaption)
773
+ : undefined;
774
+ console.log(`[botschat][deliver] sending: type=agent.media, encrypted=${encrypted}, mediaEncrypted=${mediaEncrypted}, messageId=${messageId}`);
775
+ client.send({
776
+ type: "agent.media",
777
+ sessionKey: msg.sessionKey,
778
+ mediaUrl: finalMediaUrl,
779
+ caption: caption || undefined,
780
+ threadId,
781
+ messageId,
782
+ encrypted,
783
+ mediaEncrypted,
784
+ ...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
785
+ });
562
786
  }
563
787
  }
564
- else {
565
- console.log(`[botschat][deliver] no encryption: hasKey=${!!client.e2eKey}, textLen=${text.length}`);
566
- }
567
- const notifyPreviewText = (encrypted && client.notifyPreview && payload.text)
568
- ? (payload.text.length > 100 ? payload.text.slice(0, 100) + "…" : payload.text)
569
- : undefined;
570
- console.log(`[botschat][deliver] sending: type=${payload.mediaUrl ? "agent.media" : "agent.text"}, encrypted=${encrypted}, messageId=${messageId}, notifyPreview=${!!notifyPreviewText}`);
571
- if (payload.mediaUrl) {
572
- client.send({
573
- type: "agent.media",
574
- sessionKey: msg.sessionKey,
575
- mediaUrl: payload.mediaUrl,
576
- caption: encrypted ? caption : payload.text,
577
- threadId,
578
- messageId,
579
- encrypted,
580
- ...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
581
- });
582
- }
583
788
  else if (payload.text) {
789
+ const messageId = crypto.randomUUID();
790
+ let text = payload.text;
791
+ let encrypted = false;
792
+ if (client.e2eKey && text) {
793
+ try {
794
+ const ct = await encryptText(client.e2eKey, text, messageId);
795
+ text = toBase64(ct);
796
+ encrypted = true;
797
+ console.log(`[botschat][deliver] encrypted OK: msgId=${messageId}, ctLen=${text.length}`);
798
+ }
799
+ catch (err) {
800
+ console.error("[botschat][deliver] E2E encrypt failed:", err);
801
+ }
802
+ }
803
+ const notifyPreviewText = (encrypted && client.notifyPreview && payload.text)
804
+ ? (payload.text.length > 100 ? payload.text.slice(0, 100) + "…" : payload.text)
805
+ : undefined;
806
+ console.log(`[botschat][deliver] sending: type=agent.text, encrypted=${encrypted}, messageId=${messageId}`);
584
807
  client.send({
585
808
  type: "agent.text",
586
809
  sessionKey: msg.sessionKey,
@@ -591,9 +814,6 @@ async function handleCloudMessage(msg, ctx) {
591
814
  ...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
592
815
  });
593
816
  // Detect model-change confirmations and emit model.changed
594
- // Handles both formats:
595
- // "Model set to provider/model." (no parentheses)
596
- // "Model set to Friendly Name (provider/model)." (with parentheses)
597
817
  const modelMatch = payload.text.match(/Model (?:set to|reset to default)\b.*?([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\/[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)/);
598
818
  if (modelMatch) {
599
819
  client.send({
@@ -608,10 +828,9 @@ async function handleCloudMessage(msg, ctx) {
608
828
  // Generate a runId to correlate stream events for this reply.
609
829
  const runId = `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
610
830
  let streamStarted = false;
611
- const onPartialReply = (payload) => {
831
+ const onPartialReply = async (payload) => {
612
832
  if (!client?.connected || !payload.text)
613
833
  return;
614
- // Send stream start on first chunk
615
834
  if (!streamStarted) {
616
835
  streamStarted = true;
617
836
  client.send({
@@ -620,20 +839,19 @@ async function handleCloudMessage(msg, ctx) {
620
839
  runId,
621
840
  });
622
841
  }
623
- // Send the accumulated text so far
842
+ const enc = await encryptForStream(client, payload.text);
624
843
  client.send({
625
844
  type: "agent.stream.chunk",
626
845
  sessionKey: msg.sessionKey,
627
846
  runId,
628
- text: payload.text,
847
+ text: enc.text,
848
+ ...(enc.encrypted ? { encrypted: true, chunkId: enc.id } : {}),
629
849
  });
630
850
  };
631
851
  // Use dispatchReplyFromConfig with a simple dispatcher
632
852
  const { dispatcher, replyOptions, markDispatchIdle } = runtime.channel.reply.createReplyDispatcherWithTyping({
633
853
  deliver: async (payload) => {
634
- // The payload from the dispatcher is a ReplyPayload
635
- const p = payload;
636
- await deliver(p);
854
+ await deliver(payload);
637
855
  },
638
856
  onTypingStart: () => { },
639
857
  onTypingStop: () => { },
@@ -645,6 +863,27 @@ async function handleCloudMessage(msg, ctx) {
645
863
  replyOptions: {
646
864
  ...replyOptions,
647
865
  onPartialReply,
866
+ onReasoningStream: async (payload) => {
867
+ if (!client?.connected || !payload.text)
868
+ return;
869
+ if (!streamStarted) {
870
+ streamStarted = true;
871
+ client.send({
872
+ type: "agent.stream.start",
873
+ sessionKey: msg.sessionKey,
874
+ runId,
875
+ });
876
+ }
877
+ const enc = await encryptForStream(client, payload.text);
878
+ client.send({
879
+ type: "agent.activity",
880
+ sessionKey: msg.sessionKey,
881
+ runId,
882
+ kind: "reasoning",
883
+ text: enc.text,
884
+ ...(enc.encrypted ? { encrypted: true, activityId: enc.id } : {}),
885
+ });
886
+ },
648
887
  allowPartialStream: true,
649
888
  },
650
889
  });
@@ -1089,6 +1328,29 @@ async function handleTaskRun(msg, ctx) {
1089
1328
  CommandAuthorized: true,
1090
1329
  };
1091
1330
  const finalizedCtx = runtime.channel.reply.finalizeInboundContext(msgCtx);
1331
+ // Record session metadata for cron dispatch path (same as user message path)
1332
+ if (runtime.channel.session?.recordInboundSession) {
1333
+ try {
1334
+ const storePath = runtime.channel.session.resolveStorePath(cfg);
1335
+ await runtime.channel.session.recordInboundSession({
1336
+ storePath,
1337
+ sessionKey: finalizedCtx.SessionKey ?? sessionKey,
1338
+ ctx: finalizedCtx,
1339
+ updateLastRoute: {
1340
+ sessionKey: finalizedCtx.SessionKey ?? sessionKey,
1341
+ channel: "botschat",
1342
+ to: sessionKey,
1343
+ accountId: ctx.accountId ?? "default",
1344
+ },
1345
+ onRecordError: (err) => {
1346
+ console.error("[botschat] failed updating session meta (cron):", err);
1347
+ },
1348
+ });
1349
+ }
1350
+ catch (err) {
1351
+ console.error("[botschat] recordInboundSession error (cron):", err);
1352
+ }
1353
+ }
1092
1354
  // Collect the agent's reply as summary + stream output in real-time
1093
1355
  // We accumulate completed message blocks and current streaming text.
1094
1356
  // The frontend receives the full accumulated text each time and renders
@@ -1122,10 +1384,19 @@ async function handleTaskRun(msg, ctx) {
1122
1384
  }, THROTTLE_MS);
1123
1385
  };
1124
1386
  const deliver = async (payload) => {
1125
- if (payload.text) {
1126
- completedParts.push(payload.text);
1387
+ const mediaList = payload.mediaUrls?.length
1388
+ ? payload.mediaUrls
1389
+ : payload.mediaUrl
1390
+ ? [payload.mediaUrl]
1391
+ : [];
1392
+ const parts = [];
1393
+ if (payload.text)
1394
+ parts.push(payload.text);
1395
+ for (const url of mediaList)
1396
+ parts.push(`![media](${url})`);
1397
+ if (parts.length > 0) {
1398
+ completedParts.push(parts.join("\n"));
1127
1399
  currentStreamText = "";
1128
- // Flush immediately on completed message
1129
1400
  if (sendTimer) {
1130
1401
  clearTimeout(sendTimer);
1131
1402
  sendTimer = null;
@@ -1142,8 +1413,7 @@ async function handleTaskRun(msg, ctx) {
1142
1413
  };
1143
1414
  const { dispatcher, replyOptions, markDispatchIdle } = runtime.channel.reply.createReplyDispatcherWithTyping({
1144
1415
  deliver: async (payload) => {
1145
- const p = payload;
1146
- await deliver(p);
1416
+ await deliver(payload);
1147
1417
  },
1148
1418
  onTypingStart: () => { },
1149
1419
  onTypingStop: () => { },
@@ -1155,6 +1425,19 @@ async function handleTaskRun(msg, ctx) {
1155
1425
  replyOptions: {
1156
1426
  ...replyOptions,
1157
1427
  onPartialReply,
1428
+ onReasoningStream: async (payload) => {
1429
+ if (!client?.connected || !payload.text)
1430
+ return;
1431
+ const enc = await encryptForStream(client, payload.text);
1432
+ client.send({
1433
+ type: "agent.activity",
1434
+ sessionKey,
1435
+ runId: jobId,
1436
+ kind: "reasoning",
1437
+ text: enc.text,
1438
+ ...(enc.encrypted ? { encrypted: true, activityId: enc.id } : {}),
1439
+ });
1440
+ },
1158
1441
  allowPartialStream: true,
1159
1442
  },
1160
1443
  });