@cloudrise/openclaw-channel-rocketchat 0.1.10 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudrise/openclaw-channel-rocketchat",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Rocket.Chat channel plugin for OpenClaw (Cloudrise)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,11 @@
2
2
  * Rocket.Chat message monitor - handles incoming messages via Realtime API
3
3
  */
4
4
 
5
+ import * as fs from "node:fs/promises";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import * as crypto from "node:crypto";
9
+
5
10
  import type {
6
11
  ChannelAccountSnapshot,
7
12
  OpenclawConfig,
@@ -118,15 +123,22 @@ function extractImageUrls(
118
123
  return images;
119
124
  }
120
125
 
126
+ type FetchedImage = {
127
+ path: string;
128
+ mimeType: string;
129
+ cleanup: () => Promise<void>;
130
+ };
131
+
121
132
  /**
122
- * Fetch an image from Rocket.Chat and return as base64 data URL.
133
+ * Fetch an image from Rocket.Chat and save to a temp file.
134
+ * Returns the file path (OpenClaw expects file paths, not data URLs).
123
135
  */
124
- async function fetchImageAsDataUrl(
136
+ async function fetchImageToTempFile(
125
137
  url: string,
126
138
  authToken: string,
127
139
  userId: string,
128
140
  mimeType?: string
129
- ): Promise<string | null> {
141
+ ): Promise<FetchedImage | null> {
130
142
  try {
131
143
  const res = await fetch(url, {
132
144
  headers: {
@@ -139,9 +151,24 @@ async function fetchImageAsDataUrl(
139
151
  const contentType = mimeType ?? res.headers.get("content-type") ?? "image/png";
140
152
  if (!isImageMime(contentType)) return null;
141
153
 
142
- const buffer = await res.arrayBuffer();
143
- const base64 = Buffer.from(buffer).toString("base64");
144
- return `data:${contentType};base64,${base64}`;
154
+ const buffer = Buffer.from(await res.arrayBuffer());
155
+
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";
161
+
162
+ const tempPath = path.join(os.tmpdir(), `openclaw-rc-${crypto.randomUUID()}${ext}`);
163
+ await fs.writeFile(tempPath, buffer, { mode: 0o600 });
164
+
165
+ return {
166
+ path: tempPath,
167
+ mimeType: contentType,
168
+ cleanup: async () => {
169
+ await fs.unlink(tempPath).catch(() => {});
170
+ },
171
+ };
145
172
  } catch {
146
173
  return null;
147
174
  }
@@ -374,7 +401,12 @@ async function handleIncomingMessage(
374
401
  const baseUrl = account.baseUrl;
375
402
  const authToken = account.authToken;
376
403
  const userId = account.userId;
404
+
405
+ // DEBUG: Log raw message to see what DDP sends
406
+ 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
+
377
408
  const imageRefs = extractImageUrls(msg, baseUrl);
409
+ logger.debug?.(`[RC DEBUG] Extracted ${imageRefs.length} image refs`);
378
410
 
379
411
  let rawBody = msg.msg.trim();
380
412
 
@@ -485,34 +517,73 @@ async function handleIncomingMessage(
485
517
  body: effectiveRawBody,
486
518
  }) ?? effectiveRawBody;
487
519
 
488
- // Rocket.Chat NOTE: Messages starting with "/" are treated as Rocket.Chat slash-commands.
489
- // To make model switching usable from chat, we support an alternate syntax:
490
- // --model qwen3
491
- // --qwen3
492
- // which we normalize into OpenClaw inline directives.
493
- const commandBody = rawBody
520
+
521
+ // Inline directives (e.g. /model <ref>) should be parsed from the raw inbound text.
522
+ // Rocket.Chat users often type one-line directives like: "/model qwen3 hello".
523
+ // We split a leading directive into:
524
+ // - BodyForCommands: directive-only (so OpenClaw applies it)
525
+ // - BodyForAgent: the full envelope (so the agent retains context)
526
+ const normalizedRawBody = rawBody
527
+ // Shorthands (Rocket.Chat doesn't reliably allow leading `/...`)
528
+ .replace(/^\s*--opus\b/i, "/model opus")
529
+ .replace(/^\s*-opus\b/i, "/model opus")
530
+ .replace(/^\s*--oss\b/i, "/model oss")
531
+ .replace(/^\s*-oss\b/i, "/model oss")
494
532
  .replace(/^\s*--model\b/i, "/model")
533
+ .replace(/^\s*-model\b/i, "/model")
534
+ // Generic: `--foo` => `/foo` (we intentionally do NOT do this for single-dash to avoid
535
+ // accidental triggers on markdown bullets / negative numbers).
495
536
  .replace(/^\s*--/, "/");
496
537
 
497
- // Fetch images as data URLs (authenticated fetch required for Rocket.Chat uploads)
498
- let mediaUrls: string[] | undefined;
538
+ const bodyForCommands = (() => {
539
+ const t = normalizedRawBody.trim();
540
+ if (!t.startsWith("/")) return normalizedRawBody;
541
+
542
+ // IMPORTANT: Rocket.Chat users can't reliably send leading `/...` (Rocket.Chat treats it as an internal slash command).
543
+ // We allow `--...` as a stand-in for `/...`.
544
+ //
545
+ // For directives like `/think high`, `/verbose full`, `/elevated ask`, etc, we must preserve arguments.
546
+ // So: when the message starts with a directive/command, pass the *full first line* to the command parser.
547
+ // (OpenClaw will apply it as a directive-only message if the remaining body is empty.)
548
+ return t.split("\n", 1)[0].trim();
549
+ })();
550
+
551
+ const commandAuthorized = true;
552
+ const bodyForAgent = body;
553
+
554
+ // Fetch images to temp files (OpenClaw expects file paths, not data URLs)
555
+ let mediaPaths: string[] | undefined;
556
+ let mediaTypes: string[] | undefined;
557
+ const imageCleanups: Array<() => Promise<void>> = [];
558
+
499
559
  if (imageRefs.length > 0) {
500
560
  const fetched = await Promise.all(
501
561
  imageRefs.map((ref) =>
502
- fetchImageAsDataUrl(ref.url, authToken, userId, ref.mimeType)
562
+ fetchImageToTempFile(ref.url, authToken, userId, ref.mimeType)
503
563
  )
504
564
  );
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`);
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`);
508
571
  }
509
572
  }
510
573
 
511
- // Finalize inbound context
512
574
  const ctxPayload = core.channel?.reply?.finalizeInboundContext?.({
513
575
  Body: body,
576
+ BodyForAgent: bodyForAgent,
514
577
  RawBody: rawBody,
515
- CommandBody: commandBody,
578
+ CommandBody: bodyForCommands,
579
+ // Be explicit: directives (/model, /qwen, etc.) should be parsed from the raw inbound text.
580
+ BodyForCommands: bodyForCommands,
581
+ // Hint to OpenClaw that this is plain text (not a platform-native slash command).
582
+ CommandSource: "text",
583
+
584
+ // Allow inline directives like /model ...
585
+ CommandAuthorized: commandAuthorized,
586
+
516
587
  From: isGroup ? `rocketchat:room:${roomId}` : `rocketchat:${senderId}`,
517
588
  To: `rocketchat:${roomId}`,
518
589
  SessionKey: route.sessionKey,
@@ -529,15 +600,15 @@ async function handleIncomingMessage(
529
600
  OriginatingChannel: "rocketchat",
530
601
  OriginatingTo: `rocketchat:${roomId}`,
531
602
 
532
- // Image attachments (fetched as base64 data URLs)
533
- MediaUrls: mediaUrls?.length ? mediaUrls : undefined,
603
+ // Image attachments (fetched to temp files)
604
+ MediaPaths: mediaPaths?.length ? mediaPaths : undefined,
605
+ MediaTypes: mediaTypes?.length ? mediaTypes : undefined,
534
606
  });
535
607
 
536
608
  if (!ctxPayload) {
537
609
  logger.error?.(`Failed to finalize inbound context for message ${msg._id}`);
538
610
  return;
539
611
  }
540
-
541
612
  // Record inbound session
542
613
  if (storePath) {
543
614
  await core.channel?.session?.recordInboundSession?.({
@@ -628,5 +699,9 @@ async function handleIncomingMessage(
628
699
  });
629
700
  } finally {
630
701
  await stopTyping();
702
+ // Clean up temp image files
703
+ for (const cleanup of imageCleanups) {
704
+ await cleanup();
705
+ }
631
706
  }
632
707
  }