Files
MyAiDesk/GovAI/deploy.sh
T
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

514 lines
16 KiB
Bash
Executable File
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.
#!/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="${SERVER:-govai-root}"
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 <your-repo-url>"
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"
rm -rf .next
NEXT_PUBLIC_API_URL="http://localhost:8080" npm run build
log "前端编译完成 (.next: $(du -sh .next | cut -f1))"
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 "上传后端二进制 (gzip 流)..."
gzip -c "$PROJECT_DIR/dist/server" | ssh $SERVER "gunzip -c > $REMOTE_DIR/server/server.new && mv -f $REMOTE_DIR/server/server.new $REMOTE_DIR/server/server && chmod +x $REMOTE_DIR/server/server"
log "上传迁移文件..."
rsync -az --delete "$PROJECT_DIR/server/migrations/" $SERVER:$REMOTE_DIR/migrations/
log "上传前端构建产物 (tar 压缩流, 排除 dev/cache)..."
tar czf - -C "$PROJECT_DIR/apps/web" \
--exclude='.next/dev' --exclude='.next/cache' \
.next public package.json next.config.ts \
| ssh $SERVER "rm -rf $REMOTE_DIR/web/.next && tar xzf - -C $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 "服务配置完成"
}
# ---- 安全加固 ----
security_hardening() {
step "安全加固"
ssh $SERVER bash << 'SEC_HARD'
set -e
echo "[1/7] 清理恶意 cron 任务..."
# 保留正常 cron,移除 pakchoi 和可疑磁盘清理
crontab -l 2>/dev/null | grep -v -E 'pakchoi|/opt/disk/cleanup' | crontab - 2>/dev/null || true
echo " Cron 清理完成"
echo "[2/7] 删除恶意用户..."
for user in pakchoi; do
id $user 2>/dev/null && userdel -r $user && echo " $user 已删除" || echo " $user 不存在"
done
echo "[3/7] 收紧 .env 文件权限..."
find /opt/govai /root -name '.env' -o -name '.env.*' 2>/dev/null | while read f; do
chmod 600 "$f" 2>/dev/null && echo " Fixed: $f"
done
echo "[4/7] 配置 iptables 防火墙 (仅放行 22,80,443,3000)..."
# 只对有 iptables 且不是云安全组管理的服务器生效
if command -v iptables &>/dev/null && ! iptables -L INPUT -n | grep -q 'Chain references'; then
iptables -F INPUT 2>/dev/null || true
iptables -P INPUT DROP 2>/dev/null || true
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
iptables -A INPUT -p tcp --dport 22 -j ACCEPT 2>/dev/null || true
iptables -A INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null || true
iptables -A INPUT -p tcp --dport 443 -j ACCEPT 2>/dev/null || true
iptables -A INPUT -p tcp --dport 3000 -j ACCEPT 2>/dev/null || true
iptables -A INPUT -i lo -j ACCEPT 2>/dev/null || true
fi
# 保存规则到持久化文件
iptables-save > /etc/iptables/rules.v4 2>/dev/null || \
iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
echo " 防火墙已收紧"
echo "[5/7] 配置 SSH authorized_keys 权限..."
if [ -f /root/.ssh/authorized_keys ]; then
# 只保留已知密钥(包含 govai / freedak / h2deploy 注释的)
grep -E '(govai|freedak|h2deploy)' /root/.ssh/authorized_keys > /root/.ssh/authorized_keys.clean 2>/dev/null || true
if [ -s /root/.ssh/authorized_keys.clean ]; then
mv /root/.ssh/authorized_keys.clean /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
echo " SSH authorized_keys 已清理 ($(wc -l < /root/.ssh/authorized_keys) 密钥保留)"
fi
fi
echo "[6/7] 确保 Redis 认证已启用..."
REDIS_CONF="/etc/redis/redis.conf"
if [ -f "$REDIS_CONF" ]; then
if ! grep -q "^requirepass" "$REDIS_CONF"; then
echo "requirepass $(openssl rand -base64 32 | tr -d '/+=' | head -c 32)" >> "$REDIS_CONF"
systemctl restart redis 2>/dev/null || true
echo " Redis 认证密码已设置"
else
echo " Redis 已有密码配置"
fi
fi
echo "[7/7] 确保 SSH PermitRootLogin 为 prohibit-password..."
SSHD_HARDEN="/etc/ssh/sshd_config.d/99-hardening.conf"
if [ -f "$SSHD_HARDEN" ]; then
if grep -q "PermitRootLogin no" "$SSHD_HARDEN"; then
sed -i 's/PermitRootLogin no/PermitRootLogin prohibit-password/' "$SSHD_HARDEN"
systemctl reload sshd
echo " PermitRootLogin 已修复"
fi
fi
echo ""
echo "✅ 安全加固完成"
SEC_HARD
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
security_hardening
;;
update)
echo -e "${BLUE}>>> GovAI 更新部署 -> $DOMAIN${NC}"
git_commit_and_push
build_local
upload
migrate_db
install_web_deps
start_services
security_hardening
;;
restart)
echo -e "${BLUE}>>> GovAI 重启服务${NC}"
start_services
;;
migrate)
echo -e "${BLUE}>>> GovAI 数据库迁移${NC}"
upload
migrate_db
;;
status)
remote_status
;;
logs)
remote_logs "$@"
;;
secure)
echo -e "${BLUE}>>> GovAI 安全加固${NC}"
security_hardening
;;
*)
echo "用法: bash deploy.sh [init|update|restart|migrate|status|logs|secure]"
echo ""
echo " init - 首次部署(安装依赖+初始化数据库+部署应用)"
echo " update - 更新部署(git提交+构建+上传+迁移+重启)"
echo " restart - 仅重启服务"
echo " migrate - 仅上传迁移文件并执行数据库迁移"
echo " status - 查看远程服务状态和健康检查"
echo " logs - 查看日志 (可选: logs govai-web 100)"
echo " secure - 仅执行安全加固(cron清理/防火墙/SSH/.env权限)"
exit 1
;;
esac