@dcrays/dcgchat-test 0.1.10 → 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/runtime.ts +5 -3
- package/src/skill.ts +68 -29
- 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
|
package/src/runtime.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { logDcgchat } from "./log.js";
|
|
2
3
|
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const fs = require('fs');
|
|
6
|
+
const os = require("os")
|
|
7
|
+
|
|
5
8
|
function getWorkspacePath() {
|
|
6
|
-
const
|
|
7
|
-
const workspacePath = path.join(current, '.openclaw/workspace');
|
|
9
|
+
const workspacePath = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
8
10
|
if (fs.existsSync(workspacePath)) {
|
|
9
11
|
return workspacePath;
|
|
10
12
|
}
|
|
@@ -21,7 +23,7 @@ export function setWorkspaceDir(dir?: string) {
|
|
|
21
23
|
}
|
|
22
24
|
export function getWorkspaceDir(): string {
|
|
23
25
|
if (!workspaceDir) {
|
|
24
|
-
|
|
26
|
+
logDcgchat.error("Workspace directory not initialized");
|
|
25
27
|
}
|
|
26
28
|
return workspaceDir;
|
|
27
29
|
}
|
package/src/skill.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
/** @ts-ignore */
|
|
4
4
|
import unzipper from 'unzipper';
|
|
5
|
+
import { pipeline } from "stream/promises";
|
|
5
6
|
import fs from 'fs';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import { logDcgchat } from './log.js';
|
|
@@ -53,36 +54,74 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
|
|
|
53
54
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
54
55
|
// 解压文件到目标目录,跳过顶层文件夹
|
|
55
56
|
await new Promise((resolve, reject) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
57
|
+
const tasks: Promise<void>[] = [];
|
|
58
|
+
let rootDir: string | null = null;
|
|
59
|
+
let hasError = false;
|
|
60
|
+
|
|
61
|
+
response.data
|
|
62
|
+
.pipe(unzipper.Parse())
|
|
63
|
+
.on("entry", (entry: any) => {
|
|
64
|
+
if (hasError) {
|
|
65
|
+
entry.autodrain();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const entryPath = entry.path;
|
|
71
|
+
const pathParts = entryPath.split("/");
|
|
72
|
+
|
|
73
|
+
// 检测根目录
|
|
74
|
+
if (!rootDir && pathParts.length > 1) {
|
|
75
|
+
rootDir = pathParts[0];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let newPath = entryPath;
|
|
79
|
+
|
|
80
|
+
// 移除顶层文件夹
|
|
81
|
+
if (rootDir && entryPath.startsWith(rootDir + "/")) {
|
|
82
|
+
newPath = entryPath.slice(rootDir.length + 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!newPath) {
|
|
86
|
+
entry.autodrain();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const targetPath = path.join(skillDir, newPath);
|
|
91
|
+
|
|
92
|
+
if (entry.type === "Directory") {
|
|
93
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
94
|
+
entry.autodrain();
|
|
95
|
+
} else {
|
|
96
|
+
const parentDir = path.dirname(targetPath);
|
|
97
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
98
|
+
const writeStream = fs.createWriteStream(targetPath);
|
|
99
|
+
const task = pipeline(entry, writeStream).catch((err) => {
|
|
100
|
+
hasError = true;
|
|
101
|
+
throw new Error(`解压文件失败 ${entryPath}: ${err.message}`);
|
|
102
|
+
});
|
|
103
|
+
tasks.push(task);
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
hasError = true;
|
|
107
|
+
entry.autodrain();
|
|
108
|
+
reject(new Error(`处理entry失败: ${err}`));
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.on("close", async () => {
|
|
112
|
+
try {
|
|
113
|
+
await Promise.all(tasks);
|
|
114
|
+
resolve(null);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
reject(err);
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.on("error", (err) => {
|
|
120
|
+
hasError = true;
|
|
121
|
+
reject(new Error(`解压流错误: ${err.message}`));
|
|
84
122
|
});
|
|
85
|
-
|
|
123
|
+
});
|
|
124
|
+
sendEvent({ ...msgContent, status: 'ok' })
|
|
86
125
|
} catch (error) {
|
|
87
126
|
// 如果安装失败,清理目录
|
|
88
127
|
if (fs.existsSync(skillDir)) {
|