openclaw-channel-dmwork 0.3.5 → 0.3.7

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.5",
3
+ "version": "0.3.7",
4
4
  "description": "DMWork channel plugin for OpenClaw via WuKongIM WebSocket",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -49,4 +49,4 @@
49
49
  "curve25519-js",
50
50
  "md5-typescript"
51
51
  ]
52
- }
52
+ }
package/src/accounts.ts CHANGED
@@ -15,6 +15,7 @@ export type ResolvedDmworkAccount = {
15
15
  botToken?: string;
16
16
  apiUrl: string;
17
17
  wsUrl?: string;
18
+ cdnUrl?: string; // CDN base URL for media files (public-read, no auth)
18
19
  pollIntervalMs: number;
19
20
  heartbeatIntervalMs: number;
20
21
  requireMention?: boolean;
@@ -51,6 +52,7 @@ export function resolveDmworkAccount(params: {
51
52
  const botToken = accountConfig.botToken ?? channel.botToken;
52
53
  const apiUrl = accountConfig.apiUrl ?? channel.apiUrl ?? DEFAULT_API_URL;
53
54
  const wsUrl = accountConfig.wsUrl ?? channel.wsUrl;
55
+ const cdnUrl = accountConfig.cdnUrl ?? channel.cdnUrl;
54
56
  const pollIntervalMs =
55
57
  accountConfig.pollIntervalMs ??
56
58
  channel.pollIntervalMs ??
@@ -72,6 +74,7 @@ export function resolveDmworkAccount(params: {
72
74
  botToken,
73
75
  apiUrl,
74
76
  wsUrl,
77
+ cdnUrl,
75
78
  pollIntervalMs,
76
79
  heartbeatIntervalMs,
77
80
  requireMention: accountConfig.requireMention ?? channel.requireMention,
@@ -6,6 +6,7 @@ export interface DmworkAccountConfig {
6
6
  botToken?: string;
7
7
  apiUrl?: string;
8
8
  wsUrl?: string;
9
+ cdnUrl?: string; // CDN base URL for media files (e.g. https://cdn.example.com/bucket)
9
10
  pollIntervalMs?: number;
10
11
  heartbeatIntervalMs?: number;
11
12
  requireMention?: boolean;
@@ -20,6 +21,7 @@ export interface DmworkConfig {
20
21
  botToken?: string;
21
22
  apiUrl?: string;
22
23
  wsUrl?: string;
24
+ cdnUrl?: string; // CDN base URL for media files (e.g. https://cdn.example.com/bucket)
23
25
  pollIntervalMs?: number;
24
26
  heartbeatIntervalMs?: number;
25
27
  requireMention?: boolean;
@@ -43,6 +45,7 @@ export const DmworkConfigJsonSchema = {
43
45
  botToken: { type: "string" },
44
46
  apiUrl: { type: "string" },
45
47
  wsUrl: { type: "string" },
48
+ cdnUrl: { type: "string" },
46
49
  pollIntervalMs: { type: "number", minimum: 500 },
47
50
  heartbeatIntervalMs: { type: "number", minimum: 5000 },
48
51
  requireMention: { type: "boolean" },
@@ -59,6 +62,7 @@ export const DmworkConfigJsonSchema = {
59
62
  botToken: { type: "string" },
60
63
  apiUrl: { type: "string" },
61
64
  wsUrl: { type: "string" },
65
+ cdnUrl: { type: "string" },
62
66
  pollIntervalMs: { type: "number", minimum: 500 },
63
67
  heartbeatIntervalMs: { type: "number", minimum: 5000 },
64
68
  requireMention: { type: "boolean" },
package/src/inbound.ts CHANGED
@@ -151,23 +151,35 @@ interface ResolvedContent {
151
151
  mediaType?: string;
152
152
  }
153
153
 
154
- function resolveContent(payload: BotMessage["payload"], apiUrl?: string): ResolvedContent {
154
+ function resolveContent(payload: BotMessage["payload"], apiUrl?: string, log?: ChannelLogSink, cdnUrl?: string): ResolvedContent {
155
155
  if (!payload) return { text: "" };
156
156
 
157
157
  const makeFullUrl = (relUrl?: string) => {
158
158
  if (!relUrl) return undefined;
159
159
  if (relUrl.startsWith("http")) return relUrl;
160
- // Use bot file proxy endpoint which handles auth via Bearer token
161
- // and returns 302 redirect to presigned URL (works with both MinIO and COS).
162
- // /v1/botfile/*path auto-strips "file/" prefix from the storage path.
160
+ // Strip common path prefixes to get the raw storage path
161
+ let storagePath = relUrl;
162
+ // Remove "file/preview/" or "file/" prefix
163
+ if (storagePath.startsWith("file/preview/")) {
164
+ storagePath = storagePath.substring("file/preview/".length);
165
+ } else if (storagePath.startsWith("file/")) {
166
+ storagePath = storagePath.substring("file/".length);
167
+ }
168
+ if (cdnUrl) {
169
+ // CDN direct: public-read, no auth needed, LLM can access directly
170
+ const base = cdnUrl.replace(/\/+$/, "");
171
+ return `${base}/${storagePath}`;
172
+ }
173
+ // Fallback: Nginx public /file/ path (no auth)
163
174
  const baseUrl = apiUrl?.replace(/\/+$/, "") ?? "";
164
- return `${baseUrl}/v1/botfile/${relUrl}`;
175
+ return `${baseUrl}/file/${storagePath}`;
165
176
  };
166
177
 
167
178
  switch (payload.type) {
168
179
  case MessageType.Text:
169
180
  return { text: payload.content ?? "" };
170
181
  case MessageType.Image: {
182
+ log?.debug?.(`dmwork: [resolveContent] Image payload.url=${payload.url}`);
171
183
  const imgUrl = makeFullUrl(payload.url);
172
184
  const imgMime = guessMime(payload.url, "image/jpeg");
173
185
  return { text: `[图片]\n${imgUrl ?? ""}`.trim(), mediaUrl: imgUrl, mediaType: imgMime };
@@ -187,6 +199,7 @@ function resolveContent(payload: BotMessage["payload"], apiUrl?: string): Resolv
187
199
  return { text: `[视频]\n${videoUrl ?? ""}`.trim(), mediaUrl: videoUrl, mediaType: videoMime };
188
200
  }
189
201
  case MessageType.File: {
202
+ log?.debug?.(`dmwork: [resolveContent] File payload.url=${payload.url}`);
190
203
  const fileUrl = makeFullUrl(payload.url);
191
204
  const fileMime = guessMime(payload.url, payload.name ? guessMime(payload.name, "application/octet-stream") : "application/octet-stream");
192
205
  return { text: `[文件: ${payload.name ?? "未知文件"}]\n${fileUrl ?? ""}`.trim(), mediaUrl: fileUrl, mediaType: fileMime };
@@ -434,7 +447,7 @@ export async function handleInboundMessage(params: {
434
447
  ? message.channel_id!
435
448
  : spaceId ? `${spaceId}:${message.from_uid}` : message.from_uid;
436
449
 
437
- const resolved = resolveContent(message.payload, account.config.apiUrl);
450
+ const resolved = resolveContent(message.payload, account.config.apiUrl, log, account.config.cdnUrl);
438
451
  let rawBody = resolved.text;
439
452
  let inboundMediaUrl = resolved.mediaUrl;
440
453
  // Inline text file content if possible
@@ -447,16 +460,7 @@ export async function handleInboundMessage(params: {
447
460
  }
448
461
  }
449
462
 
450
- // Convert authenticated media URLs to base64 data URLs so the Agent can access them
451
- if (inboundMediaUrl && !inboundMediaUrl.startsWith("data:")) {
452
- const dataUrl = await fetchAsDataUrl(inboundMediaUrl, account.config.botToken ?? "", log);
453
- if (dataUrl) {
454
- log?.info?.(`dmwork: converted media URL to base64 data URL (${resolved.mediaType})`);
455
- inboundMediaUrl = dataUrl;
456
- } else {
457
- log?.warn?.(`dmwork: failed to convert media URL to base64, keeping original`);
458
- }
459
- }
463
+ // Media URLs are passed directly to the Agent (storage is public-read, no auth needed)
460
464
 
461
465
  if (!rawBody) {
462
466
  log?.info?.(
@@ -541,7 +545,7 @@ export async function handleInboundMessage(params: {
541
545
  entries.push({
542
546
  sender: message.from_uid,
543
547
  body: rawBody,
544
- mediaDataUrl: inboundMediaUrl?.startsWith("data:") ? inboundMediaUrl : undefined,
548
+ mediaUrl: inboundMediaUrl,
545
549
  timestamp: message.timestamp ? message.timestamp * 1000 : Date.now(),
546
550
  });
547
551
  const historyLimit = account.config.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
@@ -580,14 +584,26 @@ export async function handleInboundMessage(params: {
580
584
  limit: fetchLimit,
581
585
  log,
582
586
  });
583
- entries = apiMessages
587
+ const filteredApiMsgs = apiMessages
584
588
  .filter((m: any) => m.from_uid !== botUid && (m.content || m.type !== 1))
585
- .slice(-historyLimit)
586
- .map((m: any) => ({
589
+ .slice(-historyLimit);
590
+ entries = filteredApiMsgs.map((m: any) => {
591
+ const entry: any = {
587
592
  sender: m.from_uid,
588
593
  body: m.content || resolveApiMessagePlaceholder(m.type, m.name),
589
- timestamp: m.timestamp, // Already in ms from getChannelMessages
590
- }));
594
+ timestamp: m.timestamp,
595
+ };
596
+ // For media message types, resolve the URL directly (storage is public-read)
597
+ const mediaTypes = [MessageType.Image, MessageType.File, MessageType.Voice, MessageType.Video];
598
+ if (mediaTypes.includes(m.type) && !m.content) {
599
+ const apiResolved = resolveContent({ type: m.type, url: m.url, name: m.name } as any, account.config.apiUrl, log, account.config.cdnUrl);
600
+ if (apiResolved.mediaUrl) {
601
+ entry.mediaUrl = apiResolved.mediaUrl;
602
+ entry.body = apiResolved.text;
603
+ }
604
+ }
605
+ return entry;
606
+ });
591
607
  log?.info?.(`dmwork: [MENTION] 从API获取到 ${entries.length} 条历史消息`);
592
608
  } catch (err) {
593
609
  log?.error?.(`dmwork: [MENTION] 从API获取历史失败: ${err}`);
@@ -595,16 +611,16 @@ export async function handleInboundMessage(params: {
595
611
  }
596
612
 
597
613
  // Build history context manually (JSON format)
598
- // Collect media data URLs from history entries for attachment to the inbound context
614
+ // Collect media URLs from history entries for attachment to the inbound context
599
615
  historyMediaUrls = entries
600
- .map((e: any) => e.mediaDataUrl)
616
+ .map((e: any) => e.mediaUrl)
601
617
  .filter((url: string | undefined): url is string => Boolean(url));
602
618
 
603
619
  if (entries.length > 0) {
604
620
  const messagesJson = JSON.stringify(entries.map((e: any) => ({
605
621
  sender: e.sender,
606
622
  body: e.body,
607
- ...(e.mediaDataUrl ? { hasMedia: true } : {}),
623
+ ...(e.mediaUrl ? { hasMedia: true } : {}),
608
624
  })), null, 2);
609
625
  const template = account.config.historyPromptTemplate || DEFAULT_HISTORY_PROMPT_TEMPLATE;
610
626
  historyPrefix = template