name: '🛡️ 语言保护罩 + Phase 1 · ZY-SVR-006 部署' # ═══════════════════════════════════════════════════════════ # 光湖智库节点 Phase 1 部署工作流 # 编号: ZY-WF-NOVEL-SHIELD # 守护: 铸渊 · ICE-GL-ZY001 # 版权: 国作登字-2026-A-00037559 # # 目标服务器: ZY-SVR-006 (43.153.203.105 · 新加坡 · 智库节点) # # 功能: # 1. deploy — 部署 Phase 1 完整栈(Nginx保护罩+fail2ban+Node.js API+PM2+SSL) # 2. setup-ssl — 单独为 novel.guanghuyaoming.com 申请/续签 SSL 证书 # 3. health — 检查保护罩 + API 运行状态 # 4. rollback — 回滚到上一份配置 # # 所需 GitHub Secrets (全部7个): # ZY_NOVEL_HOST — ZY-SVR-006 新加坡服务器 IP (43.153.203.105) # ZY_NOVEL_KEY — ZY-SVR-006 SSH 私钥 (PEM格式) # ZY_NOVEL_USER — ZY-SVR-006 SSH 用户名 (root) # ZY_NOVEL_PATH — 部署路径 (/opt/novel-db) # ZY_NOVEL_JWT_SECRET — JWT token 签名密钥 (64位随机) # ZY_NOVEL_COS_BUCKET — COS 桶名 (zy-team-hub-sg-1317346199) # ZY_NOVEL_COS_REGION — COS 区域 (ap-singapore) # ═══════════════════════════════════════════════════════════ on: push: branches: [main] paths: - 'server/nginx/novel-mirror-shield.conf' - 'server/novel-db/**' workflow_dispatch: inputs: action: description: '执行动作' required: true default: 'deploy' type: choice options: - deploy - setup-ssl - health - rollback jobs: # ═══════════════════════════════════════════════════════ # Job 1: Phase 1 完整部署 # ═══════════════════════════════════════════════════════ deploy: name: '🚀 Phase 1 完整部署' runs-on: ubuntu-latest permissions: contents: read if: | github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'deploy') steps: - name: '📥 检出代码' uses: actions/checkout@v4 - name: '🔑 配置 SSH' run: | mkdir -p ~/.ssh echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key chmod 600 ~/.ssh/novel_key ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts - name: '📦 安装服务器依赖 (nginx + fail2ban + certbot)' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' set -e apt-get update -q apt-get install -y nginx fail2ban certbot python3-certbot-nginx mkdir -p ${{ secrets.ZY_NOVEL_PATH }}/app/public mkdir -p ${{ secrets.ZY_NOVEL_PATH }}/logs mkdir -p /var/www/certbot touch /var/log/novel-shield-bans.log chmod 644 /var/log/novel-shield-bans.log echo "=== 服务器依赖安装完成 ===" ENDSSH - name: '📤 上传应用代码' run: | # 上传 app 目录 (index.js, package.json, public/) scp -r -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/novel-db/app/ \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:${{ secrets.ZY_NOVEL_PATH }}/ # 上传 PM2 生态配置 scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/novel-db/ecosystem.config.js \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:${{ secrets.ZY_NOVEL_PATH }}/ecosystem.config.js - name: '⚙️ 生成 .env 并安装 Node.js 依赖' env: JWT_SECRET: ${{ secrets.ZY_NOVEL_JWT_SECRET }} COS_BUCKET: ${{ secrets.ZY_NOVEL_COS_BUCKET }} COS_REGION: ${{ secrets.ZY_NOVEL_COS_REGION }} run: | # 在 runner 上生成 .env 文件,再 scp 上传(避免 heredoc 展开 secrets) cat > /tmp/novel-db.env << EOF NODE_ENV=production PORT=4000 JWT_SECRET=${JWT_SECRET} COS_BUCKET=${COS_BUCKET} COS_REGION=${COS_REGION} BAN_LOG_PATH=/var/log/novel-shield-bans.log NGINX_LOG=/var/log/nginx/novel-access.log EOF scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ /tmp/novel-db.env \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:${{ secrets.ZY_NOVEL_PATH }}/.env ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} \ 'chmod 600 ${{ secrets.ZY_NOVEL_PATH }}/.env && cd ${{ secrets.ZY_NOVEL_PATH }}/app && npm install --production --silent && echo "✓ Node.js 依赖安装完成"' - name: '🚀 启动/重启 PM2 应用' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' set -e cd ${{ secrets.ZY_NOVEL_PATH }} # 如果 PM2 进程已存在则重启,否则启动 pm2 describe novel-api > /dev/null 2>&1 \ && pm2 restart novel-api \ || pm2 start ecosystem.config.js pm2 save # 确保开机自启(systemd,幂等) pm2 startup systemd -u root --hp /root 2>/dev/null || true systemctl enable pm2-root 2>/dev/null || true echo "=== PM2 启动完成 ===" pm2 list ENDSSH - name: '🗄️ 备份现有 Nginx 配置' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' BACKUP_TS=$(date +%Y%m%d_%H%M%S) if [ -f /etc/nginx/sites-available/novel-mirror-shield.conf ]; then cp /etc/nginx/sites-available/novel-mirror-shield.conf \ /etc/nginx/sites-available/novel-mirror-shield.conf.bak.$BACKUP_TS echo "已备份 → novel-mirror-shield.conf.bak.$BACKUP_TS" fi ENDSSH - name: '📤 上传 Nginx 语言保护罩配置' run: | scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/nginx/novel-mirror-shield.conf \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/nginx/sites-available/novel-mirror-shield.conf - name: '🔧 配置 Nginx 全局速率限制' run: | scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/novel-db/security/nginx-rate-limit.conf \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/nginx/conf.d/shield-rate-limit.conf - name: '🔗 启用 Nginx 站点 + SSL 占位引导' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' set -e ln -sf /etc/nginx/sites-available/novel-mirror-shield.conf \ /etc/nginx/sites-enabled/novel-mirror-shield.conf rm -f /etc/nginx/sites-enabled/default mkdir -p /var/www/certbot /etc/letsencrypt/live/novel.guanghuyaoming.com # ─── SSL 占位文件引导(解决鸡蛋问题) ─── # novel-mirror-shield.conf 的 HTTPS 块引用 letsencrypt 文件 # 全新服务器上这些文件不存在 → nginx -t 失败 → certbot 也无法运行 # 修复方案: 先创建临时占位文件让 nginx -t 通过,certbot 申请成功后会替换为真实文件 if [ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]; then printf '%s\n' \ 'ssl_session_cache shared:le_nginx_SSL:10m;' \ 'ssl_session_timeout 1440m;' \ 'ssl_session_tickets off;' \ 'ssl_protocols TLSv1.2 TLSv1.3;' \ 'ssl_prefer_server_ciphers off;' \ 'ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256";' \ > /etc/letsencrypt/options-ssl-nginx.conf echo "✓ 已创建 options-ssl-nginx.conf 占位文件" fi if [ ! -f /etc/letsencrypt/ssl-dhparams.pem ]; then openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null echo "✓ 已生成 ssl-dhparams.pem 占位文件 (2048-bit)" fi if [ ! -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then openssl req -x509 -nodes -newkey rsa:2048 \ -keyout /etc/letsencrypt/live/novel.guanghuyaoming.com/privkey.pem \ -out /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem \ -days 30 -subj '/CN=novel.guanghuyaoming.com' 2>/dev/null echo "✓ 已创建临时自签名证书占位(30天有效·certbot 申请后替换)" fi nginx -t systemctl reload nginx || systemctl restart nginx echo "✓ Nginx 重载成功" ENDSSH - name: '🛡️ 部署 fail2ban L3 保护层' run: | scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/novel-db/security/fail2ban-novel.conf \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/jail.d/novel-shield.conf scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/novel-db/security/fail2ban-filter-novel-scan.conf \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/filter.d/novel-scan.conf scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/novel-db/security/fail2ban-filter-novel-4xx.conf \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/filter.d/novel-4xx.conf scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ server/novel-db/security/fail2ban-action-ban-log.conf \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/action.d/novel-ban-log.conf ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' systemctl enable fail2ban systemctl restart fail2ban echo "✓ fail2ban 启动成功" # 等待 fail2ban socket 就绪(异步启动,socket 需要数秒创建) for i in 1 2 3 4 5; do if [ -S /var/run/fail2ban/fail2ban.sock ]; then echo "✓ fail2ban socket 已就绪 (第${i}次检测)" break fi echo "⏳ 等待 fail2ban socket 就绪... (${i}/5)" sleep 2 done fail2ban-client status || echo "⚠ fail2ban-client status 查询失败,服务可能仍在初始化" ENDSSH - name: '🔒 配置 iptables 扫描端口引流 (L3 增强)' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' set -e for PORT in 8080 8443 3000 3001 3002 8888 9090; do iptables -t nat -D PREROUTING -p tcp --dport $PORT -j REDIRECT --to-port 80 2>/dev/null || true iptables -t nat -A PREROUTING -p tcp --dport $PORT -j REDIRECT --to-port 80 done apt-get install -y iptables-persistent -q || true iptables-save > /etc/iptables/rules.v4 2>/dev/null || netfilter-persistent save 2>/dev/null || true echo "✓ iptables 扫描端口引流配置完成" ENDSSH - name: "🔐 申请/续签 Let's Encrypt SSL 证书" run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' set -e mkdir -p /var/www/certbot # 检查是否是真实的 Let's Encrypt 证书,或只是上一步创建的占位自签名证书 CERT_ISSUER=$(openssl x509 -issuer -noout \ -in /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem 2>/dev/null || echo "none") if echo "$CERT_ISSUER" | grep -qi "Let's Encrypt\|letsencrypt\|ISRG"; then echo "✓ Let's Encrypt 证书已存在,执行续签检查..." certbot renew --quiet || true else echo "=== 申请 Let's Encrypt 证书 (webroot 方式,不依赖 nginx 插件) ===" # certonly --webroot: 不会尝试修改 nginx 配置,不会触发 nginx -t # nginx 已在上一步启动(HTTP + 占位HTTPS),/.well-known/acme-challenge/ 可正常响应 certbot certonly --webroot \ -w /var/www/certbot \ -d novel.guanghuyaoming.com \ --non-interactive \ --agree-tos \ --email admin@guanghuyaoming.com \ --preferred-challenges http \ --quiet echo "✓ Let's Encrypt 证书申请成功,重载 Nginx 激活真实证书..." systemctl reload nginx fi certbot certificates 2>/dev/null | grep -E "Domains|Expiry|Certificate" || true echo "✓ HTTPS 已激活 · novel.guanghuyaoming.com" ENDSSH - name: '✅ 完整部署验证' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' echo "═══ Phase 1 完整部署验证 · ZY-SVR-006 ═══" echo "" echo "─── Node.js API (PM2) ───" pm2 describe novel-api | grep -E "status|pid|uptime|restarts" | head -5 HTTP_API=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://127.0.0.1:4000/api/health 2>/dev/null || echo "超时") echo "API /api/health: $HTTP_API" echo "" echo "─── Nginx 蜜罐 (L2) ───" systemctl is-active nginx && echo "✓ Nginx 在线" || echo "✗ Nginx 离线" HTTP_80=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://127.0.0.1/ 2>/dev/null || echo "超时") echo "蜜罐 HTTP 响应: $HTTP_80" echo "" echo "─── fail2ban (L3) ───" systemctl is-active fail2ban && echo "✓ fail2ban 在线" || echo "✗ fail2ban 离线" fail2ban-client status 2>/dev/null | grep "Jail list" || true echo "" echo "─── SSL 证书 ───" if [ -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then echo "✓ SSL 证书已安装" openssl x509 -enddate -noout -in /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem 2>/dev/null || true else echo "⚠ SSL 证书待申请 (DNS 传播中?运行 setup-ssl 重试)" fi echo "" echo "═══ Phase 1 部署完成 · 语言保护罩 L2+L3 已激活 ═══" ENDSSH # ═══════════════════════════════════════════════════════ # Job 2: 申请 SSL 证书 # ═══════════════════════════════════════════════════════ setup-ssl: name: '🔐 申请 SSL 证书' runs-on: ubuntu-latest permissions: {} if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'setup-ssl' steps: - name: '🔑 配置 SSH' run: | mkdir -p ~/.ssh echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key chmod 600 ~/.ssh/novel_key ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts - name: "🔐 申请 Let's Encrypt SSL 证书 (webroot)" run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' set -e # 安装 certbot (不安装 python3-certbot-nginx,避免 nginx 插件干扰 webroot 方式) apt-get update -q apt-get install -y certbot mkdir -p /var/www/certbot /etc/letsencrypt/live/novel.guanghuyaoming.com # ─── SSL 占位文件引导(与 deploy job 相同逻辑) ─── # nginx 配置引用 letsencrypt 文件,全新服务器上不存在 → nginx -t 失败 # 先创建临时占位文件让 nginx 可以运行 if [ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]; then printf '%s\n' \ 'ssl_session_cache shared:le_nginx_SSL:10m;' \ 'ssl_session_timeout 1440m;' \ 'ssl_session_tickets off;' \ 'ssl_protocols TLSv1.2 TLSv1.3;' \ 'ssl_prefer_server_ciphers off;' \ 'ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256";' \ > /etc/letsencrypt/options-ssl-nginx.conf echo "✓ 已创建 options-ssl-nginx.conf 占位文件" fi if [ ! -f /etc/letsencrypt/ssl-dhparams.pem ]; then openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null echo "✓ 已生成 ssl-dhparams.pem 占位文件 (2048-bit)" fi if [ ! -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then openssl req -x509 -nodes -newkey rsa:2048 \ -keyout /etc/letsencrypt/live/novel.guanghuyaoming.com/privkey.pem \ -out /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem \ -days 30 -subj '/CN=novel.guanghuyaoming.com' 2>/dev/null echo "✓ 已创建临时自签名证书占位(30天有效·certbot 申请后替换)" fi # 确保 nginx 能启动(占位文件就绪) nginx -t && systemctl reload nginx || systemctl restart nginx # 使用 webroot 方式申请证书 # --preferred-challenges http 确保只用 HTTP-01 验证,不触发 nginx 插件 certbot certonly --webroot \ -w /var/www/certbot \ -d novel.guanghuyaoming.com \ --non-interactive \ --agree-tos \ --email admin@guanghuyaoming.com \ --preferred-challenges http echo "✓ Let's Encrypt 证书申请成功" # 重载 nginx 使用真实证书 systemctl reload nginx echo "✓ Nginx 重载 · HTTPS 已激活" # 设置自动续期(幂等) if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then (crontab -l 2>/dev/null; echo "0 3 1 * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab - echo "✓ SSL 自动续期 cron 已配置" fi certbot certificates 2>/dev/null | grep -E "Domains|Expiry|Certificate" || true echo "=== SSL 证书配置完成 ===" ENDSSH # ═══════════════════════════════════════════════════════ # Job 3: 健康检查 # ═══════════════════════════════════════════════════════ health: name: '🔍 保护罩健康检查' runs-on: ubuntu-latest permissions: {} if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'health' steps: - name: '🔑 配置 SSH' run: | mkdir -p ~/.ssh echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key chmod 600 ~/.ssh/novel_key ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts - name: '🔍 检查保护罩状态' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' echo "═══ 语言保护罩健康检查 · ZY-SVR-006 ═══" echo "" echo "─── Nginx 状态 ───" systemctl is-active nginx && echo "✓ Nginx 在线" || echo "✗ Nginx 离线" nginx -t 2>&1 | tail -2 echo "" echo "─── fail2ban 状态 ───" systemctl is-active fail2ban && echo "✓ fail2ban 在线" || echo "✗ fail2ban 离线" fail2ban-client status 2>/dev/null || echo "⚠ fail2ban-client 无法连接 socket" echo "" echo "─── 今日封禁记录 ───" TODAY=$(date +%Y-%m-%d) BANS=$(grep "$TODAY" /var/log/novel-shield-bans.log 2>/dev/null | wc -l) echo "今日拦截: $BANS 次" if [ "$BANS" -gt 0 ]; then echo "最近10条:" tail -10 /var/log/novel-shield-bans.log fi echo "" echo "─── iptables 扫描引流规则 ───" iptables -t nat -L PREROUTING --line-numbers 2>/dev/null | grep -E "REDIRECT|target" | head -15 || echo "无规则或无权限查看" echo "" echo "─── 蜜罐响应测试 ───" HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://127.0.0.1/ 2>/dev/null || echo "超时") echo "蜜罐 HTTP 响应码: $HTTP_CODE" echo "" echo "─── SSL 证书状态 ───" if [ -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then EXPIRY=$(openssl x509 -enddate -noout -in /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem 2>/dev/null || echo "无法读取") echo "SSL 证书到期: $EXPIRY" else echo "SSL 证书: 未申请 (运行 setup-ssl 动作)" fi echo "" echo "═══ 检查完成 ═══" ENDSSH # ═══════════════════════════════════════════════════════ # Job 4: 回滚 # ═══════════════════════════════════════════════════════ rollback: name: '⏪ 回滚配置' runs-on: ubuntu-latest permissions: {} if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'rollback' steps: - name: '🔑 配置 SSH' run: | mkdir -p ~/.ssh echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key chmod 600 ~/.ssh/novel_key ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts - name: '⏪ 回滚到上一份配置' run: | ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \ ${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH' set -e LATEST_BAK=$(ls -t /etc/nginx/sites-available/novel-mirror-shield.conf.bak.* 2>/dev/null | head -1) if [ -z "$LATEST_BAK" ]; then echo "✗ 没有找到备份文件,无法回滚" exit 1 fi echo "回滚到: $LATEST_BAK" cp "$LATEST_BAK" /etc/nginx/sites-available/novel-mirror-shield.conf nginx -t systemctl reload nginx echo "✓ 回滚成功" ENDSSH