claw-subagent-service 0.0.100 → 0.0.102
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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const FormData = require('form-data');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 文件上传服务
|
|
7
|
+
*
|
|
8
|
+
* 架构:
|
|
9
|
+
* 1. 调用服务端 requestUpload 获取上传配置
|
|
10
|
+
* 2. 根据配置选择上传方式:
|
|
11
|
+
* - server_proxy: 通过服务端代理上传
|
|
12
|
+
* - direct: 直传到 OSS(未来支持)
|
|
13
|
+
* 3. 返回下载 URL
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} config - 配置对象
|
|
16
|
+
* @param {string} config.apiBaseUrl - API 基础地址
|
|
17
|
+
* @param {Function} log - 日志函数
|
|
18
|
+
*/
|
|
19
|
+
class UploadService {
|
|
20
|
+
constructor(config, log) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.log = log;
|
|
23
|
+
this.apiBaseUrl = config.apiBaseUrl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 上传文件
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
* @param {string} options.filePath - 本地文件路径
|
|
31
|
+
* @param {string} options.fileType - 文件类型 (image, video, audio, file)
|
|
32
|
+
* @param {string} options.fileName - 原始文件名
|
|
33
|
+
* @param {number} options.fileSize - 文件大小
|
|
34
|
+
* @param {Function} options.onProgress - 进度回调 (progress: number) => void
|
|
35
|
+
* @returns {Promise<{url: string, filename: string}>}
|
|
36
|
+
*/
|
|
37
|
+
async uploadFile(options) {
|
|
38
|
+
const { filePath, fileType = 'file', fileName = '', fileSize = 0, onProgress } = options;
|
|
39
|
+
|
|
40
|
+
if (!filePath) {
|
|
41
|
+
throw new Error('filePath 不能为空');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.log?.info(`[UploadService] 开始上传: filePath=${filePath}, type=${fileType}`);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// 1. 请求上传配置
|
|
48
|
+
const uploadConfig = await this._requestUpload(fileType, fileName, fileSize);
|
|
49
|
+
this.log?.info(`[UploadService] 获取上传配置: mode=${uploadConfig.mode}`);
|
|
50
|
+
|
|
51
|
+
// 2. 根据模式上传
|
|
52
|
+
let result;
|
|
53
|
+
if (uploadConfig.mode === 'direct' && uploadConfig.presignedUrl) {
|
|
54
|
+
// 直传模式(OSS 预签名 URL)
|
|
55
|
+
result = await this._uploadDirect(filePath, uploadConfig, onProgress);
|
|
56
|
+
} else {
|
|
57
|
+
// 服务端代理模式
|
|
58
|
+
result = await this._uploadViaServer(filePath, uploadConfig, onProgress);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.log?.info(`[UploadService] 上传成功: url=${result.url}`);
|
|
62
|
+
return result;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
this.log?.error(`[UploadService] 上传失败: ${err.message}`);
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 请求上传配置
|
|
71
|
+
*/
|
|
72
|
+
async _requestUpload(fileType, fileName, fileSize) {
|
|
73
|
+
const url = `${this.apiBaseUrl}/im/api/system/service`;
|
|
74
|
+
|
|
75
|
+
const payload = {
|
|
76
|
+
service: 'upload',
|
|
77
|
+
action: 'requestUpload',
|
|
78
|
+
payload: {
|
|
79
|
+
fileType,
|
|
80
|
+
fileName,
|
|
81
|
+
fileSize,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.log?.info(`[UploadService] 请求上传配置: ${url}`);
|
|
86
|
+
|
|
87
|
+
const response = await axios.post(url, payload, {
|
|
88
|
+
timeout: 10000,
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (response.data?.code !== 200) {
|
|
95
|
+
throw new Error(response.data?.message || '获取上传配置失败');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return response.data.data;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 通过服务端代理上传
|
|
103
|
+
*/
|
|
104
|
+
async _uploadViaServer(filePath, uploadConfig, onProgress) {
|
|
105
|
+
const { uploadUrl, method, formData, fileField, headers } = uploadConfig;
|
|
106
|
+
|
|
107
|
+
// 读取文件
|
|
108
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
109
|
+
|
|
110
|
+
// 构建 FormData
|
|
111
|
+
const form = new FormData();
|
|
112
|
+
|
|
113
|
+
// 添加表单字段
|
|
114
|
+
if (formData) {
|
|
115
|
+
Object.entries(formData).forEach(([key, value]) => {
|
|
116
|
+
form.append(key, value);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 添加文件
|
|
121
|
+
form.append(fileField || 'file', fileBuffer, {
|
|
122
|
+
filename: uploadConfig.fileName || path.basename(filePath),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.log?.info(`[UploadService] 服务端代理上传: ${uploadUrl}`);
|
|
126
|
+
|
|
127
|
+
const response = await axios({
|
|
128
|
+
method: method || 'POST',
|
|
129
|
+
url: uploadUrl,
|
|
130
|
+
data: form,
|
|
131
|
+
headers: {
|
|
132
|
+
...headers,
|
|
133
|
+
...form.getHeaders(),
|
|
134
|
+
},
|
|
135
|
+
timeout: 120000, // 2分钟
|
|
136
|
+
onUploadProgress: (progressEvent) => {
|
|
137
|
+
if (progressEvent.total) {
|
|
138
|
+
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
139
|
+
onProgress?.(progress);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (response.data?.code !== 200) {
|
|
145
|
+
throw new Error(response.data?.message || '上传失败');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
url: response.data.data?.url || uploadConfig.downloadUrl,
|
|
150
|
+
filename: response.data.data?.filename || uploadConfig.fileName,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 直传到 OSS(预签名 URL)
|
|
156
|
+
* 未来支持阿里云 OSS、腾讯云 COS 等
|
|
157
|
+
*/
|
|
158
|
+
async _uploadDirect(filePath, uploadConfig, onProgress) {
|
|
159
|
+
const { presignedUrl, downloadUrl, headers = {} } = uploadConfig;
|
|
160
|
+
|
|
161
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
162
|
+
|
|
163
|
+
this.log?.info(`[UploadService] 直传上传: ${presignedUrl}`);
|
|
164
|
+
|
|
165
|
+
await axios.put(presignedUrl, fileBuffer, {
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/octet-stream',
|
|
168
|
+
...headers,
|
|
169
|
+
},
|
|
170
|
+
timeout: 120000,
|
|
171
|
+
onUploadProgress: (progressEvent) => {
|
|
172
|
+
if (progressEvent.total) {
|
|
173
|
+
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
174
|
+
onProgress?.(progress);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
url: downloadUrl,
|
|
181
|
+
filename: uploadConfig.fileName,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { UploadService };
|
|
@@ -3,6 +3,7 @@ const { OpenClawClient } = require('./openclaw-client');
|
|
|
3
3
|
const { handleNormalMessage } = require('../modules/normal-message-handler');
|
|
4
4
|
const { RongCloudServerAPI } = require('./rongcloud-server-api');
|
|
5
5
|
const { SystemConfigManager } = require('../modules/system-config');
|
|
6
|
+
const { UploadService } = require('../modules/upload-service');
|
|
6
7
|
const axios = require('axios');
|
|
7
8
|
const MENTION_REGEX = /@(claw_[a-zA-Z0-9]+)/g;
|
|
8
9
|
|
|
@@ -18,6 +19,9 @@ class MessageHandler {
|
|
|
18
19
|
// 初始化融云服务端 API 客户端(直接调用融云 API,无需通过服务端代理)
|
|
19
20
|
this.serverAPI = new RongCloudServerAPI(this.configManager, log);
|
|
20
21
|
this.log?.info('[MessageHandler] 融云服务端 API 客户端已初始化(配置从服务端动态获取)');
|
|
22
|
+
// 初始化文件上传服务
|
|
23
|
+
this.uploadService = new UploadService(config, log);
|
|
24
|
+
this.log?.info('[MessageHandler] 文件上传服务已初始化');
|
|
21
25
|
this.nodeId = config.accountId || '';
|
|
22
26
|
this.handleNormalMessage = handleNormalMessage;
|
|
23
27
|
this._streamQueue = Promise.resolve();
|
|
@@ -507,33 +511,74 @@ class MessageHandler {
|
|
|
507
511
|
|
|
508
512
|
// 图片消息
|
|
509
513
|
if (msgType === 'RC:ImgMsg') {
|
|
510
|
-
|
|
514
|
+
// content 可能是对象(包含 imageUri)或字符串(base64 缩略图)
|
|
515
|
+
let imageUri = '';
|
|
516
|
+
|
|
517
|
+
if (typeof content === 'string') {
|
|
518
|
+
// content 是 base64 缩略图,需要查找其他字段获取 URL
|
|
519
|
+
imageUri = msg.imageUri || msg.imageUrl || msg.url || msg.localPath || '';
|
|
520
|
+
} else if (typeof content === 'object' && content !== null) {
|
|
521
|
+
// content 是对象,包含 imageUri
|
|
522
|
+
imageUri = content.imageUri || content.imageUrl || content.url || '';
|
|
523
|
+
}
|
|
524
|
+
|
|
511
525
|
this.log?.info(`[_extractMessageContent] 图片消息: imageUri=${imageUri}`);
|
|
526
|
+
|
|
527
|
+
if (!imageUri) {
|
|
528
|
+
return '[图片](无法获取图片地址)';
|
|
529
|
+
}
|
|
530
|
+
|
|
512
531
|
return `[图片] ${imageUri}`;
|
|
513
532
|
}
|
|
514
533
|
|
|
515
534
|
// 视频消息
|
|
516
535
|
if (msgType === 'RC:SightMsg') {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
536
|
+
let sightUrl = '';
|
|
537
|
+
let name = '未知视频';
|
|
538
|
+
let duration = 0;
|
|
539
|
+
|
|
540
|
+
if (typeof content === 'object' && content !== null) {
|
|
541
|
+
sightUrl = content.sightUrl || content.url || '';
|
|
542
|
+
name = content.name || '未知视频';
|
|
543
|
+
duration = content.duration || 0;
|
|
544
|
+
} else {
|
|
545
|
+
sightUrl = msg.sightUrl || msg.url || msg.localPath || '';
|
|
546
|
+
}
|
|
547
|
+
|
|
520
548
|
this.log?.info(`[_extractMessageContent] 视频消息: sightUrl=${sightUrl}`);
|
|
521
549
|
return `[视频] ${sightUrl} ${name} ${duration}秒`;
|
|
522
550
|
}
|
|
523
551
|
|
|
524
552
|
// 文件消息
|
|
525
553
|
if (msgType === 'RC:FileMsg') {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
554
|
+
let fileUrl = '';
|
|
555
|
+
let name = '未知文件';
|
|
556
|
+
let size = 0;
|
|
557
|
+
|
|
558
|
+
if (typeof content === 'object' && content !== null) {
|
|
559
|
+
fileUrl = content.fileUrl || content.fileUri || content.url || '';
|
|
560
|
+
name = content.name || '未知文件';
|
|
561
|
+
size = content.size || 0;
|
|
562
|
+
} else {
|
|
563
|
+
fileUrl = msg.fileUrl || msg.fileUri || msg.url || msg.localPath || '';
|
|
564
|
+
}
|
|
565
|
+
|
|
529
566
|
this.log?.info(`[_extractMessageContent] 文件消息: fileUrl=${fileUrl}`);
|
|
530
567
|
return `[文件] ${fileUrl} ${name} ${size}`;
|
|
531
568
|
}
|
|
532
569
|
|
|
533
570
|
// 语音消息
|
|
534
571
|
if (msgType === 'RC:HQVCMsg') {
|
|
535
|
-
|
|
536
|
-
|
|
572
|
+
let remoteUrl = '';
|
|
573
|
+
let duration = 0;
|
|
574
|
+
|
|
575
|
+
if (typeof content === 'object' && content !== null) {
|
|
576
|
+
remoteUrl = content.remoteUrl || content.url || '';
|
|
577
|
+
duration = content.duration || 0;
|
|
578
|
+
} else {
|
|
579
|
+
remoteUrl = msg.remoteUrl || msg.url || msg.localPath || '';
|
|
580
|
+
}
|
|
581
|
+
|
|
537
582
|
this.log?.info(`[_extractMessageContent] 语音消息: remoteUrl=${remoteUrl}`);
|
|
538
583
|
return `[语音] ${remoteUrl} ${duration}秒`;
|
|
539
584
|
}
|
|
@@ -552,6 +597,21 @@ class MessageHandler {
|
|
|
552
597
|
senderId
|
|
553
598
|
};
|
|
554
599
|
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 上传文件(由 silent-service 代理上传)
|
|
603
|
+
*
|
|
604
|
+
* @param {Object} options
|
|
605
|
+
* @param {string} options.filePath - 本地文件路径
|
|
606
|
+
* @param {string} options.fileType - 文件类型 (image, video, audio, file)
|
|
607
|
+
* @param {string} options.fileName - 原始文件名
|
|
608
|
+
* @param {number} options.fileSize - 文件大小
|
|
609
|
+
* @param {Function} options.onProgress - 进度回调
|
|
610
|
+
* @returns {Promise<{url: string, filename: string}>}
|
|
611
|
+
*/
|
|
612
|
+
async uploadFile(options) {
|
|
613
|
+
return this.uploadService.uploadFile(options);
|
|
614
|
+
}
|
|
555
615
|
}
|
|
556
616
|
|
|
557
617
|
module.exports = { MessageHandler };
|