From 62d852c2b305c364d2e3e0595e294c27c0f30f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sat, 18 Jul 2026 08:28:06 -0700 Subject: [PATCH] fix(deploy): harden Linux Web upgrades --- CHANGELOG.md | 4 ++ README.md | 16 ++++- docs/linux-deploy.md | 23 ++++++- scripts/linux-deploy.sh | 84 ++++++++++++++++++++--- tests/linux-deploy-upgrade-policy.test.js | 40 +++++++++++ 5 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 tests/linux-deploy-upgrade-policy.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f79c65a..9e670b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [未发布 (Unreleased)] +### 修复 (Fixes) + +- **Linux Web 升级不再假成功** — 部署脚本会沿用已有 systemd 工作目录,拒绝 root/普通用户权限模式错位;Git 拉取失败或存在本地修改时立即停止,构建成功后重启服务并输出实际面板版本。README 与 Linux 部署指南同步补充系统级/用户级升级命令和版本核验步骤 + ## [0.19.0] - 2026-07-15 ### 新功能 (Features) diff --git a/README.md b/README.md index a14bf0f..4c22bea 100644 --- a/README.md +++ b/README.md @@ -249,22 +249,34 @@ ClawPanel 提供多种升级方式,根据你的安装方式选择对应方案 **方式一:一键升级脚本(推荐)** +系统级安装(`/opt/clawpanel`): + +```bash +curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/scripts/linux-deploy.sh | sudo bash +``` + +普通用户安装(`~/.local/share/clawpanel`): + ```bash curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/scripts/linux-deploy.sh | bash ``` -脚本会自动检测已有安装,拉取最新代码并重新构建。 +脚本会读取已有 systemd 服务的实际工作目录,使用 `git pull --ff-only` 拉取代码,重新构建并重启服务。Git 拉取失败、本地存在未提交修改或权限模式不匹配时会明确报错并停止,不会继续用旧源码构建。 **方式二:手动升级** ```bash cd /opt/clawpanel # 替换为实际安装目录 -git pull origin main +git status --short # 有输出时先处理本地修改 +git pull --ff-only origin main npm install npm run build +node -p "require('./package.json').version" # 应输出最新版本 sudo systemctl restart clawpanel ``` +普通用户服务将最后一行替换为 `systemctl --user restart clawpanel`。升级后如果浏览器仍显示旧版本,请使用 `Ctrl+F5` 强制刷新。 + > **升级 OpenClaw**:面板和 OpenClaw 版本需要匹配。可在「服务管理」页面一键升级,或手动执行: > ```bash > sudo npm install -g @qingchencloud/openclaw-zh@latest --registry https://registry.npmmirror.com diff --git a/docs/linux-deploy.md b/docs/linux-deploy.md index a71372f..c4c3d54 100644 --- a/docs/linux-deploy.md +++ b/docs/linux-deploy.md @@ -319,19 +319,38 @@ sudo firewall-cmd --reload ### 更新 ClawPanel +推荐直接重新运行部署脚本。必须与原安装权限保持一致: + +```bash +# 系统级安装(/opt/clawpanel) +curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/scripts/linux-deploy.sh | sudo bash + +# 普通用户安装(~/.local/share/clawpanel) +curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/scripts/linux-deploy.sh | bash +``` + +脚本会自动读取 systemd 服务的 `WorkingDirectory`,更新实际运行的目录,并在构建后重启对应服务。拉取失败、存在未提交修改或权限模式不匹配时,脚本会停止并显示具体原因。 + +手动升级: + ```bash cd /opt/clawpanel # root 部署路径 # 或 ~/.local/share/clawpanel # 普通用户路径 -git pull origin main +git status --short # 有输出时先处理本地修改 +git pull --ff-only origin main npm install --registry https://registry.npmmirror.com +npm run build +node -p "require('./package.json').version" # 核对实际源码版本 sudo systemctl restart clawpanel # 或 pm2 restart clawpanel ``` +普通用户 systemd 服务使用 `systemctl --user restart clawpanel`。升级后浏览器仍显示旧页面时,使用 `Ctrl+F5` 强制刷新。 + > 国内拉不到 GitHub?可切换到 AtomGit 镜像: > ```bash > git remote set-url origin https://atomgit.com/qingchencloud/clawpanel.git -> git pull origin main +> git pull --ff-only origin main > ``` ### 更新 OpenClaw diff --git a/scripts/linux-deploy.sh b/scripts/linux-deploy.sh index f8d3194..2254bf4 100644 --- a/scripts/linux-deploy.sh +++ b/scripts/linux-deploy.sh @@ -10,6 +10,7 @@ echo "" PANEL_PORT=1420 REPO_URL="https://github.com/qingchencloud/clawpanel.git" REPO_URL_GITEE="https://gitee.com/QtCodeCreators/clawpanel.git" +DEPLOY_SCRIPT_URL="https://raw.githubusercontent.com/qingchencloud/clawpanel/main/scripts/linux-deploy.sh" NPM_REGISTRY="https://registry.npmmirror.com" PANEL_NODE_MIN_VERSION="18.0.0" OPENCLAW_RECOMMENDED_VERSION="2026.7.1-zh.2" @@ -25,14 +26,61 @@ if [ "$(id -u)" = "0" ]; then IS_ROOT=true INSTALL_DIR="/opt/clawpanel" SYSTEMD_DIR="/etc/systemd/system" - echo "🔑 以 root 身份运行,安装到 $INSTALL_DIR" + SERVICE_SCOPE="system" else IS_ROOT=false INSTALL_DIR="$HOME/.local/share/clawpanel" SYSTEMD_DIR="$HOME/.config/systemd/user" - echo "👤 以普通用户身份运行,安装到 $INSTALL_DIR" + SERVICE_SCOPE="user" fi +# 优先沿用已有 systemd 服务的工作目录,避免用不同权限重复安装两套面板 +select_install_context() { + local system_install_dir="" + local user_install_dir="" + + if command -v systemctl &> /dev/null; then + system_install_dir=$(systemctl show clawpanel --property=WorkingDirectory --value 2>/dev/null || true) + if [ "$IS_ROOT" = false ]; then + user_install_dir=$(systemctl --user show clawpanel --property=WorkingDirectory --value 2>/dev/null || true) + fi + fi + + if [ -z "$system_install_dir" ] && [ -f "/etc/systemd/system/clawpanel.service" ]; then + system_install_dir=$(sed -n 's/^WorkingDirectory=//p' /etc/systemd/system/clawpanel.service | head -1) + fi + if [ "$IS_ROOT" = false ] && [ -z "$user_install_dir" ] && [ -f "$HOME/.config/systemd/user/clawpanel.service" ]; then + user_install_dir=$(sed -n 's/^WorkingDirectory=//p' "$HOME/.config/systemd/user/clawpanel.service" | head -1) + fi + + if [ "$IS_ROOT" = false ] && [ -n "$user_install_dir" ] && [ -f "$user_install_dir/package.json" ]; then + INSTALL_DIR="$user_install_dir" + SYSTEMD_DIR="$HOME/.config/systemd/user" + SERVICE_SCOPE="user" + elif [ -n "$system_install_dir" ] && [ -f "$system_install_dir/package.json" ]; then + if [ "$IS_ROOT" = false ]; then + echo "❌ 检测到系统级 ClawPanel 安装: $system_install_dir" + echo " 请使用 root 权限升级,避免在用户目录重复安装:" + echo " curl -fsSL $DEPLOY_SCRIPT_URL | sudo bash" + exit 1 + fi + INSTALL_DIR="$system_install_dir" + SYSTEMD_DIR="/etc/systemd/system" + SERVICE_SCOPE="system" + elif [ "$IS_ROOT" = false ] && [ -f "/opt/clawpanel/package.json" ]; then + echo "❌ 检测到系统级 ClawPanel 安装: /opt/clawpanel" + echo " 请使用 root 权限升级,避免在用户目录重复安装:" + echo " curl -fsSL $DEPLOY_SCRIPT_URL | sudo bash" + exit 1 + fi + + if [ "$SERVICE_SCOPE" = "system" ]; then + echo "🔑 使用系统级安装目录: $INSTALL_DIR" + else + echo "👤 使用用户级安装目录: $INSTALL_DIR" + fi +} + # 带权限执行(安装系统包时需要) run_pkg_cmd() { if [ "$IS_ROOT" = true ]; then @@ -313,7 +361,22 @@ install_clawpanel() { if [ -d "$INSTALL_DIR" ] && [ -f "$INSTALL_DIR/package.json" ]; then echo "📦 ClawPanel 已存在,更新中..." cd "$INSTALL_DIR" - git pull origin main 2>/dev/null || true + if [ ! -d ".git" ]; then + echo "❌ ClawPanel 源码更新失败:$INSTALL_DIR 不是 Git 仓库" + echo " 请保留 ~/.openclaw 数据后重新执行安装脚本" + exit 1 + fi + if ! git diff --quiet || ! git diff --cached --quiet; then + echo "❌ ClawPanel 源码更新失败:检测到未提交的本地修改" + echo " 请先执行 git status 并处理这些修改,脚本不会自动覆盖" + git status --short + exit 1 + fi + if ! git pull --ff-only origin main; then + echo "❌ ClawPanel 源码更新失败,请检查上方 Git 错误、网络和远程仓库地址" + echo " 当前远程: $(git remote get-url origin 2>/dev/null || echo '未知')" + exit 1 + fi # 清理可能损坏的 node_modules(上次 npm install 失败残留) if [ -d "node_modules" ] && [ ! -f "node_modules/.package-lock.json" ]; then echo "⚠️ 检测到不完整的 node_modules,清理后重新安装..." @@ -342,7 +405,9 @@ install_clawpanel() { echo "📦 构建生产版本..." cd "$INSTALL_DIR" npx vite build + PANEL_VERSION=$(node -p "require('./package.json').version") echo "✅ ClawPanel 安装完成: $INSTALL_DIR" + echo "✅ ClawPanel 版本: $PANEL_VERSION" echo "✅ 启动命令: npm run serve" } @@ -357,7 +422,7 @@ setup_systemd() { echo "🔧 创建 systemd 服务..." mkdir -p "$SYSTEMD_DIR" - if [ "$IS_ROOT" = true ]; then + if [ "$SERVICE_SCOPE" = "system" ]; then cat > "$SYSTEMD_DIR/clawpanel.service" << EOF [Unit] Description=ClawPanel Web - OpenClaw Management Panel @@ -379,7 +444,7 @@ WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable clawpanel - systemctl start clawpanel + systemctl restart clawpanel else cat > "$SYSTEMD_DIR/clawpanel.service" << EOF [Unit] @@ -401,11 +466,11 @@ WantedBy=default.target EOF systemctl --user daemon-reload systemctl --user enable clawpanel - systemctl --user start clawpanel + systemctl --user restart clawpanel # 允许用户服务在未登录时继续运行 loginctl enable-linger "$(whoami)" 2>/dev/null || true fi - echo "✅ systemd 服务已创建并启动" + echo "✅ systemd 服务已更新并重启" } # 获取本机 IP @@ -443,6 +508,7 @@ EOF # 主流程 main() { + select_install_context detect_os echo "" install_git @@ -454,7 +520,7 @@ main() { local ip=$(get_local_ip) - if [ "$IS_ROOT" = true ]; then + if [ "$SERVICE_SCOPE" = "system" ]; then local ctl_cmd="systemctl" else local ctl_cmd="systemctl --user" @@ -477,7 +543,7 @@ main() { echo " 常用命令:" echo " $ctl_cmd status clawpanel # 查看状态" echo " $ctl_cmd restart clawpanel # 重启面板" - if [ "$IS_ROOT" = true ]; then + if [ "$SERVICE_SCOPE" = "system" ]; then echo " journalctl -u clawpanel -f # 查看日志" else echo " journalctl --user -u clawpanel -f # 查看日志" diff --git a/tests/linux-deploy-upgrade-policy.test.js b/tests/linux-deploy-upgrade-policy.test.js new file mode 100644 index 0000000..61f2025 --- /dev/null +++ b/tests/linux-deploy-upgrade-policy.test.js @@ -0,0 +1,40 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' + +const read = path => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const script = read('scripts/linux-deploy.sh') +const readme = read('README.md') +const linuxGuide = read('docs/linux-deploy.md') + +test('Linux Web 升级失败必须中止并保留完整 Git 错误', () => { + assert.doesNotMatch(script, /git pull origin main 2>\/dev\/null \|\| true/) + assert.match(script, /git pull --ff-only origin main/) + assert.match(script, /源码更新失败/) +}) + +test('Linux Web 升级必须识别已有 systemd 工作目录并阻止权限模式错位', () => { + assert.match(script, /select_install_context\(\)/) + assert.match(script, /systemctl show clawpanel[^\n]*WorkingDirectory/) + assert.match(script, /检测到系统级 ClawPanel 安装/) + assert.match(script, /curl -fsSL[^\n]*\| sudo bash/) +}) + +test('Linux Web 构建后必须重启已有服务并输出实际版本', () => { + assert.match(script, /systemctl restart clawpanel/) + assert.match(script, /systemctl --user restart clawpanel/) + assert.doesNotMatch(script, /systemctl start clawpanel/) + assert.doesNotMatch(script, /systemctl --user start clawpanel/) + assert.match(script, /ClawPanel 版本:.*PANEL_VERSION/) +}) + +test('Linux Web 升级文档必须区分 system 与 user 服务并提供可诊断命令', () => { + for (const [name, content] of [ + ['README.md', readme], + ['docs/linux-deploy.md', linuxGuide], + ]) { + assert.match(content, /curl -fsSL[^\n]*\| sudo bash/, `${name} 缺少系统级升级命令`) + assert.match(content, /git pull --ff-only origin main/, `${name} 仍使用不可诊断的 git pull`) + assert.match(content, /node -p [^\n]*package\.json/, `${name} 缺少升级后版本核验`) + } +})