#!/usr/bin/env bash # ============================================================================= # Trading Studio 一键部署脚本 # 安装路径: /opt/Trading_Studio # 运行用户: root # 功能: 系统依赖 → 代码拉取 → Python 虚拟环境 → PyTorch CUDA → PM2 常驻 # # 用法: # sudo bash deploy.sh # 首次完整部署并 PM2 启动 # sudo bash deploy.sh update # 拉取最新代码、更新依赖、重启 PM2 # sudo bash deploy.sh restart # 仅重启 PM2 进程 # sudo bash deploy.sh stop # 停止 PM2 进程 # sudo bash deploy.sh status # 查看 PM2 与 GPU 状态 # sudo bash deploy.sh logs # 查看 PM2 最近日志 # # 环境变量: # USE_CN_MIRROR=1 使用清华 PyPI / PyTorch 镜像(国内服务器推荐,默认开启) # USE_CN_MIRROR=0 使用官方 PyTorch 源 # PIP_TIMEOUT=600 pip 单次下载超时秒数(默认 600) # SKIP_PYTORCH=1 跳过 PyTorch 安装(已手动装好时使用) # ============================================================================= set -euo pipefail # --------------------------------------------------------------------------- # 可配置常量 # --------------------------------------------------------------------------- INSTALL_DIR="/opt/Trading_Studio" GIT_REPO="https://git.bz121.com/dekun/Trading_Studio.git" GIT_BRANCH="main" PM2_APP_NAME="trading_studio" GRADIO_PORT=5683 GPU_POWER_LIMIT=120 # pip 网络参数(大包如下载 triton 需要更长超时) PIP_TIMEOUT="${PIP_TIMEOUT:-600}" PIP_RETRIES="${PIP_RETRIES:-15}" PIP_ATTEMPTS="${PIP_ATTEMPTS:-3}" # 国内镜像(默认开启,海外服务器可 export USE_CN_MIRROR=0) USE_CN_MIRROR="${USE_CN_MIRROR:-1}" PYTORCH_INDEX_OFFICIAL="https://download.pytorch.org/whl/cu121" PYTORCH_INDEX_CN="https://mirrors.tuna.tsinghua.edu.cn/pytorch-wheels/cu121" PIP_INDEX_CN="https://pypi.tuna.tsinghua.edu.cn/simple" HF_MIRROR="https://hf-mirror.com" # --------------------------------------------------------------------------- # 颜色输出 # --------------------------------------------------------------------------- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log_info() { echo -e "${CYAN}[INFO]${NC} $*"; } log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } # --------------------------------------------------------------------------- # 前置检查 # --------------------------------------------------------------------------- require_root() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then log_error "请使用 root 用户运行: sudo bash deploy.sh" exit 1 fi } check_gpu() { if command -v nvidia-smi &>/dev/null; then log_ok "检测到 NVIDIA GPU:" nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader else log_warn "未检测到 nvidia-smi,Whisper/ChatTTS CUDA 加速可能不可用。" fi } set_gpu_power_limit() { if command -v nvidia-smi &>/dev/null; then log_info "设置 GPU 功耗上限为 ${GPU_POWER_LIMIT}W ..." if nvidia-smi -pl "${GPU_POWER_LIMIT}" &>/dev/null; then log_ok "GPU 功耗墙已设为 ${GPU_POWER_LIMIT}W" else log_warn "无法设置功耗墙,请手动执行: nvidia-smi -pl ${GPU_POWER_LIMIT}" fi fi } # --------------------------------------------------------------------------- # 系统依赖 # --------------------------------------------------------------------------- install_system_deps() { log_info "安装系统依赖 ..." apt-get update -qq apt-get install -y \ git curl wget build-essential \ python3 python3-venv python3-dev python3-pip \ ffmpeg libsndfile1 portaudio19-dev \ ca-certificates gnupg log_ok "系统依赖安装完成" } install_node_pm2() { if command -v pm2 &>/dev/null; then log_ok "PM2 已安装: $(pm2 -v)" return fi log_info "安装 Node.js 20 LTS 与 PM2 ..." if ! command -v node &>/dev/null; then curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs fi npm install -g pm2 log_ok "PM2 安装完成: $(pm2 -v)" } # --------------------------------------------------------------------------- # 代码部署 # --------------------------------------------------------------------------- sync_git_repo() { local repo_dir="$1" cd "${repo_dir}" # 自动 stash 本地修改(如 sed 修 CRLF 等),避免 pull 被阻塞 if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then log_warn "检测到本地未提交更改,自动 git stash ..." git stash push -u -m "deploy-auto-stash-$(date +%Y%m%d%H%M%S)" || true fi # 优先 ff-only pull if git pull --ff-only origin "${GIT_BRANCH}" 2>/dev/null; then log_ok "git pull 成功 (origin/${GIT_BRANCH})" return 0 fi if git pull --ff-only 2>/dev/null; then log_ok "git pull 成功" return 0 fi # pull 失败则强制与远端同步(生产服务器标准做法) log_warn "git pull 失败,执行 fetch + reset --hard 同步远端 ..." git fetch origin "${GIT_BRANCH}" 2>/dev/null || git fetch origin if git reset --hard "origin/${GIT_BRANCH}" 2>/dev/null; then log_ok "已强制同步到 origin/${GIT_BRANCH}" return 0 fi if git reset --hard "origin/master" 2>/dev/null; then log_ok "已强制同步到 origin/master" return 0 fi log_error "代码同步失败,请手动处理:" log_error " cd ${repo_dir} && git status && git pull" return 1 } deploy_code() { if [[ -d "${INSTALL_DIR}/.git" ]]; then log_info "更新已有代码: ${INSTALL_DIR}" sync_git_repo "${INSTALL_DIR}" elif [[ -d "${INSTALL_DIR}" ]]; then log_error "${INSTALL_DIR} 已存在但不是 git 仓库,请手动处理后重试。" exit 1 else log_info "克隆仓库到 ${INSTALL_DIR} ..." git clone -b "${GIT_BRANCH}" "${GIT_REPO}" "${INSTALL_DIR}" || \ git clone "${GIT_REPO}" "${INSTALL_DIR}" fi log_ok "代码就绪: ${INSTALL_DIR}" } # --------------------------------------------------------------------------- # Python 环境 # --------------------------------------------------------------------------- configure_pip() { local venv_pip="$1" log_info "配置 pip 网络参数 (timeout=${PIP_TIMEOUT}s, retries=${PIP_RETRIES}) ..." "${venv_pip}" config set global.timeout "${PIP_TIMEOUT}" 2>/dev/null || true "${venv_pip}" config set global.retries "${PIP_RETRIES}" 2>/dev/null || true if [[ "${USE_CN_MIRROR}" == "1" ]]; then log_info "启用国内镜像: 清华 PyPI + PyTorch cu121" "${venv_pip}" config set global.index-url "${PIP_INDEX_CN}" 2>/dev/null || true export PIP_INDEX_URL="${PIP_INDEX_CN}" fi # ChatTTS / HuggingFace 模型下载加速 export HF_ENDPOINT="${HF_MIRROR}" export HF_HUB_ENABLE_HF_TRANSFER=0 } pip_install_with_retry() { local attempt=1 local pip_bin="$1" shift while [[ "${attempt}" -le "${PIP_ATTEMPTS}" ]]; do log_info "pip 安装中 (第 ${attempt}/${PIP_ATTEMPTS} 次),大包下载较慢请耐心等待 ..." if "${pip_bin}" install \ --timeout "${PIP_TIMEOUT}" \ --retries "${PIP_RETRIES}" \ --progress-bar on \ "$@"; then return 0 fi log_warn "pip 安装失败,60 秒后重试 ..." sleep 60 attempt=$((attempt + 1)) done log_error "pip 安装多次重试仍失败,请检查网络或手动安装后设置 SKIP_PYTORCH=1 重试" return 1 } install_pytorch() { local pip_bin="$1" local pytorch_index="${PYTORCH_INDEX_OFFICIAL}" if [[ "${USE_CN_MIRROR}" == "1" ]]; then pytorch_index="${PYTORCH_INDEX_CN}" fi log_info "安装 PyTorch CUDA 12.1 (源: ${pytorch_index}) ..." log_warn "PyTorch + triton 体积约 2-3GB,国内网络可能需要 10-30 分钟,并非卡死。" # 分包安装,降低单次失败成本 pip_install_with_retry "${pip_bin}" \ torch \ --index-url "${pytorch_index}" pip_install_with_retry "${pip_bin}" \ torchvision torchaudio \ --index-url "${pytorch_index}" } setup_python_venv() { local venv_path="${INSTALL_DIR}/venv" local pip_bin="${venv_path}/bin/pip" local python_bin="${venv_path}/bin/python" if [[ ! -d "${venv_path}" ]]; then log_info "创建 Python 虚拟环境 ..." python3 -m venv "${venv_path}" fi configure_pip "${pip_bin}" log_info "升级 pip ..." pip_install_with_retry "${pip_bin}" --upgrade pip setuptools wheel # 跳过已安装的 PyTorch if [[ "${SKIP_PYTORCH:-0}" == "1" ]]; then log_warn "SKIP_PYTORCH=1,跳过 PyTorch 安装" elif "${python_bin}" -c "import torch; assert torch.cuda.is_available()" 2>/dev/null; then log_ok "PyTorch 已安装且 CUDA 可用: $("${python_bin}" -c 'import torch; print(torch.__version__)')" else install_pytorch "${pip_bin}" fi log_info "安装项目依赖 (requirements.txt) ..." pip_install_with_retry "${pip_bin}" -r "${INSTALL_DIR}/requirements.txt" # 验证 CUDA if "${python_bin}" -c "import torch; assert torch.cuda.is_available()" 2>/dev/null; then log_ok "PyTorch CUDA 可用: $("${python_bin}" -c 'import torch; print(torch.cuda.get_device_name(0))')" else log_warn "PyTorch CUDA 不可用,请检查 NVIDIA 驱动与 CUDA 运行时。" fi log_ok "Python 虚拟环境配置完成" } create_runtime_dirs() { mkdir -p "${INSTALL_DIR}/logs" mkdir -p "${INSTALL_DIR}/uploads" mkdir -p "${INSTALL_DIR}/outputs" log_ok "运行时目录已创建 (logs, uploads, outputs)" } # --------------------------------------------------------------------------- # 防火墙 # --------------------------------------------------------------------------- configure_firewall() { if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then log_info "放行 Gradio 端口 ${GRADIO_PORT} ..." ufw allow "${GRADIO_PORT}/tcp" || true log_ok "防火墙规则已更新" else log_info "ufw 未启用,跳过防火墙配置" fi } # --------------------------------------------------------------------------- # PM2 管理 # --------------------------------------------------------------------------- pm2_start() { log_info "通过 PM2 启动 Trading Studio ..." cd "${INSTALL_DIR}" if pm2 describe "${PM2_APP_NAME}" &>/dev/null; then pm2 delete "${PM2_APP_NAME}" || true fi pm2 start ecosystem.config.js pm2 save local startup_cmd startup_cmd=$(pm2 startup systemd -u root --hp /root 2>&1 | grep "sudo env" || true) if [[ -n "${startup_cmd}" ]]; then eval "${startup_cmd}" || log_warn "PM2 startup 可能已配置过" fi pm2 save log_ok "PM2 启动完成" pm2 status } pm2_restart() { cd "${INSTALL_DIR}" if pm2 describe "${PM2_APP_NAME}" &>/dev/null; then pm2 restart "${PM2_APP_NAME}" log_ok "PM2 已重启: ${PM2_APP_NAME}" else log_warn "进程不存在,执行完整启动 ..." pm2_start fi pm2 status } pm2_stop() { if pm2 describe "${PM2_APP_NAME}" &>/dev/null; then pm2 stop "${PM2_APP_NAME}" log_ok "PM2 已停止: ${PM2_APP_NAME}" else log_warn "PM2 进程 ${PM2_APP_NAME} 不存在" fi pm2 status } pm2_status() { echo "" log_info "=== PM2 状态 ===" pm2 status || true echo "" log_info "=== GPU 状态 ===" nvidia-smi 2>/dev/null || log_warn "nvidia-smi 不可用" echo "" log_info "=== 端口 ${GRADIO_PORT} 监听 ===" ss -tlnp | grep ":${GRADIO_PORT}" || log_warn "端口 ${GRADIO_PORT} 未监听,服务可能未启动" echo "" log_info "访问地址: http://$(hostname -I | awk '{print $1}'):${GRADIO_PORT}" } pm2_logs() { pm2 logs "${PM2_APP_NAME}" --lines 80 --nostream || true } # --------------------------------------------------------------------------- # 主流程 # --------------------------------------------------------------------------- cmd_install() { log_info "========== Trading Studio 一键部署开始 ==========" log_info "安装目录: ${INSTALL_DIR}" log_info "运行用户: root" log_info "国内镜像: USE_CN_MIRROR=${USE_CN_MIRROR} pip超时: ${PIP_TIMEOUT}s" install_system_deps install_node_pm2 deploy_code setup_python_venv create_runtime_dirs set_gpu_power_limit configure_firewall pm2_start echo "" log_ok "========== 部署完成 ==========" echo "" echo -e " Web 中控: ${GREEN}http://$(hostname -I | awk '{print $1}'):${GRADIO_PORT}${NC}" echo -e " 项目目录: ${INSTALL_DIR}" echo -e " 查看日志: ${CYAN}pm2 logs ${PM2_APP_NAME}${NC}" echo -e " 重启服务: ${CYAN}bash ${INSTALL_DIR}/deploy.sh restart${NC}" echo "" log_warn "首次使用请打开 Web UI「音色锁定」上传参考人声,生成 speaker_emb.pt" } cmd_update() { log_info "========== 更新部署 ==========" deploy_code setup_python_venv create_runtime_dirs pm2_restart log_ok "更新完成" } cmd_deps_only() { log_info "========== 仅安装/更新 Python 依赖 ==========" setup_python_venv log_ok "依赖安装完成" } print_usage() { cat <