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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.73",
3
+ "version": "0.0.75",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -85,7 +85,6 @@ function loadConfig() {
85
85
  scriptTimeout: localConfig.scriptTimeout || 180,
86
86
  successKeyword: localConfig.successKeyword || 'Success',
87
87
  chatTimeout: localConfig.chatTimeout || 600,
88
- maxRounds: localConfig.maxRounds || 10,
89
88
  apiBaseUrl
90
89
  };
91
90
  }
@@ -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
- this._maxRounds = config.maxRounds || 10;
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.config.apiBaseUrl;
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}/${this._maxRounds}`);
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, `✅ 新一轮对话已开始,最大对话轮数为 ${this._maxRounds} 轮。`, msg.conversationType);
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, this._maxRounds - currentRounds);
139
- const statusMsg = currentRounds >= this._maxRounds
140
- ? `⛔ 本轮对话已结束(已达 ${this._maxRounds} 轮)。发送 /newround 开启新一轮。`
141
- : `📊 当前对话进度:第 ${currentRounds + 1}/${this._maxRounds} 轮,剩余 ${remaining} 轮。`;
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 >= this._maxRounds) {
149
- this.log?.info(`[MessageHandler] 群聊 ${groupId} 已达到最大轮数 ${this._maxRounds},拒绝处理`);
150
- await this.sendFn(groupId, `⛔ 本轮对话已达到最大轮数(${this._maxRounds} 轮),对话已结束。\n\n发送 /newround 可开启新一轮对话。`, msg.conversationType);
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 = `__STREAM_HISTORY__:${buffer}`;
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 状态(通过 Python 后端代理)
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 axios.post(
358
- `${this.config.apiBaseUrl}/im/api/proxy/stream/typing`,
359
- {
360
- fromUserId,
361
- targetId,
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
- const url = `${this.config.apiBaseUrl}/im/api/proxy/stream/typing`;
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
- * 发送流式消息片段(通过 Python 后端代理)
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: isStreamingEnabled=false');
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
- const payload = {
392
- fromUserId,
393
- targetId,
394
- content,
395
- streamId,
396
- isFirstChunk,
397
- isLastChunk,
398
- conversationType,
399
- seq,
400
- messageUID
401
- };
402
- this.log?.info(`[MessageHandler] _sendStreamChunk 请求体: ${JSON.stringify(payload).substring(0, 300)}`);
403
- const resp = await axios.post(
404
- `${this.config.apiBaseUrl}/im/api/proxy/stream/publish`,
405
- payload,
406
- { timeout: 10000 }
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 && resp.data?.messageUID) {
411
- this._streamMessageUIDs.set(streamId, resp.data.messageUID);
412
- 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}`);
413
475
  }
414
476
 
415
- 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}`);
416
478
  } catch (err) {
417
- const url = `${this.config.apiBaseUrl}/im/api/proxy/stream/publish`;
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
- 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
  }