@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 CHANGED
@@ -145,7 +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
+ - **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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudrise/openclaw-channel-rocketchat",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Rocket.Chat channel plugin for OpenClaw (Cloudrise)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 image URLs from Rocket.Chat message attachments/files.
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 extractImageUrls(
83
+ function extractFileUrls(
96
84
  msg: IncomingMessage,
97
85
  baseUrl: string
98
- ): Array<{ url: string; mimeType?: string }> {
99
- const images: Array<{ url: string; mimeType?: string }> = [];
86
+ ): Array<{ url: string; mimeType?: string; fileName?: string }> {
87
+ const files: Array<{ url: string; mimeType?: string; fileName?: string }> = [];
100
88
 
101
- // From attachments array (used for image_url references)
102
- if (msg.attachments?.length) {
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
- images.push({ url, mimeType: att.image_type });
106
+ files.push({ url, mimeType: att.image_type, fileName: att.title });
109
107
  }
110
108
  }
111
109
  }
112
110
 
113
- // From file/files (used for direct uploads)
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 FetchedImage = {
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 an image from Rocket.Chat and save to a temp file.
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 fetchImageToTempFile(
148
+ async function fetchFileToTemp(
137
149
  url: string,
138
150
  authToken: string,
139
151
  userId: string,
140
- mimeType?: string
141
- ): Promise<FetchedImage | null> {
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") ?? "image/png";
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
- const ext = contentType.includes("png") ? ".png"
158
- : contentType.includes("gif") ? ".gif"
159
- : contentType.includes("webp") ? ".webp"
160
- : ".jpg";
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 image attachments (if any)
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 imageRefs = extractImageUrls(msg, baseUrl);
409
- logger.debug?.(`[RC DEBUG] Extracted ${imageRefs.length} image refs`);
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 images (no text)
414
- if (!rawBody && imageRefs.length === 0) return;
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 images
428
- if (!rawBody && imageRefs.length === 0) return;
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 image-only messages, use a placeholder so the agent knows there's content
510
- const effectiveRawBody = rawBody || (imageRefs.length > 0 ? "[image]" : "");
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 images to temp files (OpenClaw expects file paths, not data URLs)
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 imageCleanups: Array<() => Promise<void>> = [];
569
+ const fileCleanups: Array<() => Promise<void>> = [];
558
570
 
559
- if (imageRefs.length > 0) {
571
+ if (fileRefs.length > 0) {
560
572
  const fetched = await Promise.all(
561
- imageRefs.map((ref) =>
562
- fetchImageToTempFile(ref.url, authToken, userId, ref.mimeType)
573
+ fileRefs.map((ref) =>
574
+ fetchFileToTemp(ref.url, authToken, userId, ref.mimeType, ref.fileName)
563
575
  )
564
576
  );
565
- const validImages = fetched.filter((img): img is FetchedImage => img !== null);
566
- if (validImages.length > 0) {
567
- mediaPaths = validImages.map((img) => img.path);
568
- mediaTypes = validImages.map((img) => img.mimeType);
569
- imageCleanups.push(...validImages.map((img) => img.cleanup));
570
- logger.debug?.(`Fetched ${validImages.length} image(s) from Rocket.Chat attachments`);
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 image files
703
- for (const cleanup of imageCleanups) {
714
+ // Clean up temp files
715
+ for (const cleanup of fileCleanups) {
704
716
  await cleanup();
705
717
  }
706
718
  }