@cloudrise/openclaw-channel-rocketchat 0.1.9 → 0.1.10
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 +1 -0
- package/package.json +1 -1
- package/src/rocketchat/monitor.ts +110 -5
- package/src/rocketchat/realtime.ts +21 -6
package/README.md
CHANGED
|
@@ -145,6 +145,7 @@ Then restart the gateway.
|
|
|
145
145
|
|
|
146
146
|
## Features
|
|
147
147
|
|
|
148
|
+
- **Image attachments**: receives images uploaded to Rocket.Chat and passes them to the vision model.
|
|
148
149
|
- **Model prefix**: honors `messages.responsePrefix` (e.g. `({model}) `) so replies can include the model name.
|
|
149
150
|
|
|
150
151
|
## Model switching
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type RocketChatRoom,
|
|
23
23
|
type RocketChatClient,
|
|
24
24
|
} from "./client.js";
|
|
25
|
-
import { RocketChatRealtime, type IncomingMessage } from "./realtime.js";
|
|
25
|
+
import { RocketChatRealtime, type IncomingMessage, type RocketChatAttachment, type RocketChatFile } from "./realtime.js";
|
|
26
26
|
import { sendMessageRocketChat } from "./send.js";
|
|
27
27
|
|
|
28
28
|
export type MonitorRocketChatOpts = {
|
|
@@ -70,6 +70,83 @@ function chatType(kind: "dm" | "group" | "channel"): "direct" | "group" | "chann
|
|
|
70
70
|
return "channel";
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Image MIME types we can send to vision models */
|
|
74
|
+
const IMAGE_MIME_TYPES = new Set([
|
|
75
|
+
"image/jpeg",
|
|
76
|
+
"image/png",
|
|
77
|
+
"image/gif",
|
|
78
|
+
"image/webp",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
function isImageMime(mime?: string): boolean {
|
|
82
|
+
if (!mime) return false;
|
|
83
|
+
return IMAGE_MIME_TYPES.has(mime.toLowerCase().split(";")[0].trim());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract image URLs from Rocket.Chat message attachments/files.
|
|
88
|
+
* Returns full URLs that can be fetched with auth headers.
|
|
89
|
+
*/
|
|
90
|
+
function extractImageUrls(
|
|
91
|
+
msg: IncomingMessage,
|
|
92
|
+
baseUrl: string
|
|
93
|
+
): Array<{ url: string; mimeType?: string }> {
|
|
94
|
+
const images: Array<{ url: string; mimeType?: string }> = [];
|
|
95
|
+
|
|
96
|
+
// From attachments array (used for image_url references)
|
|
97
|
+
if (msg.attachments?.length) {
|
|
98
|
+
for (const att of msg.attachments) {
|
|
99
|
+
if (att.image_url) {
|
|
100
|
+
const url = att.image_url.startsWith("http")
|
|
101
|
+
? att.image_url
|
|
102
|
+
: `${baseUrl}${att.image_url.startsWith("/") ? "" : "/"}${att.image_url}`;
|
|
103
|
+
images.push({ url, mimeType: att.image_type });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// From file/files (used for direct uploads)
|
|
109
|
+
const files = msg.files ?? (msg.file ? [msg.file] : []);
|
|
110
|
+
for (const f of files) {
|
|
111
|
+
if (f._id && f.name && isImageMime(f.type)) {
|
|
112
|
+
// Rocket.Chat file-upload URL pattern
|
|
113
|
+
const url = `${baseUrl}/file-upload/${f._id}/${encodeURIComponent(f.name)}`;
|
|
114
|
+
images.push({ url, mimeType: f.type });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return images;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fetch an image from Rocket.Chat and return as base64 data URL.
|
|
123
|
+
*/
|
|
124
|
+
async function fetchImageAsDataUrl(
|
|
125
|
+
url: string,
|
|
126
|
+
authToken: string,
|
|
127
|
+
userId: string,
|
|
128
|
+
mimeType?: string
|
|
129
|
+
): Promise<string | null> {
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(url, {
|
|
132
|
+
headers: {
|
|
133
|
+
"X-Auth-Token": authToken,
|
|
134
|
+
"X-User-Id": userId,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok) return null;
|
|
138
|
+
|
|
139
|
+
const contentType = mimeType ?? res.headers.get("content-type") ?? "image/png";
|
|
140
|
+
if (!isImageMime(contentType)) return null;
|
|
141
|
+
|
|
142
|
+
const buffer = await res.arrayBuffer();
|
|
143
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
144
|
+
return `data:${contentType};base64,${base64}`;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
73
150
|
export async function monitorRocketChatProvider(
|
|
74
151
|
opts: MonitorRocketChatOpts
|
|
75
152
|
): Promise<() => void> {
|
|
@@ -293,8 +370,16 @@ async function handleIncomingMessage(
|
|
|
293
370
|
? msg.ts.$date
|
|
294
371
|
: Date.parse(String(msg.ts));
|
|
295
372
|
|
|
373
|
+
// Extract image attachments (if any)
|
|
374
|
+
const baseUrl = account.baseUrl;
|
|
375
|
+
const authToken = account.authToken;
|
|
376
|
+
const userId = account.userId;
|
|
377
|
+
const imageRefs = extractImageUrls(msg, baseUrl);
|
|
378
|
+
|
|
296
379
|
let rawBody = msg.msg.trim();
|
|
297
|
-
|
|
380
|
+
|
|
381
|
+
// Allow messages with only images (no text)
|
|
382
|
+
if (!rawBody && imageRefs.length === 0) return;
|
|
298
383
|
|
|
299
384
|
// Optional per-message overrides
|
|
300
385
|
// - !thread -> force reply in thread
|
|
@@ -307,7 +392,8 @@ async function handleIncomingMessage(
|
|
|
307
392
|
forcedReplyMode = "channel";
|
|
308
393
|
rawBody = rawBody.replace(/^!channel\b\s*/i, "").trim();
|
|
309
394
|
}
|
|
310
|
-
if
|
|
395
|
+
// Skip if no text and no images
|
|
396
|
+
if (!rawBody && imageRefs.length === 0) return;
|
|
311
397
|
|
|
312
398
|
// Determine reply mode
|
|
313
399
|
const baseReplyMode: "thread" | "channel" | "auto" =
|
|
@@ -388,14 +474,16 @@ async function handleIncomingMessage(
|
|
|
388
474
|
: undefined;
|
|
389
475
|
|
|
390
476
|
// Format the envelope body
|
|
477
|
+
// For image-only messages, use a placeholder so the agent knows there's content
|
|
478
|
+
const effectiveRawBody = rawBody || (imageRefs.length > 0 ? "[image]" : "");
|
|
391
479
|
const body = core.channel?.reply?.formatAgentEnvelope?.({
|
|
392
480
|
channel: "Rocket.Chat",
|
|
393
481
|
from: fromLabel,
|
|
394
482
|
timestamp: ts,
|
|
395
483
|
previousTimestamp,
|
|
396
484
|
envelope: envelopeOptions,
|
|
397
|
-
body:
|
|
398
|
-
}) ??
|
|
485
|
+
body: effectiveRawBody,
|
|
486
|
+
}) ?? effectiveRawBody;
|
|
399
487
|
|
|
400
488
|
// Rocket.Chat NOTE: Messages starting with "/" are treated as Rocket.Chat slash-commands.
|
|
401
489
|
// To make model switching usable from chat, we support an alternate syntax:
|
|
@@ -406,6 +494,20 @@ async function handleIncomingMessage(
|
|
|
406
494
|
.replace(/^\s*--model\b/i, "/model")
|
|
407
495
|
.replace(/^\s*--/, "/");
|
|
408
496
|
|
|
497
|
+
// Fetch images as data URLs (authenticated fetch required for Rocket.Chat uploads)
|
|
498
|
+
let mediaUrls: string[] | undefined;
|
|
499
|
+
if (imageRefs.length > 0) {
|
|
500
|
+
const fetched = await Promise.all(
|
|
501
|
+
imageRefs.map((ref) =>
|
|
502
|
+
fetchImageAsDataUrl(ref.url, authToken, userId, ref.mimeType)
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
mediaUrls = fetched.filter((u): u is string => u !== null);
|
|
506
|
+
if (mediaUrls.length > 0) {
|
|
507
|
+
logger.debug?.(`Fetched ${mediaUrls.length} image(s) from Rocket.Chat attachments`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
409
511
|
// Finalize inbound context
|
|
410
512
|
const ctxPayload = core.channel?.reply?.finalizeInboundContext?.({
|
|
411
513
|
Body: body,
|
|
@@ -426,6 +528,9 @@ async function handleIncomingMessage(
|
|
|
426
528
|
Timestamp: ts,
|
|
427
529
|
OriginatingChannel: "rocketchat",
|
|
428
530
|
OriginatingTo: `rocketchat:${roomId}`,
|
|
531
|
+
|
|
532
|
+
// Image attachments (fetched as base64 data URLs)
|
|
533
|
+
MediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
|
429
534
|
});
|
|
430
535
|
|
|
431
536
|
if (!ctxPayload) {
|
|
@@ -33,6 +33,24 @@ export type RealtimeOpts = {
|
|
|
33
33
|
logger?: { debug?: (msg: string) => void; info?: (msg: string) => void };
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export type RocketChatAttachment = {
|
|
37
|
+
title?: string;
|
|
38
|
+
title_link?: string;
|
|
39
|
+
image_url?: string;
|
|
40
|
+
audio_url?: string;
|
|
41
|
+
video_url?: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
image_type?: string;
|
|
44
|
+
image_size?: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type RocketChatFile = {
|
|
48
|
+
_id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
type?: string;
|
|
51
|
+
size?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
36
54
|
export type IncomingMessage = {
|
|
37
55
|
_id: string;
|
|
38
56
|
rid: string;
|
|
@@ -41,12 +59,9 @@ export type IncomingMessage = {
|
|
|
41
59
|
u: { _id: string; username: string; name?: string };
|
|
42
60
|
tmid?: string;
|
|
43
61
|
t?: string;
|
|
44
|
-
attachments?:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
audio_url?: string;
|
|
48
|
-
video_url?: string;
|
|
49
|
-
}>;
|
|
62
|
+
attachments?: RocketChatAttachment[];
|
|
63
|
+
file?: RocketChatFile;
|
|
64
|
+
files?: RocketChatFile[];
|
|
50
65
|
};
|
|
51
66
|
|
|
52
67
|
export class RocketChatRealtime {
|