claw-subagent-service 0.0.168 → 0.0.172

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/cli.js CHANGED
@@ -24,6 +24,17 @@ const SERVICE_NAME = 'claw-subagent-service';
24
24
  const args = process.argv.slice(2);
25
25
  const command = args[0] || '--run';
26
26
 
27
+ // --version 支持:直接显示版本并退出,不启动 daemon
28
+ if (command === '--version' || command === '-v') {
29
+ try {
30
+ const version = require('./package.json').version;
31
+ console.log(version);
32
+ } catch {
33
+ console.log('unknown');
34
+ }
35
+ process.exit(0);
36
+ }
37
+
27
38
  function runDaemon() {
28
39
  console.log('[CLI] 启动 Daemon...');
29
40
  console.log(`[CLI] CLI 路径: ${__filename}`);
@@ -254,15 +265,38 @@ function controlService(action) {
254
265
  function checkStatus() {
255
266
  const platform = process.platform;
256
267
  let cmd;
257
-
268
+
258
269
  if (platform === 'win32') {
259
- cmd = `sc.exe query ${SERVICE_NAME}`;
270
+ // Windows: 先尝试 sc.exe query,若服务未安装则回退到进程检查
271
+ cmd = `sc.exe query "${SERVICE_NAME}"`;
272
+ exec(cmd, (err, stdout, stderr) => {
273
+ if (err) {
274
+ // 1060 = 服务未安装,给出更友好的提示
275
+ if (stderr && stderr.includes('1060')) {
276
+ console.log(`[CLI] 服务 "${SERVICE_NAME}" 未安装`);
277
+ console.log(`[CLI] 请运行: claw-subagent-service --install`);
278
+ } else {
279
+ console.error(`[CLI] 查询状态失败: ${err.message}`);
280
+ }
281
+ // 回退:检查 daemon 进程是否在前台运行
282
+ try {
283
+ const list = execSync('tasklist /FI "IMAGENAME eq node.exe" /FO CSV /NH', { encoding: 'utf8', windowsHide: true });
284
+ const hasDaemon = list.includes('daemon.js');
285
+ if (hasDaemon) {
286
+ console.log('[CLI] 检测到 Daemon 进程正在前台运行');
287
+ }
288
+ } catch { /* 忽略 */ }
289
+ return;
290
+ }
291
+ console.log(stdout);
292
+ });
293
+ return;
260
294
  } else if (platform === 'linux') {
261
295
  cmd = `systemctl status ${SERVICE_NAME}`;
262
296
  } else if (platform === 'darwin') {
263
297
  cmd = `launchctl list | grep ${SERVICE_NAME}`;
264
298
  }
265
-
299
+
266
300
  exec(cmd, (err, stdout) => {
267
301
  if (err) {
268
302
  console.error(`[CLI] 查询状态失败: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.168",
3
+ "version": "0.0.172",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -130,25 +130,40 @@ function installAndStartService() {
130
130
  svc.on('install', () => {
131
131
  console.log('[postinstall] 服务注册成功,正在启动...');
132
132
 
133
- // 延迟 2 秒确保服务注册到 SCM,再设置开机自启 + 崩溃恢复
133
+ // 延迟 3 秒确保服务注册到 SCM,再设置开机自启 + 崩溃恢复
134
134
  setTimeout(() => {
135
- const cmdFailure = `sc.exe failure "${SERVICE_NAME}" reset= 0 actions= restart/0/restart/0/restart/0`;
136
- exec(cmdFailure, (err) => {
137
- if (err) console.error(`[postinstall] 设置恢复策略失败: ${err.message}`);
138
- else console.log('[postinstall] 恢复策略已设置:崩溃后自动重启');
139
- });
140
-
141
- exec(`sc.exe config "${SERVICE_NAME}" start= auto`, (err) => {
142
- if (err) console.error(`[postinstall] 设置自动启动失败: ${err.message}`);
143
- else console.log('[postinstall] 启动类型已设为:自动');
144
- });
145
- }, 2000);
135
+ // 使用 execSync 确保配置在启动前生效
136
+ try {
137
+ execSync(`sc.exe failure "${SERVICE_NAME}" reset= 0 actions= restart/0/restart/0/restart/0`, { stdio: 'ignore', timeout: 10000 });
138
+ console.log('[postinstall] 恢复策略已设置:崩溃后自动重启');
139
+ } catch (err) {
140
+ console.error(`[postinstall] 设置恢复策略失败: ${err.message}`);
141
+ }
146
142
 
147
- svc.start();
143
+ try {
144
+ execSync(`sc.exe config "${SERVICE_NAME}" start= auto`, { stdio: 'ignore', timeout: 10000 });
145
+ console.log('[postinstall] 启动类型已设为:自动');
146
+ } catch (err) {
147
+ console.error(`[postinstall] 设置自动启动失败: ${err.message}`);
148
+ }
149
+
150
+ // 配置完成后启动服务
151
+ svc.start();
152
+ }, 3000);
148
153
  });
149
154
 
150
155
  svc.on('start', () => {
151
156
  console.log('[postinstall] 服务已启动');
157
+ // 启动后验证服务状态
158
+ setTimeout(() => {
159
+ try {
160
+ const status = execSync(`sc.exe query "${SERVICE_NAME}"`, { encoding: 'utf8', timeout: 5000 });
161
+ console.log('[postinstall] 服务状态验证:');
162
+ console.log(status);
163
+ } catch (err) {
164
+ console.warn(`[postinstall] 服务状态验证失败: ${err.message}`);
165
+ }
166
+ }, 2000);
152
167
  });
153
168
 
154
169
  svc.on('error', (err) => {
@@ -610,9 +610,29 @@ class RongyunMessageHandler {
610
610
  const requestId = data.request_id;
611
611
  const userId = data.userId || data.source_im_id;
612
612
  const sourceId = data.source_im_id;
613
- const content = data.content || data._raw_content;
613
+ let content = data.content || data._raw_content;
614
+ const voiceUrl = data.voiceUrl;
615
+ const voiceDuration = data.voiceDuration;
616
+
617
+ this.logInfo(`[RongyunMessageHandler] 收到客服消息, userId=${userId}, content=${content?.substring(0, 50)}, voiceUrl=${voiceUrl ? '有' : '无'}`);
614
618
 
615
- this.logInfo(`[RongyunMessageHandler] 收到客服消息, userId=${userId}, content=${content?.substring(0, 50)}`);
619
+ // 处理语音消息:如果有语音URL,先进行语音识别
620
+ if (voiceUrl && (!content || content === '[语音]')) {
621
+ try {
622
+ this.logInfo(`[RongyunMessageHandler] 检测到语音消息,开始语音识别: ${voiceUrl}`);
623
+ const recognizedText = await this.recognizeVoice(voiceUrl);
624
+ if (recognizedText) {
625
+ content = recognizedText;
626
+ this.logInfo(`[RongyunMessageHandler] 语音识别成功: ${content.substring(0, 50)}`);
627
+ } else {
628
+ content = '[语音消息识别失败]';
629
+ this.logWarn(`[RongyunMessageHandler] 语音识别失败,使用占位文本`);
630
+ }
631
+ } catch (e) {
632
+ this.logError(`[RongyunMessageHandler] 语音识别异常: ${e.message}`);
633
+ content = '[语音消息识别失败]';
634
+ }
635
+ }
616
636
 
617
637
  if (!content) {
618
638
  this.logWarn('客服消息内容为空');
@@ -691,6 +711,42 @@ class RongyunMessageHandler {
691
711
  }
692
712
  }
693
713
 
714
+ /**
715
+ * 语音识别 - 调用后端 Python 服务
716
+ * @param {string} voiceUrl - 语音文件 URL
717
+ * @returns {Promise<string>} 识别文本
718
+ */
719
+ async recognizeVoice(voiceUrl) {
720
+ try {
721
+ const axios = require('axios');
722
+
723
+ // 从配置中获取后端 API 地址
724
+ const apiBaseUrl = this.config.apiBaseUrl || process.env.API_BASE_URL || 'http://localhost:5000';
725
+ const recognizeUrl = `${apiBaseUrl}/im/api/voice/recognize`;
726
+
727
+ this.logInfo(`[RongyunMessageHandler] 调用语音识别 API: ${recognizeUrl}`);
728
+
729
+ const response = await axios.post(recognizeUrl, {
730
+ audioUrl: voiceUrl,
731
+ format: 'mp3',
732
+ sampleRate: 16000
733
+ }, {
734
+ timeout: 30000,
735
+ headers: { 'Content-Type': 'application/json' }
736
+ });
737
+
738
+ if (response.data && response.data.code === 200) {
739
+ return response.data.data.text;
740
+ } else {
741
+ this.logError(`[RongyunMessageHandler] 语音识别 API 返回错误: ${JSON.stringify(response.data)}`);
742
+ return null;
743
+ }
744
+ } catch (e) {
745
+ this.logError(`[RongyunMessageHandler] 语音识别请求失败: ${e.message}`);
746
+ return null;
747
+ }
748
+ }
749
+
694
750
  async sendResponse(msgType, content, requestId, targetId) {
695
751
  if (!this.messageSender) {
696
752
  this.logError('MessageSender 未设置,无法发送响应');
package/service/worker.js CHANGED
@@ -135,7 +135,7 @@ try {
135
135
  process.cwd();
136
136
  } catch (e) {
137
137
  if (e.code === 'ENOENT') {
138
- try { process.chdir(os.tmpdir()); } catch {}
138
+ try { process.chdir(os.tmpdir()); } catch { }
139
139
  }
140
140
  }
141
141
 
@@ -224,7 +224,6 @@ function loadRongCloudConfig() {
224
224
 
225
225
  if (!config.appKey) {
226
226
  config.appKey = process.env.DM_APP_KEY || 'bmdehs6pbyyks';
227
- log.info(`[WORKER] 使用默认 appKey: ${config.appKey}`);
228
227
  }
229
228
 
230
229
  // 设置默认心跳间隔为20秒
@@ -232,7 +231,7 @@ function loadRongCloudConfig() {
232
231
  config.heartbeatInterval = 20;
233
232
  }
234
233
 
235
- log.info(`[WORKER] 最终 apiBaseUrl: ${config.apiBaseUrl}`);
234
+ // log.info(`[WORKER] 最终 apiBaseUrl: ${config.apiBaseUrl}`);
236
235
 
237
236
  if (config.token && config.accountId) {
238
237
  return config;
@@ -263,6 +262,7 @@ async function refreshRongCloudToken() {
263
262
  const resp = await axios.get(`${serverUrl}/api/claw/token/${nodeId}`, { timeout: 15000 });
264
263
  if (resp.data?.code === 200) {
265
264
  const newToken = resp.data.data?.token || resp.data.token || '';
265
+ const newAppKey = resp.data.data?.app_key || resp.data.app_key || '';
266
266
  if (!newToken) {
267
267
  log.error('[WORKER] 服务端返回了空 token');
268
268
  return false;
@@ -271,12 +271,19 @@ async function refreshRongCloudToken() {
271
271
 
272
272
  // 更新内存配置
273
273
  rongcloudConfig.token = newToken;
274
+ if (newAppKey) {
275
+ rongcloudConfig.appKey = newAppKey;
276
+ log.info(`[WORKER] appKey 已更新: ${newAppKey}`);
277
+ }
274
278
 
275
279
  // 保存到 config.json
276
280
  try {
277
281
  if (fs.existsSync(clawBridgeConfigPath)) {
278
282
  const clawConfig = JSON.parse(fs.readFileSync(clawBridgeConfigPath, 'utf8'));
279
283
  clawConfig.token = newToken;
284
+ if (newAppKey) {
285
+ clawConfig.appKey = newAppKey;
286
+ }
280
287
  clawConfig.expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7天
281
288
  fs.writeFileSync(clawBridgeConfigPath, JSON.stringify(clawConfig, null, 2));
282
289
  log.info('[WORKER] 新 token 已保存到 config.json');
@@ -289,7 +296,7 @@ async function refreshRongCloudToken() {
289
296
  log.error(`[WORKER] 刷新 token 失败: ${resp.data?.message || '未知错误'}`);
290
297
  return false;
291
298
  } catch (err) {
292
- log.error(`[WORKER] 刷新 token 异常: ${err.message}`);
299
+ log.error(`[WORKER] 刷新 token 异常: ${err.message}`);
293
300
  return false;
294
301
  }
295
302
  }
@@ -341,12 +348,17 @@ async function syncCustomerServiceAccountId() {
341
348
 
342
349
  if (tokenResp.data?.code === 200) {
343
350
  const newToken = tokenResp.data.data?.token || tokenResp.data.token || '';
351
+ const newAppKey = tokenResp.data.data?.app_key || tokenResp.data.app_key || '';
344
352
  if (newToken) {
345
353
  log.info(`[WORKER] 获取客服账号 token 成功: ${csAccountId}`);
346
354
 
347
355
  // 更新内存配置(影响后续融云连接)
348
356
  rongcloudConfig.accountId = csAccountId;
349
357
  rongcloudConfig.token = newToken;
358
+ if (newAppKey) {
359
+ rongcloudConfig.appKey = newAppKey;
360
+ log.info(`[WORKER] 客服账号 appKey 已更新: ${newAppKey}`);
361
+ }
350
362
 
351
363
  // 持久化到本地 config.json(确保重启后仍使用新配置)
352
364
  try {
@@ -354,6 +366,9 @@ async function syncCustomerServiceAccountId() {
354
366
  const clawConfig = JSON.parse(fs.readFileSync(clawBridgeConfigPath, 'utf8'));
355
367
  clawConfig.nodeId = csAccountId;
356
368
  clawConfig.token = newToken;
369
+ if (newAppKey) {
370
+ clawConfig.appKey = newAppKey;
371
+ }
357
372
  clawConfig.expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7天
358
373
  fs.writeFileSync(clawBridgeConfigPath, JSON.stringify(clawConfig, null, 2));
359
374
  log.info(`[WORKER] 客服账号已持久化到 config.json: ${csAccountId}`);
@@ -402,7 +417,7 @@ async function initRongCloud() {
402
417
  const configChanged = await syncCustomerServiceAccountId();
403
418
  if (configChanged) {
404
419
  log.info('[WORKER] 客服账号已切换到数据库配置的 ID,将使用新配置连接融云');
405
- }
420
+ }
406
421
  log.info(`[WORKER] 代码版本特征: isOffLineMessage-pass-through, messageDirection-log, addEventListener-exclusive`);
407
422
 
408
423
  // 启动 opencode 服务(与桌面客户端对齐)
@@ -443,7 +458,7 @@ async function initRongCloud() {
443
458
  // 融云 SDK 对自定义消息可能直接返回对象而非 JSON 字符串
444
459
  if (msg.content) {
445
460
  let parsed = null;
446
-
461
+
447
462
  if (typeof msg.content === 'string') {
448
463
  // 字符串类型:尝试 JSON 解析
449
464
  try {
@@ -568,7 +583,7 @@ async function shutdownRongCloud() {
568
583
  if (global.dashboardReporter) {
569
584
  global.dashboardReporter.stop();
570
585
  }
571
-
586
+
572
587
  if (rongcloudClient) {
573
588
  // 发送 CLIENT_DISCONNECTED
574
589
  try {
@@ -578,11 +593,11 @@ async function shutdownRongCloud() {
578
593
  } catch (err) {
579
594
  log.error(`[WORKER] 发送 CLIENT_DISCONNECTED 失败: ${err.message}`);
580
595
  }
581
-
596
+
582
597
  await rongcloudClient.disconnect();
583
598
  log.info('[WORKER] 融云已断开');
584
599
  }
585
-
600
+
586
601
  // 停止 opencode 服务
587
602
  stopOpencodeService(log);
588
603
  }
@@ -644,7 +659,7 @@ server.on('error', (err) => {
644
659
  // 延迟 3 秒后重试,给进程退出和端口释放留出足够时间
645
660
  setTimeout(() => {
646
661
  log.info(`[WORKER] 重新尝试监听端口 ${PORT}...`);
647
- server.close(() => {});
662
+ server.close(() => { });
648
663
  server.listen(PORT, HOST);
649
664
  }, 3000);
650
665
  return;
@@ -681,20 +696,20 @@ async function gracefulShutdown(signal) {
681
696
  return;
682
697
  }
683
698
  isShuttingDown = true;
684
-
699
+
685
700
  log.info(`[WORKER] 收到 ${signal} 信号,开始优雅退出...`);
686
-
701
+
687
702
  try {
688
703
  await shutdownRongCloud();
689
704
  } catch (err) {
690
705
  log.error(`[WORKER] 关闭融云异常: ${err.message}`);
691
706
  }
692
-
707
+
693
708
  // 关闭 HTTP 服务
694
709
  server.close(() => {
695
710
  log.info('[WORKER] HTTP 服务已关闭');
696
711
  });
697
-
712
+
698
713
  // 给 3 秒时间完成关闭操作
699
714
  setTimeout(() => {
700
715
  log.info('[WORKER] 退出进程');
@@ -739,7 +754,7 @@ process.on('unhandledRejection', async (reason) => {
739
754
 
740
755
  // 拦截 process.exit 以定位调用来源
741
756
  const originalExit = process.exit;
742
- process.exit = function(code) {
757
+ process.exit = function (code) {
743
758
  const stack = new Error('process.exit called from:').stack;
744
759
  log.error(`[WORKER] process.exit(${code}) 被调用:\n${stack}`);
745
760
  originalExit.call(process, code);
@@ -752,7 +767,7 @@ process.on('exit', (code) => {
752
767
  // 同步发送,因为 exit 事件不支持异步
753
768
  try {
754
769
  const messageSender = new RongyunMessageSender(rongcloudClient, rongcloudConfig, log);
755
- messageSender.sendClientDisconnected().catch(() => {});
770
+ messageSender.sendClientDisconnected().catch(() => { });
756
771
  } catch (e) {
757
772
  // 忽略错误
758
773
  }