claw-subagent-service 0.0.102 → 0.0.104

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": "claw-subagent-service",
3
- "version": "0.0.102",
3
+ "version": "0.0.104",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -3,7 +3,6 @@ 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');
7
6
  const axios = require('axios');
8
7
  const MENTION_REGEX = /@(claw_[a-zA-Z0-9]+)/g;
9
8
 
@@ -19,9 +18,6 @@ class MessageHandler {
19
18
  // 初始化融云服务端 API 客户端(直接调用融云 API,无需通过服务端代理)
20
19
  this.serverAPI = new RongCloudServerAPI(this.configManager, log);
21
20
  this.log?.info('[MessageHandler] 融云服务端 API 客户端已初始化(配置从服务端动态获取)');
22
- // 初始化文件上传服务
23
- this.uploadService = new UploadService(config, log);
24
- this.log?.info('[MessageHandler] 文件上传服务已初始化');
25
21
  this.nodeId = config.accountId || '';
26
22
  this.handleNormalMessage = handleNormalMessage;
27
23
  this._streamQueue = Promise.resolve();
@@ -511,17 +507,36 @@ class MessageHandler {
511
507
 
512
508
  // 图片消息
513
509
  if (msgType === 'RC:ImgMsg') {
514
- // content 可能是对象(包含 imageUri)或字符串(base64 缩略图)
510
+ // content 可能是对象(包含 imageUri)或字符串(base64 缩略图或 JSON)
515
511
  let imageUri = '';
516
512
 
517
513
  if (typeof content === 'string') {
518
- // content base64 缩略图,需要查找其他字段获取 URL
519
- imageUri = msg.imageUri || msg.imageUrl || msg.url || msg.localPath || '';
514
+ // 尝试解析 content 是否为 JSON(包含 imageUri)
515
+ try {
516
+ const contentObj = JSON.parse(content);
517
+ if (contentObj.imageUri) {
518
+ imageUri = contentObj.imageUri;
519
+ }
520
+ } catch (e) {
521
+ // content 不是 JSON,可能是 base64 缩略图
522
+ // 从 msg 其他字段查找 URL
523
+ imageUri = msg.imageUri || msg.imageUrl || msg.url || msg.localPath || '';
524
+ }
520
525
  } else if (typeof content === 'object' && content !== null) {
521
526
  // content 是对象,包含 imageUri
522
527
  imageUri = content.imageUri || content.imageUrl || content.url || '';
523
528
  }
524
529
 
530
+ // 如果还是没有找到 URL,尝试从 extra 字段获取
531
+ if (!imageUri && msg.extra) {
532
+ try {
533
+ const extraData = JSON.parse(msg.extra);
534
+ imageUri = extraData.imageUrl || extraData.imageUri || '';
535
+ } catch (e) {
536
+ // extra 不是 JSON,忽略
537
+ }
538
+ }
539
+
525
540
  this.log?.info(`[_extractMessageContent] 图片消息: imageUri=${imageUri}`);
526
541
 
527
542
  if (!imageUri) {
@@ -597,21 +612,6 @@ class MessageHandler {
597
612
  senderId
598
613
  };
599
614
  }
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
- }
615
615
  }
616
616
 
617
617
  module.exports = { MessageHandler };
@@ -405,11 +405,30 @@ class OpenClawClient {
405
405
  headers['Authorization'] = `Bearer ${gatewayToken}`;
406
406
  }
407
407
 
408
+ // 检测消息是否包含图片 URL
409
+ const imageUrlMatch = message.match(/\[图片\]\s*(https?:\/\/[^\s]+)/);
410
+ let messages;
411
+
412
+ if (imageUrlMatch) {
413
+ // 多模态格式:图片 + 文本
414
+ const imageUrl = imageUrlMatch[1];
415
+ const textContent = message.replace(/\[图片\]\s*https?:\/\/[^\s]+/, '').trim();
416
+
417
+ messages = [{
418
+ role: 'user',
419
+ content: [
420
+ { type: 'text', text: textContent || '描述这张图片' },
421
+ { type: 'image_url', image_url: { url: imageUrl } }
422
+ ]
423
+ }];
424
+ } else {
425
+ // 纯文本格式
426
+ messages = [{ role: 'user', content: message }];
427
+ }
428
+
408
429
  const payload = {
409
430
  model: 'openclaw',
410
- messages: [
411
- { role: 'user', content: message }
412
- ],
431
+ messages: messages,
413
432
  stream: true,
414
433
  max_tokens: 2048
415
434
  };
@@ -1,186 +0,0 @@
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 };