botschat 0.1.18 → 0.1.20

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 (59) hide show
  1. package/README.md +4 -0
  2. package/migrations/0013_agents_table.sql +29 -0
  3. package/migrations/0014_agent_sessions.sql +19 -0
  4. package/migrations/0015_message_traces.sql +27 -0
  5. package/migrations/0016_multi_agent_channels_messages.sql +9 -0
  6. package/migrations/0017_rename_cron_job_id.sql +2 -0
  7. package/package.json +1 -1
  8. package/packages/api/src/do/connection-do.ts +375 -42
  9. package/packages/api/src/index.ts +67 -24
  10. package/packages/api/src/protocol-v2.ts +154 -0
  11. package/packages/api/src/routes/agents-v2.ts +192 -0
  12. package/packages/api/src/routes/agents.ts +3 -3
  13. package/packages/api/src/routes/channels.ts +11 -11
  14. package/packages/api/src/routes/history-v2.ts +221 -0
  15. package/packages/api/src/routes/migrate-v2.ts +110 -0
  16. package/packages/api/src/routes/sessions.ts +5 -5
  17. package/packages/api/src/routes/tasks.ts +33 -33
  18. package/packages/plugin/dist/index.d.ts +1 -0
  19. package/packages/plugin/dist/index.d.ts.map +1 -1
  20. package/packages/plugin/dist/index.js +2 -1
  21. package/packages/plugin/dist/index.js.map +1 -1
  22. package/packages/plugin/dist/src/channel.d.ts +10 -0
  23. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  24. package/packages/plugin/dist/src/channel.js +311 -69
  25. package/packages/plugin/dist/src/channel.js.map +1 -1
  26. package/packages/plugin/dist/src/runtime.d.ts +2 -0
  27. package/packages/plugin/dist/src/runtime.d.ts.map +1 -1
  28. package/packages/plugin/dist/src/runtime.js +10 -0
  29. package/packages/plugin/dist/src/runtime.js.map +1 -1
  30. package/packages/plugin/dist/src/types.d.ts +25 -0
  31. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  32. package/packages/plugin/package.json +1 -1
  33. package/packages/web/dist/assets/{index-B5GU1yVt.css → index-BARPtt0v.css} +1 -1
  34. package/packages/web/dist/assets/index-Bf-XL3te.js +2 -0
  35. package/packages/web/dist/assets/{index-DzYqprDN.js → index-CYQMu_-c.js} +1 -1
  36. package/packages/web/dist/assets/index-CYlvfpX9.js +1519 -0
  37. package/packages/web/dist/assets/index-CxcpA4Qo.js +1 -0
  38. package/packages/web/dist/assets/{index-D3T7sc-R.js → index-DYCO-ry1.js} +1 -1
  39. package/packages/web/dist/assets/index-QebPVqwj.js +2 -0
  40. package/packages/web/dist/assets/{index.esm-COzWPkKi.js → index.esm-CvOpngZM.js} +1 -1
  41. package/packages/web/dist/assets/{web-CxXbaApe.js → web-1cdhq2RW.js} +1 -1
  42. package/packages/web/dist/assets/{web-DFQypSd0.js → web-D3LMODYp.js} +1 -1
  43. package/packages/web/dist/index.html +2 -2
  44. package/packages/web/src/App.tsx +84 -5
  45. package/packages/web/src/api.ts +61 -3
  46. package/packages/web/src/components/AgentSettings.tsx +328 -0
  47. package/packages/web/src/components/ChatWindow.tsx +124 -4
  48. package/packages/web/src/components/CronDetail.tsx +1 -1
  49. package/packages/web/src/components/SessionTabs.tsx +1 -1
  50. package/packages/web/src/components/Sidebar.tsx +3 -1
  51. package/packages/web/src/store.ts +86 -11
  52. package/packages/web/src/ws.ts +22 -1
  53. package/scripts/dev.sh +53 -0
  54. package/scripts/mock-openclaw-v2.mjs +486 -0
  55. package/scripts/mock-openclaw.mjs +35 -0
  56. package/packages/web/dist/assets/index-CO9YgLst.js +0 -2
  57. package/packages/web/dist/assets/index-ClDrCe_c.js +0 -1
  58. package/packages/web/dist/assets/index-DPEosppm.js +0 -2
  59. 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,11 +203,16 @@ 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
  },
87
211
  },
88
- reload: { configPrefixes: ["channels.botschat"] },
212
+ reload: {
213
+ configPrefixes: ["channels.botschat"],
214
+ noopPrefixes: ["plugins.installs.botschat"],
215
+ },
89
216
  config: {
90
217
  listAccountIds: (cfg) => listBotsChatAccountIds(cfg),
91
218
  resolveAccount: (cfg, accountId) => resolveBotsChatAccount(cfg, accountId),
@@ -104,15 +231,23 @@ export const botschatPlugin = {
104
231
  },
105
232
  outbound: {
106
233
  deliveryMode: "direct",
234
+ resolveTarget: ({ to }) => {
235
+ const trimmed = to?.trim();
236
+ if (trimmed)
237
+ return { ok: true, to: trimmed };
238
+ return { ok: true, to: "@default" };
239
+ },
107
240
  sendText: async (ctx) => {
108
- const client = getCloudClient(ctx.accountId ?? "default");
241
+ const accountId = ctx.accountId ?? "default";
242
+ const client = getCloudClient(accountId);
109
243
  if (!client?.connected) {
110
244
  return { ok: false, error: new Error("Not connected to BotsChat cloud") };
111
245
  }
246
+ const to = resolveTarget(ctx.to, accountId);
112
247
  const messageId = crypto.randomUUID();
113
248
  let text = ctx.text;
114
249
  let encrypted = false;
115
- console.log(`[botschat][sendText] e2eKey=${!!client.e2eKey}, textLen=${text.length}`);
250
+ console.log(`[botschat][sendText] e2eKey=${!!client.e2eKey}, textLen=${text.length}, to=${to}`);
116
251
  if (client.e2eKey) {
117
252
  try {
118
253
  // Encrypt text using messageId as contextId
@@ -131,7 +266,7 @@ export const botschatPlugin = {
131
266
  : undefined;
132
267
  client.send({
133
268
  type: "agent.text",
134
- sessionKey: ctx.to,
269
+ sessionKey: to,
135
270
  text,
136
271
  replyToId: ctx.replyToId ?? undefined,
137
272
  threadId: ctx.threadId?.toString(),
@@ -148,6 +283,7 @@ export const botschatPlugin = {
148
283
  if (!client?.connected) {
149
284
  return { ok: false, error: new Error("Not connected to BotsChat cloud") };
150
285
  }
286
+ const to = resolveTarget(ctx.to, accountId);
151
287
  const messageId = crypto.randomUUID();
152
288
  let text = ctx.text;
153
289
  let encrypted = false;
@@ -163,21 +299,23 @@ export const botschatPlugin = {
163
299
  }
164
300
  }
165
301
  let finalMediaUrl = ctx.mediaUrl;
166
- if (client.e2eKey && ctx.mediaUrl && !ctx.mediaUrl.startsWith("/api/media/")) {
302
+ if (ctx.mediaUrl && !ctx.mediaUrl.startsWith("/api/media/")) {
167
303
  try {
168
304
  const baseUrl = cloudUrls.get(accountId);
169
305
  const token = pairingTokens.get(accountId);
170
306
  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";
307
+ const media = await readMedia(ctx.mediaUrl);
308
+ if (media) {
309
+ let uploadBytes = media.bytes;
310
+ if (client.e2eKey) {
311
+ uploadBytes = await encryptBytes(client.e2eKey, media.bytes, `${messageId}:media`);
312
+ mediaEncrypted = true;
313
+ }
176
314
  const extMap = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
177
- const ext = extMap[contentType] ?? (contentType.startsWith("image/") ? "png" : "bin");
315
+ const ext = extMap[media.contentType] ?? (media.contentType.startsWith("image/") ? "png" : "bin");
178
316
  const formData = new FormData();
179
- const blob = new Blob([encBytes], { type: contentType });
180
- formData.append("file", blob, `encrypted.${ext}`);
317
+ const blob = new Blob([uploadBytes], { type: media.contentType });
318
+ formData.append("file", blob, `${mediaEncrypted ? "encrypted" : "media"}.${ext}`);
181
319
  const uploadUrl = `${baseUrl.replace(/\/$/, "")}/api/plugin-upload`;
182
320
  const uploadResp = await fetch(uploadUrl, {
183
321
  method: "POST",
@@ -188,20 +326,19 @@ export const botschatPlugin = {
188
326
  if (uploadResp.ok) {
189
327
  const result = await uploadResp.json();
190
328
  finalMediaUrl = result.url;
191
- mediaEncrypted = true;
192
- console.log(`[botschat][sendMedia] E2E encrypted media uploaded (${rawBytes.length} → ${encBytes.length} bytes)`);
329
+ console.log(`[botschat][sendMedia] media uploaded to R2 (${media.bytes.length} bytes, e2e=${mediaEncrypted})`);
193
330
  }
194
331
  else {
195
332
  console.error(`[botschat][sendMedia] Plugin upload failed: HTTP ${uploadResp.status}`);
196
333
  }
197
334
  }
198
335
  else {
199
- console.error(`[botschat][sendMedia] Failed to download media: HTTP ${resp.status}`);
336
+ console.error(`[botschat][sendMedia] Failed to read media: ${ctx.mediaUrl}`);
200
337
  }
201
338
  }
202
339
  }
203
340
  catch (err) {
204
- console.error(`[botschat][sendMedia] E2E media encryption failed, sending unencrypted:`, err);
341
+ console.error(`[botschat][sendMedia] media upload failed, sending raw:`, err);
205
342
  }
206
343
  }
207
344
  const notifyPreview = (encrypted && client.notifyPreview && ctx.text)
@@ -210,7 +347,7 @@ export const botschatPlugin = {
210
347
  if (finalMediaUrl) {
211
348
  client.send({
212
349
  type: "agent.media",
213
- sessionKey: ctx.to,
350
+ sessionKey: to,
214
351
  mediaUrl: finalMediaUrl,
215
352
  caption: text || undefined,
216
353
  messageId,
@@ -222,7 +359,7 @@ export const botschatPlugin = {
222
359
  else {
223
360
  client.send({
224
361
  type: "agent.text",
225
- sessionKey: ctx.to,
362
+ sessionKey: to,
226
363
  text: text,
227
364
  messageId,
228
365
  encrypted,
@@ -283,6 +420,7 @@ export const botschatPlugin = {
283
420
  cloudUrls.set(accountId, account.cloudUrl);
284
421
  pairingTokens.set(accountId, account.pairingToken);
285
422
  client.connect();
423
+ registerActivityHooks();
286
424
  ctx.abortSignal.addEventListener("abort", () => {
287
425
  client.disconnect();
288
426
  cloudClients.delete(accountId);
@@ -401,6 +539,7 @@ export const botschatPlugin = {
401
539
  async function handleCloudMessage(msg, ctx) {
402
540
  switch (msg.type) {
403
541
  case "user.message": {
542
+ lastSessionKeys.set(ctx.accountId, msg.sessionKey);
404
543
  let text = msg.text;
405
544
  // Decrypt if needed
406
545
  const client = getCloudClient(ctx.accountId);
@@ -540,47 +679,113 @@ async function handleCloudMessage(msg, ctx) {
540
679
  // NOTE: reuses `client` from line ~424 (same block scope, same value)
541
680
  console.log(`[botschat] client for accountId=${ctx.accountId}: connected=${client?.connected}`);
542
681
  const deliver = async (payload) => {
543
- console.log(`[botschat][deliver] called, connected=${client?.connected}, hasKey=${!!client?.e2eKey}, textLen=${(payload.text || "").length}`);
682
+ const mediaList = payload.mediaUrls?.length
683
+ ? payload.mediaUrls
684
+ : payload.mediaUrl
685
+ ? [payload.mediaUrl]
686
+ : [];
687
+ console.log(`[botschat][deliver] called, connected=${client?.connected}, hasKey=${!!client?.e2eKey}, textLen=${(payload.text || "").length}, mediaCount=${mediaList.length}`);
544
688
  if (!client?.connected) {
545
689
  console.log("[botschat][deliver] SKIP - not connected");
546
690
  return;
547
691
  }
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);
692
+ if (mediaList.length > 0) {
693
+ const accountId = ctx.accountId ?? "default";
694
+ const baseUrl = cloudUrls.get(accountId);
695
+ const token = pairingTokens.get(accountId);
696
+ let first = true;
697
+ for (const rawMediaUrl of mediaList) {
698
+ const messageId = crypto.randomUUID();
699
+ const rawCaption = first ? (payload.text ?? "") : "";
700
+ first = false;
701
+ let caption = rawCaption;
702
+ let encrypted = false;
703
+ if (client.e2eKey && caption) {
704
+ try {
705
+ const ct = await encryptText(client.e2eKey, caption, messageId);
706
+ caption = toBase64(ct);
707
+ encrypted = true;
708
+ }
709
+ catch (err) {
710
+ console.error("[botschat][deliver] E2E encrypt caption failed:", err);
711
+ }
712
+ }
713
+ let finalMediaUrl = rawMediaUrl;
714
+ let mediaEncrypted = false;
715
+ if (!rawMediaUrl.startsWith("/api/media/") && baseUrl && token) {
716
+ try {
717
+ const media = await readMedia(rawMediaUrl);
718
+ if (media) {
719
+ let uploadBytes = media.bytes;
720
+ if (client.e2eKey) {
721
+ uploadBytes = await encryptBytes(client.e2eKey, media.bytes, `${messageId}:media`);
722
+ mediaEncrypted = true;
723
+ }
724
+ const extMap = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
725
+ const ext = extMap[media.contentType] ?? (media.contentType.startsWith("image/") ? "png" : "bin");
726
+ const formData = new FormData();
727
+ const blob = new Blob([uploadBytes], { type: media.contentType });
728
+ formData.append("file", blob, `${mediaEncrypted ? "encrypted" : "media"}.${ext}`);
729
+ const uploadUrl = `${baseUrl.replace(/\/$/, "")}/api/plugin-upload`;
730
+ const uploadResp = await fetch(uploadUrl, {
731
+ method: "POST",
732
+ headers: { "X-Pairing-Token": token },
733
+ body: formData,
734
+ signal: AbortSignal.timeout(30_000),
735
+ });
736
+ if (uploadResp.ok) {
737
+ const result = await uploadResp.json();
738
+ finalMediaUrl = result.url;
739
+ console.log(`[botschat][deliver] media uploaded to R2: ${rawMediaUrl.slice(0, 80)} → ${finalMediaUrl} (${media.bytes.length} bytes, e2e=${mediaEncrypted})`);
740
+ }
741
+ else {
742
+ console.error(`[botschat][deliver] plugin-upload failed: HTTP ${uploadResp.status}`);
743
+ }
744
+ }
745
+ else {
746
+ console.error(`[botschat][deliver] failed to read media: ${rawMediaUrl.slice(0, 120)}`);
747
+ }
748
+ }
749
+ catch (err) {
750
+ console.error("[botschat][deliver] media upload failed, sending raw URL:", err);
751
+ }
752
+ }
753
+ const notifyPreviewText = (encrypted && client.notifyPreview && rawCaption)
754
+ ? (rawCaption.length > 100 ? rawCaption.slice(0, 100) + "…" : rawCaption)
755
+ : undefined;
756
+ console.log(`[botschat][deliver] sending: type=agent.media, encrypted=${encrypted}, mediaEncrypted=${mediaEncrypted}, messageId=${messageId}`);
757
+ client.send({
758
+ type: "agent.media",
759
+ sessionKey: msg.sessionKey,
760
+ mediaUrl: finalMediaUrl,
761
+ caption: caption || undefined,
762
+ threadId,
763
+ messageId,
764
+ encrypted,
765
+ mediaEncrypted,
766
+ ...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
767
+ });
562
768
  }
563
769
  }
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
770
  else if (payload.text) {
771
+ const messageId = crypto.randomUUID();
772
+ let text = payload.text;
773
+ let encrypted = false;
774
+ if (client.e2eKey && text) {
775
+ try {
776
+ const ct = await encryptText(client.e2eKey, text, messageId);
777
+ text = toBase64(ct);
778
+ encrypted = true;
779
+ console.log(`[botschat][deliver] encrypted OK: msgId=${messageId}, ctLen=${text.length}`);
780
+ }
781
+ catch (err) {
782
+ console.error("[botschat][deliver] E2E encrypt failed:", err);
783
+ }
784
+ }
785
+ const notifyPreviewText = (encrypted && client.notifyPreview && payload.text)
786
+ ? (payload.text.length > 100 ? payload.text.slice(0, 100) + "…" : payload.text)
787
+ : undefined;
788
+ console.log(`[botschat][deliver] sending: type=agent.text, encrypted=${encrypted}, messageId=${messageId}`);
584
789
  client.send({
585
790
  type: "agent.text",
586
791
  sessionKey: msg.sessionKey,
@@ -591,9 +796,6 @@ async function handleCloudMessage(msg, ctx) {
591
796
  ...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
592
797
  });
593
798
  // 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
799
  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
800
  if (modelMatch) {
599
801
  client.send({
@@ -608,10 +810,9 @@ async function handleCloudMessage(msg, ctx) {
608
810
  // Generate a runId to correlate stream events for this reply.
609
811
  const runId = `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
610
812
  let streamStarted = false;
611
- const onPartialReply = (payload) => {
813
+ const onPartialReply = async (payload) => {
612
814
  if (!client?.connected || !payload.text)
613
815
  return;
614
- // Send stream start on first chunk
615
816
  if (!streamStarted) {
616
817
  streamStarted = true;
617
818
  client.send({
@@ -620,20 +821,19 @@ async function handleCloudMessage(msg, ctx) {
620
821
  runId,
621
822
  });
622
823
  }
623
- // Send the accumulated text so far
824
+ const enc = await encryptForStream(client, payload.text);
624
825
  client.send({
625
826
  type: "agent.stream.chunk",
626
827
  sessionKey: msg.sessionKey,
627
828
  runId,
628
- text: payload.text,
829
+ text: enc.text,
830
+ ...(enc.encrypted ? { encrypted: true, chunkId: enc.id } : {}),
629
831
  });
630
832
  };
631
833
  // Use dispatchReplyFromConfig with a simple dispatcher
632
834
  const { dispatcher, replyOptions, markDispatchIdle } = runtime.channel.reply.createReplyDispatcherWithTyping({
633
835
  deliver: async (payload) => {
634
- // The payload from the dispatcher is a ReplyPayload
635
- const p = payload;
636
- await deliver(p);
836
+ await deliver(payload);
637
837
  },
638
838
  onTypingStart: () => { },
639
839
  onTypingStop: () => { },
@@ -645,6 +845,27 @@ async function handleCloudMessage(msg, ctx) {
645
845
  replyOptions: {
646
846
  ...replyOptions,
647
847
  onPartialReply,
848
+ onReasoningStream: async (payload) => {
849
+ if (!client?.connected || !payload.text)
850
+ return;
851
+ if (!streamStarted) {
852
+ streamStarted = true;
853
+ client.send({
854
+ type: "agent.stream.start",
855
+ sessionKey: msg.sessionKey,
856
+ runId,
857
+ });
858
+ }
859
+ const enc = await encryptForStream(client, payload.text);
860
+ client.send({
861
+ type: "agent.activity",
862
+ sessionKey: msg.sessionKey,
863
+ runId,
864
+ kind: "reasoning",
865
+ text: enc.text,
866
+ ...(enc.encrypted ? { encrypted: true, activityId: enc.id } : {}),
867
+ });
868
+ },
648
869
  allowPartialStream: true,
649
870
  },
650
871
  });
@@ -1122,10 +1343,19 @@ async function handleTaskRun(msg, ctx) {
1122
1343
  }, THROTTLE_MS);
1123
1344
  };
1124
1345
  const deliver = async (payload) => {
1125
- if (payload.text) {
1126
- completedParts.push(payload.text);
1346
+ const mediaList = payload.mediaUrls?.length
1347
+ ? payload.mediaUrls
1348
+ : payload.mediaUrl
1349
+ ? [payload.mediaUrl]
1350
+ : [];
1351
+ const parts = [];
1352
+ if (payload.text)
1353
+ parts.push(payload.text);
1354
+ for (const url of mediaList)
1355
+ parts.push(`![media](${url})`);
1356
+ if (parts.length > 0) {
1357
+ completedParts.push(parts.join("\n"));
1127
1358
  currentStreamText = "";
1128
- // Flush immediately on completed message
1129
1359
  if (sendTimer) {
1130
1360
  clearTimeout(sendTimer);
1131
1361
  sendTimer = null;
@@ -1142,8 +1372,7 @@ async function handleTaskRun(msg, ctx) {
1142
1372
  };
1143
1373
  const { dispatcher, replyOptions, markDispatchIdle } = runtime.channel.reply.createReplyDispatcherWithTyping({
1144
1374
  deliver: async (payload) => {
1145
- const p = payload;
1146
- await deliver(p);
1375
+ await deliver(payload);
1147
1376
  },
1148
1377
  onTypingStart: () => { },
1149
1378
  onTypingStop: () => { },
@@ -1155,6 +1384,19 @@ async function handleTaskRun(msg, ctx) {
1155
1384
  replyOptions: {
1156
1385
  ...replyOptions,
1157
1386
  onPartialReply,
1387
+ onReasoningStream: async (payload) => {
1388
+ if (!client?.connected || !payload.text)
1389
+ return;
1390
+ const enc = await encryptForStream(client, payload.text);
1391
+ client.send({
1392
+ type: "agent.activity",
1393
+ sessionKey,
1394
+ runId: jobId,
1395
+ kind: "reasoning",
1396
+ text: enc.text,
1397
+ ...(enc.encrypted ? { encrypted: true, activityId: enc.id } : {}),
1398
+ });
1399
+ },
1158
1400
  allowPartialStream: true,
1159
1401
  },
1160
1402
  });