claw-subagent-service 0.0.72 → 0.0.74

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.72",
3
+ "version": "0.0.74",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -0,0 +1,78 @@
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}/api/system/config/${configKey}`;
24
+ const response = await axios.get(url, { timeout: 10000 });
25
+
26
+ if (response.data?.code === 200 && response.data?.data?.value) {
27
+ this.configs.set(configKey, response.data.data.value);
28
+ this.lastFetchTime = Date.now();
29
+ this.log?.info(`[SystemConfig] 获取配置成功: ${configKey}`);
30
+ return response.data.data.value;
31
+ }
32
+
33
+ this.log?.warn(`[SystemConfig] 获取配置失败: ${configKey}, code=${response.data?.code}`);
34
+ return null;
35
+ } catch (err) {
36
+ this.log?.error(`[SystemConfig] 获取配置异常: ${configKey}, ${err.message}`);
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 获取配置值(带缓存)
43
+ */
44
+ async getConfig(configKey) {
45
+ // 检查缓存是否过期
46
+ const now = Date.now();
47
+ if (this.configs.has(configKey) && (now - this.lastFetchTime) < this.fetchInterval) {
48
+ return this.configs.get(configKey);
49
+ }
50
+
51
+ // 重新获取
52
+ return this.fetchConfig(configKey);
53
+ }
54
+
55
+ /**
56
+ * 批量获取配置
57
+ */
58
+ async getConfigs(keys) {
59
+ const results = {};
60
+ for (const key of keys) {
61
+ results[key] = await this.getConfig(key);
62
+ }
63
+ return results;
64
+ }
65
+
66
+ /**
67
+ * 强制刷新配置
68
+ */
69
+ async refresh() {
70
+ this.lastFetchTime = 0;
71
+ const keys = Array.from(this.configs.keys());
72
+ for (const key of keys) {
73
+ await this.fetchConfig(key);
74
+ }
75
+ }
76
+ }
77
+
78
+ 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,18 +13,30 @@ 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();
17
24
  // 存储流式消息的 RongCloud messageUID:streamId -> messageUID
18
25
  this._streamMessageUIDs = new Map();
26
+ // 群聊对话轮数统计:groupId -> currentRound
27
+ this._groupRoundCounts = new Map();
28
+ // 群组配置缓存:groupId -> { maxRounds, expiresAt }
29
+ this._groupConfigCache = new Map();
30
+ this._defaultMaxRounds = 10;
31
+ this._groupConfigCacheTTL = config.groupConfigCacheTTL || 60000; // 默认缓存 60 秒
19
32
  }
20
33
 
21
34
  /**
22
35
  * 判断是否支持流式处理
36
+ * 现在配置从服务端动态获取,总是启用
23
37
  */
24
38
  get isStreamingEnabled() {
25
- return !!this.config.apiBaseUrl;
39
+ return !!this.serverAPI;
26
40
  }
27
41
 
28
42
  extractMentions(content) {
@@ -84,6 +98,71 @@ class MessageHandler {
84
98
  return true;
85
99
  }
86
100
 
101
+ /**
102
+ * 获取群聊当前轮数
103
+ */
104
+ _getGroupRoundCount(groupId) {
105
+ return this._groupRoundCounts.get(groupId) || 0;
106
+ }
107
+
108
+ /**
109
+ * 增加群聊轮数
110
+ */
111
+ _incrementGroupRoundCount(groupId, maxRounds) {
112
+ const current = this._getGroupRoundCount(groupId);
113
+ this._groupRoundCounts.set(groupId, current + 1);
114
+ this.log?.info(`[MessageHandler] 群聊 ${groupId} 轮数 +1,当前: ${current + 1}/${maxRounds}`);
115
+ }
116
+
117
+ /**
118
+ * 重置群聊轮数
119
+ */
120
+ _resetGroupRoundCount(groupId) {
121
+ this._groupRoundCounts.set(groupId, 0);
122
+ this.log?.info(`[MessageHandler] 群聊 ${groupId} 轮数已重置`);
123
+ }
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
+
87
166
  async handleMessage(msg) {
88
167
  if (!this.shouldHandleMessage(msg)) {
89
168
  return;
@@ -94,16 +173,64 @@ class MessageHandler {
94
173
  const logContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
95
174
  this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${logContent.substring(0, 50)}`);
96
175
 
176
+ // 群聊轮数控制(仅对群聊生效)
177
+ let maxRounds = this._defaultMaxRounds;
178
+ if (msg.conversationType === 3) {
179
+ const groupId = msg.targetId;
180
+ maxRounds = await this._getGroupMaxRounds(groupId);
181
+ const currentRounds = this._getGroupRoundCount(groupId);
182
+
183
+ // 处理内置轮数相关命令
184
+ if (type === MessageType.COMMAND) {
185
+ const payload = this.parseCommand(msg.content, msg.senderUserId);
186
+ if (payload.command === 'newround') {
187
+ this._resetGroupRoundCount(groupId);
188
+ await this.sendFn(groupId, `✅ 新一轮对话已开始,最大对话轮数为 ${maxRounds} 轮。`, msg.conversationType);
189
+ return;
190
+ }
191
+ if (payload.command === 'roundstatus') {
192
+ const remaining = Math.max(0, maxRounds - currentRounds);
193
+ const statusMsg = currentRounds >= maxRounds
194
+ ? `⛔ 本轮对话已结束(已达 ${maxRounds} 轮)。发送 /newround 开启新一轮。`
195
+ : `📊 当前对话进度:第 ${currentRounds + 1}/${maxRounds} 轮,剩余 ${remaining} 轮。`;
196
+ await this.sendFn(groupId, statusMsg, msg.conversationType);
197
+ return;
198
+ }
199
+ }
200
+
201
+ // 检查是否已达最大轮数
202
+ if (currentRounds >= maxRounds) {
203
+ this.log?.info(`[MessageHandler] 群聊 ${groupId} 已达到最大轮数 ${maxRounds},拒绝处理`);
204
+ await this.sendFn(groupId, `⛔ 本轮对话已达到最大轮数(${maxRounds} 轮),对话已结束。\n\n发送 /newround 可开启新一轮对话。`, msg.conversationType);
205
+ return;
206
+ }
207
+ }
208
+
209
+ // 发送已读回执(fire-and-forget,不阻塞消息处理)
210
+ if (this.sendReadReceiptFn && msg.messageUId) {
211
+ this.sendReadReceiptFn(msg).catch((err) => {
212
+ this.log?.warn(`[MessageHandler] 发送已读回执失败: ${err.message}`);
213
+ });
214
+ }
215
+
97
216
  // 如果配置了代理地址,使用流式处理
98
217
  if (this.isStreamingEnabled) {
99
218
  try {
100
219
  await this.handleNormalMessageStream(msg);
220
+ // 流式处理成功,群聊轮数 +1
221
+ if (msg.conversationType === 3) {
222
+ this._incrementGroupRoundCount(msg.targetId, maxRounds);
223
+ }
101
224
  } catch (err) {
102
225
  this.log?.error(`[MessageHandler] 流式处理失败,回退到非流式: ${err.message}`);
103
226
  const reply = await this.handleNormalMessage(msg);
104
227
  if (reply) {
105
228
  const targetId = this.getReplyTarget(msg);
106
229
  await this.sendFn(targetId, reply, msg.conversationType);
230
+ // 非流式回退成功,群聊轮数 +1
231
+ if (msg.conversationType === 3) {
232
+ this._incrementGroupRoundCount(msg.targetId, maxRounds);
233
+ }
107
234
  }
108
235
  }
109
236
  } else {
@@ -112,6 +239,10 @@ class MessageHandler {
112
239
  if (reply) {
113
240
  const targetId = this.getReplyTarget(msg);
114
241
  await this.sendFn(targetId, reply, msg.conversationType);
242
+ // 非流式处理成功,群聊轮数 +1
243
+ if (msg.conversationType === 3) {
244
+ this._incrementGroupRoundCount(msg.targetId, maxRounds);
245
+ }
115
246
  }
116
247
  }
117
248
  } catch (err) {
@@ -174,6 +305,23 @@ class MessageHandler {
174
305
 
175
306
  // 2. 调用 OpenClaw SSE
176
307
  let hasSentChunk = false;
308
+ // typing 刷新定时器:每次 delta 时刷新 typing,超时后自动停止
309
+ let typingTimer = null;
310
+ const refreshTyping = async () => {
311
+ // 清除旧的超时
312
+ if (typingTimer) {
313
+ clearTimeout(typingTimer);
314
+ typingTimer = null;
315
+ }
316
+ // 发送 typing
317
+ await this._sendTypingStatus(fromUserId, targetId, conversationType);
318
+ // 设置 3 秒超时,如果 3 秒内没有新的 delta,typing 自动消失
319
+ typingTimer = setTimeout(() => {
320
+ this.log?.info(`[MessageHandler] typing 超时自动清除: streamId=${streamId}`);
321
+ typingTimer = null;
322
+ }, 3000);
323
+ };
324
+
177
325
  try {
178
326
  // 确保传入的内容是字符串(claw 类型消息 content 可能是对象)
179
327
  const chatContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || JSON.stringify(msg.content));
@@ -188,9 +336,18 @@ class MessageHandler {
188
336
  // 发送增量(delta),让前端做增量拼接,避免内容重复
189
337
  await this._sendStreamChunk(fromUserId, targetId, conversationType, delta, streamId, seq === 1, false, seq);
190
338
  hasSentChunk = true;
339
+ // 每次有输出时刷新 typing 状态
340
+ await refreshTyping();
191
341
  },
192
342
  async (fullText) => {
193
343
  this.log?.info(`[MessageHandler] onDone 触发, fullText.length=${fullText.length}, buffer.length=${buffer.length}, hasSentChunk=${hasSentChunk}`);
344
+
345
+ // 清除 typing 定时器
346
+ if (typingTimer) {
347
+ clearTimeout(typingTimer);
348
+ typingTimer = null;
349
+ }
350
+
194
351
  if (buffer.trim()) {
195
352
  // 发送尾流:空字符串表示流结束,前端保留已拼接的完整内容
196
353
  seq += 1;
@@ -214,10 +371,34 @@ class MessageHandler {
214
371
 
215
372
  // 清理已存储的 messageUID,防止内存泄漏
216
373
  this._streamMessageUIDs.delete(streamId);
374
+
375
+ // 发送持久化的普通文本消息作为历史记录(融云会保存 RC:TxtMsg)
376
+ if (buffer.trim()) {
377
+ try {
378
+ this.log?.info(`[MessageHandler] 发送历史记录文本消息: length=${buffer.length}`);
379
+ // 使用 JSON 格式包含 streamId,前端可据此关联并更新流式消息内容
380
+ const historyContent = JSON.stringify({
381
+ __stream_history__: true,
382
+ streamId: streamId,
383
+ text: buffer,
384
+ sentTime: Date.now()
385
+ });
386
+ await this.sendFn(targetId, historyContent, conversationType);
387
+ } catch (err) {
388
+ this.log?.error(`[MessageHandler] 发送历史记录失败: ${err.message}`);
389
+ }
390
+ }
217
391
  }
218
392
  );
219
393
  } catch (err) {
220
394
  this.log?.error(`[MessageHandler] 流式处理错误: ${err.message}`);
395
+
396
+ // 清除 typing 定时器
397
+ if (typingTimer) {
398
+ clearTimeout(typingTimer);
399
+ typingTimer = null;
400
+ }
401
+
221
402
  await this._sendStreamChunk(fromUserId, targetId, conversationType, '抱歉,AI 响应出现错误,请稍后重试。', streamId, true, true, 1);
222
403
 
223
404
  // 错误时也要清理
@@ -227,36 +408,30 @@ class MessageHandler {
227
408
  }
228
409
 
229
410
  /**
230
- * 发送 typing 状态(通过 Python 后端代理)
411
+ * 发送 typing 状态(直接调用融云 API)
231
412
  */
232
413
  async _sendTypingStatus(fromUserId, targetId, conversationType) {
233
- if (!this.isStreamingEnabled) return;
414
+ if (!this.isStreamingEnabled || !this.serverAPI) return;
234
415
  try {
235
- await axios.post(
236
- `${this.config.apiBaseUrl}/im/api/proxy/stream/typing`,
237
- {
238
- fromUserId,
239
- targetId,
240
- conversationType
241
- },
242
- { timeout: 5000 }
243
- );
416
+ await this.serverAPI.sendTypingStatus({
417
+ fromUserId,
418
+ toUserId: targetId,
419
+ conversationType
420
+ });
244
421
  this.log?.info(`[MessageHandler] typing 状态已发送: ${fromUserId} -> ${targetId}`);
245
422
  } catch (err) {
246
- const url = `${this.config.apiBaseUrl}/im/api/proxy/stream/typing`;
247
- const status = err.response?.status;
248
- this.log?.warn(`[MessageHandler] 发送 typing 状态失败: ${err.message}, url=${url}, status=${status || 'N/A'}`);
423
+ this.log?.warn(`[MessageHandler] 发送 typing 状态失败: ${err.message}`);
249
424
  }
250
425
  }
251
426
 
252
427
  /**
253
- * 发送流式消息片段(通过 Python 后端代理)
428
+ * 发送流式消息片段(直接调用融云 API)
254
429
  */
255
430
  async _sendStreamChunk(fromUserId, targetId, conversationType, content, streamId, isFirstChunk, isLastChunk, seq = 1) {
256
431
  const contentPreview = typeof content === 'string' ? content.substring(0, 100) : JSON.stringify(content).substring(0, 100);
257
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}`);
258
- if (!this.isStreamingEnabled) {
259
- this.log?.warn('[MessageHandler] _sendStreamChunk skipped: isStreamingEnabled=false');
433
+ if (!this.isStreamingEnabled || !this.serverAPI) {
434
+ this.log?.warn('[MessageHandler] _sendStreamChunk skipped: 未配置 appKey/appSecret');
260
435
  return;
261
436
  }
262
437
 
@@ -266,36 +441,42 @@ class MessageHandler {
266
441
  // 获取已存储的 RongCloud messageUID(首流响应返回的)
267
442
  const messageUID = this._streamMessageUIDs.get(streamId);
268
443
 
269
- const payload = {
270
- fromUserId,
271
- targetId,
272
- content,
273
- streamId,
274
- isFirstChunk,
275
- isLastChunk,
276
- conversationType,
277
- seq,
278
- messageUID
279
- };
280
- this.log?.info(`[MessageHandler] _sendStreamChunk 请求体: ${JSON.stringify(payload).substring(0, 300)}`);
281
- const resp = await axios.post(
282
- `${this.config.apiBaseUrl}/im/api/proxy/stream/publish`,
283
- payload,
284
- { timeout: 10000 }
285
- );
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
+ }
286
470
 
287
471
  // 首流时存储 RongCloud 返回的 messageUID
288
- if (isFirstChunk && resp.data?.messageUID) {
289
- this._streamMessageUIDs.set(streamId, resp.data.messageUID);
290
- this.log?.info(`[MessageHandler] 首流 messageUID 已存储: ${resp.data.messageUID}, streamId=${streamId}`);
472
+ if (isFirstChunk && result?.messageUID) {
473
+ this._streamMessageUIDs.set(streamId, result.messageUID);
474
+ this.log?.info(`[MessageHandler] 首流 messageUID 已存储: ${result.messageUID}, streamId=${streamId}`);
291
475
  }
292
476
 
293
- this.log?.info(`[MessageHandler] _sendStreamChunk 成功: status=${resp.status}, seq=${seq}, response=${JSON.stringify(resp.data).substring(0, 200)}`);
477
+ this.log?.info(`[MessageHandler] _sendStreamChunk 成功: seq=${seq}`);
294
478
  } catch (err) {
295
- const url = `${this.config.apiBaseUrl}/im/api/proxy/stream/publish`;
296
- const status = err.response?.status;
297
- const responseData = err.response?.data ? JSON.stringify(err.response.data).substring(0, 200) : 'N/A';
298
- 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}`);
299
480
  }
300
481
  });
301
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
- if (localConfig.appSecret) config.appSecret = localConfig.appSecret;
202
+ // appSecret 不再从本地配置加载,统一从服务端获取
203
203
  if (localConfig.apiBaseUrl) config.apiBaseUrl = localConfig.apiBaseUrl;
204
204
  log.info(`[WORKER] 从本地配置加载: appKey=${config.appKey?.substring(0, 8)}...`);
205
205
  }