Skip to content

Commit 0488f27

Browse files
fix(plugin): support mediaUrls normalization and fix type safety in deliver
1 parent 6bcfb2b commit 0488f27

1 file changed

Lines changed: 79 additions & 46 deletions

File tree

packages/plugin/src/channel.ts

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import { BotsChatCloudClient } from "./ws-client.js";
1111
import crypto from "crypto";
1212
import { encryptText, encryptBytes, decryptText, decryptBytes, toBase64, fromBase64 } from "./e2e-crypto.js";
1313

14+
/** Subset of OpenClaw's ReplyPayload that the deliver callback receives. */
15+
type DeliverPayload = {
16+
text?: string;
17+
mediaUrl?: string;
18+
mediaUrls?: string[];
19+
};
20+
1421
// ---------------------------------------------------------------------------
1522
// A2UI message-tool hints — injected via agentPrompt.messageToolHints so
1623
// the agent knows it can output interactive UI components. These strings
@@ -638,44 +645,69 @@ async function handleCloudMessage(
638645
// Create a reply dispatcher that sends responses back through the cloud WSS
639646
// NOTE: reuses `client` from line ~424 (same block scope, same value)
640647
console.log(`[botschat] client for accountId=${ctx.accountId}: connected=${client?.connected}`);
641-
const deliver = async (payload: { text?: string; mediaUrl?: string }) => {
642-
console.log(`[botschat][deliver] called, connected=${client?.connected}, hasKey=${!!client?.e2eKey}, textLen=${(payload.text || "").length}`);
648+
const deliver = async (payload: DeliverPayload) => {
649+
const mediaList = payload.mediaUrls?.length
650+
? payload.mediaUrls
651+
: payload.mediaUrl
652+
? [payload.mediaUrl]
653+
: [];
654+
console.log(`[botschat][deliver] called, connected=${client?.connected}, hasKey=${!!client?.e2eKey}, textLen=${(payload.text || "").length}, mediaCount=${mediaList.length}`);
643655
if (!client?.connected) { console.log("[botschat][deliver] SKIP - not connected"); return; }
644-
const messageId = crypto.randomUUID();
645-
let text = payload.text ?? "";
646-
let caption = payload.text ?? "";
647-
let encrypted = false;
648656

649-
if (client.e2eKey && text) {
650-
try {
651-
const ct = await encryptText(client.e2eKey, text, messageId);
652-
text = toBase64(ct);
653-
caption = text;
654-
encrypted = true;
655-
console.log(`[botschat][deliver] encrypted OK: msgId=${messageId}, ctLen=${text.length}, encrypted=${encrypted}`);
656-
} catch (err) {
657-
console.error("[botschat][deliver] E2E encrypt failed:", err);
658-
}
659-
} else {
660-
console.log(`[botschat][deliver] no encryption: hasKey=${!!client.e2eKey}, textLen=${text.length}`);
661-
}
657+
if (mediaList.length > 0) {
658+
let first = true;
659+
for (const mediaUrl of mediaList) {
660+
const messageId = crypto.randomUUID();
661+
const rawCaption = first ? (payload.text ?? "") : "";
662+
first = false;
663+
let caption = rawCaption;
664+
let encrypted = false;
665+
666+
if (client.e2eKey && caption) {
667+
try {
668+
const ct = await encryptText(client.e2eKey, caption, messageId);
669+
caption = toBase64(ct);
670+
encrypted = true;
671+
} catch (err) {
672+
console.error("[botschat][deliver] E2E encrypt caption failed:", err);
673+
}
674+
}
662675

663-
const notifyPreviewText = (encrypted && client.notifyPreview && payload.text)
664-
? (payload.text.length > 100 ? payload.text.slice(0, 100) + "…" : payload.text)
665-
: undefined;
666-
console.log(`[botschat][deliver] sending: type=${payload.mediaUrl ? "agent.media" : "agent.text"}, encrypted=${encrypted}, messageId=${messageId}, notifyPreview=${!!notifyPreviewText}`);
667-
if (payload.mediaUrl) {
668-
client.send({
669-
type: "agent.media",
670-
sessionKey: msg.sessionKey,
671-
mediaUrl: payload.mediaUrl,
672-
caption: encrypted ? caption : payload.text,
673-
threadId,
674-
messageId,
675-
encrypted,
676-
...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
677-
});
676+
const notifyPreviewText = (encrypted && client.notifyPreview && rawCaption)
677+
? (rawCaption.length > 100 ? rawCaption.slice(0, 100) + "…" : rawCaption)
678+
: undefined;
679+
console.log(`[botschat][deliver] sending: type=agent.media, encrypted=${encrypted}, messageId=${messageId}`);
680+
client.send({
681+
type: "agent.media",
682+
sessionKey: msg.sessionKey,
683+
mediaUrl,
684+
caption: caption || undefined,
685+
threadId,
686+
messageId,
687+
encrypted,
688+
...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
689+
});
690+
}
678691
} else if (payload.text) {
692+
const messageId = crypto.randomUUID();
693+
let text = payload.text;
694+
let encrypted = false;
695+
696+
if (client.e2eKey && text) {
697+
try {
698+
const ct = await encryptText(client.e2eKey, text, messageId);
699+
text = toBase64(ct);
700+
encrypted = true;
701+
console.log(`[botschat][deliver] encrypted OK: msgId=${messageId}, ctLen=${text.length}`);
702+
} catch (err) {
703+
console.error("[botschat][deliver] E2E encrypt failed:", err);
704+
}
705+
}
706+
707+
const notifyPreviewText = (encrypted && client.notifyPreview && payload.text)
708+
? (payload.text.length > 100 ? payload.text.slice(0, 100) + "…" : payload.text)
709+
: undefined;
710+
console.log(`[botschat][deliver] sending: type=agent.text, encrypted=${encrypted}, messageId=${messageId}`);
679711
client.send({
680712
type: "agent.text",
681713
sessionKey: msg.sessionKey,
@@ -686,9 +718,6 @@ async function handleCloudMessage(
686718
...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
687719
});
688720
// Detect model-change confirmations and emit model.changed
689-
// Handles both formats:
690-
// "Model set to provider/model." (no parentheses)
691-
// "Model set to Friendly Name (provider/model)." (with parentheses)
692721
const modelMatch = payload.text.match(
693722
/Model (?:set to|reset to default)\b.*?([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\/[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)/,
694723
);
@@ -731,9 +760,7 @@ async function handleCloudMessage(
731760
const { dispatcher, replyOptions, markDispatchIdle } =
732761
runtime.channel.reply.createReplyDispatcherWithTyping({
733762
deliver: async (payload: unknown) => {
734-
// The payload from the dispatcher is a ReplyPayload
735-
const p = payload as { text?: string; mediaUrl?: string };
736-
await deliver(p);
763+
await deliver(payload as DeliverPayload);
737764
},
738765
onTypingStart: () => {},
739766
onTypingStop: () => {},
@@ -1280,11 +1307,18 @@ async function handleTaskRun(
12801307
}, THROTTLE_MS);
12811308
};
12821309

1283-
const deliver = async (payload: { text?: string; mediaUrl?: string }) => {
1284-
if (payload.text) {
1285-
completedParts.push(payload.text);
1310+
const deliver = async (payload: DeliverPayload) => {
1311+
const mediaList = payload.mediaUrls?.length
1312+
? payload.mediaUrls
1313+
: payload.mediaUrl
1314+
? [payload.mediaUrl]
1315+
: [];
1316+
const parts: string[] = [];
1317+
if (payload.text) parts.push(payload.text);
1318+
for (const url of mediaList) parts.push(`![media](${url})`);
1319+
if (parts.length > 0) {
1320+
completedParts.push(parts.join("\n"));
12861321
currentStreamText = "";
1287-
// Flush immediately on completed message
12881322
if (sendTimer) { clearTimeout(sendTimer); sendTimer = null; }
12891323
sendOutput();
12901324
}
@@ -1300,8 +1334,7 @@ async function handleTaskRun(
13001334
const { dispatcher, replyOptions, markDispatchIdle } =
13011335
runtime.channel.reply.createReplyDispatcherWithTyping({
13021336
deliver: async (payload: unknown) => {
1303-
const p = payload as { text?: string; mediaUrl?: string };
1304-
await deliver(p);
1337+
await deliver(payload as DeliverPayload);
13051338
},
13061339
onTypingStart: () => {},
13071340
onTypingStop: () => {},

0 commit comments

Comments
 (0)