guanghulab/.github/workflows/deploy-zhiku-guanghu-online.yml
2026-05-10 13:12:44 +08:00

695 lines
29 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

name: '📚 光湖智库 · guanghu.online 部署'
# ═══════════════════════════════════════════════════════════
# 光湖智库节点独立部署工作流
# 项目编号: ZY-PROJ-006
# 域名: guanghu.online
# 服务器: ZY-SVR-006 (43.153.203.105 · 新加坡)
# 守护: 铸渊 · ICE-GL-ZY001
# 版权: 国作登字-2026-A-00037559
#
# 功能:
# 1. deploy — 部署前端 + 后端 + Nginx + PM2
# 2. setup-ssl — 申请/续签 SSL 证书
# 3. health — 检查 API + 前端运行状态
# 4. rollback — 回滚到上一份配置
#
# 所需 GitHub Secrets:
# ZY_NOVEL_HOST — ZY-SVR-006 IP (43.153.203.105)
# ZY_NOVEL_KEY — ZY-SVR-006 SSH 私钥
# ZY_NOVEL_USER — ZY-SVR-006 SSH 用户名 (root)
# ZY_NOVEL_JWT_SECRET — JWT 签名密钥
# ZY_OSS_KEY — 腾讯云 COS SecretId仓库已有·映射为 ZY_COS_SECRET_ID
# ZY_OSS_SECRET — 腾讯云 COS SecretKey仓库已有·映射为 ZY_COS_SECRET_KEY
# ZY_NOVEL_COS_BUCKET — COS 桶名(仓库已有·映射为 ZY_COS_BUCKET
# ZY_COS_REGION 不使用 Secret · 硬编码 ap-singapore · 避免覆盖广州桶配置)
# ZY_ZHIKU_DOMAIN — 域名 (guanghu.online)
# ZY_SMTP_USER — SMTP 发件邮箱 (如 565183519@qq.com)
# ZY_SMTP_PASS — SMTP 应用专用密码 (非邮箱登录密码)
# ZY_DEEPSEEK_API_KEY — DeepSeek LLM API 密钥 (Agent对话)
# ═══════════════════════════════════════════════════════════
on:
push:
branches: [main]
paths:
- 'server/zhiku-node/**'
- 'server/nginx/zhiku-guanghu-online.conf'
workflow_dispatch:
inputs:
action:
description: '执行动作'
required: true
default: 'deploy'
type: choice
options:
- deploy
- setup-datasources
- setup-ssl
- health
- rollback
jobs:
# ═══════════════════════════════════════════════════════
# Job 1: 完整部署
# ═══════════════════════════════════════════════════════
deploy:
name: '🚀 智库节点部署 · guanghu.online'
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/zhiku_key
chmod 600 ~/.ssh/zhiku_key
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
- name: '📦 安装服务器基础依赖'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
# 安装 Node.js 20 (如果还没有)
if ! command -v node &>/dev/null || [ "$(node -v | cut -d. -f1 | tr -d v)" -lt 20 ]; then
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
fi
# 安装 PM2
npm list -g pm2 &>/dev/null || npm install -g pm2
# 创建目录结构
mkdir -p /opt/zhiku/public/assets/{css,js,img}
mkdir -p /opt/zhiku/public/modules
mkdir -p /opt/zhiku/server
mkdir -p /opt/zhiku/data/index
mkdir -p /opt/zhiku/data/books
mkdir -p /opt/zhiku/data/guardian
mkdir -p /opt/zhiku/datasources/fqweb
mkdir -p /opt/zhiku/datasources/swiftcat
mkdir -p /var/log/zhiku
mkdir -p /var/www/certbot
echo "[ZY-SVR-006] 基础目录已就绪"
ENDSSH
- name: '📤 上传前端文件'
run: |
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no -r \
server/zhiku-node/public/* \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/public/
- name: '📤 上传后端文件'
run: |
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no -r \
server/zhiku-node/server/package.json \
server/zhiku-node/server/package-lock.json \
server/zhiku-node/server/server.js \
server/zhiku-node/server/.env.example \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/server/
- name: '📤 上传镜面 Agent + 镜防模块 + 书岚 Agent + 内置数据源'
run: |
# 创建远程目录(含铸渊哨兵 + 永久记忆存储)
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} \
'mkdir -p /opt/zhiku/server/mirror-agent/data/{snapshots,tickets,memory} /opt/zhiku/server/mirror-shield /opt/zhiku/server/shulan-agent /opt/zhiku/server/builtin-source /opt/zhiku/server/zhuyuan-sentinel /opt/zhiku/data/guardian /opt/zhiku/data/sentinel'
# 上传 mirror-agent
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no -r \
server/zhiku-node/server/mirror-agent/*.js \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/server/mirror-agent/
# 上传 mirror-shield
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no -r \
server/zhiku-node/server/mirror-shield/*.js \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/server/mirror-shield/
# 上传 shulan-agent书岚人格体系统
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no -r \
server/zhiku-node/server/shulan-agent/*.js \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/server/shulan-agent/
# 上传 builtin-source内置数据源直连 · 搜索下载fallback
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no -r \
server/zhiku-node/server/builtin-source/*.js \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/server/builtin-source/
# 上传 zhuyuan-sentinel铸渊哨兵 · 永久记忆 + 书源自动运维)
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no -r \
server/zhiku-node/server/zhuyuan-sentinel/*.js \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/server/zhuyuan-sentinel/
- name: '📤 上传 PM2 配置'
run: |
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
server/zhiku-node/ecosystem.config.js \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/opt/zhiku/
- name: '📤 上传 Nginx 配置'
run: |
scp -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
server/nginx/zhiku-guanghu-online.conf \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/nginx/sites-available/zhiku
- name: '⚙️ 配置环境变量 + 安装依赖 + 启动服务'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
# ─── 写入 .env ───
cat > /opt/zhiku/server/.env << 'ENVEOF'
PORT=3006
NODE_ENV=production
ZY_ZHIKU_JWT_SECRET=${{ secrets.ZY_NOVEL_JWT_SECRET }}
ZY_ZHIKU_TOKEN_TTL=86400
ZY_COS_SECRET_ID=${{ secrets.ZY_OSS_KEY }}
ZY_COS_SECRET_KEY=${{ secrets.ZY_OSS_SECRET }}
ZY_COS_BUCKET=${{ secrets.ZY_NOVEL_COS_BUCKET }}
ZY_COS_REGION=ap-singapore
ZY_ZHIKU_DATA_DIR=/opt/zhiku/data
ZY_ZHIKU_LOG_DIR=/var/log/zhiku
ZY_ZHIKU_DOMAIN=${{ secrets.ZY_ZHIKU_DOMAIN }}
ZY_FANQIE_API_URL=http://127.0.0.1:9999
ZY_QIMAO_API_URL=http://127.0.0.1:7700
ZY_SMTP_PORT=465
ZY_SMTP_USER=${{ secrets.ZY_SMTP_USER }}
ZY_SMTP_PASS=${{ secrets.ZY_SMTP_PASS }}
ZY_DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
ZY_DEEPSEEK_API_KEY=${{ secrets.ZY_DEEPSEEK_API_KEY }}
ENVEOF
# 去除可能的前导空格
sed -i 's/^[[:space:]]*//' /opt/zhiku/server/.env
# ─── 安装 Node.js 依赖 ───
cd /opt/zhiku/server
npm install --production 2>&1 | tail -5
# ─── Nginx 配置 ───
ln -sf /etc/nginx/sites-available/zhiku /etc/nginx/sites-enabled/zhiku
# 检查 SSL 证书是否存在,不存在则先用 HTTP 模式
if [ ! -f /etc/letsencrypt/live/guanghu.online/fullchain.pem ]; then
echo "[ZY-SVR-006] SSL 证书尚未申请,先使用 HTTP 模式"
# 临时替换 Nginx 配置为 HTTP only
cat > /etc/nginx/sites-available/zhiku << 'NGINX_TEMP'
server {
listen 80;
listen [::]:80;
server_name guanghu.online www.guanghu.online;
root /opt/zhiku/public;
index index.html;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location /api/ {
proxy_pass http://127.0.0.1:3006;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /assets/ {
expires 7d;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.html;
}
}
NGINX_TEMP
fi
nginx -t && systemctl reload nginx
echo "[ZY-SVR-006] Nginx 已重载"
# ─── PM2 启动/重启 ───
cd /opt/zhiku
if pm2 describe zhiku-api &>/dev/null; then
pm2 restart zhiku-api
echo "[ZY-SVR-006] zhiku-api 已重启"
else
pm2 start ecosystem.config.js
echo "[ZY-SVR-006] zhiku-api 已启动"
fi
pm2 save
pm2 startup systemd -u root --hp /root 2>/dev/null || true
echo "[ZY-SVR-006] 部署完成 · guanghu.online"
ENDSSH
- name: '🏥 健康检查'
run: |
sleep 5
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
echo "=== PM2 状态 ==="
pm2 list
echo ""
echo "=== API 健康检查 ==="
curl -s http://127.0.0.1:3006/api/health | python3 -m json.tool || echo "API 未响应"
echo ""
echo "=== Nginx 状态 ==="
systemctl status nginx --no-pager -l | head -10
ENDSSH
# ═══════════════════════════════════════════════════════
# Job 2: 部署数据源服务 (FQWeb + SwiftCat)
# ═══════════════════════════════════════════════════════
setup-datasources:
name: '📡 部署数据源服务 · FQWeb + SwiftCat'
runs-on: ubuntu-latest
permissions:
contents: read
if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'setup-datasources'
steps:
- name: '🔑 配置 SSH'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/zhiku_key
chmod 600 ~/.ssh/zhiku_key
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
- name: '☕ 安装 JDK 17 + Python3 运行环境'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
echo "[ZY-SVR-006] ─── 安装运行环境 ───"
# JDK 17+ (FQWeb 运行需要 JRE · Gradle 构建需要完整 JDK)
if ! javac -version 2>&1 | grep -q '17\|21'; then
echo "[ZY-SVR-006] 安装 JDK 17..."
apt-get update -q
apt-get install -y openjdk-17-jdk-headless
else
echo "[ZY-SVR-006] JDK 已安装: $(javac -version 2>&1 | head -1)"
fi
# Python 3 + pip (SwiftCat 需要)
if ! command -v python3 &>/dev/null; then
echo "[ZY-SVR-006] 安装 Python3..."
apt-get update -q
apt-get install -y python3 python3-pip python3-venv
else
echo "[ZY-SVR-006] Python3 已安装: $(python3 --version)"
fi
# 创建数据源服务目录
mkdir -p /opt/zhiku/datasources/fqweb
mkdir -p /opt/zhiku/datasources/swiftcat
mkdir -p /var/log/zhiku
echo "[ZY-SVR-006] 运行环境就绪"
ENDSSH
- name: '📚 部署 FQWeb (番茄小说 API 服务)'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
echo "[ZY-SVR-006] ─── 部署 FQWeb ───"
cd /opt/zhiku/datasources/fqweb
# 检查已有 JAR
FQWEB_JAR=$(ls -t *.jar 2>/dev/null | head -1)
if [ -z "$FQWEB_JAR" ]; then
echo "[ZY-SVR-006] 下载 FQWeb JAR..."
# 依次尝试: 上游 6DDUU6/FQWeb → fork benefit77/FanQieWeb
RELEASE_URL=""
for REPO in "6DDUU6/FQWeb" "benefit77/FanQieWeb"; do
echo "[ZY-SVR-006] 查询 ${REPO} Release..."
RELEASE_URL=$(curl -s "https://api.github.com/repos/${REPO}/releases/latest" \
| python3 -c "import sys,json; assets=json.load(sys.stdin).get('assets',[]); print(next((a['browser_download_url'] for a in assets if a['name'].endswith('.jar')), ''))" 2>/dev/null)
if [ -n "$RELEASE_URL" ]; then
echo "[ZY-SVR-006] 找到 Release: ${REPO}"
break
fi
done
if [ -n "$RELEASE_URL" ]; then
curl -L -o fqweb.jar "$RELEASE_URL"
FQWEB_JAR="fqweb.jar"
echo "[ZY-SVR-006] FQWeb JAR 已下载"
else
echo "[ZY-SVR-006] ⚠️ 无 Release 可用,尝试从源码构建..."
# 备用:克隆并用 gradlew 构建(需要 JDK
rm -rf source 2>/dev/null
CLONE_OK=false
for REPO in "6DDUU6/FQWeb" "benefit77/FanQieWeb"; do
echo "[ZY-SVR-006] 尝试克隆 ${REPO}..."
if git clone --depth 1 "https://github.com/${REPO}.git" source 2>/dev/null; then
CLONE_OK=true
echo "[ZY-SVR-006] 已克隆 ${REPO}"
break
fi
done
if [ "$CLONE_OK" = true ] && [ -d "source" ]; then
cd source
if [ -f "gradlew" ]; then
chmod +x gradlew
echo "[ZY-SVR-006] 开始 Gradle 构建(需要 JDK 17+..."
./gradlew build -x test 2>&1 | tail -10
BUILT_JAR=$(find build/libs -name "*.jar" -not -name "*plain*" 2>/dev/null | head -1)
if [ -n "$BUILT_JAR" ]; then
cp "$BUILT_JAR" /opt/zhiku/datasources/fqweb/fqweb.jar
FQWEB_JAR="fqweb.jar"
echo "[ZY-SVR-006] ✅ FQWeb 构建成功"
else
echo "[ZY-SVR-006] ⚠️ 构建产物未找到build/libs 目录不存在或无 JAR"
fi
else
echo "[ZY-SVR-006] ⚠️ 无 gradlew 文件,无法构建"
fi
cd /opt/zhiku/datasources/fqweb
fi
fi
fi
if [ -n "$FQWEB_JAR" ]; then
echo "[ZY-SVR-006] FQWeb JAR: $FQWEB_JAR"
# 写启动脚本
cat > start.sh << 'STARTEOF'
#!/bin/bash
cd /opt/zhiku/datasources/fqweb
exec java -jar $(ls -t *.jar | head -1) --server.port=9999
STARTEOF
chmod +x start.sh
echo "[ZY-SVR-006] FQWeb 部署完成"
else
echo "[ZY-SVR-006] ⚠️ FQWeb JAR 未找到,跳过"
fi
ENDSSH
- name: '🐱 部署 SwiftCat (七猫小说下载器 API)'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
echo "[ZY-SVR-006] ─── 部署 SwiftCat ───"
cd /opt/zhiku/datasources/swiftcat
SWIFTCAT_READY=false
# ─── 策略1: 已有可用代码git pull 更新)───
if [ -d ".git" ] && { [ -f "requirements.txt" ] || [ -f "setup.py" ] || [ -f "pyproject.toml" ]; }; then
echo "[ZY-SVR-006] 已有 SwiftCat 代码,尝试更新..."
git pull --ff-only 2>/dev/null || true
SWIFTCAT_READY=true
else
# 清理非 Python 源码的残留目录(如上次克隆了 binary-only 的 swiftcat-downloader
if [ -d ".git" ] && ! { [ -f "requirements.txt" ] || [ -f "setup.py" ] || [ -f "pyproject.toml" ]; }; then
echo "[ZY-SVR-006] 清理无效的旧克隆..."
cd /opt/zhiku/datasources
rm -rf swiftcat
mkdir -p swiftcat
cd swiftcat
fi
# ─── 策略2: 克隆有 Python 源码的仓库 ───
# 7mao-novel-downloader (已归档但仍可克隆·有 Python 源码)
echo "[ZY-SVR-006] 尝试克隆 7mao-novel-downloader (Python 源码)..."
if git clone --depth 1 https://github.com/shing-yu/7mao-novel-downloader.git . 2>/dev/null; then
if [ -f "requirements.txt" ] || [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then
echo "[ZY-SVR-006] ✅ 7mao-novel-downloader 克隆成功 (有 Python 依赖)"
SWIFTCAT_READY=true
else
echo "[ZY-SVR-006] 7mao-novel-downloader 无 Python 依赖文件"
fi
else
echo "[ZY-SVR-006] 7mao-novel-downloader 仓库不可达"
fi
# ─── 策略3: 如果源码不可用,下载 release 二进制 ───
if [ "$SWIFTCAT_READY" = false ]; then
echo "[ZY-SVR-006] 尝试下载 swiftcat-downloader release 二进制..."
# 清理可能的残留
rm -rf .git 2>/dev/null
BINARY_URL=$(curl -s "https://api.github.com/repos/shing-yu/swiftcat-downloader/releases/latest" \
| python3 -c "import sys,json; assets=json.load(sys.stdin).get('assets',[]); print(next((a['browser_download_url'] for a in assets if 'linux' in a['name'].lower()), ''))" 2>/dev/null)
if [ -n "$BINARY_URL" ]; then
curl -L -o swiftcat "$BINARY_URL"
chmod +x swiftcat
echo "[ZY-SVR-006] ✅ SwiftCat 二进制已下载"
# 为二进制创建启动脚本
cat > start.sh << 'STARTEOF'
#!/bin/bash
cd /opt/zhiku/datasources/swiftcat
export PORT=7700
if [ -f "swiftcat" ]; then
exec ./swiftcat --port ${PORT} 2>/dev/null || exec ./swiftcat
fi
STARTEOF
chmod +x start.sh
SWIFTCAT_READY=binary
echo "[ZY-SVR-006] SwiftCat 二进制部署完成"
else
echo "[ZY-SVR-006] ⚠️ SwiftCat release 也无 Linux 二进制"
fi
fi
fi
# ─── 为 Python 源码创建虚拟环境和启动脚本 ───
if [ "$SWIFTCAT_READY" = true ]; then
if [ -f "requirements.txt" ] || [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then
python3 -m venv venv 2>/dev/null || true
source venv/bin/activate 2>/dev/null || true
if [ -f "requirements.txt" ]; then
pip3 install -r requirements.txt 2>&1 | tail -3
elif [ -f "setup.py" ]; then
pip3 install -e . 2>&1 | tail -3
elif [ -f "pyproject.toml" ]; then
pip3 install . 2>&1 | tail -3
fi
cat > start.sh << 'STARTEOF'
#!/bin/bash
cd /opt/zhiku/datasources/swiftcat
source venv/bin/activate 2>/dev/null || true
export PORT=7700
if [ -f "app.py" ]; then
exec python3 app.py --port ${PORT} 2>/dev/null || exec python3 app.py
elif [ -f "main.py" ]; then
exec python3 main.py --port ${PORT} 2>/dev/null || exec python3 main.py
elif [ -f "server.py" ]; then
exec python3 server.py --port ${PORT} 2>/dev/null || exec python3 server.py
else
echo "[SwiftCat] 未找到入口文件 (app.py/main.py/server.py)"
ls -la *.py 2>/dev/null || echo "无 Python 文件"
fi
STARTEOF
chmod +x start.sh
echo "[ZY-SVR-006] SwiftCat Python 部署完成"
fi
elif [ "$SWIFTCAT_READY" = false ]; then
echo "[ZY-SVR-006] ⚠️ SwiftCat 所有部署策略均失败"
echo "[ZY-SVR-006] 说明: swiftcat-downloader 仅发布二进制(Python版不开源)7mao-novel-downloader 已归档"
echo "[ZY-SVR-006] 建议: 智库系统将自动使用内置直连适配器作为 fallback"
fi
ENDSSH
- name: '🔄 注册到 PM2 并启动'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
echo "[ZY-SVR-006] ─── 注册数据源服务到 PM2 ───"
# FQWeb (番茄小说)
if [ -f "/opt/zhiku/datasources/fqweb/start.sh" ] && ls /opt/zhiku/datasources/fqweb/*.jar &>/dev/null; then
pm2 describe fqweb &>/dev/null && pm2 delete fqweb || true
pm2 start /opt/zhiku/datasources/fqweb/start.sh \
--name fqweb \
--log /var/log/zhiku/fqweb.log \
--time
echo "[ZY-SVR-006] FQWeb 已注册到 PM2"
else
echo "[ZY-SVR-006] ⚠️ FQWeb 未就绪,跳过 PM2 注册"
fi
# SwiftCat (七猫小说)
if [ -f "/opt/zhiku/datasources/swiftcat/start.sh" ]; then
pm2 describe swiftcat &>/dev/null && pm2 delete swiftcat || true
pm2 start /opt/zhiku/datasources/swiftcat/start.sh \
--name swiftcat \
--log /var/log/zhiku/swiftcat.log \
--time
echo "[ZY-SVR-006] SwiftCat 已注册到 PM2"
else
echo "[ZY-SVR-006] ⚠️ SwiftCat 未就绪,跳过 PM2 注册"
fi
pm2 save
echo ""
echo "=== PM2 所有进程状态 ==="
pm2 list
ENDSSH
- name: '🏥 数据源健康检查'
run: |
sleep 8
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
echo "=== FQWeb 检查 (port 9999) ==="
curl -s --max-time 5 http://127.0.0.1:9999/version 2>/dev/null \
&& echo "" && echo "✅ FQWeb 可达" \
|| echo "⚠️ FQWeb 不可达 (端口 9999)"
echo ""
echo "=== SwiftCat 检查 (port 7700) ==="
curl -s --max-time 5 http://127.0.0.1:7700/ 2>/dev/null \
&& echo "" && echo "✅ SwiftCat 可达" \
|| echo "⚠️ SwiftCat 不可达 (端口 7700)"
echo ""
echo "=== 智库 API 搜索测试 ==="
curl -s --max-time 10 http://127.0.0.1:3006/api/health 2>/dev/null | python3 -m json.tool 2>/dev/null \
|| echo "智库 API 未响应"
ENDSSH
# ═══════════════════════════════════════════════════════
# Job 3: SSL 证书申请
# ═══════════════════════════════════════════════════════
setup-ssl:
name: '🔐 SSL 证书申请'
runs-on: ubuntu-latest
permissions:
contents: read
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/zhiku_key
chmod 600 ~/.ssh/zhiku_key
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
- name: '🔐 申请 Let''s Encrypt SSL'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
# 安装 certbot
apt-get update -q
apt-get install -y certbot python3-certbot-nginx
# 申请证书
certbot --nginx -d guanghu.online -d www.guanghu.online \
--non-interactive --agree-tos \
--email admin@guanghu.online \
--redirect
echo "[ZY-SVR-006] SSL 证书已申请 · guanghu.online"
# 上传正式 Nginx 配置(如果存在 SSL 模式的备份)
nginx -t && systemctl reload nginx
ENDSSH
# ═══════════════════════════════════════════════════════
# Job 4: 健康检查
# ═══════════════════════════════════════════════════════
health:
name: '🏥 健康检查'
runs-on: ubuntu-latest
permissions:
contents: read
if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'health'
steps:
- name: '🔑 配置 SSH'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/zhiku_key
chmod 600 ~/.ssh/zhiku_key
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
- name: '🏥 检查服务状态'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
echo "=== PM2 状态 ==="
pm2 list
echo ""
echo "=== zhiku-api 健康检查 ==="
curl -s http://127.0.0.1:3006/api/health | python3 -m json.tool || echo "API 未响应"
echo ""
echo "=== Nginx 状态 ==="
systemctl status nginx --no-pager -l | head -10
echo ""
echo "=== 磁盘使用 ==="
df -h /opt/zhiku
echo ""
echo "=== 日志最后10行 ==="
tail -10 /var/log/zhiku/pm2-zhiku-api.log 2>/dev/null || echo "暂无日志"
ENDSSH
# ═══════════════════════════════════════════════════════
# Job 5: 回滚
# ═══════════════════════════════════════════════════════
rollback:
name: '⏪ 回滚'
runs-on: ubuntu-latest
permissions:
contents: read
if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'rollback'
steps:
- name: '🔑 配置 SSH'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/zhiku_key
chmod 600 ~/.ssh/zhiku_key
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
- name: '⏪ 回滚到备份'
run: |
ssh -i ~/.ssh/zhiku_key -o StrictHostKeyChecking=no \
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
set -e
if [ -d /opt/zhiku.bak ]; then
pm2 stop zhiku-api || true
rm -rf /opt/zhiku.rollback
mv /opt/zhiku /opt/zhiku.rollback
cp -r /opt/zhiku.bak /opt/zhiku
cd /opt/zhiku && pm2 start ecosystem.config.js
pm2 save
echo "[ZY-SVR-006] 已回滚到备份版本"
else
echo "[ZY-SVR-006] 无备份可回滚"
exit 1
fi
ENDSSH