@dcrays/dcgchat-test 0.1.11 → 0.1.12
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 -14
- package/src/bot.ts +22 -50
- package/src/channel.ts +74 -22
- package/src/libs/mime-types-3.0.2.tgz +0 -0
- package/src/types.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/dcgchat-test",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for DCG Chat (WebSocket)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -20,22 +20,10 @@
|
|
|
20
20
|
"typecheck": "tsc --noEmit"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"ali-oss": "file:src/libs/ali-oss-6.23.0.tgz",
|
|
24
23
|
"ws": "file:src/libs/ws-8.19.0.tgz",
|
|
25
|
-
"
|
|
26
|
-
"md5": "file:src/libs/md5-2.3.0.tgz",
|
|
24
|
+
"mime-types": "file:src/libs/mime-types-3.0.2.tgz",
|
|
27
25
|
"unzipper": "file:src/libs/unzipper-0.12.3.tgz"
|
|
28
26
|
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@types/node": "^22.0.0",
|
|
31
|
-
"@types/ws": "^8.5.0",
|
|
32
|
-
"openclaw": "2026.2.13",
|
|
33
|
-
"tsx": "^4.19.0",
|
|
34
|
-
"typescript": "^5.7.0"
|
|
35
|
-
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"openclaw": ">=2026.2.13"
|
|
38
|
-
},
|
|
39
27
|
"openclaw": {
|
|
40
28
|
"extensions": [
|
|
41
29
|
"./index.ts"
|
package/src/bot.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
3
4
|
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
4
5
|
import type { InboundMessage, OutboundReply } from "./types.js";
|
|
5
6
|
import { getDcgchatRuntime } from "./runtime.js";
|
|
6
7
|
import { resolveAccount } from "./channel.js";
|
|
7
8
|
import { setMsgStatus } from "./tool.js";
|
|
9
|
+
import mime from "mime-types"
|
|
10
|
+
|
|
11
|
+
const targetPath = path.join(os.homedir(), '.openclaw');
|
|
8
12
|
|
|
9
13
|
type MediaInfo = {
|
|
10
14
|
path: string;
|
|
@@ -13,63 +17,26 @@ type MediaInfo = {
|
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
async function resolveMediaFromUrls(
|
|
16
|
-
|
|
17
|
-
maxBytes: number,
|
|
20
|
+
files: { url: string, name: string }[],
|
|
18
21
|
log?: (msg: string) => void,
|
|
19
22
|
): Promise<MediaInfo[]> {
|
|
20
|
-
const core = getDcgchatRuntime();
|
|
21
23
|
const out: MediaInfo[] = [];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (let i = 0; i < fileUrls.length; i++) {
|
|
26
|
-
const url = fileUrls[i];
|
|
27
|
-
log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] fetching ${url}`);
|
|
24
|
+
for (let i = 0; i < files.length; i++) {
|
|
25
|
+
const url = path.join(targetPath, files[i]?.url);
|
|
28
26
|
try {
|
|
29
27
|
const response = await fetch(url);
|
|
30
|
-
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] fetch failed with HTTP ${response.status}, skipping`);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
36
|
-
log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] downloaded buffer size=${buffer.length} bytes`);
|
|
37
|
-
|
|
38
|
-
let contentType = response.headers.get("content-type") || "";
|
|
39
|
-
if (!contentType) {
|
|
40
|
-
log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] no content-type header, detecting mime...`);
|
|
41
|
-
// @ts-ignore
|
|
42
|
-
contentType = await core.media.detectMime({ buffer });
|
|
43
|
-
}
|
|
44
|
-
log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] resolved contentType=${contentType}`);
|
|
45
|
-
|
|
46
|
-
const fileName = path.basename(new URL(url).pathname) || "file";
|
|
47
|
-
log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] fileName=${fileName}, saving to disk (maxBytes=${maxBytes})...`);
|
|
48
|
-
|
|
49
|
-
const saved = await core.channel.media.saveMediaBuffer(
|
|
50
|
-
buffer,
|
|
51
|
-
contentType,
|
|
52
|
-
"inbound",
|
|
53
|
-
maxBytes,
|
|
54
|
-
fileName,
|
|
55
|
-
);
|
|
56
|
-
|
|
28
|
+
const contentType = response.headers.get("content-type") || "";
|
|
57
29
|
const isImage = contentType.startsWith("image/");
|
|
58
30
|
out.push({
|
|
59
|
-
path:
|
|
31
|
+
path: url,
|
|
60
32
|
// @ts-ignore
|
|
61
33
|
contentType: saved.contentType,
|
|
62
34
|
placeholder: isImage ? "<media:image>" : "<media:file>",
|
|
63
35
|
});
|
|
64
|
-
|
|
65
|
-
log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] saved to ${saved.path} (contentType=${saved.contentType}, isImage=${isImage})`);
|
|
66
36
|
} catch (err) {
|
|
67
|
-
log?.(`dcgchat media: [${i + 1}/${
|
|
37
|
+
log?.(`dcgchat media: [${i + 1}/${files.length}] FAILED to process ${url}: ${String(err)}`);
|
|
68
38
|
}
|
|
69
39
|
}
|
|
70
|
-
|
|
71
|
-
log?.(`dcgchat media: resolve complete, ${out.length}/${fileUrls.length} file(s) succeeded`);
|
|
72
|
-
|
|
73
40
|
return out;
|
|
74
41
|
}
|
|
75
42
|
|
|
@@ -141,15 +108,20 @@ export async function handleDcgchatMessage(params: {
|
|
|
141
108
|
});
|
|
142
109
|
|
|
143
110
|
// 处理用户上传的文件
|
|
144
|
-
const
|
|
145
|
-
log(`dcgchat[${accountId}]: incoming message from user=${userId}, text="${text?.slice(0, 80)}", file_urls count=${fileUrls.length}`);
|
|
111
|
+
const files = msg.content.files ?? [];
|
|
146
112
|
let mediaPayload: Record<string, unknown> = {};
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
113
|
+
if (files.length > 0) {
|
|
114
|
+
const mediaList = files?.map(item => {
|
|
115
|
+
const contentType = mime.lookup(item.name) || "application/octet-stream";
|
|
116
|
+
const isImage = contentType.startsWith("image/");
|
|
117
|
+
return {
|
|
118
|
+
path: path.join(targetPath, item?.url),
|
|
119
|
+
contentType: contentType,
|
|
120
|
+
placeholder: isImage ? "<media:image>" : "<media:file>",
|
|
121
|
+
}
|
|
122
|
+
});
|
|
151
123
|
mediaPayload = buildMediaPayload(mediaList);
|
|
152
|
-
log(`dcgchat[${accountId}]: media resolved ${mediaList.length}/${
|
|
124
|
+
log(`dcgchat[${accountId}]: media resolved ${mediaList.length}/${files.length} file(s), payload=${JSON.stringify(mediaList)}`);
|
|
153
125
|
}
|
|
154
126
|
|
|
155
127
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
package/src/channel.ts
CHANGED
|
@@ -1,11 +1,56 @@
|
|
|
1
|
+
import { copyFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
5
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
3
6
|
import type { ResolvedDcgchatAccount, DcgchatConfig } from "./types.js";
|
|
4
7
|
import { logDcgchat } from "./log.js";
|
|
5
8
|
import { getWsConnection } from "./connection.js";
|
|
6
|
-
import { ossUpload } from "./oss.js";
|
|
9
|
+
// import { ossUpload } from "./oss.js";
|
|
7
10
|
import { getMsgParams } from "./tool.js";
|
|
8
11
|
|
|
12
|
+
const uploadRoot = resolve('/', "upload");
|
|
13
|
+
|
|
14
|
+
function isPathInside(parentPath: string, targetPath: string): boolean {
|
|
15
|
+
const relativePath = relative(parentPath, targetPath);
|
|
16
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function ensureMediaInUploadDir(url: string): Promise<string> {
|
|
20
|
+
if (!url || /^([a-z][a-z\d+\-.]*):\/\//i.test(url) || !isAbsolute(url)) {
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sourcePath = resolve(url);
|
|
25
|
+
if (isPathInside(uploadRoot, sourcePath)) {
|
|
26
|
+
return sourcePath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fileName = basename(sourcePath);
|
|
30
|
+
if (!fileName) {
|
|
31
|
+
return sourcePath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const targetPath = resolve(uploadRoot, fileName);
|
|
35
|
+
if (targetPath === sourcePath) {
|
|
36
|
+
return targetPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await mkdir(uploadRoot, { recursive: true });
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await rename(sourcePath, targetPath);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if ((error as NodeJS.ErrnoException).code !== "EXDEV") {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
await copyFile(sourcePath, targetPath);
|
|
48
|
+
await unlink(sourcePath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return targetPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
9
54
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
10
55
|
const id = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
11
56
|
const raw = (cfg.channels?.["dcgchat"] as DcgchatConfig | undefined) ?? {};
|
|
@@ -129,39 +174,46 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
129
174
|
const target = ctx.to || "(implicit)";
|
|
130
175
|
const ws = getWsConnection()
|
|
131
176
|
const params = getMsgParams();
|
|
177
|
+
|
|
132
178
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
133
179
|
const {botToken} = resolveAccount(ctx.cfg, ctx.accountId);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
180
|
+
|
|
181
|
+
// try {
|
|
182
|
+
const url = ctx.mediaUrl;
|
|
183
|
+
const fileName = url?.split(/[\\/]/).pop() || ''
|
|
137
184
|
const content = {
|
|
138
185
|
messageType: "openclaw_bot_chat",
|
|
139
186
|
_userId: target,
|
|
140
187
|
source: "client",
|
|
141
188
|
content: {
|
|
142
189
|
bot_token: botToken,
|
|
143
|
-
response: ctx.text
|
|
190
|
+
response: ctx.text,
|
|
191
|
+
files: [{
|
|
192
|
+
url: url,
|
|
193
|
+
name: fileName,
|
|
194
|
+
}],
|
|
144
195
|
session_id: params.sessionId,
|
|
145
|
-
message_id: params.messageId ||Date.now().toString(),
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
ws.send(JSON.stringify(content));
|
|
149
|
-
logDcgchat.info(`dcgchat[${ctx.accountId}]: sendMedia to ${target}, ${JSON.stringify(content)}`);
|
|
150
|
-
} catch (error) {
|
|
151
|
-
const content = {
|
|
152
|
-
messageType: "openclaw_bot_chat",
|
|
153
|
-
_userId: target,
|
|
154
|
-
source: "client",
|
|
155
|
-
content: {
|
|
156
|
-
bot_token: botToken,
|
|
157
|
-
response: ctx.text + '\n' + ctx.mediaUrl,
|
|
158
|
-
session_id: params.sessionId || Date.now().toString(),
|
|
159
|
-
message_id: params.messageId ||Date.now().toString(),
|
|
196
|
+
message_id: params.messageId || Date.now().toString(),
|
|
160
197
|
},
|
|
161
198
|
};
|
|
162
199
|
ws.send(JSON.stringify(content));
|
|
163
|
-
logDcgchat.info(`dcgchat[${ctx.accountId}]: sendMedia to ${target}, ${JSON.stringify(content)}`);
|
|
164
|
-
|
|
200
|
+
logDcgchat.info(`dcgchat[${ctx.accountId}]: agent sendMedia to ${target}, ${JSON.stringify(content)}`);
|
|
201
|
+
logDcgchat.info(`dcgchat[${ctx.accountId}]: agent ctx to ${target}, ${JSON.stringify(ctx)}`);
|
|
202
|
+
// } catch (error) {
|
|
203
|
+
// const content = {
|
|
204
|
+
// messageType: "openclaw_bot_chat",
|
|
205
|
+
// _userId: target,
|
|
206
|
+
// source: "client",
|
|
207
|
+
// content: {
|
|
208
|
+
// bot_token: botToken,
|
|
209
|
+
// response: ctx.text + '\n' + ctx.mediaUrl,
|
|
210
|
+
// session_id: params.sessionId || Date.now().toString(),
|
|
211
|
+
// message_id: params.messageId ||Date.now().toString(),
|
|
212
|
+
// },
|
|
213
|
+
// };
|
|
214
|
+
// ws.send(JSON.stringify(content));
|
|
215
|
+
// logDcgchat.info(`dcgchat[${ctx.accountId}]: sendMedia to ${target}, ${JSON.stringify(content)}`);
|
|
216
|
+
// }
|
|
165
217
|
} else {
|
|
166
218
|
logDcgchat.warn(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound -> ${ws?.readyState}: ${ctx.text}`);
|
|
167
219
|
}
|
|
Binary file
|