#!/usr/bin/env bash # ============================================================ # GovAI 政务AI平台 - 一键部署脚本(Git + 无Docker) # 目标服务器: agents (154.8.162.18) # 域名: gov.opc8ai.com # 用法: bash deploy.sh [init|update|restart|migrate|status|logs] # init - 首次部署(安装依赖+初始化数据库+部署应用) # update - 更新部署(git提交+构建+上传+迁移+重启) # restart - 仅重启服务 # migrate - 仅执行数据库迁移 # status - 查看远程服务状态 # logs - 查看远程服务日志 # ============================================================ set -euo pipefail SERVER="h2agent" REMOTE_DIR="/opt/govai" DOMAIN="gov.opc8ai.com" PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" GIT_REMOTE="${GIT_REMOTE:-origin}" GIT_BRANCH="${GIT_BRANCH:-main}" DB_USER="govai" DB_PASS="GovAI@2024Secure" DB_NAME="govai_portal" JWT_SECRET="govai-production-jwt-secret-x7k9m2p4" OPENAI_API_KEY="${OPENAI_API_KEY:-sk-placeholder}" OPENAI_BASE_URL="${OPENAI_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}" OPENAI_MODEL="${OPENAI_MODEL:-qwen-plus}" GREEN='\033[0;32m'; BLUE='\033[0;34m'; RED='\033[0;31m'; YELLOW='\033[0;33m'; NC='\033[0m' log() { echo -e "${GREEN}[✓]${NC} $1"; } step() { echo -e "\n${BLUE}==== $1 ====${NC}"; } warn() { echo -e "${YELLOW}[!]${NC} $1"; } err() { echo -e "${RED}[✗]${NC} $1"; exit 1; } # ---- Git 操作 ---- git_commit_and_push() { step "Git 提交与推送" cd "$PROJECT_DIR" if [ ! -d .git ]; then log "初始化 Git 仓库..." git init git add . git commit -m "feat: 初始化 GovAI 政务AI平台" log "Git 仓库已初始化" fi if ! git remote get-url "$GIT_REMOTE" &>/dev/null; then warn "未配置 Git 远程仓库 ($GIT_REMOTE),跳过推送" warn "请手动执行: git remote add origin " return 0 fi local changes changes=$(git status --porcelain) if [ -n "$changes" ]; then log "检测到未提交的更改:" echo "$changes" | head -20 [ "$(echo "$changes" | wc -l)" -gt 20 ] && echo " ... 还有更多文件" echo "" git add . local msg="deploy: $(date '+%Y-%m-%d %H:%M') 更新部署" git commit -m "$msg" log "已提交: $msg" else log "工作区干净,无需提交" fi log "推送到远程 ($GIT_REMOTE/$GIT_BRANCH)..." git push -u "$GIT_REMOTE" "$GIT_BRANCH" 2>&1 || warn "推送失败,继续部署..." } # ---- 本地构建 ---- build_local() { step "本地构建" mkdir -p "$PROJECT_DIR/dist" log "编译 Go 后端 (linux/amd64)..." cd "$PROJECT_DIR/server" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o "$PROJECT_DIR/dist/server" ./cmd/server/ log "后端: dist/server ($(du -h "$PROJECT_DIR/dist/server" | cut -f1))" log "编译 Next.js 前端..." cd "$PROJECT_DIR/apps/web" NEXT_PUBLIC_API_URL="http://localhost:8080" npm run build log "前端编译完成" cd "$PROJECT_DIR" } # ---- 上传文件 ---- upload() { step "上传文件到服务器" ssh $SERVER "mkdir -p $REMOTE_DIR/{server,web,migrations}" log "停止服务以释放文件锁..." ssh $SERVER "systemctl stop govai-api govai-web 2>/dev/null || true" log "上传后端二进制..." scp "$PROJECT_DIR/dist/server" $SERVER:$REMOTE_DIR/server/server log "上传迁移文件..." rsync -az --delete "$PROJECT_DIR/server/migrations/" $SERVER:$REMOTE_DIR/migrations/ log "上传前端构建产物..." rsync -az --delete "$PROJECT_DIR/apps/web/.next/" $SERVER:$REMOTE_DIR/web/.next/ rsync -az "$PROJECT_DIR/apps/web/public/" $SERVER:$REMOTE_DIR/web/public/ 2>/dev/null || true scp "$PROJECT_DIR/apps/web/package.json" $SERVER:$REMOTE_DIR/web/ scp "$PROJECT_DIR/apps/web/next.config.ts" $SERVER:$REMOTE_DIR/web/ log "上传完成" } # ---- 首次初始化服务器 ---- server_init() { step "服务器环境初始化" ssh $SERVER bash << 'REMOTE_INIT' set -e echo "[1/5] 安装软件..." sed -i 's/exclude=.*nginx.*/exclude=cloud-init/' /etc/dnf/dnf.conf 2>/dev/null || true dnf install -y postgresql-server postgresql postgresql-contrib redis nginx-core curl tar git 2>/dev/null if ! command -v node &>/dev/null; then echo "[*] 安装 Node.js 20 (二进制)..." cd /tmp curl -sL https://nodejs.org/dist/v20.18.0/node-v20.18.0-linux-x64.tar.xz -o node.tar.xz tar -xf node.tar.xz cp -r node-v20.18.0-linux-x64/{bin,lib,include,share} /usr/local/ rm -rf node.tar.xz node-v20.18.0-linux-x64 fi echo " Node: $(node -v)" echo "[2/5] 初始化 PostgreSQL..." if [ ! -f /var/lib/pgsql/data/PG_VERSION ]; then postgresql-setup --initdb fi cat > /var/lib/pgsql/data/pg_hba.conf << 'PGHBA' local all postgres peer local all all md5 host all all 127.0.0.1/32 md5 host all all ::1/128 md5 PGHBA echo "[3/5] 启动基础服务..." systemctl enable --now postgresql redis systemctl restart postgresql echo "[4/5] 创建数据库..." cd /tmp sudo -u postgres psql -tc "SELECT 1 FROM pg_roles WHERE rolname='govai'" | grep -q 1 || \ sudo -u postgres psql -c "CREATE USER govai WITH PASSWORD 'GovAI@2024Secure';" sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='govai_portal'" | grep -q 1 || \ sudo -u postgres psql -c "CREATE DATABASE govai_portal OWNER govai;" sudo -u postgres psql -d govai_portal -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" sudo -u postgres psql -d govai_portal -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true echo "[5/5] 安装 migrate..." if ! command -v migrate &>/dev/null; then curl -sL https://gh.idayer.com/https://github.com/golang-migrate/migrate/releases/download/v4.17.0/migrate.linux-amd64.tar.gz -o /tmp/migrate.tar.gz cd /tmp && tar xzf migrate.tar.gz && mv migrate /usr/local/bin/ && rm -f migrate.tar.gz fi echo "✅ 初始化完成" REMOTE_INIT } # ---- 安装前端依赖 ---- install_web_deps() { step "安装前端依赖" ssh $SERVER "cd $REMOTE_DIR/web && npm install --omit=dev --ignore-scripts 2>&1 | tail -3" log "前端依赖安装完成" } # ---- 数据库迁移 ---- migrate_db() { step "数据库迁移" ssh $SERVER bash << REMOTE_MIGRATE set -e export PGPASSWORD="$DB_PASS" echo "[*] 执行 schema 迁移..." migrate -database "postgres://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME?sslmode=disable" \ -path $REMOTE_DIR/migrations up 2>&1 || true echo "[*] 导入种子数据..." for f in $REMOTE_DIR/migrations/seed*.sql; do [ -f "\$f" ] || continue echo " -> \$(basename \$f)" psql -h localhost -U $DB_USER -d $DB_NAME -f "\$f" 2>&1 | tail -1 done echo "✅ 迁移完成" REMOTE_MIGRATE } # ---- 配置服务 ---- setup_services() { step "配置系统服务" ssh $SERVER bash << REMOTE_SVC set -e # .env cat > $REMOTE_DIR/.env << 'DOTENV' SERVER_HOST=0.0.0.0 SERVER_PORT=8080 DATABASE_URL=postgres://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME?sslmode=disable REDIS_URL=redis://localhost:6379 JWT_SECRET=$JWT_SECRET JWT_EXPIRY=24h LLM_PROVIDER=openai OPENAI_API_KEY=$OPENAI_API_KEY OPENAI_BASE_URL=$OPENAI_BASE_URL OPENAI_MODEL=$OPENAI_MODEL DOTENV # govai-api.service cat > /etc/systemd/system/govai-api.service << 'EOF' [Unit] Description=GovAI API Server After=network.target postgresql.service redis.service [Service] Type=simple WorkingDirectory=/opt/govai/server EnvironmentFile=/opt/govai/.env ExecStart=/opt/govai/server/server Restart=always RestartSec=5 LimitNOFILE=65535 [Install] WantedBy=multi-user.target EOF # govai-web.service cat > /etc/systemd/system/govai-web.service << 'EOF' [Unit] Description=GovAI Web Frontend After=network.target govai-api.service [Service] Type=simple WorkingDirectory=/opt/govai/web Environment=NODE_ENV=production Environment=PORT=3000 Environment=NEXT_PUBLIC_API_URL=http://localhost:8080 ExecStart=/usr/bin/node node_modules/.bin/next start -p 3000 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF # nginx cat > /etc/nginx/conf.d/govai.conf << 'NGINXCONF' server { listen 80; server_name $DOMAIN; client_max_body_size 50m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; 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; proxy_read_timeout 300s; } location /api/ { proxy_pass http://127.0.0.1:8080; 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; proxy_read_timeout 300s; } } NGINXCONF # 移除 nginx 默认 server 块 if grep -q 'listen.*80.*default_server' /etc/nginx/nginx.conf; then sed -i '/server {/,/^ }/d' /etc/nginx/nginx.conf 2>/dev/null || true fi chmod +x /opt/govai/server/server systemctl daemon-reload systemctl enable govai-api govai-web nginx REMOTE_SVC log "服务配置完成" } # ---- 启动/重启 ---- start_services() { step "启动服务" ssh $SERVER bash << 'REMOTE_START' systemctl restart govai-api sleep 2 systemctl restart govai-web sleep 2 systemctl restart nginx echo "服务状态:" for svc in govai-api govai-web nginx postgresql redis; do printf " %-15s %s\n" "$svc" "$(systemctl is-active $svc)" done sleep 2 if curl -sf http://localhost:8080/api/v1/store/featured -o /dev/null; then echo -e "\n✅ API 正常" else echo -e "\n⚠️ API 启动中,请稍后检查: journalctl -u govai-api -n 20" fi REMOTE_START echo "" echo -e "${GREEN}════════════════════════════════════════${NC}" echo -e "${GREEN} 部署完成!${NC}" echo -e "${GREEN} http://$DOMAIN${NC}" echo -e "${GREEN} http://154.8.162.18${NC}" echo -e "${GREEN}════════════════════════════════════════${NC}" } # ---- 查看远程状态 ---- remote_status() { step "远程服务状态" ssh $SERVER bash << 'REMOTE_STATUS' echo "── 服务状态 ──" for svc in govai-api govai-web nginx postgresql redis; do status=$(systemctl is-active $svc 2>/dev/null || echo "inactive") uptime="" if [ "$status" = "active" ]; then uptime="($(systemctl show $svc --property=ActiveEnterTimestamp --value 2>/dev/null))" fi printf " %-15s %-10s %s\n" "$svc" "$status" "$uptime" done echo "" echo "── 磁盘使用 ──" df -h / | tail -1 | awk '{printf " 总: %s 已用: %s (%s) 可用: %s\n", $2, $3, $5, $4}' echo "" echo "── 内存使用 ──" free -h | awk '/^Mem:/{printf " 总: %s 已用: %s 可用: %s\n", $2, $3, $7}' echo "" echo "── API 健康检查 ──" if curl -sf http://localhost:8080/api/v1/store/featured -o /dev/null; then echo " ✅ API 正常响应" else echo " ❌ API 无法访问" fi if curl -sf http://localhost:3000 -o /dev/null; then echo " ✅ 前端正常响应" else echo " ❌ 前端无法访问" fi REMOTE_STATUS } # ---- 查看远程日志 ---- remote_logs() { local service="${2:-govai-api}" local lines="${3:-50}" step "查看 $service 日志 (最近 $lines 行)" ssh $SERVER "journalctl -u $service -n $lines --no-pager" } # ---- 主流程 ---- ACTION="${1:-update}" case "$ACTION" in init) echo -e "${BLUE}>>> GovAI 首次部署 -> $DOMAIN${NC}" git_commit_and_push build_local upload server_init install_web_deps migrate_db setup_services start_services ;; update) echo -e "${BLUE}>>> GovAI 更新部署 -> $DOMAIN${NC}" git_commit_and_push build_local upload migrate_db install_web_deps start_services ;; restart) echo -e "${BLUE}>>> GovAI 重启服务${NC}" start_services ;; migrate) echo -e "${BLUE}>>> GovAI 数据库迁移${NC}" upload migrate_db ;; status) remote_status ;; logs) remote_logs "$@" ;; *) echo "用法: bash deploy.sh [init|update|restart|migrate|status|logs]" echo "" echo " init - 首次部署(安装依赖+初始化数据库+部署应用)" echo " update - 更新部署(git提交+构建+上传+迁移+重启)" echo " restart - 仅重启服务" echo " migrate - 仅上传迁移文件并执行数据库迁移" echo " status - 查看远程服务状态和健康检查" echo " logs - 查看日志 (可选: logs govai-web 100)" exit 1 ;; esac