mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
fix: Linux Gateway 服务管理 (#7,#8,#10) + 非root部署 (#9) + 官网锚点滚动修复
This commit is contained in:
@@ -19,6 +19,7 @@ const DEVICES_DIR = path.join(OPENCLAW_DIR, 'devices')
|
||||
const PAIRED_PATH = path.join(DEVICES_DIR, 'paired.json')
|
||||
const isWindows = process.platform === 'win32'
|
||||
const isMac = process.platform === 'darwin'
|
||||
const isLinux = process.platform === 'linux'
|
||||
const SCOPES = ['operator.admin', 'operator.approvals', 'operator.pairing', 'operator.read', 'operator.write']
|
||||
|
||||
function readBody(req) {
|
||||
@@ -243,6 +244,126 @@ function readGatewayPort() {
|
||||
}
|
||||
}
|
||||
|
||||
// === Linux 服务管理 ===
|
||||
|
||||
/**
|
||||
* 扫描常见 Node 版本管理器路径查找 openclaw 二进制文件。
|
||||
* 解决 systemd 服务环境中 PATH 不含 nvm/volta/fnm 路径的问题。
|
||||
*/
|
||||
function findOpenclawBin() {
|
||||
try {
|
||||
return execSync('which openclaw 2>/dev/null', { stdio: 'pipe' }).toString().trim()
|
||||
} catch {}
|
||||
|
||||
const home = homedir()
|
||||
const candidates = [
|
||||
'/usr/local/bin/openclaw',
|
||||
'/usr/bin/openclaw',
|
||||
'/snap/bin/openclaw',
|
||||
path.join(home, '.local/bin/openclaw'),
|
||||
]
|
||||
|
||||
// nvm
|
||||
const nvmDir = process.env.NVM_DIR || path.join(home, '.nvm')
|
||||
const nvmVersions = path.join(nvmDir, 'versions/node')
|
||||
if (fs.existsSync(nvmVersions)) {
|
||||
try {
|
||||
for (const entry of fs.readdirSync(nvmVersions)) {
|
||||
candidates.push(path.join(nvmVersions, entry, 'bin/openclaw'))
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// volta
|
||||
candidates.push(path.join(home, '.volta/bin/openclaw'))
|
||||
|
||||
// nodenv
|
||||
candidates.push(path.join(home, '.nodenv/shims/openclaw'))
|
||||
|
||||
// fnm
|
||||
const fnmDir = process.env.FNM_DIR || path.join(home, '.local/share/fnm')
|
||||
const fnmVersions = path.join(fnmDir, 'node-versions')
|
||||
if (fs.existsSync(fnmVersions)) {
|
||||
try {
|
||||
for (const entry of fs.readdirSync(fnmVersions)) {
|
||||
candidates.push(path.join(fnmVersions, entry, 'installation/bin/openclaw'))
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// /usr/local/lib/nodejs(手动安装的 Node.js)
|
||||
const nodejsLib = '/usr/local/lib/nodejs'
|
||||
if (fs.existsSync(nodejsLib)) {
|
||||
try {
|
||||
for (const entry of fs.readdirSync(nodejsLib)) {
|
||||
candidates.push(path.join(nodejsLib, entry, 'bin/openclaw'))
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const p of candidates) {
|
||||
if (fs.existsSync(p)) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function linuxCheckGateway() {
|
||||
const port = readGatewayPort()
|
||||
// ss 查端口监听
|
||||
try {
|
||||
const out = execSync(`ss -tlnp 'sport = :${port}' 2>/dev/null`, { timeout: 3000 }).toString().trim()
|
||||
const pidMatch = out.match(/pid=(\d+)/)
|
||||
if (pidMatch) return { running: true, pid: parseInt(pidMatch[1]) }
|
||||
if (out.includes(`:${port}`)) return { running: true, pid: null }
|
||||
} catch {}
|
||||
// fallback: lsof
|
||||
try {
|
||||
const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { timeout: 3000 }).toString().trim()
|
||||
if (out) {
|
||||
const pid = parseInt(out.split('\n')[0]) || null
|
||||
return { running: !!pid, pid }
|
||||
}
|
||||
} catch {}
|
||||
// fallback: /proc/net/tcp
|
||||
try {
|
||||
const hexPort = port.toString(16).toUpperCase().padStart(4, '0')
|
||||
const tcp = fs.readFileSync('/proc/net/tcp', 'utf8')
|
||||
if (tcp.includes(`:${hexPort}`)) return { running: true, pid: null }
|
||||
} catch {}
|
||||
return { running: false, pid: null }
|
||||
}
|
||||
|
||||
function linuxStartGateway() {
|
||||
if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true })
|
||||
const logPath = path.join(LOGS_DIR, 'gateway.log')
|
||||
const errPath = path.join(LOGS_DIR, 'gateway.err.log')
|
||||
const out = fs.openSync(logPath, 'a')
|
||||
const err = fs.openSync(errPath, 'a')
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
fs.appendFileSync(logPath, `\n[${timestamp}] [ClawPanel] Starting Gateway on Linux...\n`)
|
||||
|
||||
const bin = findOpenclawBin() || 'openclaw'
|
||||
const child = spawn(bin, ['gateway'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', out, err],
|
||||
shell: false,
|
||||
cwd: homedir(),
|
||||
})
|
||||
child.unref()
|
||||
}
|
||||
|
||||
function linuxStopGateway() {
|
||||
const { running, pid } = linuxCheckGateway()
|
||||
if (!running || !pid) throw new Error('Gateway 未运行')
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM')
|
||||
} catch (e) {
|
||||
try { process.kill(pid, 'SIGKILL') } catch {}
|
||||
throw new Error('停止失败: ' + (e.message || e))
|
||||
}
|
||||
}
|
||||
|
||||
// === API Handlers ===
|
||||
|
||||
const handlers = {
|
||||
@@ -274,7 +395,7 @@ const handlers = {
|
||||
// 服务管理
|
||||
get_services_status() {
|
||||
const label = 'ai.openclaw.gateway'
|
||||
const { running, pid } = isMac ? macCheckService(label) : winCheckGateway()
|
||||
const { running, pid } = isMac ? macCheckService(label) : isLinux ? linuxCheckGateway() : winCheckGateway()
|
||||
|
||||
let cliInstalled = false
|
||||
if (isMac) {
|
||||
@@ -283,13 +404,7 @@ const handlers = {
|
||||
try { cliInstalled = fs.existsSync(path.join(process.env.APPDATA || '', 'npm', 'openclaw.cmd')) }
|
||||
catch { cliInstalled = false }
|
||||
} else {
|
||||
// Linux - 使用 which 命令动态查找
|
||||
try {
|
||||
execSync('which openclaw', { stdio: 'pipe' })
|
||||
cliInstalled = true
|
||||
} catch {
|
||||
cliInstalled = false
|
||||
}
|
||||
cliInstalled = !!findOpenclawBin()
|
||||
}
|
||||
|
||||
return [{ label, running, pid, description: 'OpenClaw Gateway', cli_installed: cliInstalled }]
|
||||
@@ -297,20 +412,31 @@ const handlers = {
|
||||
|
||||
start_service({ label }) {
|
||||
if (isMac) { macStartService(label); return true }
|
||||
if (isLinux) { linuxStartGateway(); return true }
|
||||
winStartGateway()
|
||||
return true
|
||||
},
|
||||
|
||||
stop_service({ label }) {
|
||||
if (isMac) { macStopService(label); return true }
|
||||
if (isLinux) { linuxStopGateway(); return true }
|
||||
winStopGateway()
|
||||
return true
|
||||
},
|
||||
|
||||
async restart_service({ label }) {
|
||||
if (isMac) { macRestartService(label); return true }
|
||||
if (isLinux) {
|
||||
try { linuxStopGateway() } catch {}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const { running } = linuxCheckGateway()
|
||||
if (!running) break
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
}
|
||||
linuxStartGateway()
|
||||
return true
|
||||
}
|
||||
try { winStopGateway() } catch {}
|
||||
// 等待进程退出
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const { running } = winCheckGateway()
|
||||
if (!running) break
|
||||
@@ -324,16 +450,12 @@ const handlers = {
|
||||
if (isMac) {
|
||||
macRestartService('ai.openclaw.gateway')
|
||||
return 'Gateway 已重启'
|
||||
} else if (isWindows) {
|
||||
throw new Error('Windows 请使用 Tauri 桌面应用')
|
||||
} else if (isLinux) {
|
||||
try { linuxStopGateway() } catch {}
|
||||
linuxStartGateway()
|
||||
return 'Gateway 已重启'
|
||||
} else {
|
||||
// Linux
|
||||
try {
|
||||
execSync('systemctl restart clawpanel', { stdio: 'inherit' })
|
||||
return 'Gateway 已重启'
|
||||
} catch (err) {
|
||||
throw new Error(`重启失败: ${err.message}`)
|
||||
}
|
||||
throw new Error('Windows 请使用 Tauri 桌面应用')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -341,16 +463,12 @@ const handlers = {
|
||||
if (isMac) {
|
||||
macRestartService('ai.openclaw.gateway')
|
||||
return 'Gateway 已重启'
|
||||
} else if (isWindows) {
|
||||
throw new Error('Windows 请使用 Tauri 桌面应用')
|
||||
} else if (isLinux) {
|
||||
try { linuxStopGateway() } catch {}
|
||||
linuxStartGateway()
|
||||
return 'Gateway 已重启'
|
||||
} else {
|
||||
// Linux
|
||||
try {
|
||||
execSync('systemctl restart clawpanel', { stdio: 'inherit' })
|
||||
return 'Gateway 已重启'
|
||||
} catch (err) {
|
||||
throw new Error(`重启失败: ${err.message}`)
|
||||
}
|
||||
throw new Error('Windows 请使用 Tauri 桌面应用')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -7,11 +7,32 @@ echo " 在 Linux 上通过浏览器管理 OpenClaw"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
INSTALL_DIR="/opt/clawpanel"
|
||||
PANEL_PORT=1420
|
||||
REPO_URL="https://github.com/qingchencloud/clawpanel.git"
|
||||
NPM_REGISTRY="https://registry.npmmirror.com"
|
||||
|
||||
# 检测权限模式
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
IS_ROOT=true
|
||||
INSTALL_DIR="/opt/clawpanel"
|
||||
SYSTEMD_DIR="/etc/systemd/system"
|
||||
echo "🔑 以 root 身份运行,安装到 $INSTALL_DIR"
|
||||
else
|
||||
IS_ROOT=false
|
||||
INSTALL_DIR="$HOME/.local/share/clawpanel"
|
||||
SYSTEMD_DIR="$HOME/.config/systemd/user"
|
||||
echo "👤 以普通用户身份运行,安装到 $INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# 带权限执行(安装系统包时需要)
|
||||
run_pkg_cmd() {
|
||||
if [ "$IS_ROOT" = true ]; then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检测系统
|
||||
detect_os() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
@@ -41,18 +62,18 @@ install_node() {
|
||||
echo "📦 安装 Node.js 22 LTS..."
|
||||
case "$OS" in
|
||||
ubuntu|debian|linuxmint|pop)
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | run_pkg_cmd bash -
|
||||
run_pkg_cmd apt-get install -y nodejs
|
||||
;;
|
||||
centos|rhel|fedora|rocky|alma)
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
|
||||
sudo yum install -y nodejs
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | run_pkg_cmd bash -
|
||||
run_pkg_cmd yum install -y nodejs
|
||||
;;
|
||||
alpine)
|
||||
sudo apk add nodejs npm git
|
||||
run_pkg_cmd apk add nodejs npm git
|
||||
;;
|
||||
arch|manjaro)
|
||||
sudo pacman -Sy --noconfirm nodejs npm git
|
||||
run_pkg_cmd pacman -Sy --noconfirm nodejs npm git
|
||||
;;
|
||||
*)
|
||||
echo "❌ 不支持自动安装 Node.js,请手动安装后重试"
|
||||
@@ -73,16 +94,16 @@ install_git() {
|
||||
echo "📦 安装 Git..."
|
||||
case "$OS" in
|
||||
ubuntu|debian|linuxmint|pop)
|
||||
sudo apt-get update && sudo apt-get install -y git
|
||||
run_pkg_cmd apt-get update && run_pkg_cmd apt-get install -y git
|
||||
;;
|
||||
centos|rhel|fedora|rocky|alma)
|
||||
sudo yum install -y git
|
||||
run_pkg_cmd yum install -y git
|
||||
;;
|
||||
alpine)
|
||||
sudo apk add git
|
||||
run_pkg_cmd apk add git
|
||||
;;
|
||||
arch|manjaro)
|
||||
sudo pacman -Sy --noconfirm git
|
||||
run_pkg_cmd pacman -Sy --noconfirm git
|
||||
;;
|
||||
esac
|
||||
echo "✅ Git 安装完成"
|
||||
@@ -114,8 +135,7 @@ install_clawpanel() {
|
||||
npm install
|
||||
else
|
||||
echo "📦 克隆 ClawPanel..."
|
||||
sudo mkdir -p "$INSTALL_DIR"
|
||||
sudo chown -R $(whoami) "$INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
npm install
|
||||
@@ -132,7 +152,10 @@ setup_systemd() {
|
||||
fi
|
||||
|
||||
echo "🔧 创建 systemd 服务..."
|
||||
sudo tee /etc/systemd/system/clawpanel.service > /dev/null << EOF
|
||||
mkdir -p "$SYSTEMD_DIR"
|
||||
|
||||
if [ "$IS_ROOT" = true ]; then
|
||||
cat > "$SYSTEMD_DIR/clawpanel.service" << EOF
|
||||
[Unit]
|
||||
Description=ClawPanel Web - OpenClaw Management Panel
|
||||
After=network.target
|
||||
@@ -150,10 +173,33 @@ Environment=HOME=$HOME
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable clawpanel
|
||||
systemctl start clawpanel
|
||||
else
|
||||
cat > "$SYSTEMD_DIR/clawpanel.service" << EOF
|
||||
[Unit]
|
||||
Description=ClawPanel Web - OpenClaw Management Panel
|
||||
After=network.target
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable clawpanel
|
||||
sudo systemctl start clawpanel
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$(which npx) vite --port $PANEL_PORT --host 0.0.0.0
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=NODE_ENV=production
|
||||
Environment=HOME=$HOME
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable clawpanel
|
||||
systemctl --user start clawpanel
|
||||
# 允许用户服务在未登录时继续运行
|
||||
loginctl enable-linger "$(whoami)" 2>/dev/null || true
|
||||
fi
|
||||
echo "✅ systemd 服务已创建并启动"
|
||||
}
|
||||
|
||||
@@ -175,6 +221,13 @@ main() {
|
||||
setup_systemd
|
||||
|
||||
local ip=$(get_local_ip)
|
||||
|
||||
if [ "$IS_ROOT" = true ]; then
|
||||
local ctl_cmd="systemctl"
|
||||
else
|
||||
local ctl_cmd="systemctl --user"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " ✅ ClawPanel Web 版部署完成!"
|
||||
@@ -185,9 +238,13 @@ main() {
|
||||
echo " 📋 配置目录: $HOME/.openclaw/"
|
||||
echo ""
|
||||
echo " 常用命令:"
|
||||
echo " systemctl status clawpanel # 查看状态"
|
||||
echo " systemctl restart clawpanel # 重启面板"
|
||||
echo " journalctl -u clawpanel -f # 查看日志"
|
||||
echo " $ctl_cmd status clawpanel # 查看状态"
|
||||
echo " $ctl_cmd restart clawpanel # 重启面板"
|
||||
if [ "$IS_ROOT" = true ]; then
|
||||
echo " journalctl -u clawpanel -f # 查看日志"
|
||||
else
|
||||
echo " journalctl --user -u clawpanel -f # 查看日志"
|
||||
fi
|
||||
echo ""
|
||||
echo " 用浏览器打开上面的地址,即可管理 OpenClaw。"
|
||||
echo "=========================================="
|
||||
|
||||
Reference in New Issue
Block a user