Spaces:
Runtime error
Runtime error
fix: macOS compatibility for status commands and gateway client id
Browse filesReplace Linux-only commands (uptime -s, free -m, df --output=pcent) with
cross-platform alternatives using process.platform detection and Node.js
os module. Rename gateway client ID from control-ui to openclaw-control-ui.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- .env.example +1 -1
- src/app/api/status/route.ts +58 -29
- src/lib/websocket.ts +1 -1
.env.example
CHANGED
|
@@ -65,7 +65,7 @@ NEXT_PUBLIC_GATEWAY_PROTOCOL=
|
|
| 65 |
NEXT_PUBLIC_GATEWAY_URL=
|
| 66 |
# NEXT_PUBLIC_GATEWAY_TOKEN= # Optional, set if gateway requires auth token
|
| 67 |
# Gateway client id used in websocket handshake (role=operator UI client).
|
| 68 |
-
NEXT_PUBLIC_GATEWAY_CLIENT_ID=control-ui
|
| 69 |
|
| 70 |
# === Data Paths (all optional, defaults to .data/ in project root) ===
|
| 71 |
# MISSION_CONTROL_DATA_DIR=.data
|
|
|
|
| 65 |
NEXT_PUBLIC_GATEWAY_URL=
|
| 66 |
# NEXT_PUBLIC_GATEWAY_TOKEN= # Optional, set if gateway requires auth token
|
| 67 |
# Gateway client id used in websocket handshake (role=operator UI client).
|
| 68 |
+
NEXT_PUBLIC_GATEWAY_CLIENT_ID=openclaw-control-ui
|
| 69 |
|
| 70 |
# === Data Paths (all optional, defaults to .data/ in project root) ===
|
| 71 |
# MISSION_CONTROL_DATA_DIR=.data
|
src/app/api/status/route.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import net from 'node:net'
|
|
|
|
| 3 |
import { existsSync, statSync } from 'node:fs'
|
| 4 |
import path from 'node:path'
|
| 5 |
import { runCommand, runOpenClaw, runClawdbot } from '@/lib/command'
|
|
@@ -195,29 +196,48 @@ async function getSystemStatus(workspaceId: number) {
|
|
| 195 |
}
|
| 196 |
|
| 197 |
try {
|
| 198 |
-
// System uptime
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
} catch (error) {
|
| 205 |
logger.error({ err: error }, 'Error getting uptime')
|
| 206 |
}
|
| 207 |
|
| 208 |
try {
|
| 209 |
-
// Memory info
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
}
|
| 222 |
}
|
| 223 |
} catch (error) {
|
|
@@ -414,14 +434,17 @@ async function performHealthCheck() {
|
|
| 414 |
})
|
| 415 |
}
|
| 416 |
|
| 417 |
-
// Check disk space
|
| 418 |
try {
|
| 419 |
-
const { stdout } = await runCommand('df', ['
|
| 420 |
timeoutMs: 3000
|
| 421 |
})
|
| 422 |
const lines = stdout.trim().split('\n')
|
| 423 |
const last = lines[lines.length - 1] || ''
|
| 424 |
-
const
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
health.checks.push({
|
| 427 |
name: 'Disk Space',
|
|
@@ -436,15 +459,21 @@ async function performHealthCheck() {
|
|
| 436 |
})
|
| 437 |
}
|
| 438 |
|
| 439 |
-
// Check memory usage
|
| 440 |
try {
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
health.checks.push({
|
| 450 |
name: 'Memory Usage',
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import net from 'node:net'
|
| 3 |
+
import os from 'node:os'
|
| 4 |
import { existsSync, statSync } from 'node:fs'
|
| 5 |
import path from 'node:path'
|
| 6 |
import { runCommand, runOpenClaw, runClawdbot } from '@/lib/command'
|
|
|
|
| 196 |
}
|
| 197 |
|
| 198 |
try {
|
| 199 |
+
// System uptime (cross-platform)
|
| 200 |
+
if (process.platform === 'darwin') {
|
| 201 |
+
const { stdout } = await runCommand('sysctl', ['-n', 'kern.boottime'], {
|
| 202 |
+
timeoutMs: 3000
|
| 203 |
+
})
|
| 204 |
+
// Output format: { sec = 1234567890, usec = 0 } ...
|
| 205 |
+
const match = stdout.match(/sec\s*=\s*(\d+)/)
|
| 206 |
+
if (match) {
|
| 207 |
+
status.uptime = Date.now() - parseInt(match[1]) * 1000
|
| 208 |
+
}
|
| 209 |
+
} else {
|
| 210 |
+
const { stdout } = await runCommand('uptime', ['-s'], {
|
| 211 |
+
timeoutMs: 3000
|
| 212 |
+
})
|
| 213 |
+
const bootTime = new Date(stdout.trim())
|
| 214 |
+
status.uptime = Date.now() - bootTime.getTime()
|
| 215 |
+
}
|
| 216 |
} catch (error) {
|
| 217 |
logger.error({ err: error }, 'Error getting uptime')
|
| 218 |
}
|
| 219 |
|
| 220 |
try {
|
| 221 |
+
// Memory info (cross-platform)
|
| 222 |
+
if (process.platform === 'darwin') {
|
| 223 |
+
const totalBytes = os.totalmem()
|
| 224 |
+
const freeBytes = os.freemem()
|
| 225 |
+
const totalMB = Math.round(totalBytes / (1024 * 1024))
|
| 226 |
+
const usedMB = Math.round((totalBytes - freeBytes) / (1024 * 1024))
|
| 227 |
+
const availableMB = Math.round(freeBytes / (1024 * 1024))
|
| 228 |
+
status.memory = { total: totalMB, used: usedMB, available: availableMB }
|
| 229 |
+
} else {
|
| 230 |
+
const { stdout: memOutput } = await runCommand('free', ['-m'], {
|
| 231 |
+
timeoutMs: 3000
|
| 232 |
+
})
|
| 233 |
+
const memLine = memOutput.split('\n').find(line => line.startsWith('Mem:'))
|
| 234 |
+
if (memLine) {
|
| 235 |
+
const parts = memLine.split(/\s+/)
|
| 236 |
+
status.memory = {
|
| 237 |
+
total: parseInt(parts[1]) || 0,
|
| 238 |
+
used: parseInt(parts[2]) || 0,
|
| 239 |
+
available: parseInt(parts[6]) || 0
|
| 240 |
+
}
|
| 241 |
}
|
| 242 |
}
|
| 243 |
} catch (error) {
|
|
|
|
| 434 |
})
|
| 435 |
}
|
| 436 |
|
| 437 |
+
// Check disk space (cross-platform: use df -h / and parse capacity column)
|
| 438 |
try {
|
| 439 |
+
const { stdout } = await runCommand('df', ['-h', '/'], {
|
| 440 |
timeoutMs: 3000
|
| 441 |
})
|
| 442 |
const lines = stdout.trim().split('\n')
|
| 443 |
const last = lines[lines.length - 1] || ''
|
| 444 |
+
const parts = last.split(/\s+/)
|
| 445 |
+
// On macOS capacity is col 4 ("85%"), on Linux use% is col 4 as well
|
| 446 |
+
const pctField = parts.find(p => p.endsWith('%')) || '0%'
|
| 447 |
+
const usagePercent = parseInt(pctField.replace('%', '') || '0')
|
| 448 |
|
| 449 |
health.checks.push({
|
| 450 |
name: 'Disk Space',
|
|
|
|
| 459 |
})
|
| 460 |
}
|
| 461 |
|
| 462 |
+
// Check memory usage (cross-platform)
|
| 463 |
try {
|
| 464 |
+
let usagePercent: number
|
| 465 |
+
if (process.platform === 'darwin') {
|
| 466 |
+
const totalBytes = os.totalmem()
|
| 467 |
+
const freeBytes = os.freemem()
|
| 468 |
+
usagePercent = Math.round(((totalBytes - freeBytes) / totalBytes) * 100)
|
| 469 |
+
} else {
|
| 470 |
+
const { stdout } = await runCommand('free', ['-m'], { timeoutMs: 3000 })
|
| 471 |
+
const memLine = stdout.split('\n').find((line) => line.startsWith('Mem:'))
|
| 472 |
+
const parts = (memLine || '').split(/\s+/)
|
| 473 |
+
const total = parseInt(parts[1] || '0')
|
| 474 |
+
const available = parseInt(parts[6] || '0')
|
| 475 |
+
usagePercent = Math.round(((total - available) / total) * 100)
|
| 476 |
+
}
|
| 477 |
|
| 478 |
health.checks.push({
|
| 479 |
name: 'Memory Usage',
|
src/lib/websocket.ts
CHANGED
|
@@ -16,7 +16,7 @@ const log = createClientLogger('WebSocket')
|
|
| 16 |
|
| 17 |
// Gateway protocol version (v3 required by OpenClaw 2026.x)
|
| 18 |
const PROTOCOL_VERSION = 3
|
| 19 |
-
const DEFAULT_GATEWAY_CLIENT_ID = process.env.NEXT_PUBLIC_GATEWAY_CLIENT_ID || 'control-ui'
|
| 20 |
|
| 21 |
// Heartbeat configuration
|
| 22 |
const PING_INTERVAL_MS = 30_000
|
|
|
|
| 16 |
|
| 17 |
// Gateway protocol version (v3 required by OpenClaw 2026.x)
|
| 18 |
const PROTOCOL_VERSION = 3
|
| 19 |
+
const DEFAULT_GATEWAY_CLIENT_ID = process.env.NEXT_PUBLIC_GATEWAY_CLIENT_ID || 'openclaw-control-ui'
|
| 20 |
|
| 21 |
// Heartbeat configuration
|
| 22 |
const PING_INTERVAL_MS = 30_000
|