claw-subagent-service 0.0.27 → 0.0.29
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 +3 -2
- package/scripts/sync-local.js +248 -0
- package/service/daemon.js +29 -8
- package/service/logger.js +6 -2
- package/service/rongcloud/message-handler.js +0 -3
- package/service/rongcloud/openclaw-client.js +99 -67
- package/service/rongcloud/rongcloud-client.js +23 -0
- package/service/worker.js +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claw-subagent-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"description": "虾说静态服务",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"dev": "nodemon cli.js --run",
|
|
15
15
|
"publish:patch": "npm version patch && npm publish",
|
|
16
16
|
"publish:minor": "npm version minor && npm publish",
|
|
17
|
-
"publish:major": "npm version major && npm publish"
|
|
17
|
+
"publish:major": "npm version major && npm publish",
|
|
18
|
+
"sync": "node scripts/sync-local.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"cli.js",
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sync-local.js - 本地开发快速同步脚本
|
|
5
|
+
*
|
|
6
|
+
* 运行后将当前代码直接替换到全局安装的 claw-subagent-service 目录,
|
|
7
|
+
* 无需发布到 npm 后再重新下载安装。
|
|
8
|
+
*
|
|
9
|
+
* 用法:
|
|
10
|
+
* npm run sync (推荐)
|
|
11
|
+
* node scripts/sync-local.js
|
|
12
|
+
*
|
|
13
|
+
* 注意: Windows 下若服务正在运行,会自动停止服务、同步后再启动。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { execSync, spawn } = require('child_process');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
const isWindows = process.platform === 'win32';
|
|
22
|
+
const SERVICE_NAME = 'claw-subagent-service';
|
|
23
|
+
const PACKAGE_NAME = 'claw-subagent-service';
|
|
24
|
+
const ROOT = path.join(__dirname, '..');
|
|
25
|
+
|
|
26
|
+
// 需要同步的文件/目录(与 package.json files 保持一致)
|
|
27
|
+
const FILES_TO_SYNC = [
|
|
28
|
+
'cli.js',
|
|
29
|
+
'service',
|
|
30
|
+
'scripts',
|
|
31
|
+
'command',
|
|
32
|
+
'version.json',
|
|
33
|
+
'README.md',
|
|
34
|
+
'package.json',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function getGlobalPackageDir() {
|
|
38
|
+
try {
|
|
39
|
+
const globalRoot = execSync('npm root -g', {
|
|
40
|
+
encoding: 'utf-8',
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
}).trim();
|
|
43
|
+
return path.join(globalRoot, PACKAGE_NAME);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error('❌ 无法获取全局 npm 路径:', e.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stopService() {
|
|
51
|
+
if (!isWindows) {
|
|
52
|
+
try {
|
|
53
|
+
execSync('systemctl stop claw-subagent-service 2>/dev/null', {
|
|
54
|
+
stdio: 'ignore',
|
|
55
|
+
timeout: 15000,
|
|
56
|
+
});
|
|
57
|
+
console.log('✅ 服务已停止');
|
|
58
|
+
} catch {}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('🛑 正在停止 Windows 服务...');
|
|
63
|
+
|
|
64
|
+
// 1. 停止服务
|
|
65
|
+
try {
|
|
66
|
+
execSync('net stop "claw-subagent-service" 2>nul', {
|
|
67
|
+
stdio: 'ignore',
|
|
68
|
+
timeout: 35000,
|
|
69
|
+
});
|
|
70
|
+
console.log(' ✅ 服务已停止');
|
|
71
|
+
} catch {}
|
|
72
|
+
|
|
73
|
+
// 2. 杀掉 wrapper 进程
|
|
74
|
+
try {
|
|
75
|
+
execSync('taskkill /f /im "claw-subagent-service.exe" 2>nul', {
|
|
76
|
+
stdio: 'ignore',
|
|
77
|
+
timeout: 10000,
|
|
78
|
+
});
|
|
79
|
+
console.log(' ✅ wrapper 进程已终止');
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
// 3. 杀掉相关 node 进程
|
|
83
|
+
try {
|
|
84
|
+
execSync(
|
|
85
|
+
'powershell -NoProfile -Command "Get-Process node -ErrorAction SilentlyContinue | Where-Object {$_.Path -like \'*claw-subagent*\'} | Stop-Process -Force -ErrorAction SilentlyContinue" 2>nul',
|
|
86
|
+
{ stdio: 'ignore', timeout: 15000 }
|
|
87
|
+
);
|
|
88
|
+
} catch {}
|
|
89
|
+
|
|
90
|
+
// 4. 等待文件锁释放
|
|
91
|
+
console.log(' ⏳ 等待文件锁释放 (5秒)...');
|
|
92
|
+
execSync('ping -n 6 127.0.0.1 >nul', { stdio: 'ignore', windowsHide: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function startService() {
|
|
96
|
+
console.log('🚀 正在启动服务...');
|
|
97
|
+
try {
|
|
98
|
+
if (isWindows) {
|
|
99
|
+
execSync('net start "claw-subagent-service" 2>nul', {
|
|
100
|
+
stdio: 'inherit',
|
|
101
|
+
timeout: 30000,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
execSync('systemctl start claw-subagent-service', {
|
|
106
|
+
stdio: 'inherit',
|
|
107
|
+
timeout: 15000,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
console.log('✅ 服务已启动');
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error('⚠️ 服务启动失败:', e.message);
|
|
113
|
+
console.log(' 请手动运行: claw-subagent-service --install');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isServiceRunning() {
|
|
118
|
+
try {
|
|
119
|
+
if (isWindows) {
|
|
120
|
+
execSync('sc query "claw-subagent-service" | findstr RUNNING >nul', {
|
|
121
|
+
stdio: 'ignore',
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
} else {
|
|
126
|
+
execSync('systemctl is-active --quiet claw-subagent-service', {
|
|
127
|
+
stdio: 'ignore',
|
|
128
|
+
});
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function copyDirSync(src, dest) {
|
|
137
|
+
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
|
+
}
|
|
148
|
+
}
|
|
149
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
150
|
+
|
|
151
|
+
const cmd = `robocopy "${src}" "${dest}" /E /NJH /NJS /NDL /NC /NS /NFL`;
|
|
152
|
+
execSync(cmd, { encoding: 'utf-8', windowsHide: true });
|
|
153
|
+
} else {
|
|
154
|
+
if (!fs.existsSync(dest)) {
|
|
155
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
158
|
+
const s = path.join(src, entry.name);
|
|
159
|
+
const d = path.join(dest, entry.name);
|
|
160
|
+
if (entry.isDirectory()) {
|
|
161
|
+
copyDirSync(s, d);
|
|
162
|
+
} else {
|
|
163
|
+
fs.copyFileSync(s, d);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function syncFiles(targetDir) {
|
|
170
|
+
console.log(`📦 同步到: ${targetDir}\n`);
|
|
171
|
+
|
|
172
|
+
for (const file of FILES_TO_SYNC) {
|
|
173
|
+
const src = path.join(ROOT, file);
|
|
174
|
+
const dest = path.join(targetDir, file);
|
|
175
|
+
|
|
176
|
+
if (!fs.existsSync(src)) {
|
|
177
|
+
console.log(` ⚠️ 跳过: ${file} (不存在)`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const stat = fs.statSync(src);
|
|
183
|
+
if (stat.isDirectory()) {
|
|
184
|
+
copyDirSync(src, dest);
|
|
185
|
+
} else {
|
|
186
|
+
fs.copyFileSync(src, dest);
|
|
187
|
+
}
|
|
188
|
+
console.log(` ✅ ${file}`);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error(` ❌ ${file}: ${e.message}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function installDepsIfNeeded(targetDir) {
|
|
197
|
+
const nodeModulesPath = path.join(targetDir, 'node_modules');
|
|
198
|
+
const hasNodeModules = fs.existsSync(nodeModulesPath) && fs.readdirSync(nodeModulesPath).length > 0;
|
|
199
|
+
|
|
200
|
+
if (hasNodeModules) {
|
|
201
|
+
console.log('\n📦 依赖已存在,跳过安装');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log('\n📦 安装依赖...');
|
|
206
|
+
try {
|
|
207
|
+
execSync('npm install --ignore-scripts --prefer-offline --no-audit --no-fund --progress=false', {
|
|
208
|
+
cwd: targetDir,
|
|
209
|
+
stdio: 'inherit',
|
|
210
|
+
encoding: 'utf-8',
|
|
211
|
+
windowsHide: true,
|
|
212
|
+
timeout: 300000,
|
|
213
|
+
});
|
|
214
|
+
console.log('✅ 依赖安装完成');
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error('⚠️ 依赖安装失败,请手动运行:');
|
|
217
|
+
console.error(` cd "${targetDir}" && npm install`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function main() {
|
|
222
|
+
console.log('🦞 claw-subagent-service - 本地同步\n');
|
|
223
|
+
|
|
224
|
+
const targetDir = getGlobalPackageDir();
|
|
225
|
+
const wasRunning = isServiceRunning();
|
|
226
|
+
|
|
227
|
+
if (wasRunning) {
|
|
228
|
+
console.log('ℹ️ 检测到服务正在运行,先停止服务以释放文件锁\n');
|
|
229
|
+
stopService();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
syncFiles(targetDir);
|
|
233
|
+
installDepsIfNeeded(targetDir);
|
|
234
|
+
|
|
235
|
+
console.log('\n✅ 同步完成!');
|
|
236
|
+
console.log(` 目标目录: ${targetDir}`);
|
|
237
|
+
|
|
238
|
+
if (wasRunning) {
|
|
239
|
+
console.log('\n🔄 正在重新启动服务...');
|
|
240
|
+
startService();
|
|
241
|
+
} else {
|
|
242
|
+
console.log('\n💡 提示:');
|
|
243
|
+
console.log(' 服务未在运行,如需启动请运行:');
|
|
244
|
+
console.log(' claw-subagent-service --install');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
main();
|
package/service/daemon.js
CHANGED
|
@@ -10,9 +10,16 @@ const log = createLogger('daemon');
|
|
|
10
10
|
const WORKER_PATH = path.join(__dirname, 'worker.js');
|
|
11
11
|
const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 28765;
|
|
12
12
|
// 使用全局 PID 文件,防止不同安装路径的多个实例同时运行
|
|
13
|
-
const PID_FILE =
|
|
13
|
+
const PID_FILE = (() => {
|
|
14
|
+
if (process.platform === 'win32') {
|
|
15
|
+
const programData = process.env.PROGRAMDATA || process.env.ALLUSERSPROFILE || 'C:\\ProgramData';
|
|
16
|
+
return path.join(programData, 'claw-subagent-service', 'daemon.pid');
|
|
17
|
+
}
|
|
18
|
+
return path.join(os.tmpdir(), '.claw-subagent-service.pid');
|
|
19
|
+
})();
|
|
14
20
|
|
|
15
21
|
let worker = null;
|
|
22
|
+
let currentWorkerPid = null;
|
|
16
23
|
let stopping = false;
|
|
17
24
|
let isRollingBack = false;
|
|
18
25
|
let healthTimer = null;
|
|
@@ -36,7 +43,12 @@ function checkSingleton() {
|
|
|
36
43
|
process.kill(pid, 0);
|
|
37
44
|
log.error(`[DAEMON] 另一个 Daemon 实例已在运行 (PID: ${pid}),当前实例退出`);
|
|
38
45
|
return false;
|
|
39
|
-
} catch {
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.code === 'EPERM') {
|
|
48
|
+
// 进程存在但无权限发送信号,视为仍在运行
|
|
49
|
+
log.error(`[DAEMON] 另一个 Daemon 实例已在运行 (PID: ${pid}, 权限不足),当前实例退出`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
40
52
|
// 进程不存在,继续启动
|
|
41
53
|
}
|
|
42
54
|
}
|
|
@@ -46,6 +58,10 @@ function checkSingleton() {
|
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
try {
|
|
61
|
+
const pidDir = path.dirname(PID_FILE);
|
|
62
|
+
if (!fs.existsSync(pidDir)) {
|
|
63
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
64
|
+
}
|
|
49
65
|
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
50
66
|
} catch (err) {
|
|
51
67
|
log.warn(`[DAEMON] 写入 PID 文件失败: ${err.message}`);
|
|
@@ -92,7 +108,7 @@ function freePortIfNeeded(port) {
|
|
|
92
108
|
const localAddr = parts[1] || '';
|
|
93
109
|
if (localAddr.endsWith(`:${port}`) || localAddr.endsWith(`]:${port}`)) {
|
|
94
110
|
const pid = parseInt(parts[parts.length - 1], 10);
|
|
95
|
-
if (pid && pid > 0) {
|
|
111
|
+
if (pid && pid > 0 && pid !== currentWorkerPid && pid !== process.pid) {
|
|
96
112
|
log.warn(`[DAEMON] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
|
|
97
113
|
try {
|
|
98
114
|
execSync(`taskkill /F /PID ${pid}`, { timeout: 5000, windowsHide: true });
|
|
@@ -126,7 +142,7 @@ function getRestartDelay() {
|
|
|
126
142
|
crashCount++;
|
|
127
143
|
// 指数退避: 1s, 2s, 4s, 8s, 16s, 32s, 60s(封顶)
|
|
128
144
|
const delay = Math.min(1000 * Math.pow(2, crashCount - 1), 60000);
|
|
129
|
-
log.info(`[DAEMON] Worker 连续崩溃 ${crashCount} 次,等待 ${delay/1000}s 后重启`);
|
|
145
|
+
log.info(`[DAEMON] Worker 连续崩溃 ${crashCount} 次,等待 ${delay / 1000}s 后重启`);
|
|
130
146
|
return delay;
|
|
131
147
|
}
|
|
132
148
|
|
|
@@ -146,9 +162,13 @@ function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
|
|
|
146
162
|
};
|
|
147
163
|
|
|
148
164
|
worker = fork(WORKER_PATH, [], forkOptions);
|
|
165
|
+
currentWorkerPid = worker.pid || null;
|
|
149
166
|
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
// 只有 pipe 模式才需要手动转发到 logger
|
|
168
|
+
if (forkOptions.stdio !== 'inherit') {
|
|
169
|
+
worker.stdout?.on('data', d => log.info(`[WORKER] ${d.toString().trim()}`));
|
|
170
|
+
worker.stderr?.on('data', d => log.error(`[WORKER] ${d.toString().trim()}`));
|
|
171
|
+
}
|
|
152
172
|
|
|
153
173
|
if (isAfterUpdate && backupDirForRollback) {
|
|
154
174
|
currentBackupDir = backupDirForRollback;
|
|
@@ -166,6 +186,7 @@ function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
|
|
|
166
186
|
|
|
167
187
|
log.error(`[DAEMON] Worker 退出,code=${code}, signal=${signal}`);
|
|
168
188
|
worker = null;
|
|
189
|
+
currentWorkerPid = null;
|
|
169
190
|
|
|
170
191
|
if (isAfterUpdate && code !== 0 && currentBackupDir && !isRollingBack) {
|
|
171
192
|
isRollingBack = true;
|
|
@@ -186,8 +207,8 @@ function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
|
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
if (!stopping && !isRollingBack) {
|
|
189
|
-
//
|
|
190
|
-
|
|
210
|
+
// 使用指数退避延迟后重启;端口释放交由 startWorker 中的 freePortIfNeeded 处理,
|
|
211
|
+
// 避免此处误杀刚刚由其他实例启动的 worker
|
|
191
212
|
const delay = getRestartDelay();
|
|
192
213
|
setTimeout(() => startWorker(false, null), delay);
|
|
193
214
|
}
|
package/service/logger.js
CHANGED
|
@@ -13,12 +13,16 @@ function createLogger(name) {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const write = (level, msg) => {
|
|
16
|
-
const line = `[${new Date().toISOString()}] [${level}] ${msg}
|
|
16
|
+
const line = `[${new Date().toISOString()}] [${level}] ${msg}`;
|
|
17
17
|
try {
|
|
18
|
-
fs.appendFileSync(getFile(), line);
|
|
18
|
+
fs.appendFileSync(getFile(), line + '\n');
|
|
19
19
|
} catch (e) {
|
|
20
20
|
// ignore write errors
|
|
21
21
|
}
|
|
22
|
+
if (process.stdout && process.stdout.isTTY) {
|
|
23
|
+
const fn = level === 'ERROR' ? 'error' : level === 'WARN' ? 'warn' : 'log';
|
|
24
|
+
console[fn](line);
|
|
25
|
+
}
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
return {
|
|
@@ -122,9 +122,6 @@ class MessageHandler {
|
|
|
122
122
|
this.sendReadReceiptFn(msg).catch(() => {});
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// 先回复"正在处理",让用户知道消息已被接收
|
|
126
|
-
this.sendFn(targetId, '🤖 正在思考中,请稍候...', msg.conversationType).catch(() => {});
|
|
127
|
-
|
|
128
125
|
// 后台执行 openclaw,不阻塞消息队列
|
|
129
126
|
this.openclawClient.chat(msg.content, msg.senderUserId)
|
|
130
127
|
.then(reply => {
|
|
@@ -93,6 +93,38 @@ function getOpenClawEnv(baseEnv = process.env) {
|
|
|
93
93
|
return env;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 尝试读取 OpenClaw 的 gateway token
|
|
98
|
+
* openclaw agent 连接 gateway 需要认证,token 通常存储在配置文件中
|
|
99
|
+
*/
|
|
100
|
+
function getGatewayToken() {
|
|
101
|
+
const homeDir = getRealHomeDir();
|
|
102
|
+
const possibleFiles = [
|
|
103
|
+
path.join(homeDir, '.openclaw', 'openclaw.json'),
|
|
104
|
+
path.join(homeDir, '.openclaw', 'config.json'),
|
|
105
|
+
path.join(homeDir, '.openclaw', 'tools.json'),
|
|
106
|
+
path.join(homeDir, '.openclaw', 'settings.json'),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const filePath of possibleFiles) {
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(filePath)) {
|
|
112
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
113
|
+
const config = JSON.parse(content);
|
|
114
|
+
// 可能的 token 字段名
|
|
115
|
+
const token = config.gatewayToken || config.token || config.apiKey || config.api_key || config.password;
|
|
116
|
+
if (token) {
|
|
117
|
+
return String(token);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// 忽略读取/解析错误
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
/**
|
|
97
129
|
* 检测端口是否监听
|
|
98
130
|
*/
|
|
@@ -139,10 +171,9 @@ function startOpenClawGateway(log) {
|
|
|
139
171
|
const interval = setInterval(async () => {
|
|
140
172
|
attempts++;
|
|
141
173
|
const gatewayRunning = await checkPort(18789);
|
|
142
|
-
const apiRunning = await checkPort(5678);
|
|
143
174
|
if (gatewayRunning) {
|
|
144
175
|
clearInterval(interval);
|
|
145
|
-
log?.info(
|
|
176
|
+
log?.info('[OpenClawClient] OpenClaw gateway 启动成功 (18789)');
|
|
146
177
|
resolve(true);
|
|
147
178
|
} else if (attempts >= maxAttempts) {
|
|
148
179
|
clearInterval(interval);
|
|
@@ -173,30 +204,13 @@ class OpenClawClient {
|
|
|
173
204
|
if (this.gatewayStarted) return true;
|
|
174
205
|
|
|
175
206
|
const gatewayRunning = await checkPort(18789);
|
|
176
|
-
const apiRunning = await checkPort(5678);
|
|
177
207
|
|
|
178
|
-
if (gatewayRunning
|
|
179
|
-
this.log?.info('[OpenClawClient] OpenClaw gateway
|
|
208
|
+
if (gatewayRunning) {
|
|
209
|
+
this.log?.info('[OpenClawClient] OpenClaw gateway 已在运行 (18789)');
|
|
180
210
|
this.gatewayStarted = true;
|
|
181
211
|
return true;
|
|
182
212
|
}
|
|
183
213
|
|
|
184
|
-
if (gatewayRunning && !apiRunning) {
|
|
185
|
-
this.log?.info('[OpenClawClient] gateway 已运行 (18789),但 API (5678) 未就绪,继续等待...');
|
|
186
|
-
// 等待最多 10 秒让 API 就绪
|
|
187
|
-
for (let i = 0; i < 10; i++) {
|
|
188
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
189
|
-
if (await checkPort(5678)) {
|
|
190
|
-
this.log?.info('[OpenClawClient] API (5678) 已就绪');
|
|
191
|
-
this.gatewayStarted = true;
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
this.log?.warn('[OpenClawClient] API (5678) 等待超时,gateway 可能未完全初始化');
|
|
196
|
-
// gateway 在运行但 API 没好,仍然允许继续,让 HTTP fallback 到 CLI
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
214
|
// 避免并发启动
|
|
201
215
|
if (this.gatewayStarting) {
|
|
202
216
|
this.log?.info('[OpenClawClient] gateway 正在启动中,等待...');
|
|
@@ -235,44 +249,54 @@ class OpenClawClient {
|
|
|
235
249
|
|
|
236
250
|
this.log?.info(`[OpenClawClient] 准备发送消息到 OpenClaw,from=${fromUser}, message=${message.substring(0, 50)}`);
|
|
237
251
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
{ role: 'user', content: message }
|
|
247
|
-
],
|
|
248
|
-
stream: false
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
headers: { 'Content-Type': 'application/json' },
|
|
252
|
-
timeout: 120000
|
|
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}`;
|
|
253
260
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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}`;
|
|
263
290
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} else {
|
|
267
|
-
this.log?.error(`[OpenClawClient] OpenClaw 返回错误: ${response.status}`);
|
|
268
|
-
return `OpenClaw 返回错误: ${response.status}`;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
this.log?.error(`[OpenClawClient] HTTP 调用失败: ${err.message}`);
|
|
269
293
|
}
|
|
270
|
-
}
|
|
271
|
-
this.log?.
|
|
272
|
-
// fallback 到 CLI 调用
|
|
273
|
-
this.log?.info('[OpenClawClient] HTTP 失败,尝试 CLI fallback');
|
|
274
|
-
return this.chatViaCLI(message, fromUser);
|
|
294
|
+
} else {
|
|
295
|
+
this.log?.info('[OpenClawClient] Gateway HTTP API (18789) 未就绪,直接走 CLI');
|
|
275
296
|
}
|
|
297
|
+
|
|
298
|
+
// fallback 到 CLI 调用
|
|
299
|
+
return this.chatViaCLI(message, fromUser);
|
|
276
300
|
}
|
|
277
301
|
|
|
278
302
|
async chatViaCLI(message, fromUser) {
|
|
@@ -284,25 +308,33 @@ class OpenClawClient {
|
|
|
284
308
|
.replace(/\n/g, ' ')
|
|
285
309
|
.replace(/\r/g, ' ');
|
|
286
310
|
|
|
287
|
-
const gatewayUrl = 'http://127.0.0.1:5678';
|
|
288
311
|
const realHome = getRealHomeDir();
|
|
289
312
|
this.log?.info(`[OpenClawClient] 使用用户目录: ${realHome}`);
|
|
290
313
|
|
|
314
|
+
// 尝试读取 gateway token,解决 SYSTEM 账户下认证问题
|
|
315
|
+
const gatewayToken = getGatewayToken();
|
|
316
|
+
if (gatewayToken) {
|
|
317
|
+
this.log?.info('[OpenClawClient] 已读取到 gateway token');
|
|
318
|
+
}
|
|
319
|
+
|
|
291
320
|
const args = ['agent', '-m', escapedMessage, '--session-id', sessionId];
|
|
292
|
-
|
|
321
|
+
if (gatewayToken) {
|
|
322
|
+
args.push('--token', gatewayToken);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.log?.info(`[OpenClawClient] 执行: openclaw ${args.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`);
|
|
293
326
|
|
|
294
327
|
return new Promise((resolve) => {
|
|
295
328
|
let stdout = '';
|
|
296
329
|
let stderr = '';
|
|
297
330
|
let killed = false;
|
|
298
331
|
|
|
332
|
+
// 关键:不设置 OPENCLAW_GATEWAY_URL,避免触发 "gateway url override requires explicit credentials"
|
|
333
|
+
// 让 openclaw agent 通过默认方式自动发现本地 gateway
|
|
299
334
|
const child = spawn('openclaw', args, {
|
|
300
335
|
shell: true,
|
|
301
336
|
windowsHide: true,
|
|
302
|
-
env:
|
|
303
|
-
...getOpenClawEnv(),
|
|
304
|
-
OPENCLAW_GATEWAY_URL: gatewayUrl,
|
|
305
|
-
},
|
|
337
|
+
env: getOpenClawEnv(),
|
|
306
338
|
});
|
|
307
339
|
|
|
308
340
|
this.log?.info(`[OpenClawClient] CLI 子进程 PID=${child.pid}`);
|
|
@@ -314,15 +346,15 @@ class OpenClawClient {
|
|
|
314
346
|
const text = chunk.toString();
|
|
315
347
|
stderr += text;
|
|
316
348
|
// 实时记录 stderr,方便调试卡死问题
|
|
317
|
-
this.log?.info(`[OpenClawClient] CLI stderr: ${text.trim().substring(0,
|
|
349
|
+
this.log?.info(`[OpenClawClient] CLI stderr: ${text.trim().substring(0, 300)}`);
|
|
318
350
|
});
|
|
319
351
|
|
|
320
|
-
// 超时兜底(
|
|
352
|
+
// 超时兜底(30 分钟)
|
|
321
353
|
const timeout = setTimeout(() => {
|
|
322
354
|
killed = true;
|
|
323
355
|
child.kill('SIGTERM');
|
|
324
|
-
this.log?.error('[OpenClawClient] CLI 执行超时(
|
|
325
|
-
},
|
|
356
|
+
this.log?.error('[OpenClawClient] CLI 执行超时(30分钟),强制终止');
|
|
357
|
+
}, 1800000);
|
|
326
358
|
|
|
327
359
|
child.on('error', (err) => {
|
|
328
360
|
clearTimeout(timeout);
|
|
@@ -338,7 +370,7 @@ class OpenClawClient {
|
|
|
338
370
|
clearTimeout(timeout);
|
|
339
371
|
|
|
340
372
|
if (killed) {
|
|
341
|
-
resolve('OpenClaw
|
|
373
|
+
resolve('OpenClaw 响应超时(30分钟),请检查 openclaw 服务状态');
|
|
342
374
|
return;
|
|
343
375
|
}
|
|
344
376
|
|
|
@@ -348,7 +380,7 @@ class OpenClawClient {
|
|
|
348
380
|
if (code !== 0) {
|
|
349
381
|
const errOutput = stderr || stdout || '';
|
|
350
382
|
this.log?.error(`[OpenClawClient] CLI 错误输出: ${errOutput.substring(0, 500)}`);
|
|
351
|
-
resolve(`OpenClaw
|
|
383
|
+
resolve(`OpenClaw 调用失败: ${errOutput.substring(0, 200)}`);
|
|
352
384
|
return;
|
|
353
385
|
}
|
|
354
386
|
|
|
@@ -20,6 +20,8 @@ class RongCloudClient {
|
|
|
20
20
|
this.isConnected = false;
|
|
21
21
|
this.handler = null;
|
|
22
22
|
this.processingQueue = Promise.resolve();
|
|
23
|
+
this.processedMessageUIds = new Set();
|
|
24
|
+
this.messageDedupMaxSize = 1000;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
async connect(handler) {
|
|
@@ -98,10 +100,31 @@ class RongCloudClient {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
handleReceivedMessage(message) {
|
|
103
|
+
// 1. 过滤离线消息
|
|
101
104
|
if (message.isOffLineMessage) {
|
|
102
105
|
return;
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
// 2. 过滤自己发送的消息(融云 SDK 可能将发送消息回传)
|
|
109
|
+
// messageDirection: 1=发送, 2=接收
|
|
110
|
+
if (message.messageDirection === 1) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (message.senderUserId === this.config.accountId) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 3. 消息去重:防止同一条消息被多次触发(融云重推或多端同步)
|
|
118
|
+
const dedupKey = message.messageUId || `${message.senderUserId}-${message.sentTime}-${message.messageType}`;
|
|
119
|
+
if (this.processedMessageUIds.has(dedupKey)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.processedMessageUIds.add(dedupKey);
|
|
123
|
+
if (this.processedMessageUIds.size > this.messageDedupMaxSize) {
|
|
124
|
+
const first = this.processedMessageUIds.values().next().value;
|
|
125
|
+
this.processedMessageUIds.delete(first);
|
|
126
|
+
}
|
|
127
|
+
|
|
105
128
|
try {
|
|
106
129
|
const msgType = message.messageType;
|
|
107
130
|
let rawContent = message.content;
|
package/service/worker.js
CHANGED
|
@@ -15,6 +15,23 @@ const { startOpencodeService, stopOpencodeService } = require('./modules/opencod
|
|
|
15
15
|
const log = createLogger('worker');
|
|
16
16
|
const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 28765;
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
// 捕获所有异常,强制打印到控制台(绕过 logger)
|
|
20
|
+
process.on('uncaughtException', (err) => {
|
|
21
|
+
console.error('[WORKER_FATAL] 未捕获异常:', err);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
26
|
+
console.error('[WORKER_FATAL] 未捕获Promise:', reason);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 如果 logger 初始化后,也同步写到 logger
|
|
30
|
+
const originalStderr = process.stderr.write.bind(process.stderr);
|
|
31
|
+
process.stderr.write = (chunk, encoding, callback) => {
|
|
32
|
+
originalStderr(chunk, encoding, callback);
|
|
33
|
+
};
|
|
34
|
+
|
|
18
35
|
/**
|
|
19
36
|
* 查找占用指定端口的进程 PID
|
|
20
37
|
* Windows 下检查本地地址(第二列)是否匹配目标端口,不限于 LISTENING 状态
|