99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { readFileSync } from 'node:fs'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { requireRole } from '@/lib/auth'
|
|
|
|
interface DiscoveredGateway {
|
|
user: string
|
|
port: number
|
|
active: boolean
|
|
description: string
|
|
}
|
|
|
|
/**
|
|
* GET /api/gateways/discover
|
|
* Discovers OpenClaw gateways via systemd services and port scanning.
|
|
* Does not require filesystem access to other users' configs.
|
|
*/
|
|
export async function GET(request: NextRequest) {
|
|
const auth = requireRole(request, 'viewer')
|
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
|
|
|
const discovered: DiscoveredGateway[] = []
|
|
|
|
// Parse systemd services for openclaw-gateway instances
|
|
try {
|
|
const output = execFileSync('systemctl', [
|
|
'list-units', '--type=service', '--plain', '--no-legend', '--no-pager',
|
|
], { encoding: 'utf-8', timeout: 3000 })
|
|
|
|
const gwLines = output.split('\n').filter(l => l.includes('openclaw') && l.includes('gateway'))
|
|
|
|
for (const line of gwLines) {
|
|
// e.g. "openclaw-gateway@quant.service loaded active running OpenClaw Gateway (quant)"
|
|
const parts = line.trim().split(/\s+/)
|
|
const serviceName = parts[0] || ''
|
|
const state = parts[2] || '' // active/inactive
|
|
const description = parts.slice(4).join(' ') // "OpenClaw Gateway (quant)"
|
|
|
|
// Extract user from service name
|
|
let user = ''
|
|
const templateMatch = serviceName.match(/openclaw-gateway@(\w+)\.service/)
|
|
if (templateMatch) {
|
|
user = templateMatch[1]
|
|
} else {
|
|
// Custom service name like "openclaw-leads-gateway.service"
|
|
const customMatch = serviceName.match(/openclaw-(\w+)-gateway\.service/)
|
|
if (customMatch) user = customMatch[1]
|
|
}
|
|
if (!user) continue
|
|
|
|
// Find the port by checking what openclaw-gateway processes are listening on
|
|
let port = 0
|
|
try {
|
|
const configPath = `/home/${user}/.openclaw/openclaw.json`
|
|
const raw = readFileSync(configPath, 'utf-8')
|
|
const config = JSON.parse(raw)
|
|
if (typeof config?.gateway?.port === 'number') port = config.gateway.port
|
|
} catch {
|
|
// Can't read config — try to detect from ss output
|
|
}
|
|
|
|
// If we couldn't read config, try finding port via ss for the service PID
|
|
if (!port) {
|
|
try {
|
|
const pidOutput = execFileSync('systemctl', [
|
|
'show', serviceName, '--property=ExecMainPID', '--value',
|
|
], { encoding: 'utf-8', timeout: 2000 }).trim()
|
|
const pid = parseInt(pidOutput, 10)
|
|
if (pid > 0) {
|
|
const ssOutput = execFileSync('ss', ['-ltnp'], {
|
|
encoding: 'utf-8', timeout: 2000,
|
|
})
|
|
const pidPattern = `pid=${pid},`
|
|
for (const ssLine of ssOutput.split('\n')) {
|
|
if (ssLine.includes(pidPattern)) {
|
|
const portMatch = ssLine.match(/:(\d+)\s/)
|
|
if (portMatch) { port = parseInt(portMatch[1], 10); break }
|
|
}
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
if (!port) continue
|
|
|
|
discovered.push({
|
|
user,
|
|
port,
|
|
active: state === 'active',
|
|
description: description.replace(/[()]/g, '').trim(),
|
|
})
|
|
}
|
|
} catch {
|
|
// systemctl not available or failed — fall back silently
|
|
}
|
|
|
|
return NextResponse.json({ gateways: discovered })
|
|
}
|