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.
- package/README.md +4 -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
|
@@ -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
|
|
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:
|
|
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 (
|
|
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
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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([
|
|
180
|
-
formData.append("file", blob,
|
|
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
|
-
|
|
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
|
|
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]
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
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(``);
|
|
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
|
-
|
|
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
|
});
|