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.
- package/README.md +4 -0
- package/migrations/0013_agents_table.sql +29 -0
- package/migrations/0014_agent_sessions.sql +19 -0
- package/migrations/0015_message_traces.sql +27 -0
- package/migrations/0016_multi_agent_channels_messages.sql +9 -0
- package/migrations/0017_rename_cron_job_id.sql +2 -0
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +375 -42
- package/packages/api/src/index.ts +67 -24
- package/packages/api/src/protocol-v2.ts +154 -0
- package/packages/api/src/routes/agents-v2.ts +192 -0
- package/packages/api/src/routes/agents.ts +3 -3
- package/packages/api/src/routes/channels.ts +11 -11
- package/packages/api/src/routes/history-v2.ts +221 -0
- package/packages/api/src/routes/migrate-v2.ts +110 -0
- package/packages/api/src/routes/sessions.ts +5 -5
- package/packages/api/src/routes/tasks.ts +33 -33
- 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 +10 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +311 -69
- 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 +25 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -1
- package/packages/plugin/package.json +1 -1
- package/packages/web/dist/assets/{index-B5GU1yVt.css → index-BARPtt0v.css} +1 -1
- package/packages/web/dist/assets/index-Bf-XL3te.js +2 -0
- package/packages/web/dist/assets/{index-DzYqprDN.js → index-CYQMu_-c.js} +1 -1
- package/packages/web/dist/assets/index-CYlvfpX9.js +1519 -0
- package/packages/web/dist/assets/index-CxcpA4Qo.js +1 -0
- package/packages/web/dist/assets/{index-D3T7sc-R.js → index-DYCO-ry1.js} +1 -1
- package/packages/web/dist/assets/index-QebPVqwj.js +2 -0
- package/packages/web/dist/assets/{index.esm-COzWPkKi.js → index.esm-CvOpngZM.js} +1 -1
- package/packages/web/dist/assets/{web-CxXbaApe.js → web-1cdhq2RW.js} +1 -1
- package/packages/web/dist/assets/{web-DFQypSd0.js → web-D3LMODYp.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +84 -5
- package/packages/web/src/api.ts +61 -3
- package/packages/web/src/components/AgentSettings.tsx +328 -0
- package/packages/web/src/components/ChatWindow.tsx +124 -4
- package/packages/web/src/components/CronDetail.tsx +1 -1
- package/packages/web/src/components/SessionTabs.tsx +1 -1
- package/packages/web/src/components/Sidebar.tsx +3 -1
- package/packages/web/src/store.ts +86 -11
- package/packages/web/src/ws.ts +22 -1
- package/scripts/dev.sh +53 -0
- package/scripts/mock-openclaw-v2.mjs +486 -0
- package/scripts/mock-openclaw.mjs +35 -0
- 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,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: {
|
|
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
|
|
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:
|
|
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 (
|
|
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
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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([
|
|
180
|
-
formData.append("file", blob,
|
|
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
|
-
|
|
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
|
|
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]
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
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(``);
|
|
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
|
-
|
|
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
|
});
|