claw-subagent-service 0.0.29 → 0.0.30

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.29",
3
+ "version": "0.0.30",
4
4
  "description": "虾说静态服务",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -10,13 +10,12 @@
10
10
  * npm run sync (推荐)
11
11
  * node scripts/sync-local.js
12
12
  *
13
- * 注意: Windows 下若服务正在运行,会自动停止服务、同步后再启动。
13
+ * 注意: Windows 下若服务正在运行,会自动卸载服务、同步后再重新安装。
14
14
  */
15
15
 
16
- const { execSync, spawn } = require('child_process');
16
+ const { execSync } = require('child_process');
17
17
  const path = require('path');
18
18
  const fs = require('fs');
19
- const os = require('os');
20
19
 
21
20
  const isWindows = process.platform === 'win32';
22
21
  const SERVICE_NAME = 'claw-subagent-service';
@@ -47,7 +46,7 @@ function getGlobalPackageDir() {
47
46
  }
48
47
  }
49
48
 
50
- function stopService() {
49
+ function stopAndUninstallService() {
51
50
  if (!isWindows) {
52
51
  try {
53
52
  execSync('systemctl stop claw-subagent-service 2>/dev/null', {
@@ -59,7 +58,7 @@ function stopService() {
59
58
  return;
60
59
  }
61
60
 
62
- console.log('🛑 正在停止 Windows 服务...');
61
+ console.log('🛑 正在停止并卸载 Windows 服务(释放文件锁)...');
63
62
 
64
63
  // 1. 停止服务
65
64
  try {
@@ -70,7 +69,7 @@ function stopService() {
70
69
  console.log(' ✅ 服务已停止');
71
70
  } catch {}
72
71
 
73
- // 2. 杀掉 wrapper 进程
72
+ // 2. 杀掉 wrapper 进程(node-windows 生成的 .exe)
74
73
  try {
75
74
  execSync('taskkill /f /im "claw-subagent-service.exe" 2>nul', {
76
75
  stdio: 'ignore',
@@ -87,18 +86,36 @@ function stopService() {
87
86
  );
88
87
  } catch {}
89
88
 
90
- // 4. 等待文件锁释放
91
- console.log(' ⏳ 等待文件锁释放 (5秒)...');
92
- execSync('ping -n 6 127.0.0.1 >nul', { stdio: 'ignore', windowsHide: true });
89
+ // 4. 通过 wmic 按命令行匹配杀掉相关 node 进程
90
+ try {
91
+ execSync('wmic process where "name=\'node.exe\' and commandline like \'%claw-subagent%\'" delete 2>nul', {
92
+ stdio: 'ignore',
93
+ timeout: 10000,
94
+ });
95
+ console.log(' ✅ 相关 node 进程已清理');
96
+ } catch {}
97
+
98
+ // 5. 从注册表删除服务(彻底释放 node-windows wrapper 的文件锁)
99
+ try {
100
+ execSync('sc.exe delete "claw-subagent-service" 2>nul', {
101
+ stdio: 'ignore',
102
+ timeout: 10000,
103
+ });
104
+ console.log(' ✅ 服务已从注册表删除');
105
+ } catch {}
106
+
107
+ // 6. 等待 Windows 回收文件句柄(关键!node-windows wrapper 需要足够时间释放)
108
+ console.log(' ⏳ 等待系统释放文件锁(10秒)...');
109
+ execSync('ping -n 11 127.0.0.1 >nul', { stdio: 'ignore', windowsHide: true });
93
110
  }
94
111
 
95
- function startService() {
96
- console.log('🚀 正在启动服务...');
112
+ function installAndStartService() {
113
+ console.log('🚀 正在安装并启动服务...');
97
114
  try {
98
115
  if (isWindows) {
99
- execSync('net start "claw-subagent-service" 2>nul', {
116
+ execSync('claw-subagent-service --install', {
100
117
  stdio: 'inherit',
101
- timeout: 30000,
118
+ timeout: 60000,
102
119
  windowsHide: true,
103
120
  });
104
121
  } else {
@@ -107,23 +124,23 @@ function startService() {
107
124
  timeout: 15000,
108
125
  });
109
126
  }
110
- console.log('✅ 服务已启动');
127
+ console.log('✅ 服务已安装/启动');
111
128
  } catch (e) {
112
- console.error('⚠️ 服务启动失败:', e.message);
129
+ console.error('⚠️ 服务安装/启动失败:', e.message);
113
130
  console.log(' 请手动运行: claw-subagent-service --install');
114
131
  }
115
132
  }
116
133
 
117
- function isServiceRunning() {
134
+ function isServiceInstalled() {
118
135
  try {
119
136
  if (isWindows) {
120
- execSync('sc query "claw-subagent-service" | findstr RUNNING >nul', {
137
+ execSync('sc query "claw-subagent-service" | findstr SERVICE_NAME >nul', {
121
138
  stdio: 'ignore',
122
139
  windowsHide: true,
123
140
  });
124
141
  return true;
125
142
  } else {
126
- execSync('systemctl is-active --quiet claw-subagent-service', {
143
+ execSync('systemctl is-active --quiet claw-subagent-service || systemctl is-enabled --quiet claw-subagent-service', {
127
144
  stdio: 'ignore',
128
145
  });
129
146
  return true;
@@ -135,21 +152,25 @@ function isServiceRunning() {
135
152
 
136
153
  function copyDirSync(src, dest) {
137
154
  if (isWindows) {
138
- // 先清空目标目录
139
- if (fs.existsSync(dest)) {
140
- try {
141
- fs.rmSync(dest, { recursive: true, force: true, maxRetries: 5, retryDelay: 500 });
142
- } catch (e) {
143
- console.error(` ❌ 无法清空目录: ${dest}`);
144
- console.error(` 错误: ${e.message}`);
145
- console.error(` 请确保服务已完全停止,或尝试以管理员身份运行`);
146
- process.exit(1);
147
- }
155
+ // 确保目标目录存在
156
+ if (!fs.existsSync(dest)) {
157
+ fs.mkdirSync(dest, { recursive: true });
148
158
  }
149
- fs.mkdirSync(dest, { recursive: true });
150
159
 
151
- const cmd = `robocopy "${src}" "${dest}" /E /NJH /NJS /NDL /NC /NS /NFL`;
152
- execSync(cmd, { encoding: 'utf-8', windowsHide: true });
160
+ // 使用 robocopy,遇到锁定的文件时跳过(/R:0 /W:0 表示不重试)
161
+ const cmd = `robocopy "${src}" "${dest}" /E /R:0 /W:0 /NJH /NJS /NDL /NC /NS /NFL`;
162
+ try {
163
+ execSync(cmd, { encoding: 'utf-8', windowsHide: true });
164
+ } catch (e) {
165
+ // robocopy 返回码含义:
166
+ // 0 = 成功,1 = 有文件被跳过,2 = 有额外文件,4 = 有不匹配,8 = 有错误,16 = 严重错误
167
+ const status = e.status || 0;
168
+ if (status === 1) {
169
+ console.log(' ℹ️ 部分文件被跳过(可能正在使用)');
170
+ } else if (status >= 8) {
171
+ throw new Error(`robocopy 失败 (code ${status})`);
172
+ }
173
+ }
153
174
  } else {
154
175
  if (!fs.existsSync(dest)) {
155
176
  fs.mkdirSync(dest, { recursive: true });
@@ -222,11 +243,11 @@ function main() {
222
243
  console.log('🦞 claw-subagent-service - 本地同步\n');
223
244
 
224
245
  const targetDir = getGlobalPackageDir();
225
- const wasRunning = isServiceRunning();
246
+ const wasInstalled = isServiceInstalled();
226
247
 
227
- if (wasRunning) {
228
- console.log('ℹ️ 检测到服务正在运行,先停止服务以释放文件锁\n');
229
- stopService();
248
+ if (wasInstalled) {
249
+ console.log('ℹ️ 检测到服务已安装,先卸载以释放文件锁\n');
250
+ stopAndUninstallService();
230
251
  }
231
252
 
232
253
  syncFiles(targetDir);
@@ -235,12 +256,12 @@ function main() {
235
256
  console.log('\n✅ 同步完成!');
236
257
  console.log(` 目标目录: ${targetDir}`);
237
258
 
238
- if (wasRunning) {
239
- console.log('\n🔄 正在重新启动服务...');
240
- startService();
259
+ if (wasInstalled) {
260
+ console.log('\n🔄 正在重新安装并启动服务...');
261
+ installAndStartService();
241
262
  } else {
242
263
  console.log('\n💡 提示:');
243
- console.log(' 服务未在运行,如需启动请运行:');
264
+ console.log(' 服务未安装,如需安装请运行:');
244
265
  console.log(' claw-subagent-service --install');
245
266
  }
246
267
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * 普通消息处理器 - 接收 rongcloud 转发的普通消息并调用 AI 服务
3
- *
4
- * 被调用位置:rongcloud/message-handler.js 第68行
3
+ *
4
+ * 被调用位置:rongcloud/message-handler.js
5
5
  * 调用方式:await this.handleNormalMessage(msg)
6
- *
6
+ *
7
7
  * @param {Object} msg - 消息对象
8
8
  * @param {string} msg.content - 消息内容
9
9
  * @param {string} msg.senderUserId - 发送者ID
@@ -11,7 +11,9 @@
11
11
  * @param {number} msg.conversationType - 会话类型 (1=私聊, 3=群聊)
12
12
  * @returns {string} 回复内容
13
13
  */
14
- const { forwardChatMessage } = require('./opencode-service');
14
+ const { OpenClawClient } = require('../rongcloud/openclaw-client');
15
+
16
+ const openclawClient = new OpenClawClient(console);
15
17
 
16
18
  async function handleNormalMessage(msg) {
17
19
  console.log(`[NormalMessageHandler] 收到普通消息:`, {
@@ -21,34 +23,14 @@ async function handleNormalMessage(msg) {
21
23
  });
22
24
 
23
25
  try {
24
- // 使用 senderUserId 作为 session ID,确保每个用户有独立的会话
25
- const sessionId = msg.senderUserId || 'default';
26
26
  const content = msg.content;
27
-
28
27
  if (!content || !content.trim()) {
29
28
  return '消息内容为空';
30
29
  }
31
30
 
32
- // 调用 opencode 服务进行 AI 聊天
33
- let fullResponse = '';
34
-
35
- await forwardChatMessage(
36
- sessionId,
37
- content,
38
- async (delta) => {
39
- // 收集响应片段
40
- fullResponse += delta;
41
- },
42
- (level, message) => {
43
- // 日志回调
44
- console.log(`[CHAT-${level}] ${message}`);
45
- },
46
- 600000 // 10分钟超时
47
- );
48
-
49
- console.log(`[NormalMessageHandler] AI 回复: ${fullResponse.substring(0, 50)}...`);
50
- return fullResponse;
51
-
31
+ const reply = await openclawClient.chat(content, msg.senderUserId);
32
+ console.log(`[NormalMessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
33
+ return reply;
52
34
  } catch (err) {
53
35
  console.error(`[NormalMessageHandler] 处理异常:`, err.message);
54
36
  return `抱歉,处理消息时出错: ${err.message}`;
@@ -41,7 +41,13 @@ class MessageHandler {
41
41
  return false;
42
42
  }
43
43
 
44
- const mentions = this.extractMentions(msg.content);
44
+ // claw 控制消息不需要 @mention 过滤
45
+ if (msg.messageType === 'claw') {
46
+ return true;
47
+ }
48
+
49
+ const textContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
50
+ const mentions = this.extractMentions(textContent);
45
51
 
46
52
  if (mentions.length > 0) {
47
53
  if (!mentions.includes(this.nodeId)) {
@@ -49,8 +55,11 @@ class MessageHandler {
49
55
  return false;
50
56
  }
51
57
  this.log?.info(`[MessageHandler] 消息提及本节点(${this.nodeId}),处理`);
58
+ } else if (msg.conversationType === 3) {
59
+ this.log?.info(`[MessageHandler] 群聊消息未 @ 本节点(${this.nodeId}),忽略`);
60
+ return false;
52
61
  } else {
53
- this.log?.info(`[MessageHandler] 消息未指定节点,本节点(${this.nodeId})处理`);
62
+ this.log?.info(`[MessageHandler] 单聊消息未指定节点,本节点(${this.nodeId})处理`);
54
63
  }
55
64
 
56
65
  return true;
@@ -63,12 +72,17 @@ class MessageHandler {
63
72
 
64
73
  try {
65
74
  const type = this.getMessageType(msg);
66
- this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${msg.content.substring(0, 50)}`);
75
+ const logContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
76
+ this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${logContent.substring(0, 50)}`);
67
77
  if (msg.messageType === 'claw') {
68
78
  this.log?.info(`收到龙虾消息,交由 OpenClawClient 处理`);
69
79
  await this.handleClaw(msg);
70
80
  } else {
71
- await this.handleNormalMessage(msg);
81
+ const reply = await this.handleNormalMessage(msg);
82
+ if (reply) {
83
+ const targetId = this.getReplyTarget(msg);
84
+ await this.sendFn(targetId, reply, msg.conversationType);
85
+ }
72
86
  }
73
87
  } catch (err) {
74
88
  this.log?.error(`[MessageHandler] 处理消息异常: ${err.message}`);
@@ -81,7 +95,8 @@ class MessageHandler {
81
95
  if (msg.messageType === 'claw') {
82
96
  return MessageType.CLAW;
83
97
  }
84
- if (msg.content && msg.content.startsWith('/')) {
98
+ const text = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
99
+ if (text.startsWith('/')) {
85
100
  return MessageType.COMMAND;
86
101
  }
87
102
  return MessageType.NORMAL;
@@ -249,53 +249,7 @@ class OpenClawClient {
249
249
 
250
250
  this.log?.info(`[OpenClawClient] 准备发送消息到 OpenClaw,from=${fromUser}, message=${message.substring(0, 50)}`);
251
251
 
252
- // 先检查 OpenClaw Gateway HTTP API 端口是否可用(默认 18789
253
- const httpAvailable = await checkPort(18789);
254
- if (httpAvailable) {
255
- try {
256
- const gatewayToken = getGatewayToken();
257
- const headers = { 'Content-Type': 'application/json' };
258
- if (gatewayToken) {
259
- headers['Authorization'] = `Bearer ${gatewayToken}`;
260
- }
261
-
262
- const response = await axios.post(
263
- 'http://127.0.0.1:18789/v1/chat/completions',
264
- {
265
- model: 'openclaw:main',
266
- messages: [
267
- { role: 'user', content: message }
268
- ],
269
- stream: false
270
- },
271
- {
272
- headers,
273
- timeout: 120000
274
- }
275
- );
276
-
277
- if (response.status === 200 && response.data) {
278
- const result = response.data;
279
- const aiMessage = result.choices?.[0]?.message;
280
- const content = aiMessage?.content;
281
- if (content) {
282
- this.log?.info(`[OpenClawClient] AI 回复: ${content.substring(0, 50)}...`);
283
- return content;
284
- }
285
- this.log?.warn('[OpenClawClient] OpenClaw 返回了空响应');
286
- return 'OpenClaw 未返回有效响应';
287
- } else {
288
- this.log?.error(`[OpenClawClient] OpenClaw 返回错误: ${response.status}`);
289
- return `OpenClaw 返回错误: ${response.status}`;
290
- }
291
- } catch (err) {
292
- this.log?.error(`[OpenClawClient] HTTP 调用失败: ${err.message}`);
293
- }
294
- } else {
295
- this.log?.info('[OpenClawClient] Gateway HTTP API (18789) 未就绪,直接走 CLI');
296
- }
297
-
298
- // fallback 到 CLI 调用
252
+ // 直接走 CLI 调用(OpenClaw Gateway 未暴露兼容的 HTTP REST API)
299
253
  return this.chatViaCLI(message, fromUser);
300
254
  }
301
255
 
@@ -317,12 +271,14 @@ class OpenClawClient {
317
271
  this.log?.info('[OpenClawClient] 已读取到 gateway token');
318
272
  }
319
273
 
320
- const args = ['agent', '-m', escapedMessage, '--session-id', sessionId];
274
+ const quoteArg = (s) => `"${s}"`;
275
+ const cmdParts = ['openclaw', 'agent', '-m', quoteArg(escapedMessage), '--session-id', quoteArg(sessionId)];
321
276
  if (gatewayToken) {
322
- args.push('--token', gatewayToken);
277
+ cmdParts.push('--token', quoteArg(gatewayToken));
323
278
  }
279
+ const command = cmdParts.join(' ');
324
280
 
325
- this.log?.info(`[OpenClawClient] 执行: openclaw ${args.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`);
281
+ this.log?.info(`[OpenClawClient] 执行: ${command}`);
326
282
 
327
283
  return new Promise((resolve) => {
328
284
  let stdout = '';
@@ -331,7 +287,7 @@ class OpenClawClient {
331
287
 
332
288
  // 关键:不设置 OPENCLAW_GATEWAY_URL,避免触发 "gateway url override requires explicit credentials"
333
289
  // 让 openclaw agent 通过默认方式自动发现本地 gateway
334
- const child = spawn('openclaw', args, {
290
+ const child = spawn(command, {
335
291
  shell: true,
336
292
  windowsHide: true,
337
293
  env: getOpenClawEnv(),