claw-subagent-service 0.0.73 → 0.0.75
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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统配置获取模块
|
|
3
|
+
* 从 Python 服务端动态获取融云配置(appKey, appSecret 等)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
|
|
8
|
+
class SystemConfigManager {
|
|
9
|
+
constructor(config, log) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.log = log;
|
|
12
|
+
this.serverUrl = config.apiBaseUrl || process.env.DM_SERVER_URL || 'https://newsradar.dreamdt.cn/im';
|
|
13
|
+
this.configs = new Map();
|
|
14
|
+
this.lastFetchTime = 0;
|
|
15
|
+
this.fetchInterval = 5 * 60 * 1000; // 5分钟刷新一次
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 从服务端获取配置
|
|
20
|
+
*/
|
|
21
|
+
async fetchConfig(configKey) {
|
|
22
|
+
try {
|
|
23
|
+
const url = `${this.serverUrl}/im/api/system/config/${configKey}`;
|
|
24
|
+
this.log?.info(`[SystemConfig] 正在请求配置: ${configKey}, URL=${url}`);
|
|
25
|
+
const response = await axios.get(url, { timeout: 10000 });
|
|
26
|
+
|
|
27
|
+
if (response.data?.code === 200 && response.data?.data?.value) {
|
|
28
|
+
this.configs.set(configKey, response.data.data.value);
|
|
29
|
+
this.lastFetchTime = Date.now();
|
|
30
|
+
this.log?.info(`[SystemConfig] 获取配置成功: ${configKey}`);
|
|
31
|
+
return response.data.data.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.log?.warn(`[SystemConfig] 获取配置失败: ${configKey}, code=${response.data?.code}`);
|
|
35
|
+
return null;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
this.log?.error(`[SystemConfig] 获取配置异常: ${configKey}, URL=${this.serverUrl}/im/api/system/config/${configKey}, ${err.message}`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 获取配置值(带缓存)
|
|
44
|
+
*/
|
|
45
|
+
async getConfig(configKey) {
|
|
46
|
+
// 检查缓存是否过期
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (this.configs.has(configKey) && (now - this.lastFetchTime) < this.fetchInterval) {
|
|
49
|
+
return this.configs.get(configKey);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 重新获取
|
|
53
|
+
return this.fetchConfig(configKey);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 批量获取配置
|
|
58
|
+
*/
|
|
59
|
+
async getConfigs(keys) {
|
|
60
|
+
const results = {};
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
results[key] = await this.getConfig(key);
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 强制刷新配置
|
|
69
|
+
*/
|
|
70
|
+
async refresh() {
|
|
71
|
+
this.lastFetchTime = 0;
|
|
72
|
+
const keys = Array.from(this.configs.keys());
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
await this.fetchConfig(key);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { SystemConfigManager };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { MessageType } = require('./types');
|
|
2
2
|
const { OpenClawClient } = require('./openclaw-client');
|
|
3
3
|
const { handleNormalMessage } = require('../modules/normal-message-handler');
|
|
4
|
+
const { RongCloudServerAPI } = require('./rongcloud-server-api');
|
|
5
|
+
const { SystemConfigManager } = require('../modules/system-config');
|
|
4
6
|
const axios = require('axios');
|
|
5
7
|
const MENTION_REGEX = /@(claw_[a-zA-Z0-9]+)/g;
|
|
6
8
|
|
|
@@ -11,6 +13,11 @@ class MessageHandler {
|
|
|
11
13
|
this.log = log;
|
|
12
14
|
this.sendReadReceiptFn = sendReadReceiptFn;
|
|
13
15
|
this.openclawClient = new OpenClawClient(log);
|
|
16
|
+
// 初始化系统配置管理器(从 Python 服务端动态获取配置)
|
|
17
|
+
this.configManager = new SystemConfigManager(config, log);
|
|
18
|
+
// 初始化融云服务端 API 客户端(直接调用融云 API,无需通过服务端代理)
|
|
19
|
+
this.serverAPI = new RongCloudServerAPI(this.configManager, log);
|
|
20
|
+
this.log?.info('[MessageHandler] 融云服务端 API 客户端已初始化(配置从服务端动态获取)');
|
|
14
21
|
this.nodeId = config.accountId || '';
|
|
15
22
|
this.handleNormalMessage = handleNormalMessage;
|
|
16
23
|
this._streamQueue = Promise.resolve();
|
|
@@ -18,14 +25,18 @@ class MessageHandler {
|
|
|
18
25
|
this._streamMessageUIDs = new Map();
|
|
19
26
|
// 群聊对话轮数统计:groupId -> currentRound
|
|
20
27
|
this._groupRoundCounts = new Map();
|
|
21
|
-
|
|
28
|
+
// 群组配置缓存:groupId -> { maxRounds, expiresAt }
|
|
29
|
+
this._groupConfigCache = new Map();
|
|
30
|
+
this._defaultMaxRounds = 10;
|
|
31
|
+
this._groupConfigCacheTTL = config.groupConfigCacheTTL || 60000; // 默认缓存 60 秒
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* 判断是否支持流式处理
|
|
36
|
+
* 现在配置从服务端动态获取,总是启用
|
|
26
37
|
*/
|
|
27
38
|
get isStreamingEnabled() {
|
|
28
|
-
return !!this.
|
|
39
|
+
return !!this.serverAPI;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
extractMentions(content) {
|
|
@@ -97,10 +108,10 @@ class MessageHandler {
|
|
|
97
108
|
/**
|
|
98
109
|
* 增加群聊轮数
|
|
99
110
|
*/
|
|
100
|
-
_incrementGroupRoundCount(groupId) {
|
|
111
|
+
_incrementGroupRoundCount(groupId, maxRounds) {
|
|
101
112
|
const current = this._getGroupRoundCount(groupId);
|
|
102
113
|
this._groupRoundCounts.set(groupId, current + 1);
|
|
103
|
-
this.log?.info(`[MessageHandler] 群聊 ${groupId} 轮数 +1,当前: ${current + 1}/${
|
|
114
|
+
this.log?.info(`[MessageHandler] 群聊 ${groupId} 轮数 +1,当前: ${current + 1}/${maxRounds}`);
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
/**
|
|
@@ -111,6 +122,47 @@ class MessageHandler {
|
|
|
111
122
|
this.log?.info(`[MessageHandler] 群聊 ${groupId} 轮数已重置`);
|
|
112
123
|
}
|
|
113
124
|
|
|
125
|
+
/**
|
|
126
|
+
* 从后端 API 获取群组配置
|
|
127
|
+
*/
|
|
128
|
+
async _fetchGroupConfig(groupId) {
|
|
129
|
+
try {
|
|
130
|
+
const apiUrl = `${this.config.apiBaseUrl}/im/api/group/info`;
|
|
131
|
+
this.log?.info(`[MessageHandler] 查询群组配置: groupId=${groupId}, url=${apiUrl}`);
|
|
132
|
+
const resp = await axios.get(apiUrl, {
|
|
133
|
+
params: { groupId: groupId },
|
|
134
|
+
timeout: 5000
|
|
135
|
+
});
|
|
136
|
+
if (resp.data?.code === 200 && resp.data.data) {
|
|
137
|
+
const maxRounds = resp.data.data.maxRounds;
|
|
138
|
+
if (typeof maxRounds === 'number') {
|
|
139
|
+
this.log?.info(`[MessageHandler] 群组 ${groupId} 配置: maxRounds=${maxRounds}`);
|
|
140
|
+
return maxRounds;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
this.log?.warn(`[MessageHandler] 获取群组配置失败: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
return this._defaultMaxRounds;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取群组最大轮数(带缓存)
|
|
151
|
+
*/
|
|
152
|
+
async _getGroupMaxRounds(groupId) {
|
|
153
|
+
const cached = this._groupConfigCache.get(groupId);
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
if (cached && cached.expiresAt > now) {
|
|
156
|
+
return cached.maxRounds;
|
|
157
|
+
}
|
|
158
|
+
const maxRounds = await this._fetchGroupConfig(groupId);
|
|
159
|
+
this._groupConfigCache.set(groupId, {
|
|
160
|
+
maxRounds,
|
|
161
|
+
expiresAt: now + this._groupConfigCacheTTL
|
|
162
|
+
});
|
|
163
|
+
return maxRounds;
|
|
164
|
+
}
|
|
165
|
+
|
|
114
166
|
async handleMessage(msg) {
|
|
115
167
|
if (!this.shouldHandleMessage(msg)) {
|
|
116
168
|
return;
|
|
@@ -122,8 +174,10 @@ class MessageHandler {
|
|
|
122
174
|
this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${logContent.substring(0, 50)}`);
|
|
123
175
|
|
|
124
176
|
// 群聊轮数控制(仅对群聊生效)
|
|
177
|
+
let maxRounds = this._defaultMaxRounds;
|
|
125
178
|
if (msg.conversationType === 3) {
|
|
126
179
|
const groupId = msg.targetId;
|
|
180
|
+
maxRounds = await this._getGroupMaxRounds(groupId);
|
|
127
181
|
const currentRounds = this._getGroupRoundCount(groupId);
|
|
128
182
|
|
|
129
183
|
// 处理内置轮数相关命令
|
|
@@ -131,23 +185,23 @@ class MessageHandler {
|
|
|
131
185
|
const payload = this.parseCommand(msg.content, msg.senderUserId);
|
|
132
186
|
if (payload.command === 'newround') {
|
|
133
187
|
this._resetGroupRoundCount(groupId);
|
|
134
|
-
await this.sendFn(groupId, `✅ 新一轮对话已开始,最大对话轮数为 ${
|
|
188
|
+
await this.sendFn(groupId, `✅ 新一轮对话已开始,最大对话轮数为 ${maxRounds} 轮。`, msg.conversationType);
|
|
135
189
|
return;
|
|
136
190
|
}
|
|
137
191
|
if (payload.command === 'roundstatus') {
|
|
138
|
-
const remaining = Math.max(0,
|
|
139
|
-
const statusMsg = currentRounds >=
|
|
140
|
-
? `⛔ 本轮对话已结束(已达 ${
|
|
141
|
-
: `📊 当前对话进度:第 ${currentRounds + 1}/${
|
|
192
|
+
const remaining = Math.max(0, maxRounds - currentRounds);
|
|
193
|
+
const statusMsg = currentRounds >= maxRounds
|
|
194
|
+
? `⛔ 本轮对话已结束(已达 ${maxRounds} 轮)。发送 /newround 开启新一轮。`
|
|
195
|
+
: `📊 当前对话进度:第 ${currentRounds + 1}/${maxRounds} 轮,剩余 ${remaining} 轮。`;
|
|
142
196
|
await this.sendFn(groupId, statusMsg, msg.conversationType);
|
|
143
197
|
return;
|
|
144
198
|
}
|
|
145
199
|
}
|
|
146
200
|
|
|
147
201
|
// 检查是否已达最大轮数
|
|
148
|
-
if (currentRounds >=
|
|
149
|
-
this.log?.info(`[MessageHandler] 群聊 ${groupId} 已达到最大轮数 ${
|
|
150
|
-
await this.sendFn(groupId, `⛔ 本轮对话已达到最大轮数(${
|
|
202
|
+
if (currentRounds >= maxRounds) {
|
|
203
|
+
this.log?.info(`[MessageHandler] 群聊 ${groupId} 已达到最大轮数 ${maxRounds},拒绝处理`);
|
|
204
|
+
await this.sendFn(groupId, `⛔ 本轮对话已达到最大轮数(${maxRounds} 轮),对话已结束。\n\n发送 /newround 可开启新一轮对话。`, msg.conversationType);
|
|
151
205
|
return;
|
|
152
206
|
}
|
|
153
207
|
}
|
|
@@ -165,7 +219,7 @@ class MessageHandler {
|
|
|
165
219
|
await this.handleNormalMessageStream(msg);
|
|
166
220
|
// 流式处理成功,群聊轮数 +1
|
|
167
221
|
if (msg.conversationType === 3) {
|
|
168
|
-
this._incrementGroupRoundCount(msg.targetId);
|
|
222
|
+
this._incrementGroupRoundCount(msg.targetId, maxRounds);
|
|
169
223
|
}
|
|
170
224
|
} catch (err) {
|
|
171
225
|
this.log?.error(`[MessageHandler] 流式处理失败,回退到非流式: ${err.message}`);
|
|
@@ -175,7 +229,7 @@ class MessageHandler {
|
|
|
175
229
|
await this.sendFn(targetId, reply, msg.conversationType);
|
|
176
230
|
// 非流式回退成功,群聊轮数 +1
|
|
177
231
|
if (msg.conversationType === 3) {
|
|
178
|
-
this._incrementGroupRoundCount(msg.targetId);
|
|
232
|
+
this._incrementGroupRoundCount(msg.targetId, maxRounds);
|
|
179
233
|
}
|
|
180
234
|
}
|
|
181
235
|
}
|
|
@@ -187,7 +241,7 @@ class MessageHandler {
|
|
|
187
241
|
await this.sendFn(targetId, reply, msg.conversationType);
|
|
188
242
|
// 非流式处理成功,群聊轮数 +1
|
|
189
243
|
if (msg.conversationType === 3) {
|
|
190
|
-
this._incrementGroupRoundCount(msg.targetId);
|
|
244
|
+
this._incrementGroupRoundCount(msg.targetId, maxRounds);
|
|
191
245
|
}
|
|
192
246
|
}
|
|
193
247
|
}
|
|
@@ -322,8 +376,13 @@ class MessageHandler {
|
|
|
322
376
|
if (buffer.trim()) {
|
|
323
377
|
try {
|
|
324
378
|
this.log?.info(`[MessageHandler] 发送历史记录文本消息: length=${buffer.length}`);
|
|
325
|
-
//
|
|
326
|
-
const historyContent =
|
|
379
|
+
// 使用 JSON 格式包含 streamId,前端可据此关联并更新流式消息内容
|
|
380
|
+
const historyContent = JSON.stringify({
|
|
381
|
+
__stream_history__: true,
|
|
382
|
+
streamId: streamId,
|
|
383
|
+
text: buffer,
|
|
384
|
+
sentTime: Date.now()
|
|
385
|
+
});
|
|
327
386
|
await this.sendFn(targetId, historyContent, conversationType);
|
|
328
387
|
} catch (err) {
|
|
329
388
|
this.log?.error(`[MessageHandler] 发送历史记录失败: ${err.message}`);
|
|
@@ -349,36 +408,30 @@ class MessageHandler {
|
|
|
349
408
|
}
|
|
350
409
|
|
|
351
410
|
/**
|
|
352
|
-
* 发送 typing
|
|
411
|
+
* 发送 typing 状态(直接调用融云 API)
|
|
353
412
|
*/
|
|
354
413
|
async _sendTypingStatus(fromUserId, targetId, conversationType) {
|
|
355
|
-
if (!this.isStreamingEnabled) return;
|
|
414
|
+
if (!this.isStreamingEnabled || !this.serverAPI) return;
|
|
356
415
|
try {
|
|
357
|
-
await
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
conversationType
|
|
363
|
-
},
|
|
364
|
-
{ timeout: 5000 }
|
|
365
|
-
);
|
|
416
|
+
await this.serverAPI.sendTypingStatus({
|
|
417
|
+
fromUserId,
|
|
418
|
+
toUserId: targetId,
|
|
419
|
+
conversationType
|
|
420
|
+
});
|
|
366
421
|
this.log?.info(`[MessageHandler] typing 状态已发送: ${fromUserId} -> ${targetId}`);
|
|
367
422
|
} catch (err) {
|
|
368
|
-
|
|
369
|
-
const status = err.response?.status;
|
|
370
|
-
this.log?.warn(`[MessageHandler] 发送 typing 状态失败: ${err.message}, url=${url}, status=${status || 'N/A'}`);
|
|
423
|
+
this.log?.warn(`[MessageHandler] 发送 typing 状态失败: ${err.message}`);
|
|
371
424
|
}
|
|
372
425
|
}
|
|
373
426
|
|
|
374
427
|
/**
|
|
375
|
-
*
|
|
428
|
+
* 发送流式消息片段(直接调用融云 API)
|
|
376
429
|
*/
|
|
377
430
|
async _sendStreamChunk(fromUserId, targetId, conversationType, content, streamId, isFirstChunk, isLastChunk, seq = 1) {
|
|
378
431
|
const contentPreview = typeof content === 'string' ? content.substring(0, 100) : JSON.stringify(content).substring(0, 100);
|
|
379
432
|
this.log?.info(`[MessageHandler] _sendStreamChunk ENTRY: target=${targetId}, streamId=${streamId}, seq=${seq}, first=${isFirstChunk}, last=${isLastChunk}, content_len=${content?.length || 0}, content_preview=${contentPreview}`);
|
|
380
|
-
if (!this.isStreamingEnabled) {
|
|
381
|
-
this.log?.warn('[MessageHandler] _sendStreamChunk skipped:
|
|
433
|
+
if (!this.isStreamingEnabled || !this.serverAPI) {
|
|
434
|
+
this.log?.warn('[MessageHandler] _sendStreamChunk skipped: 未配置 appKey/appSecret');
|
|
382
435
|
return;
|
|
383
436
|
}
|
|
384
437
|
|
|
@@ -388,36 +441,42 @@ class MessageHandler {
|
|
|
388
441
|
// 获取已存储的 RongCloud messageUID(首流响应返回的)
|
|
389
442
|
const messageUID = this._streamMessageUIDs.get(streamId);
|
|
390
443
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
444
|
+
let result;
|
|
445
|
+
if (conversationType === 3) {
|
|
446
|
+
// 群聊流式消息
|
|
447
|
+
result = await this.serverAPI.sendStreamGroup({
|
|
448
|
+
fromUserId,
|
|
449
|
+
toGroupId: targetId,
|
|
450
|
+
content,
|
|
451
|
+
streamId,
|
|
452
|
+
isFirstChunk,
|
|
453
|
+
isLastChunk,
|
|
454
|
+
seq,
|
|
455
|
+
messageUID
|
|
456
|
+
});
|
|
457
|
+
} else {
|
|
458
|
+
// 单聊流式消息
|
|
459
|
+
result = await this.serverAPI.sendStreamPrivate({
|
|
460
|
+
fromUserId,
|
|
461
|
+
toUserId: targetId,
|
|
462
|
+
content,
|
|
463
|
+
streamId,
|
|
464
|
+
isFirstChunk,
|
|
465
|
+
isLastChunk,
|
|
466
|
+
seq,
|
|
467
|
+
messageUID
|
|
468
|
+
});
|
|
469
|
+
}
|
|
408
470
|
|
|
409
471
|
// 首流时存储 RongCloud 返回的 messageUID
|
|
410
|
-
if (isFirstChunk &&
|
|
411
|
-
this._streamMessageUIDs.set(streamId,
|
|
412
|
-
this.log?.info(`[MessageHandler] 首流 messageUID 已存储: ${
|
|
472
|
+
if (isFirstChunk && result?.messageUID) {
|
|
473
|
+
this._streamMessageUIDs.set(streamId, result.messageUID);
|
|
474
|
+
this.log?.info(`[MessageHandler] 首流 messageUID 已存储: ${result.messageUID}, streamId=${streamId}`);
|
|
413
475
|
}
|
|
414
476
|
|
|
415
|
-
this.log?.info(`[MessageHandler] _sendStreamChunk 成功:
|
|
477
|
+
this.log?.info(`[MessageHandler] _sendStreamChunk 成功: seq=${seq}`);
|
|
416
478
|
} catch (err) {
|
|
417
|
-
|
|
418
|
-
const status = err.response?.status;
|
|
419
|
-
const responseData = err.response?.data ? JSON.stringify(err.response.data).substring(0, 200) : 'N/A';
|
|
420
|
-
this.log?.warn(`[MessageHandler] 发送流式消息失败: ${err.message}, url=${url}, status=${status || 'N/A'}, response=${responseData}, seq=${seq}`);
|
|
479
|
+
this.log?.warn(`[MessageHandler] 发送流式消息失败: ${err.message}, seq=${seq}`);
|
|
421
480
|
}
|
|
422
481
|
});
|
|
423
482
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 融云服务端 API 客户端
|
|
3
|
+
* 直接从 silent-service 调用融云 REST API,无需通过服务端代理
|
|
4
|
+
* 文档: https://docs.rongcloud.cn/platform-chat-api/message/send-private-stream
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
// 国内数据中心 API 地址
|
|
11
|
+
const API_HOSTS_CN = [
|
|
12
|
+
'api.rong-api.com',
|
|
13
|
+
'api-b.rong-api.com'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
class RongCloudServerAPI {
|
|
17
|
+
constructor(configManager, log) {
|
|
18
|
+
this.configManager = configManager;
|
|
19
|
+
this.log = log;
|
|
20
|
+
this.hosts = API_HOSTS_CN;
|
|
21
|
+
this.currentHostIndex = 0;
|
|
22
|
+
this.timeout = 10000;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get currentHost() {
|
|
26
|
+
return this.hosts[this.currentHostIndex];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_switchHost() {
|
|
30
|
+
if (this.currentHostIndex < this.hosts.length - 1) {
|
|
31
|
+
this.currentHostIndex++;
|
|
32
|
+
this.log?.info(`[RongCloudServerAPI] 切换到备用域名: ${this.currentHost}`);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_generateNonce(length = 18) {
|
|
39
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
40
|
+
let result = '';
|
|
41
|
+
for (let i = 0; i < length; i++) {
|
|
42
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_generateSignature(appSecret) {
|
|
48
|
+
const nonce = this._generateNonce();
|
|
49
|
+
const timestamp = Date.now();
|
|
50
|
+
const source = appSecret + nonce + timestamp;
|
|
51
|
+
const signature = crypto.createHash('sha1').update(source).digest('hex');
|
|
52
|
+
return { nonce, timestamp, signature };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_getHeaders(appKey, appSecret) {
|
|
56
|
+
const sign = this._generateSignature(appSecret);
|
|
57
|
+
return {
|
|
58
|
+
'App-Key': appKey,
|
|
59
|
+
'Nonce': sign.nonce,
|
|
60
|
+
'Timestamp': String(sign.timestamp),
|
|
61
|
+
'Signature': sign.signature,
|
|
62
|
+
'Content-Type': 'application/json; charset=UTF-8'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async request(path, data, appKey, appSecret, retry = true) {
|
|
67
|
+
const url = `https://${this.currentHost}${path}`;
|
|
68
|
+
const headers = this._getHeaders(appKey, appSecret);
|
|
69
|
+
|
|
70
|
+
this.log?.info(`[RongCloudServerAPI] 请求: POST ${url}`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await axios.post(url, data, {
|
|
74
|
+
headers,
|
|
75
|
+
timeout: this.timeout,
|
|
76
|
+
responseType: 'json'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = response.data;
|
|
80
|
+
|
|
81
|
+
if (result.code && result.code !== 200) {
|
|
82
|
+
throw new Error(`[${result.code}] ${result.errorMessage || 'Unknown error'}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.response?.status === 401) {
|
|
88
|
+
this.log?.error('[RongCloudServerAPI] 签名验证失败,请检查 App Key 和 App Secret');
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (retry && this._switchHost()) {
|
|
93
|
+
this.log?.warn(`[RongCloudServerAPI] 请求失败,使用备用域名重试: ${err.message}`);
|
|
94
|
+
return this.request(path, data, appKey, appSecret, false);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.log?.error(`[RongCloudServerAPI] 请求失败: ${err.message}`);
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 获取融云配置
|
|
104
|
+
*/
|
|
105
|
+
async _getRongCloudConfig() {
|
|
106
|
+
const configs = await this.configManager.getConfigs([
|
|
107
|
+
'rongcloud_app_key',
|
|
108
|
+
'rongcloud_app_secret'
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
if (!configs.rongcloud_app_key || !configs.rongcloud_app_secret) {
|
|
112
|
+
throw new Error('融云配置未找到,请先配置 rongcloud_app_key 和 rongcloud_app_secret');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
appKey: configs.rongcloud_app_key,
|
|
117
|
+
appSecret: configs.rongcloud_app_secret
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 发送单聊流式消息
|
|
123
|
+
*/
|
|
124
|
+
async sendStreamPrivate({
|
|
125
|
+
fromUserId,
|
|
126
|
+
toUserId,
|
|
127
|
+
content,
|
|
128
|
+
streamId,
|
|
129
|
+
isFirstChunk = false,
|
|
130
|
+
isLastChunk = false,
|
|
131
|
+
seq = 1,
|
|
132
|
+
streamType = 'markdown',
|
|
133
|
+
messageUID = null
|
|
134
|
+
}) {
|
|
135
|
+
const { appKey, appSecret } = await this._getRongCloudConfig();
|
|
136
|
+
|
|
137
|
+
const contentBody = {
|
|
138
|
+
content,
|
|
139
|
+
complete: isLastChunk,
|
|
140
|
+
seq
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (isFirstChunk) {
|
|
144
|
+
contentBody.type = streamType;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!isFirstChunk && messageUID) {
|
|
148
|
+
contentBody.messageUID = messageUID;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const data = {
|
|
152
|
+
fromUserId,
|
|
153
|
+
toUserId,
|
|
154
|
+
objectName: 'RC:StreamMsg',
|
|
155
|
+
content: contentBody,
|
|
156
|
+
isPersisted: 1,
|
|
157
|
+
isCounted: isFirstChunk ? 1 : 0,
|
|
158
|
+
disableUpdateLastMsg: !isLastChunk
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
this.log?.info(`[RongCloudServerAPI] 发送单聊流式消息: to=${toUserId}, streamId=${streamId}, first=${isFirstChunk}, last=${isLastChunk}, seq=${seq}`);
|
|
162
|
+
return this.request('/v3/message/private/publish_stream.json', data, appKey, appSecret);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 发送群聊流式消息
|
|
167
|
+
*/
|
|
168
|
+
async sendStreamGroup({
|
|
169
|
+
fromUserId,
|
|
170
|
+
toGroupId,
|
|
171
|
+
content,
|
|
172
|
+
streamId,
|
|
173
|
+
isFirstChunk = false,
|
|
174
|
+
isLastChunk = false,
|
|
175
|
+
seq = 1,
|
|
176
|
+
streamType = 'markdown',
|
|
177
|
+
messageUID = null
|
|
178
|
+
}) {
|
|
179
|
+
const { appKey, appSecret } = await this._getRongCloudConfig();
|
|
180
|
+
|
|
181
|
+
const contentBody = {
|
|
182
|
+
content,
|
|
183
|
+
complete: isLastChunk,
|
|
184
|
+
seq
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (isFirstChunk) {
|
|
188
|
+
contentBody.type = streamType;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!isFirstChunk && messageUID) {
|
|
192
|
+
contentBody.messageUID = messageUID;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const data = {
|
|
196
|
+
fromUserId,
|
|
197
|
+
toGroupId,
|
|
198
|
+
objectName: 'RC:StreamMsg',
|
|
199
|
+
content: contentBody,
|
|
200
|
+
isPersisted: 1,
|
|
201
|
+
isCounted: isFirstChunk ? 1 : 0,
|
|
202
|
+
isIncludeSender: 1,
|
|
203
|
+
disableUpdateLastMsg: !isLastChunk
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
this.log?.info(`[RongCloudServerAPI] 发送群聊流式消息: to=${toGroupId}, streamId=${streamId}, first=${isFirstChunk}, last=${isLastChunk}, seq=${seq}`);
|
|
207
|
+
return this.request('/v3/message/group/publish_stream.json', data, appKey, appSecret);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 发送 typing 状态
|
|
212
|
+
*/
|
|
213
|
+
async sendTypingStatus({ fromUserId, toUserId, conversationType = 1 }) {
|
|
214
|
+
const { appKey, appSecret } = await this._getRongCloudConfig();
|
|
215
|
+
|
|
216
|
+
const content = JSON.stringify({ typingContentType: 'RC:TxtMsg' }, { ensureAscii: false });
|
|
217
|
+
|
|
218
|
+
const data = {
|
|
219
|
+
fromUserId,
|
|
220
|
+
toUserId,
|
|
221
|
+
objectName: 'RC:TypSts',
|
|
222
|
+
content,
|
|
223
|
+
isPersisted: 0,
|
|
224
|
+
isCounted: 0
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
this.log?.info(`[RongCloudServerAPI] 发送 typing 状态: ${fromUserId} -> ${toUserId}`);
|
|
228
|
+
|
|
229
|
+
if (conversationType === 3) {
|
|
230
|
+
return this.request('/message/group/publish.json', data, appKey, appSecret);
|
|
231
|
+
}
|
|
232
|
+
return this.request('/message/private/publish.json', data, appKey, appSecret);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { RongCloudServerAPI };
|
package/service/worker.js
CHANGED
|
@@ -199,7 +199,7 @@ function loadRongCloudConfig() {
|
|
|
199
199
|
config.appKey = localConfig.appKey || config.appKey;
|
|
200
200
|
if (localConfig.token) config.token = localConfig.token;
|
|
201
201
|
if (localConfig.accountId) config.accountId = localConfig.accountId;
|
|
202
|
-
|
|
202
|
+
// appSecret 不再从本地配置加载,统一从服务端获取
|
|
203
203
|
if (localConfig.apiBaseUrl) config.apiBaseUrl = localConfig.apiBaseUrl;
|
|
204
204
|
log.info(`[WORKER] 从本地配置加载: appKey=${config.appKey?.substring(0, 8)}...`);
|
|
205
205
|
}
|