@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.1.10",
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
- "axios": "file:src/libs/axios-1.13.6.tgz",
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
- fileUrls: string[],
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
- log?.(`dcgchat media: starting resolve for ${fileUrls.length} file(s): ${JSON.stringify(fileUrls)}`);
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
- log?.(`dcgchat media: [${i + 1}/${fileUrls.length}] fetch response status=${response.status}, content-type=${response.headers.get("content-type")}, content-length=${response.headers.get("content-length")}`);
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: saved.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}/${fileUrls.length}] FAILED to process ${url}: ${String(err)}`);
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 fileUrls = msg.content.file_urls ?? [];
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 (fileUrls.length > 0) {
148
- log(`dcgchat[${accountId}]: processing ${fileUrls.length} file(s): ${JSON.stringify(fileUrls)}`);
149
- const mediaMaxBytes = 30 * 1024 * 1024;
150
- const mediaList = await resolveMediaFromUrls(fileUrls, mediaMaxBytes, log);
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}/${fileUrls.length} file(s), payload=${JSON.stringify(mediaPayload)}`);
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
- try {
135
- const url = ctx.mediaUrl ? await ossUpload(ctx.mediaUrl, botToken) : '';
136
- const fileName = ctx.mediaUrl?.split(/[\\/]/).pop() || ''
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 + '\n' + `[${fileName}](${url})`,
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 current = process.cwd();
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
- throw new Error("Workspace directory not initialized");
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
- response.data
57
- .pipe(unzipper.Parse())
58
- .on('entry', (entry: any) => {
59
- const entryPath = entry.path;
60
- // 跳过顶层目录,只处理子文件和文件夹
61
- const pathParts = entryPath.split('/');
62
- if (pathParts.length > 1) {
63
- // 移除第一级目录
64
- const newPath = pathParts.slice(1).join('/');
65
- const targetPath = path.join(skillDir, newPath);
66
-
67
- if (entry.type === 'Directory') {
68
- fs.mkdirSync(targetPath, { recursive: true });
69
- entry.autodrain();
70
- } else {
71
- // 确保父目录存在
72
- const parentDir = path.dirname(targetPath);
73
- if (!fs.existsSync(parentDir)) {
74
- fs.mkdirSync(parentDir, { recursive: true });
75
- }
76
- entry.pipe(fs.createWriteStream(targetPath));
77
- }
78
- } else {
79
- entry.autodrain();
80
- }
81
- })
82
- .on('close', resolve)
83
- .on('error', reject);
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
- sendEvent({ ...msgContent, status: 'ok' })
123
+ });
124
+ sendEvent({ ...msgContent, status: 'ok' })
86
125
  } catch (error) {
87
126
  // 如果安装失败,清理目录
88
127
  if (fs.existsSync(skillDir)) {
package/src/types.ts CHANGED
@@ -42,7 +42,10 @@ export type InboundMessage = {
42
42
  session_id: string;
43
43
  message_id: string;
44
44
  text: string;
45
- file_urls?: string[];
45
+ files?: {
46
+ url: string;
47
+ name: string;
48
+ }[];
46
49
  };
47
50
  };
48
51