fix: Linux Gateway 服务管理 (#7,#8,#10) + 非root部署 (#9) + 官网锚点滚动修复

This commit is contained in:
晴天
2026-03-06 20:28:13 +08:00
parent 881b49c9ef
commit 7d387a4f94
8 changed files with 275 additions and 66 deletions

View File

@@ -5,6 +5,14 @@
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [0.5.5] - 2026-03-06
### 修复 (Bug Fixes)
- **Linux Gateway 服务管理不可用 (#7, #10)** — 新增 `linuxCheckGateway()`ss → lsof → /proc/net/tcp 三级 fallback`linuxStartGateway()`detached 子进程)、`linuxStopGateway()`SIGTERM所有 handler 分支加入 Linux 支持;修复 `reload_gateway` / `restart_gateway` 错误执行 `systemctl restart clawpanel`(重启面板而非 Gateway的问题
- **systemd 环境下 OpenClaw CLI 检测失败 (#8)** — 新增 `findOpenclawBin()` 路径扫描,覆盖 nvm / volta / nodenv / fnm / `/usr/local/lib/nodejs` 等所有常见路径,替代仅依赖 `which` 的方式
- **非 root 用户无法部署 ClawPanel (#9)** — `linux-deploy.sh` 支持非 root 安装:普通用户安装到 `$HOME/.local/share/clawpanel`,使用 user-level systemd 服务 + `loginctl enable-linger`;系统包安装通过 `run_pkg_cmd()` 按需 sudo
## [0.4.8] - 2026-03-06
### 修复 (Bug Fixes)

View File

@@ -34,7 +34,7 @@
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。",
"url": "https://claw.qt.cool/",
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
"softwareVersion": "0.5.4",
"softwareVersion": "0.5.5",
"author": {
"@type": "Organization",
"name": "晴辰云 QingchenCloud",
@@ -69,9 +69,9 @@
/* ══════════════ Reset & Base ══════════════ */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
html { scroll-behavior: auto; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, sans-serif; background: var(--bg); color: var(--text); -webkit-font-smoothing: antialiased; overflow-x: hidden; line-height: 1.6; transition: background 0.3s, color 0.3s; }
a { color: inherit; text-decoration: none; } img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; } img { max-width: 100%; height: auto; display: block; } .screenshot-frame img, .gallery-card img, .hero-image-wrap img, .arch-gif-wrap img { aspect-ratio: 16/10; object-fit: cover; background: var(--bg-s); }
button { font: inherit; cursor: pointer; border: none; background: none; } ul { list-style: none; }
.mono { font-family: 'SF Mono', 'Fira Code', Consolas, monospace; }
@@ -1374,19 +1374,45 @@
if (e.key === 'Escape') closeDoc();
});
/* ── Fix anchor position on load ── */
/* ── Robust Anchor Scroll (handles lazy-loaded image layout shift) ── */
function smoothScrollTo(target) {
if (!target) return;
var lastTop = -1;
function doScroll() {
var top = target.getBoundingClientRect().top + window.scrollY - 120;
window.scrollTo({ top: top, behavior: 'smooth' });
return top;
}
lastTop = doScroll();
// Re-check position after images may have loaded and shifted layout
var retries = [300, 600, 1200];
retries.forEach(function(delay) {
setTimeout(function() {
var newTop = target.getBoundingClientRect().top + window.scrollY - 120;
if (Math.abs(newTop - window.scrollY) > 30) {
doScroll();
}
}, delay);
});
}
// Intercept all anchor link clicks
document.querySelectorAll('a[href^="#"]').forEach(function(link) {
link.addEventListener('click', function(e) {
var hash = this.getAttribute('href');
if (!hash || hash === '#') return;
e.preventDefault();
var target = document.querySelector(hash);
if (target) {
smoothScrollTo(target);
history.pushState(null, '', hash);
}
});
});
// Fix anchor position on initial page load with hash
window.addEventListener('load', function() {
if (window.location.hash) {
var hash = window.location.hash;
setTimeout(function() {
var target = document.querySelector(hash);
if (target) {
window.scrollTo({
top: target.offsetTop - 120,
behavior: 'smooth'
});
}
}, 300);
var target = document.querySelector(window.location.hash);
if (target) setTimeout(function() { smoothScrollTo(target); }, 100);
}
});
</script>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "clawpanel",
"version": "1.0.0",
"version": "0.5.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawpanel",
"version": "1.0.0",
"version": "0.5.2",
"dependencies": {
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-shell": "^2.2.1"

View File

@@ -1,6 +1,6 @@
{
"name": "clawpanel",
"version": "0.5.4",
"version": "0.5.5",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",

View File

@@ -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 桌面应用')
}
},

View File

@@ -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 "=========================================="

View File

@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
version = "0.5.1"
version = "0.5.5"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel",
"version": "0.5.4",
"version": "0.5.5",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",