@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 +1 -1
- package/src/rocketchat/monitor.ts +98 -23
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
136
|
+
async function fetchImageToTempFile(
|
|
125
137
|
url: string,
|
|
126
138
|
authToken: string,
|
|
127
139
|
userId: string,
|
|
128
140
|
mimeType?: string
|
|
129
|
-
): Promise<
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|
-
|
|
562
|
+
fetchImageToTempFile(ref.url, authToken, userId, ref.mimeType)
|
|
503
563
|
)
|
|
504
564
|
);
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
|
|
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:
|
|
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
|
|
533
|
-
|
|
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
|
}
|