claw-subagent-service 0.0.133 → 0.0.135
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/command/linux/start.sh
CHANGED
|
@@ -246,14 +246,53 @@ start_docker() {
|
|
|
246
246
|
cat /tmp/openclaw-run-help.txt | head -30
|
|
247
247
|
fi
|
|
248
248
|
|
|
249
|
+
# 先停止所有已有的 openclaw 进程,避免端口冲突
|
|
250
|
+
log_info "检查并停止已有的 openclaw 进程..."
|
|
251
|
+
local existing_pids=""
|
|
252
|
+
if command -v pgrep &>/dev/null; then
|
|
253
|
+
existing_pids=$(pgrep -f "openclaw" | tr '\n' ' ')
|
|
254
|
+
elif command -v pidof &>/dev/null; then
|
|
255
|
+
existing_pids=$(pidof openclaw)
|
|
256
|
+
else
|
|
257
|
+
existing_pids=$(ps aux | grep -v grep | grep "openclaw" | awk '{print $2}' | tr '\n' ' ')
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
if [ -n "$existing_pids" ]; then
|
|
261
|
+
log_warn "发现已有 openclaw 进程: $existing_pids,先停止它们..."
|
|
262
|
+
for ep in $existing_pids; do
|
|
263
|
+
log_info "停止进程 $ep..."
|
|
264
|
+
kill -15 "$ep" 2>/dev/null || true
|
|
265
|
+
done
|
|
266
|
+
# 等待 3 秒让进程优雅退出
|
|
267
|
+
sleep 3
|
|
268
|
+
# 检查是否还有残留进程,如果有则强制停止
|
|
269
|
+
local remaining_pids=""
|
|
270
|
+
if command -v pgrep &>/dev/null; then
|
|
271
|
+
remaining_pids=$(pgrep -f "openclaw" | tr '\n' ' ')
|
|
272
|
+
fi
|
|
273
|
+
if [ -n "$remaining_pids" ]; then
|
|
274
|
+
log_warn "强制停止残留进程: $remaining_pids"
|
|
275
|
+
for rp in $remaining_pids; do
|
|
276
|
+
kill -9 "$rp" 2>/dev/null || true
|
|
277
|
+
done
|
|
278
|
+
sleep 1
|
|
279
|
+
fi
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# 确保端口未被占用
|
|
283
|
+
if command -v fuser &>/dev/null; then
|
|
284
|
+
fuser -k "${PORT}/tcp" 2>/dev/null || true
|
|
285
|
+
fi
|
|
286
|
+
|
|
249
287
|
# 尝试使用正确的参数启动
|
|
250
288
|
# 注意:openclaw gateway 可能使用不同的参数名
|
|
251
|
-
# 在 Docker 环境中,使用 run 子命令前台运行,而不是后台启动
|
|
252
289
|
log_info "尝试启动: openclaw gateway run --port $PORT"
|
|
290
|
+
|
|
291
|
+
# 使用 nohup 后台启动,将输出重定向到日志文件
|
|
253
292
|
nohup openclaw gateway run --port "$PORT" > "$log_file" 2>&1 &
|
|
254
293
|
|
|
255
|
-
# 等待
|
|
256
|
-
sleep
|
|
294
|
+
# 等待 15 秒检查进程是否启动(给更多时间初始化)
|
|
295
|
+
sleep 15
|
|
257
296
|
local started_pid=$!
|
|
258
297
|
log_info "启动的进程 PID: $started_pid"
|
|
259
298
|
|
|
@@ -272,7 +311,7 @@ start_docker() {
|
|
|
272
311
|
|
|
273
312
|
# 检查进程监听的端口
|
|
274
313
|
log_info "检查进程监听的端口..."
|
|
275
|
-
sleep
|
|
314
|
+
sleep 5
|
|
276
315
|
local listening_ports=$(netstat -tlnp 2>/dev/null | grep "$started_pid" || ss -tlnp 2>/dev/null | grep "$started_pid" || echo "")
|
|
277
316
|
if [ -n "$listening_ports" ]; then
|
|
278
317
|
log_info "进程 $started_pid 监听的端口:"
|
package/command/linux/stop.sh
CHANGED
|
@@ -460,6 +460,12 @@ show_help() {
|
|
|
460
460
|
main() {
|
|
461
461
|
local force=""
|
|
462
462
|
|
|
463
|
+
# 检查 FORCE 环境变量(来自 script-executor 的强制停止模式)
|
|
464
|
+
if [ "${FORCE:-}" = "1" ]; then
|
|
465
|
+
force="1"
|
|
466
|
+
log_info "检测到 FORCE=1 环境变量,启用强制停止模式"
|
|
467
|
+
fi
|
|
468
|
+
|
|
463
469
|
# 解析命令行参数
|
|
464
470
|
while [ $# -gt 0 ]; do
|
|
465
471
|
case $1 in
|
package/package.json
CHANGED
package/service/daemon.js
CHANGED
|
@@ -28,6 +28,31 @@ let crashCount = 0;
|
|
|
28
28
|
let lastCrashTime = 0;
|
|
29
29
|
const updater = new Updater();
|
|
30
30
|
|
|
31
|
+
// 检测是否是 npm 更新后的重启
|
|
32
|
+
const VERSION_FILE = path.join(os.tmpdir(), '.claw-subagent-service.version');
|
|
33
|
+
function detectUpdateRestart() {
|
|
34
|
+
try {
|
|
35
|
+
const currentVersion = updater.loadCurrentVersion();
|
|
36
|
+
let previousVersion = null;
|
|
37
|
+
|
|
38
|
+
if (fs.existsSync(VERSION_FILE)) {
|
|
39
|
+
previousVersion = fs.readFileSync(VERSION_FILE, 'utf8').trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 写入当前版本
|
|
43
|
+
fs.writeFileSync(VERSION_FILE, currentVersion);
|
|
44
|
+
|
|
45
|
+
// 如果之前有版本记录,且当前版本不同,说明是更新后的重启
|
|
46
|
+
if (previousVersion && previousVersion !== currentVersion) {
|
|
47
|
+
log.info(`[DAEMON] 检测到版本变化: ${previousVersion} → ${currentVersion},这是更新后的重启`);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.warn(`[DAEMON] 检测版本变化失败: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
process.chdir(__dirname);
|
|
32
57
|
|
|
33
58
|
/**
|
|
@@ -359,7 +384,14 @@ process.on('message', (msg) => {
|
|
|
359
384
|
if (msg === 'shutdown') gracefulShutdown();
|
|
360
385
|
});
|
|
361
386
|
|
|
362
|
-
|
|
387
|
+
// 检测是否是 npm 更新后的重启
|
|
388
|
+
const isAfterNpmUpdate = detectUpdateRestart();
|
|
389
|
+
if (isAfterNpmUpdate) {
|
|
390
|
+
log.info('[DAEMON] npm 更新后的重启,启动 Worker 并设置健康观察...');
|
|
391
|
+
startWorker(true, null);
|
|
392
|
+
} else {
|
|
393
|
+
startWorker(false, null);
|
|
394
|
+
}
|
|
363
395
|
updater.startSchedule(restartWorkerWithUpdate);
|
|
364
396
|
|
|
365
397
|
process.on('uncaughtException', (err) => {
|
|
@@ -112,14 +112,16 @@ async function manageWithServiceManager(command) {
|
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* 使用 ScriptExecutor 执行脚本(Docker 模式)
|
|
115
|
+
* @param {number} command - 命令代码
|
|
116
|
+
* @param {boolean} force - 是否强制停止
|
|
115
117
|
* @returns {Object} { result, output }
|
|
116
118
|
*/
|
|
117
|
-
async function executeWithScript(command) {
|
|
119
|
+
async function executeWithScript(command, force = false) {
|
|
118
120
|
const executor = getExecutor();
|
|
119
121
|
const scriptName = getScriptName(command);
|
|
120
122
|
|
|
121
123
|
try {
|
|
122
|
-
const result = await executor.executeWithStatus(command, scriptName);
|
|
124
|
+
const result = await executor.executeWithStatus(command, scriptName, force);
|
|
123
125
|
// ScriptExecutor 现在返回 { status, message, output }
|
|
124
126
|
return {
|
|
125
127
|
result: { status: result.status, message: result.message },
|
|
@@ -200,10 +202,11 @@ async function verifyCommandResult(command, result, scriptOutput = '') {
|
|
|
200
202
|
return result;
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
async function executeCommand(command, window, sendResponse) {
|
|
205
|
+
async function executeCommand(command, window, sendResponse, force = false) {
|
|
204
206
|
const cmdName = getCommandName(command);
|
|
205
|
-
|
|
206
|
-
console.log(`[OpenClawControl]
|
|
207
|
+
const actionName = force ? '强制' + cmdName : cmdName;
|
|
208
|
+
console.log(`[OpenClawControl] ====== 开始执行 ${actionName} 命令 ======`);
|
|
209
|
+
console.log(`[OpenClawControl] 命令代码: ${command}, force: ${force}`);
|
|
207
210
|
console.log(`[OpenClawControl] 平台: ${process.platform}`);
|
|
208
211
|
|
|
209
212
|
let result;
|
|
@@ -223,7 +226,7 @@ async function executeCommand(command, window, sendResponse) {
|
|
|
223
226
|
let scriptOutput = '';
|
|
224
227
|
if (!result) {
|
|
225
228
|
console.log(`[OpenClawControl] 使用脚本方式执行...`);
|
|
226
|
-
const scriptResult = await executeWithScript(command);
|
|
229
|
+
const scriptResult = await executeWithScript(command, force);
|
|
227
230
|
result = scriptResult.result;
|
|
228
231
|
scriptOutput = scriptResult.output;
|
|
229
232
|
console.log(`[OpenClawControl] 脚本执行结果: ${result.status} - ${result.message}`);
|
|
@@ -242,13 +245,13 @@ async function executeCommand(command, window, sendResponse) {
|
|
|
242
245
|
result.status === OpenClawServiceStatus.STOP_SUCCESS ||
|
|
243
246
|
result.status === OpenClawServiceStatus.RESTART_SUCCESS ||
|
|
244
247
|
result.status === OpenClawServiceStatus.CONFIG_FIX_SUCCESS) {
|
|
245
|
-
console.log(`[OpenClawControl] ${
|
|
248
|
+
console.log(`[OpenClawControl] ${actionName} 成功: ${result.message}`);
|
|
246
249
|
} else if (result.status === OpenClawServiceStatus.ERROR) {
|
|
247
|
-
console.log(`[OpenClawControl] ${
|
|
250
|
+
console.log(`[OpenClawControl] ${actionName} 失败: ${result.message}`);
|
|
248
251
|
} else if (result.status === OpenClawServiceStatus.NOT_INSTALL) {
|
|
249
|
-
console.log(`[OpenClawControl] ${
|
|
252
|
+
console.log(`[OpenClawControl] ${actionName}: OpenClaw未安装`);
|
|
250
253
|
} else {
|
|
251
|
-
console.log(`[OpenClawControl] ${
|
|
254
|
+
console.log(`[OpenClawControl] ${actionName} 状态: ${result.message}`);
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
if (sendResponse) {
|
|
@@ -264,16 +267,19 @@ async function executeCommand(command, window, sendResponse) {
|
|
|
264
267
|
const { getOpenClawStatus } = require('./port-checker');
|
|
265
268
|
const portStatus = await getOpenClawStatus(18789);
|
|
266
269
|
console.log(`[OpenClawControl] 端口状态: ${portStatus}`);
|
|
267
|
-
// 更新真实状态
|
|
270
|
+
// 更新真实状态 - 始终以端口检测结果为准
|
|
268
271
|
if (command === OpenClawCommandEnum.STOP) {
|
|
269
272
|
realStatus = portStatus === 1 ? OpenClawServiceStatus.RUNNING : OpenClawServiceStatus.STOP_SUCCESS;
|
|
270
273
|
realMessage = portStatus === 1 ? '停止失败: 服务仍在运行' : '服务已停止';
|
|
274
|
+
if (portStatus === 1) httpStatus = 'error';
|
|
271
275
|
} else if (command === OpenClawCommandEnum.START) {
|
|
272
276
|
realStatus = portStatus === 1 ? OpenClawServiceStatus.START_SUCCESS : OpenClawServiceStatus.ERROR;
|
|
273
277
|
realMessage = portStatus === 1 ? '服务已启动' : '启动失败: 服务未运行';
|
|
278
|
+
if (portStatus !== 1) httpStatus = 'error';
|
|
274
279
|
} else if (command === OpenClawCommandEnum.RESTART) {
|
|
275
280
|
realStatus = portStatus === 1 ? OpenClawServiceStatus.RESTART_SUCCESS : OpenClawServiceStatus.ERROR;
|
|
276
281
|
realMessage = portStatus === 1 ? '服务已重启' : '重启失败: 服务未运行';
|
|
282
|
+
if (portStatus !== 1) httpStatus = 'error';
|
|
277
283
|
}
|
|
278
284
|
console.log(`[OpenClawControl] 操作后真实状态: portStatus=${portStatus}, realStatus=${realStatus}, message=${realMessage}`);
|
|
279
285
|
} catch (e) {
|
|
@@ -64,27 +64,40 @@ class RongyunMessageHandler {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
// 解析 content 字段(前端将业务数据放在 content 内)
|
|
68
|
+
let data = parsed;
|
|
69
|
+
if (parsed.content && typeof parsed.content === 'string') {
|
|
70
|
+
try {
|
|
71
|
+
const contentData = JSON.parse(parsed.content);
|
|
72
|
+
// 将 content 内的数据合并到顶层,方便处理器直接访问
|
|
73
|
+
// 保留原始 content 字段(用于聊天消息等场景)
|
|
74
|
+
data = { ...parsed, ...contentData, _raw_content: parsed.content };
|
|
75
|
+
} catch (e) {
|
|
76
|
+
this.logWarn(`解析 content 失败: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const msgType = data.msg_type;
|
|
68
81
|
this.logInfo(`[RongyunMessageHandler] 处理消息类型: ${msgType}`);
|
|
69
82
|
|
|
70
83
|
switch (msgType) {
|
|
71
84
|
case RongyunMessageTypeEnum.COMMAND:
|
|
72
|
-
await this.handleCommand(
|
|
85
|
+
await this.handleCommand(data);
|
|
73
86
|
break;
|
|
74
87
|
case RongyunMessageTypeEnum.CHAT_MESSAGE:
|
|
75
|
-
await this.handleChatMessage(
|
|
88
|
+
await this.handleChatMessage(data);
|
|
76
89
|
break;
|
|
77
90
|
case RongyunMessageTypeEnum.CREATE_OPENCODE_SESSION:
|
|
78
|
-
await this.handleCreateSession(
|
|
91
|
+
await this.handleCreateSession(data);
|
|
79
92
|
break;
|
|
80
93
|
case RongyunMessageTypeEnum.DELETE_OPENCODE_SESSION:
|
|
81
|
-
await this.handleDeleteSession(
|
|
94
|
+
await this.handleDeleteSession(data);
|
|
82
95
|
break;
|
|
83
96
|
case RongyunMessageTypeEnum.DEVICE_CONTROL:
|
|
84
|
-
await this.handleDeviceControl(
|
|
97
|
+
await this.handleDeviceControl(data);
|
|
85
98
|
break;
|
|
86
99
|
case RongyunMessageTypeEnum.DEVICE_STATUS_REQUEST:
|
|
87
|
-
await this.handleDeviceStatusRequest(
|
|
100
|
+
await this.handleDeviceStatusRequest(data);
|
|
88
101
|
break;
|
|
89
102
|
default:
|
|
90
103
|
this.logWarn(`未处理的消息类型: ${msgType}`);
|
|
@@ -100,8 +113,9 @@ class RongyunMessageHandler {
|
|
|
100
113
|
const commandId = data.command_id;
|
|
101
114
|
const requestId = data.request_id;
|
|
102
115
|
const sourceId = data.source_im_id;
|
|
116
|
+
const force = data.force === true; // 是否强制停止
|
|
103
117
|
|
|
104
|
-
this.logInfo(`[RongyunMessageHandler] 收到命令: command=${command}, command_id=${commandId}, from=${sourceId || 'guardserver'}`);
|
|
118
|
+
this.logInfo(`[RongyunMessageHandler] 收到命令: command=${command}, command_id=${commandId}, force=${force}, from=${sourceId || 'guardserver'}`);
|
|
105
119
|
|
|
106
120
|
// 验证命令是否有效
|
|
107
121
|
const validCommands = Object.values(OpenClawCommandEnum);
|
|
@@ -143,11 +157,12 @@ class RongyunMessageHandler {
|
|
|
143
157
|
|
|
144
158
|
if (isAsyncCommand) {
|
|
145
159
|
// 立即响应,告知前端命令已接收
|
|
160
|
+
const actionName = force ? '强制' + getCommandName(command) : getCommandName(command);
|
|
146
161
|
await this.sendResponse(RongyunMessageTypeEnum.COMMAND_RESULT, {
|
|
147
162
|
command,
|
|
148
163
|
command_id: commandId,
|
|
149
164
|
status: 'success',
|
|
150
|
-
message: `${
|
|
165
|
+
message: `${actionName}命令已接收,正在执行...`
|
|
151
166
|
}, requestId, sourceId);
|
|
152
167
|
}
|
|
153
168
|
|
|
@@ -163,8 +178,8 @@ class RongyunMessageHandler {
|
|
|
163
178
|
}
|
|
164
179
|
// 异步命令不在这里响应,因为已经提前响应了
|
|
165
180
|
// 但我们可以在这里记录执行结果
|
|
166
|
-
this.logInfo(`[RongyunMessageHandler] 命令执行完成: command=${command}, status=${response.status}, message=${response.message}`);
|
|
167
|
-
}).then(() => {
|
|
181
|
+
this.logInfo(`[RongyunMessageHandler] 命令执行完成: command=${command}, force=${force}, status=${response.status}, message=${response.message}`);
|
|
182
|
+
}, force).then(() => {
|
|
168
183
|
// 命令执行完成后,立即释放锁
|
|
169
184
|
this.commandLock = false;
|
|
170
185
|
if (this.commandLockTimer) {
|
|
@@ -206,7 +221,8 @@ class RongyunMessageHandler {
|
|
|
206
221
|
async handleChatMessage(data) {
|
|
207
222
|
const roomId = data.room_id;
|
|
208
223
|
const sessionId = data.gateway_session_id || data.session_id;
|
|
209
|
-
|
|
224
|
+
// 使用解析后的 content(聊天内容),如果没有则使用原始 content
|
|
225
|
+
const content = data.content || data._raw_content;
|
|
210
226
|
const requestId = data.request_id;
|
|
211
227
|
|
|
212
228
|
this.logInfo(`[RongyunMessageHandler] 收到聊天消息, roomId=${roomId}, sessionId=${sessionId}`);
|
|
@@ -79,7 +79,7 @@ class ScriptExecutor {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
async executeWithStatus(command, scriptName) {
|
|
82
|
+
async executeWithStatus(command, scriptName, force = false) {
|
|
83
83
|
// 配置修复命令特殊处理:直接执行命令,不需要脚本
|
|
84
84
|
if (command === OpenClawCommandEnum.CONFIG_FIX) {
|
|
85
85
|
return await this.executeCommandDirect('openclaw doctor --fix', 120);
|
|
@@ -88,8 +88,8 @@ class ScriptExecutor {
|
|
|
88
88
|
const scriptPath = path.join(this.scriptDir, scriptName);
|
|
89
89
|
|
|
90
90
|
try {
|
|
91
|
-
console.log(`[ScriptExecutor] 开始执行脚本: ${scriptPath}`);
|
|
92
|
-
const { stdout, stderr, exitCode } = await this.runScript(scriptPath);
|
|
91
|
+
console.log(`[ScriptExecutor] 开始执行脚本: ${scriptPath}, force=${force}`);
|
|
92
|
+
const { stdout, stderr, exitCode } = await this.runScript(scriptPath, force);
|
|
93
93
|
const fullOutput = stdout + stderr;
|
|
94
94
|
|
|
95
95
|
// 调试日志:记录脚本实际输出
|
|
@@ -206,7 +206,7 @@ class ScriptExecutor {
|
|
|
206
206
|
});
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
runScript(scriptPath) {
|
|
209
|
+
runScript(scriptPath, force = false) {
|
|
210
210
|
// Linux/Mac 系统:自动修复 shell 脚本的 CRLF 换行符
|
|
211
211
|
if (process.platform !== 'win32') {
|
|
212
212
|
try {
|
|
@@ -233,6 +233,12 @@ class ScriptExecutor {
|
|
|
233
233
|
cmd = 'bash';
|
|
234
234
|
args = [scriptPath];
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
// 如果是强制停止,添加 FORCE 环境变量
|
|
238
|
+
if (force) {
|
|
239
|
+
args.unshift('FORCE=1');
|
|
240
|
+
console.log(`[ScriptExecutor] 强制停止模式: FORCE=1`);
|
|
241
|
+
}
|
|
236
242
|
|
|
237
243
|
// 调试日志:记录执行环境
|
|
238
244
|
if (process.platform !== 'win32') {
|