From 2537e5beef1d09166c033225f7f9f352869e22f0 Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Sat, 13 Jun 2026 01:17:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=80=E9=94=AE=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC=20deploy.sh=EF=BC=88=E6=9E=84?= =?UTF-8?q?=E5=BB=BA/=E4=B8=8A=E4=BC=A0/DB/nginx/SSL/PM2=20=E5=85=A8?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=EF=BC=8C=E6=94=AF=E6=8C=81=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E4=B8=8E=E6=9B=B4=E6=96=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + deploy.env.example | 32 ++++++ deploy.sh | 269 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 deploy.env.example create mode 100755 deploy.sh diff --git a/.gitignore b/.gitignore index e31b802..15851e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage/ *.log .env .env.local +deploy.env diff --git a/deploy.env.example b/deploy.env.example new file mode 100644 index 0000000..cfe6d47 --- /dev/null +++ b/deploy.env.example @@ -0,0 +1,32 @@ +# 部署配置模板:复制为 deploy.env 并按需修改(deploy.env 已被 .gitignore 忽略,勿提交)。 +# 所有项均可省略,省略时使用 deploy.sh 内的默认值。 + +# ---- 目标服务器 ---- +SERVER_HOST=152.136.182.184 +SERVER_USER=ubuntu +# 服务器登录密码(需要 sshpass)。留空则使用 SSH 密钥免密登录(推荐)。 +SERVER_PASS= +# 绑定域名(需提前把 DNS A 记录解析到 SERVER_HOST) +DOMAIN=pm.hr8ai.top + +# ---- 应用 ---- +APP_DIR=/opt/riskagent +APP_NAME=riskagent +BACKEND_PORT=3005 + +# ---- 数据库(仅 --first-run 使用)---- +DB_NAME=riskagent +DB_USER=riskagent +# 留空则首次部署自动生成随机密码并写入服务器 .env +DB_PASS= +PG_SUPER=postgres + +# ---- 运行环境(首次写入服务器 .env,之后不覆盖)---- +# 留空则自动生成高强度随机值(启用 RBAC) +AUTH_SECRET= +# 通义千问 / DashScope key;留空则关闭 LLM,回退确定性规则 +DASHSCOPE_API_KEY= +LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +LLM_MODEL=qwen-plus +LLM_TIMEOUT_MS=15000 +TARGET_NET_MARGIN=0.05 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6def3d0 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env bash +# +# 一键部署脚本:外包项目风险评估系统 +# --------------------------------------------------------------------------- +# 用法: +# ./deploy.sh # 常规更新:本地构建 → 上传 → 重启(保留服务器 .env) +# ./deploy.sh --first-run # 首次部署:额外执行 建库/装 pgvector/配 nginx/签 SSL +# ./deploy.sh --restart-only # 仅重启后端(不构建不上传) +# ./deploy.sh --logs # 查看后端实时日志 +# +# 配置:复制 deploy.env.example 为 deploy.env 并填写(deploy.env 已被 .gitignore 忽略)。 +# 也可直接用环境变量覆盖任意配置项。 +# --------------------------------------------------------------------------- +set -euo pipefail + +cd "$(dirname "$0")" + +# ----------------------------- 加载配置 ----------------------------- +if [[ -f deploy.env ]]; then + # shellcheck disable=SC1091 + source deploy.env +fi + +# 目标服务器 +SERVER_HOST="${SERVER_HOST:-152.136.182.184}" +SERVER_USER="${SERVER_USER:-ubuntu}" +SERVER_PASS="${SERVER_PASS:-}" # 留空则使用 SSH 密钥免密登录 +DOMAIN="${DOMAIN:-pm.hr8ai.top}" + +# 应用部署参数 +APP_DIR="${APP_DIR:-/opt/riskagent}" +APP_NAME="${APP_NAME:-riskagent}" +BACKEND_PORT="${BACKEND_PORT:-3005}" + +# 数据库(首次部署用) +DB_NAME="${DB_NAME:-riskagent}" +DB_USER="${DB_USER:-riskagent}" +DB_PASS="${DB_PASS:-}" # 首次部署留空则自动生成 +PG_SUPER="${PG_SUPER:-postgres}" # PostgreSQL 超级用户(用于建库/装扩展) + +# 应用环境变量(写入服务器 .env,仅当 .env 不存在时) +AUTH_SECRET="${AUTH_SECRET:-}" # 留空则自动生成 +DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-}" +LLM_BASE_URL="${LLM_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}" +LLM_MODEL="${LLM_MODEL:-qwen-plus}" +LLM_TIMEOUT_MS="${LLM_TIMEOUT_MS:-15000}" +TARGET_NET_MARGIN="${TARGET_NET_MARGIN:-0.05}" + +# ----------------------------- 工具函数 ----------------------------- +c_info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; } +c_ok() { printf '\033[1;32m[ OK ]\033[0m %s\n' "$*"; } +c_warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } +c_err() { printf '\033[1;31m[FAIL]\033[0m %s\n' "$*" >&2; } + +# SSH / rsync 封装(自动选择 sshpass 或密钥) +SSH_BASE=(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=20) +if [[ -n "$SERVER_PASS" ]]; then + command -v sshpass >/dev/null || { c_err "需要 sshpass(SERVER_PASS 已设置)。安装:brew install sshpass / apt install sshpass"; exit 1; } + SSH=(sshpass -p "$SERVER_PASS" "${SSH_BASE[@]}") + RSYNC_RSH="sshpass -p $SERVER_PASS ssh -o StrictHostKeyChecking=no" +else + SSH=("${SSH_BASE[@]}") + RSYNC_RSH="ssh -o StrictHostKeyChecking=no" +fi + +remote() { "${SSH[@]}" "${SERVER_USER}@${SERVER_HOST}" "$@"; } +# 以 sudo 执行远程命令(sshpass 时通过 -S 传入密码) +remote_sudo() { + if [[ -n "$SERVER_PASS" ]]; then + remote "echo '$SERVER_PASS' | sudo -S bash -c $(printf '%q' "$*")" + else + remote "sudo bash -c $(printf '%q' "$*")" + fi +} + +# ----------------------------- 子命令 ----------------------------- +MODE="update" +case "${1:-}" in + --first-run) MODE="first-run" ;; + --restart-only) MODE="restart" ;; + --logs) MODE="logs" ;; + "") MODE="update" ;; + *) c_err "未知参数: $1"; exit 1 ;; +esac + +if [[ "$MODE" == "logs" ]]; then + remote "pm2 logs $APP_NAME --lines 60" + exit 0 +fi + +if [[ "$MODE" == "restart" ]]; then + c_info "重启后端 $APP_NAME ..." + remote "pm2 restart $APP_NAME --update-env && pm2 save" + remote "sleep 2 && curl -s http://127.0.0.1:${BACKEND_PORT}/api/health"; echo + c_ok "已重启" + exit 0 +fi + +c_info "目标: ${SERVER_USER}@${SERVER_HOST} 域名: ${DOMAIN} 模式: ${MODE}" + +# ----------------------------- 1. 本地构建 ----------------------------- +c_info "本地构建后端 (tsc --build) ..." +npx tsc --build +[[ -f dist/server/index.js ]] || { c_err "后端构建产物缺失 dist/server/index.js"; exit 1; } +c_ok "后端构建完成" + +c_info "本地构建前端 (vite build) ..." +npx vite build +[[ -f web/dist/index.html ]] || { c_err "前端构建产物缺失 web/dist/index.html"; exit 1; } +c_ok "前端构建完成" + +# ----------------------------- 2. 远程目录 ----------------------------- +c_info "确保远程目录 ${APP_DIR} ..." +remote_sudo "mkdir -p ${APP_DIR} && chown ${SERVER_USER}:${SERVER_USER} ${APP_DIR}" + +# ----------------------------- 3. 首次部署:DB / pgvector ----------------------------- +if [[ "$MODE" == "first-run" ]]; then + if [[ -z "$DB_PASS" ]]; then + DB_PASS="$(openssl rand -hex 16)" + c_info "已自动生成数据库密码" + fi + c_info "安装 pgvector 扩展包 ..." + remote_sudo "apt-get install -y postgresql-16-pgvector >/dev/null 2>&1 || apt-get install -y postgresql-$(remote_sudo 'pg_lsclusters -h | awk "{print \$1; exit}"' 2>/dev/null || echo 16)-pgvector" || c_warn "pgvector 安装可能失败,请确认 PG 版本对应包" + + c_info "创建数据库角色 / 库 / 扩展(幂等)..." + remote_sudo "sudo -u ${PG_SUPER} psql -v ON_ERROR_STOP=1 -c \"DO \\\$\\\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='${DB_USER}') THEN CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASS}'; END IF; END \\\$\\\$;\"" + remote_sudo "sudo -u ${PG_SUPER} psql -tAc \"SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'\" | grep -q 1 || sudo -u ${PG_SUPER} createdb -O ${DB_USER} ${DB_NAME}" + remote_sudo "sudo -u ${PG_SUPER} psql -d ${DB_NAME} -c 'CREATE EXTENSION IF NOT EXISTS vector;'" + c_ok "数据库就绪:${DB_NAME}(owner=${DB_USER}, pgvector 已启用)" +fi + +# ----------------------------- 4. 上传产物 ----------------------------- +c_info "上传后端产物与依赖清单 ..." +rsync -az --delete -e "$RSYNC_RSH" \ + dist migrations package.json package-lock.json \ + "${SERVER_USER}@${SERVER_HOST}:${APP_DIR}/" + +c_info "上传前端静态资源 → ${APP_DIR}/webroot ..." +rsync -az --delete -e "$RSYNC_RSH" \ + web/dist/ "${SERVER_USER}@${SERVER_HOST}:${APP_DIR}/webroot/" +c_ok "上传完成" + +# ----------------------------- 5. 写入 .env(仅当不存在)----------------------------- +if remote "test -f ${APP_DIR}/.env"; then + c_info ".env 已存在,保留服务器现有配置(不覆盖密钥)" +else + [[ -z "$DB_PASS" ]] && DB_PASS="$(remote 'echo ${DB_PASS:-}')" # 防御 + [[ -z "$AUTH_SECRET" ]] && AUTH_SECRET="$(openssl rand -hex 32)" && c_info "已自动生成 AUTH_SECRET" + c_info "写入 ${APP_DIR}/.env ..." + remote "cat > ${APP_DIR}/.env <&1 | tail -3" +c_ok "依赖安装完成" + +# ----------------------------- 7. 首次部署:nginx + SSL ----------------------------- +if [[ "$MODE" == "first-run" ]]; then + c_info "备份 nginx 配置 ..." + remote_sudo "tar czf /home/${SERVER_USER}/nginx-backup-\$(date +%Y%m%d%H%M%S).tgz -C /etc nginx 2>/dev/null || true" + remote_sudo "mkdir -p /var/www/html/.well-known/acme-challenge && chown -R ${SERVER_USER}:${SERVER_USER} /var/www/html/.well-known" + + c_info "写入 nginx 站点(HTTP,先用于签发证书)..." + remote_sudo "tee /etc/nginx/sites-available/${DOMAIN} >/dev/null <&1 | tail -3 || true" + remote_sudo "mkdir -p /etc/nginx/ssl/${DOMAIN} && chown ${SERVER_USER}:${SERVER_USER} /etc/nginx/ssl/${DOMAIN}" + # 允许 acme 续期后免密 reload nginx + remote_sudo "echo '${SERVER_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx, /bin/systemctl reload nginx' > /etc/sudoers.d/${APP_NAME}-nginx-reload && chmod 440 /etc/sudoers.d/${APP_NAME}-nginx-reload" + remote "~/.acme.sh/acme.sh --install-cert -d ${DOMAIN} --ecc --key-file /etc/nginx/ssl/${DOMAIN}/key.pem --fullchain-file /etc/nginx/ssl/${DOMAIN}/fullchain.pem --reloadcmd 'sudo systemctl reload nginx' 2>&1 | tail -3 || true" + + if remote "test -s /etc/nginx/ssl/${DOMAIN}/fullchain.pem"; then + c_info "切换 nginx 到 HTTPS(HTTP 301 跳转)..." + remote_sudo "tee /etc/nginx/sites-available/${DOMAIN} >/dev/null </dev/null 2>&1"; then + remote "pm2 restart ${APP_NAME} --update-env" +else + remote "cd ${APP_DIR} && pm2 start dist/server/index.js --name ${APP_NAME} --cwd ${APP_DIR}" +fi +remote "pm2 save >/dev/null 2>&1 || true" +c_ok "后端已运行" + +# ----------------------------- 9. 健康检查 ----------------------------- +c_info "健康检查 ..." +sleep 2 +HEALTH="$(remote "curl -s http://127.0.0.1:${BACKEND_PORT}/api/health" || true)" +LLM="$(remote "curl -s http://127.0.0.1:${BACKEND_PORT}/api/llm/status" || true)" +echo " /api/health → ${HEALTH}" +echo " /api/llm/status → ${LLM}" +if [[ "$HEALTH" == *'"status":"ok"'* ]]; then + c_ok "部署完成 ✅ 访问: https://${DOMAIN}" +else + c_err "健康检查未通过,请查看日志: ./deploy.sh --logs" + exit 1 +fi