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 +2 -2
- package/src/accounts.ts +3 -0
- package/src/config-schema.ts +4 -0
- package/src/inbound.ts +41 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-channel-dmwork",
|
|
3
|
-
"version": "0.3.
|
|
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,
|
package/src/config-schema.ts
CHANGED
|
@@ -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
|
-
//
|
|
161
|
-
|
|
162
|
-
// /
|
|
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}/
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
587
|
+
const filteredApiMsgs = apiMessages
|
|
584
588
|
.filter((m: any) => m.from_uid !== botUid && (m.content || m.type !== 1))
|
|
585
|
-
.slice(-historyLimit)
|
|
586
|
-
|
|
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,
|
|
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
|
|
614
|
+
// Collect media URLs from history entries for attachment to the inbound context
|
|
599
615
|
historyMediaUrls = entries
|
|
600
|
-
.map((e: any) => e.
|
|
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.
|
|
623
|
+
...(e.mediaUrl ? { hasMedia: true } : {}),
|
|
608
624
|
})), null, 2);
|
|
609
625
|
const template = account.config.historyPromptTemplate || DEFAULT_HISTORY_PROMPT_TEMPLATE;
|
|
610
626
|
historyPrefix = template
|