openclaw-channel-dmwork 0.3.2 → 0.3.4

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": "openclaw-channel-dmwork",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "DMWork channel plugin for OpenClaw via WuKongIM WebSocket",
5
5
  "main": "index.ts",
6
6
  "type": "module",
package/src/channel.ts CHANGED
@@ -380,7 +380,9 @@ export const dmworkPlugin: ChannelPlugin<ResolvedDmworkAccount> = {
380
380
  // Skip self messages
381
381
  if (msg.from_uid === credentials.robot_id) return;
382
382
  // Skip messages from any other bot in this plugin instance (prevent bot-to-bot loops)
383
- if (_knownBotUids.has(msg.from_uid)) return;
383
+ // But allow group messages through — bot-to-bot @mention in groups is legitimate;
384
+ // mention gating in inbound.ts ensures only @-targeted messages trigger AI.
385
+ if (_knownBotUids.has(msg.from_uid) && msg.channel_type === ChannelType.DM) return;
384
386
  // Skip unsupported message types (Location, Card)
385
387
  const supportedTypes = [MessageType.Text, MessageType.Image, MessageType.GIF, MessageType.Voice, MessageType.Video, MessageType.File];
386
388
  if (!msg.payload || !supportedTypes.includes(msg.payload.type)) return;
package/src/inbound.ts CHANGED
@@ -126,6 +126,25 @@ async function uploadAndSendMedia(params: {
126
126
  });
127
127
  }
128
128
 
129
+ /** Guess MIME type from file extension */
130
+ function guessMime(pathOrName?: string, fallback = "application/octet-stream"): string {
131
+ if (!pathOrName) return fallback;
132
+ const ext = pathOrName.split(".").pop()?.toLowerCase() ?? "";
133
+ const map: Record<string, string> = {
134
+ jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", bmp: "image/bmp",
135
+ mp3: "audio/mpeg", ogg: "audio/ogg", wav: "audio/wav", m4a: "audio/mp4", aac: "audio/aac", opus: "audio/opus",
136
+ mp4: "video/mp4", mov: "video/quicktime", webm: "video/webm", avi: "video/x-msvideo", mkv: "video/x-matroska",
137
+ pdf: "application/pdf", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
138
+ xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
139
+ ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
140
+ zip: "application/zip", gz: "application/gzip", tar: "application/x-tar",
141
+ txt: "text/plain", json: "application/json", csv: "text/csv", md: "text/markdown",
142
+ py: "text/x-python", js: "text/javascript", ts: "text/typescript", go: "text/x-go", java: "text/x-java",
143
+ html: "text/html", css: "text/css", xml: "text/xml", yaml: "text/yaml", yml: "text/yaml",
144
+ };
145
+ return map[ext] ?? fallback;
146
+ }
147
+
129
148
  interface ResolvedContent {
130
149
  text: string;
131
150
  mediaUrl?: string;
@@ -150,23 +169,27 @@ function resolveContent(payload: BotMessage["payload"], apiUrl?: string): Resolv
150
169
  return { text: payload.content ?? "" };
151
170
  case MessageType.Image: {
152
171
  const imgUrl = makeFullUrl(payload.url);
153
- return { text: `[图片]\n${imgUrl ?? ""}`.trim(), mediaUrl: imgUrl, mediaType: "image" };
172
+ const imgMime = guessMime(payload.url, "image/jpeg");
173
+ return { text: `[图片]\n${imgUrl ?? ""}`.trim(), mediaUrl: imgUrl, mediaType: imgMime };
154
174
  }
155
175
  case MessageType.GIF: {
156
176
  const gifUrl = makeFullUrl(payload.url);
157
- return { text: `[GIF]\n${gifUrl ?? ""}`.trim(), mediaUrl: gifUrl, mediaType: "image" };
177
+ return { text: `[GIF]\n${gifUrl ?? ""}`.trim(), mediaUrl: gifUrl, mediaType: "image/gif" };
158
178
  }
159
179
  case MessageType.Voice: {
160
180
  const voiceUrl = makeFullUrl(payload.url);
161
- return { text: `[语音消息]\n${voiceUrl ?? ""}`.trim(), mediaUrl: voiceUrl, mediaType: "audio" };
181
+ const voiceMime = guessMime(payload.url, "audio/mpeg");
182
+ return { text: `[语音消息]\n${voiceUrl ?? ""}`.trim(), mediaUrl: voiceUrl, mediaType: voiceMime };
162
183
  }
163
184
  case MessageType.Video: {
164
185
  const videoUrl = makeFullUrl(payload.url);
165
- return { text: `[视频]\n${videoUrl ?? ""}`.trim(), mediaUrl: videoUrl, mediaType: "video" };
186
+ const videoMime = guessMime(payload.url, "video/mp4");
187
+ return { text: `[视频]\n${videoUrl ?? ""}`.trim(), mediaUrl: videoUrl, mediaType: videoMime };
166
188
  }
167
189
  case MessageType.File: {
168
190
  const fileUrl = makeFullUrl(payload.url);
169
- return { text: `[文件: ${payload.name ?? "未知文件"}]\n${fileUrl ?? ""}`.trim(), mediaUrl: fileUrl, mediaType: "file" };
191
+ const fileMime = guessMime(payload.url, payload.name ? guessMime(payload.name, "application/octet-stream") : "application/octet-stream");
192
+ return { text: `[文件: ${payload.name ?? "未知文件"}]\n${fileUrl ?? ""}`.trim(), mediaUrl: fileUrl, mediaType: fileMime };
170
193
  }
171
194
  case MessageType.Location: {
172
195
  const lat = payload.latitude ?? payload.lat;
@@ -410,7 +433,8 @@ export async function handleInboundMessage(params: {
410
433
  let rawBody = resolved.text;
411
434
  let inboundMediaUrl = resolved.mediaUrl;
412
435
  // Inline text file content if possible
413
- if (resolved.mediaType === "file" && resolved.mediaUrl) {
436
+ const isFileMessage = message.payload?.type === MessageType.File;
437
+ if (isFileMessage && resolved.mediaUrl) {
414
438
  const fileContent = await resolveFileContent(resolved.mediaUrl, account.config.botToken ?? "");
415
439
  if (fileContent) {
416
440
  rawBody = `[文件: ${message.payload.name ?? "未知文件"}]\n\n--- 文件内容 ---\n${fileContent}\n--- 文件结束 ---`;