@dcrays/dcgchat-test 0.1.11 → 0.1.13
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 -4
- package/src/bot.ts +22 -50
- package/src/channel.ts +78 -29
- 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.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for DCG Chat (WebSocket)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -20,10 +20,8 @@
|
|
|
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
27
|
"devDependencies": {
|
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) ?? {};
|
|
@@ -98,14 +143,13 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
98
143
|
// textChunkLimit: 25,
|
|
99
144
|
textChunkLimit: 4000,
|
|
100
145
|
sendText: async (ctx) => {
|
|
101
|
-
const target = ctx.to || "(implicit)";
|
|
102
146
|
const ws = getWsConnection()
|
|
103
147
|
const params = getMsgParams();
|
|
104
148
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
105
149
|
const {botToken} = resolveAccount(ctx.cfg, ctx.accountId);
|
|
106
150
|
const content = {
|
|
107
151
|
messageType: "openclaw_bot_chat",
|
|
108
|
-
_userId:
|
|
152
|
+
_userId: params.userId,
|
|
109
153
|
source: "client",
|
|
110
154
|
content: {
|
|
111
155
|
bot_token: botToken,
|
|
@@ -115,60 +159,65 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
115
159
|
},
|
|
116
160
|
};
|
|
117
161
|
ws.send(JSON.stringify(content));
|
|
118
|
-
logDcgchat.info(`dcgchat[${ctx.accountId}]: sendText to ${
|
|
162
|
+
logDcgchat.info(`dcgchat[${ctx.accountId}]: sendText to ${params.userId}, ${JSON.stringify(content)}`);
|
|
119
163
|
} else {
|
|
120
164
|
logDcgchat.warn(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound -> ${ws?.readyState}: ${ctx.text}`);
|
|
121
165
|
}
|
|
122
166
|
return {
|
|
123
167
|
channel: "dcgchat",
|
|
124
168
|
messageId: `dcg-${Date.now()}`,
|
|
125
|
-
chatId:
|
|
169
|
+
chatId: params.userId.toString(),
|
|
126
170
|
};
|
|
127
171
|
},
|
|
128
172
|
sendMedia: async (ctx) => {
|
|
129
|
-
const target = ctx.to || "(implicit)";
|
|
130
173
|
const ws = getWsConnection()
|
|
131
174
|
const params = getMsgParams();
|
|
175
|
+
|
|
132
176
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
133
177
|
const {botToken} = resolveAccount(ctx.cfg, ctx.accountId);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
178
|
+
|
|
179
|
+
// try {
|
|
180
|
+
const url = ctx.mediaUrl;
|
|
181
|
+
const fileName = url?.split(/[\\/]/).pop() || ''
|
|
137
182
|
const content = {
|
|
138
183
|
messageType: "openclaw_bot_chat",
|
|
139
|
-
_userId:
|
|
184
|
+
_userId: params.userId,
|
|
140
185
|
source: "client",
|
|
141
186
|
content: {
|
|
142
187
|
bot_token: botToken,
|
|
143
|
-
response: ctx.text
|
|
188
|
+
response: ctx.text,
|
|
189
|
+
files: [{
|
|
190
|
+
url: url,
|
|
191
|
+
name: fileName,
|
|
192
|
+
}],
|
|
144
193
|
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(),
|
|
194
|
+
message_id: params.messageId || Date.now().toString(),
|
|
160
195
|
},
|
|
161
196
|
};
|
|
162
197
|
ws.send(JSON.stringify(content));
|
|
163
|
-
logDcgchat.info(`dcgchat[${ctx.accountId}]: sendMedia to ${
|
|
164
|
-
}
|
|
198
|
+
logDcgchat.info(`dcgchat[${ctx.accountId}]: agent sendMedia to ${params.userId}, ${JSON.stringify(content)}`);
|
|
199
|
+
// } catch (error) {
|
|
200
|
+
// const content = {
|
|
201
|
+
// messageType: "openclaw_bot_chat",
|
|
202
|
+
// _userId: target,
|
|
203
|
+
// source: "client",
|
|
204
|
+
// content: {
|
|
205
|
+
// bot_token: botToken,
|
|
206
|
+
// response: ctx.text + '\n' + ctx.mediaUrl,
|
|
207
|
+
// session_id: params.sessionId || Date.now().toString(),
|
|
208
|
+
// message_id: params.messageId ||Date.now().toString(),
|
|
209
|
+
// },
|
|
210
|
+
// };
|
|
211
|
+
// ws.send(JSON.stringify(content));
|
|
212
|
+
// logDcgchat.info(`dcgchat[${ctx.accountId}]: sendMedia to ${target}, ${JSON.stringify(content)}`);
|
|
213
|
+
// }
|
|
165
214
|
} else {
|
|
166
215
|
logDcgchat.warn(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound -> ${ws?.readyState}: ${ctx.text}`);
|
|
167
216
|
}
|
|
168
217
|
return {
|
|
169
218
|
channel: "dcgchat",
|
|
170
219
|
messageId: `dcg-${Date.now()}`,
|
|
171
|
-
chatId:
|
|
220
|
+
chatId: params.userId.toString(),
|
|
172
221
|
};
|
|
173
222
|
},
|
|
174
223
|
},
|
|
Binary file
|