@cloudrise/openclaw-channel-rocketchat 0.1.11 → 0.1.12
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 -1
- package/package.json +1 -1
- package/src/rocketchat/monitor.ts +78 -66
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ Then restart the gateway.
|
|
|
145
145
|
|
|
146
146
|
## Features
|
|
147
147
|
|
|
148
|
-
- **
|
|
148
|
+
- **File attachments**: receives images, PDFs, documents, audio uploaded to Rocket.Chat and passes them to the vision model.
|
|
149
149
|
- **Model prefix**: honors `messages.responsePrefix` (e.g. `({model}) `) so replies can include the model name.
|
|
150
150
|
|
|
151
151
|
## Model switching
|
package/package.json
CHANGED
|
@@ -75,70 +75,83 @@ function chatType(kind: "dm" | "group" | "channel"): "direct" | "group" | "chann
|
|
|
75
75
|
return "channel";
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Image MIME types we can send to vision models */
|
|
79
|
-
const IMAGE_MIME_TYPES = new Set([
|
|
80
|
-
"image/jpeg",
|
|
81
|
-
"image/png",
|
|
82
|
-
"image/gif",
|
|
83
|
-
"image/webp",
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
function isImageMime(mime?: string): boolean {
|
|
87
|
-
if (!mime) return false;
|
|
88
|
-
return IMAGE_MIME_TYPES.has(mime.toLowerCase().split(";")[0].trim());
|
|
89
|
-
}
|
|
90
|
-
|
|
91
78
|
/**
|
|
92
|
-
* Extract
|
|
79
|
+
* Extract file URLs from Rocket.Chat message attachments/files.
|
|
93
80
|
* Returns full URLs that can be fetched with auth headers.
|
|
81
|
+
* Supports all file types - OpenClaw's media understanding handles type detection.
|
|
94
82
|
*/
|
|
95
|
-
function
|
|
83
|
+
function extractFileUrls(
|
|
96
84
|
msg: IncomingMessage,
|
|
97
85
|
baseUrl: string
|
|
98
|
-
): Array<{ url: string; mimeType?: string }> {
|
|
99
|
-
const
|
|
86
|
+
): Array<{ url: string; mimeType?: string; fileName?: string }> {
|
|
87
|
+
const files: Array<{ url: string; mimeType?: string; fileName?: string }> = [];
|
|
100
88
|
|
|
101
|
-
// From
|
|
102
|
-
|
|
89
|
+
// From file/files (used for direct uploads) - check these first as they're more reliable
|
|
90
|
+
const fileList = msg.files ?? (msg.file ? [msg.file] : []);
|
|
91
|
+
for (const f of fileList) {
|
|
92
|
+
if (f._id && f.name) {
|
|
93
|
+
// Rocket.Chat file-upload URL pattern
|
|
94
|
+
const url = `${baseUrl}/file-upload/${f._id}/${encodeURIComponent(f.name)}`;
|
|
95
|
+
files.push({ url, mimeType: f.type, fileName: decodeURIComponent(f.name) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// From attachments array (fallback for image_url references if no files found)
|
|
100
|
+
if (files.length === 0 && msg.attachments?.length) {
|
|
103
101
|
for (const att of msg.attachments) {
|
|
104
102
|
if (att.image_url) {
|
|
105
103
|
const url = att.image_url.startsWith("http")
|
|
106
104
|
? att.image_url
|
|
107
105
|
: `${baseUrl}${att.image_url.startsWith("/") ? "" : "/"}${att.image_url}`;
|
|
108
|
-
|
|
106
|
+
files.push({ url, mimeType: att.image_type, fileName: att.title });
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
const files = msg.files ?? (msg.file ? [msg.file] : []);
|
|
115
|
-
for (const f of files) {
|
|
116
|
-
if (f._id && f.name && isImageMime(f.type)) {
|
|
117
|
-
// Rocket.Chat file-upload URL pattern
|
|
118
|
-
const url = `${baseUrl}/file-upload/${f._id}/${encodeURIComponent(f.name)}`;
|
|
119
|
-
images.push({ url, mimeType: f.type });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return images;
|
|
111
|
+
return files;
|
|
124
112
|
}
|
|
125
113
|
|
|
126
|
-
type
|
|
114
|
+
type FetchedFile = {
|
|
127
115
|
path: string;
|
|
128
116
|
mimeType: string;
|
|
129
117
|
cleanup: () => Promise<void>;
|
|
130
118
|
};
|
|
131
119
|
|
|
120
|
+
/** Map common MIME types to file extensions */
|
|
121
|
+
function getExtensionFromMime(mimeType: string): string {
|
|
122
|
+
const mime = mimeType.toLowerCase().split(";")[0].trim();
|
|
123
|
+
const map: Record<string, string> = {
|
|
124
|
+
"image/png": ".png",
|
|
125
|
+
"image/jpeg": ".jpg",
|
|
126
|
+
"image/gif": ".gif",
|
|
127
|
+
"image/webp": ".webp",
|
|
128
|
+
"application/pdf": ".pdf",
|
|
129
|
+
"text/plain": ".txt",
|
|
130
|
+
"text/markdown": ".md",
|
|
131
|
+
"text/csv": ".csv",
|
|
132
|
+
"application/json": ".json",
|
|
133
|
+
"application/xml": ".xml",
|
|
134
|
+
"text/xml": ".xml",
|
|
135
|
+
"audio/mpeg": ".mp3",
|
|
136
|
+
"audio/wav": ".wav",
|
|
137
|
+
"audio/ogg": ".ogg",
|
|
138
|
+
"video/mp4": ".mp4",
|
|
139
|
+
"video/webm": ".webm",
|
|
140
|
+
};
|
|
141
|
+
return map[mime] ?? ".bin";
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
/**
|
|
133
|
-
* Fetch
|
|
145
|
+
* Fetch a file from Rocket.Chat and save to a temp file.
|
|
134
146
|
* Returns the file path (OpenClaw expects file paths, not data URLs).
|
|
135
147
|
*/
|
|
136
|
-
async function
|
|
148
|
+
async function fetchFileToTemp(
|
|
137
149
|
url: string,
|
|
138
150
|
authToken: string,
|
|
139
151
|
userId: string,
|
|
140
|
-
mimeType?: string
|
|
141
|
-
|
|
152
|
+
mimeType?: string,
|
|
153
|
+
fileName?: string
|
|
154
|
+
): Promise<FetchedFile | null> {
|
|
142
155
|
try {
|
|
143
156
|
const res = await fetch(url, {
|
|
144
157
|
headers: {
|
|
@@ -148,16 +161,15 @@ async function fetchImageToTempFile(
|
|
|
148
161
|
});
|
|
149
162
|
if (!res.ok) return null;
|
|
150
163
|
|
|
151
|
-
const contentType = mimeType ?? res.headers.get("content-type") ?? "
|
|
152
|
-
if (!isImageMime(contentType)) return null;
|
|
153
|
-
|
|
164
|
+
const contentType = mimeType ?? res.headers.get("content-type") ?? "application/octet-stream";
|
|
154
165
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
155
166
|
|
|
156
|
-
// Determine extension from mime type
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
// Determine extension from filename or mime type
|
|
168
|
+
let ext = getExtensionFromMime(contentType);
|
|
169
|
+
if (fileName) {
|
|
170
|
+
const fnExt = path.extname(fileName);
|
|
171
|
+
if (fnExt) ext = fnExt;
|
|
172
|
+
}
|
|
161
173
|
|
|
162
174
|
const tempPath = path.join(os.tmpdir(), `openclaw-rc-${crypto.randomUUID()}${ext}`);
|
|
163
175
|
await fs.writeFile(tempPath, buffer, { mode: 0o600 });
|
|
@@ -397,7 +409,7 @@ async function handleIncomingMessage(
|
|
|
397
409
|
? msg.ts.$date
|
|
398
410
|
: Date.parse(String(msg.ts));
|
|
399
411
|
|
|
400
|
-
// Extract
|
|
412
|
+
// Extract file attachments (images, PDFs, documents, etc.)
|
|
401
413
|
const baseUrl = account.baseUrl;
|
|
402
414
|
const authToken = account.authToken;
|
|
403
415
|
const userId = account.userId;
|
|
@@ -405,13 +417,13 @@ async function handleIncomingMessage(
|
|
|
405
417
|
// DEBUG: Log raw message to see what DDP sends
|
|
406
418
|
logger.debug?.(`[RC DEBUG] Raw message: file=${JSON.stringify(msg.file)} files=${JSON.stringify(msg.files)} attachments=${JSON.stringify(msg.attachments?.slice(0, 2))}`);
|
|
407
419
|
|
|
408
|
-
const
|
|
409
|
-
logger.debug?.(`[RC DEBUG] Extracted ${
|
|
420
|
+
const fileRefs = extractFileUrls(msg, baseUrl);
|
|
421
|
+
logger.debug?.(`[RC DEBUG] Extracted ${fileRefs.length} file refs`);
|
|
410
422
|
|
|
411
423
|
let rawBody = msg.msg.trim();
|
|
412
424
|
|
|
413
|
-
// Allow messages with only
|
|
414
|
-
if (!rawBody &&
|
|
425
|
+
// Allow messages with only file attachments (no text)
|
|
426
|
+
if (!rawBody && fileRefs.length === 0) return;
|
|
415
427
|
|
|
416
428
|
// Optional per-message overrides
|
|
417
429
|
// - !thread -> force reply in thread
|
|
@@ -424,8 +436,8 @@ async function handleIncomingMessage(
|
|
|
424
436
|
forcedReplyMode = "channel";
|
|
425
437
|
rawBody = rawBody.replace(/^!channel\b\s*/i, "").trim();
|
|
426
438
|
}
|
|
427
|
-
// Skip if no text and no
|
|
428
|
-
if (!rawBody &&
|
|
439
|
+
// Skip if no text and no file attachments
|
|
440
|
+
if (!rawBody && fileRefs.length === 0) return;
|
|
429
441
|
|
|
430
442
|
// Determine reply mode
|
|
431
443
|
const baseReplyMode: "thread" | "channel" | "auto" =
|
|
@@ -506,8 +518,8 @@ async function handleIncomingMessage(
|
|
|
506
518
|
: undefined;
|
|
507
519
|
|
|
508
520
|
// Format the envelope body
|
|
509
|
-
// For
|
|
510
|
-
const effectiveRawBody = rawBody || (
|
|
521
|
+
// For attachment-only messages, use a placeholder so the agent knows there's content
|
|
522
|
+
const effectiveRawBody = rawBody || (fileRefs.length > 0 ? "[attachment]" : "");
|
|
511
523
|
const body = core.channel?.reply?.formatAgentEnvelope?.({
|
|
512
524
|
channel: "Rocket.Chat",
|
|
513
525
|
from: fromLabel,
|
|
@@ -551,23 +563,23 @@ async function handleIncomingMessage(
|
|
|
551
563
|
const commandAuthorized = true;
|
|
552
564
|
const bodyForAgent = body;
|
|
553
565
|
|
|
554
|
-
// Fetch
|
|
566
|
+
// Fetch files to temp (OpenClaw expects file paths, not data URLs)
|
|
555
567
|
let mediaPaths: string[] | undefined;
|
|
556
568
|
let mediaTypes: string[] | undefined;
|
|
557
|
-
const
|
|
569
|
+
const fileCleanups: Array<() => Promise<void>> = [];
|
|
558
570
|
|
|
559
|
-
if (
|
|
571
|
+
if (fileRefs.length > 0) {
|
|
560
572
|
const fetched = await Promise.all(
|
|
561
|
-
|
|
562
|
-
|
|
573
|
+
fileRefs.map((ref) =>
|
|
574
|
+
fetchFileToTemp(ref.url, authToken, userId, ref.mimeType, ref.fileName)
|
|
563
575
|
)
|
|
564
576
|
);
|
|
565
|
-
const
|
|
566
|
-
if (
|
|
567
|
-
mediaPaths =
|
|
568
|
-
mediaTypes =
|
|
569
|
-
|
|
570
|
-
logger.debug?.(`Fetched ${
|
|
577
|
+
const validFiles = fetched.filter((f): f is FetchedFile => f !== null);
|
|
578
|
+
if (validFiles.length > 0) {
|
|
579
|
+
mediaPaths = validFiles.map((f) => f.path);
|
|
580
|
+
mediaTypes = validFiles.map((f) => f.mimeType);
|
|
581
|
+
fileCleanups.push(...validFiles.map((f) => f.cleanup));
|
|
582
|
+
logger.debug?.(`Fetched ${validFiles.length} file(s) from Rocket.Chat attachments`);
|
|
571
583
|
}
|
|
572
584
|
}
|
|
573
585
|
|
|
@@ -699,8 +711,8 @@ async function handleIncomingMessage(
|
|
|
699
711
|
});
|
|
700
712
|
} finally {
|
|
701
713
|
await stopTyping();
|
|
702
|
-
// Clean up temp
|
|
703
|
-
for (const cleanup of
|
|
714
|
+
// Clean up temp files
|
|
715
|
+
for (const cleanup of fileCleanups) {
|
|
704
716
|
await cleanup();
|
|
705
717
|
}
|
|
706
718
|
}
|