#!/usr/bin/env bash # deploy/lib/common.sh — 部署管理器公共函数 set -e set -u if [ -n "${BASH_VERSION:-}" ]; then set -o pipefail fi INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_monitor}" GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_monitor.git}" GIT_BRANCH="${GIT_BRANCH:-main}" BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}" TZ_NAME="${CM_TZ:-Asia/Shanghai}" NODE_MAJOR="${NODE_MAJOR:-20}" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)" REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" PM2_APPS=( crypto_binance crypto_gate crypto_okx manual-trading-hub manual-agent-binance manual-agent-okx manual-agent-gate ) # 历史进程名(仅 stop/delete 时兼容,不计入验收) PM2_APPS_LEGACY=( crypto-monitor-binance crypto-monitor-gate crypto-monitor-okx ) CONFIG_PATHS=( crypto_monitor_binance/.env crypto_monitor_okx/.env crypto_monitor_gate/.env manual_trading_hub/.env manual_trading_hub/hub_settings.json ) INSTANCE_DIRS=( crypto_monitor_binance crypto_monitor_gate crypto_monitor_okx manual_trading_hub ) log() { printf '[%s] %s\n' "$(TZ="${TZ_NAME}" date '+%Y-%m-%d %H:%M:%S')" "$*"; } step() { echo ""; log "==> $*"; } die() { echo "错误: $*" >&2 exit 1 } require_root() { if [[ "$(id -u)" -ne 0 ]]; then die "请使用 root 执行(推荐: sudo -i 后运行)" fi } require_ubuntu() { if [[ ! -f /etc/os-release ]]; then log "警告: 未检测到 /etc/os-release,跳过 Ubuntu 版本检查" return 0 fi # shellcheck source=/dev/null source /etc/os-release if [[ "${ID:-}" != "ubuntu" ]]; then log "警告: 当前系统为 ${ID:-unknown},官方仅测试 Ubuntu 22.04/24.04" return 0 fi local ver="${VERSION_ID:-}" if [[ "${ver}" != "22.04" && "${ver}" != "24.04" ]]; then log "警告: Ubuntu ${ver} 未在文档中明确测试,继续执行" fi } detect_server_ip() { local ip="" if command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | awk '{print $1}')" fi if [[ -z "${ip}" ]]; then ip="127.0.0.1" fi echo "${ip}" } confirm_yes() { local msg="$1" local ans="" cm_read ans "${msg} [y/N] " [[ "${ans}" == [yY] || "${ans}" == [yY][eE][sS] ]] } confirm_uninstall() { local ans="" echo "此操作将停止全部 PM2 进程并移走安装目录." cm_read ans "输入 UNINSTALL 确认卸载: " [[ "${ans}" == "UNINSTALL" ]] } # curl | bash 时 stdin 是管道,须从 /dev/tty 读取用户输入 cm_read() { local __var="$1" local __prompt="$2" local __val="" if [[ -r /dev/tty ]]; then IFS= read -r -p "${__prompt}" __val /dev/null 2>&1; then die "未检测到 apt-get,请手动安装 python3-venv git curl" fi export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y python3 python3-pip python3-venv curl git ca-certificates local pyver="" if command -v python3 >/dev/null 2>&1; then pyver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv fi } install_node_pm2() { step "检查 Node.js 与 PM2" if command -v pm2 >/dev/null 2>&1; then log "PM2 已安装: $(pm2 -v)" return 0 fi if ! command -v node >/dev/null 2>&1; then log "安装 Node.js ${NODE_MAJOR}.x ..." curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - apt-get install -y nodejs fi log "安装 PM2 ..." npm install -g pm2 log "PM2: $(pm2 -v)" } # pip 安装时显示进度条(避免 -q 静默导致用户以为卡死) # pip>=26 已移除 ascii,仅支持 auto|on|off|raw pip_progress_bar_arg() { echo "on" } pip_upgrade_tools() { local pip_bin="$1" local bar bar="$(pip_progress_bar_arg)" echo " 升级 pip ..." "${pip_bin}" install -U pip setuptools wheel \ --disable-pip-version-check \ --progress-bar "${bar}" } pip_install_requirements() { local pip_bin="$1" local req_file="$2" local label="${3:-依赖}" local bar bar="$(pip_progress_bar_arg)" echo " 安装${label} (下方为 pip 进度) ..." [[ -f "${req_file}" ]] || die "缺少依赖文件: ${req_file}" "${pip_bin}" install -r "${req_file}" \ --disable-pip-version-check \ --retries 5 \ --progress-bar "${bar}" echo " ${label}安装完成" } install_backup_cron_all() { step "安装三所每日备份 cron" local dir for dir in crypto_monitor_binance crypto_monitor_gate crypto_monitor_okx; do local inst="${REPO_ROOT}/${dir}/scripts/install_backup_cron.sh" if [[ -f "${inst}" ]]; then chmod +x "${inst}" bash "${inst}" || log "警告: ${dir} cron 安装失败" fi done } remove_backup_cron_all() { step "移除三所备份 cron" local tmp removed=0 tmp="$(mktemp)" if ! crontab -l 2>/dev/null >"${tmp}"; then rm -f "${tmp}" return 0 fi local filtered filtered="$(grep -vF "backup_data.sh" "${tmp}" || true)" if [[ "${filtered}" != "$(cat "${tmp}")" ]]; then printf '%s\n' "${filtered}" | awk ' BEGIN { tz = 0 } /^CRON_TZ=Asia\/Shanghai$/ { if (tz++) next } { print } ' | crontab - removed=1 fi rm -f "${tmp}" if [[ "${removed}" -eq 1 ]]; then log "已移除 backup_data.sh 相关 cron" fi } pm2_app_exists() { local name="$1" pm2 pid "${name}" >/dev/null 2>&1 } # 仅操作本项目 PM2 进程,不影响服务器上其它应用 pm2_stop_project_apps() { if ! command -v pm2 >/dev/null 2>&1; then log "未安装 pm2,跳过" return 0 fi local name stopped=0 for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do if pm2_app_exists "${name}"; then log "pm2 stop ${name}" pm2 stop "${name}" 2>/dev/null || true stopped=$((stopped + 1)) fi done if [[ "${stopped}" -eq 0 ]]; then log "未发现本项目 PM2 进程(其它 PM2 不受影响)" fi } pm2_delete_project_apps() { if ! command -v pm2 >/dev/null 2>&1; then return 0 fi local name deleted=0 for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do if pm2_app_exists "${name}"; then log "pm2 delete ${name}" pm2 delete "${name}" 2>/dev/null || true deleted=$((deleted + 1)) fi done if [[ "${deleted}" -gt 0 ]]; then pm2 save 2>/dev/null || true fi } pm2_save_startup() { step "PM2 save & startup" pm2 save 2>/dev/null || true if pm2 startup systemd -u root --hp /root 2>/dev/null | grep -q "sudo"; then pm2 startup systemd -u root --hp /root 2>/dev/null | grep "^sudo" | bash || true else pm2 startup 2>/dev/null || true fi } pm2_count_online() { local name online=0 if ! command -v pm2 >/dev/null 2>&1; then echo "0" return 0 fi for name in "${PM2_APPS[@]}"; do if pm2 pid "${name}" >/dev/null 2>&1; then online=$((online + 1)) fi done echo "${online}" } is_deployed() { local root="$1" [[ -x "${root}/crypto_monitor_binance/.venv/bin/python" ]] \ || [[ -x "${root}/crypto_monitor_gate/.venv/bin/python" ]] \ || [[ -x "${root}/crypto_monitor_okx/.venv/bin/python" ]] \ || [[ -x "${root}/manual_trading_hub/.venv/bin/python" ]] } # 单所实例: key -> 目录 / PM2 名 / 端口 / 展示名 exchange_dir() { case "$1" in okx) echo "crypto_monitor_okx" ;; binance) echo "crypto_monitor_binance" ;; gate) echo "crypto_monitor_gate" ;; *) return 1 ;; esac } exchange_pm2_name() { case "$1" in okx) echo "crypto_okx" ;; binance) echo "crypto_binance" ;; gate) echo "crypto_gate" ;; *) return 1 ;; esac } exchange_http_port() { case "$1" in okx) echo "5004" ;; binance) echo "5001" ;; gate) echo "5000" ;; *) return 1 ;; esac } exchange_label() { case "$1" in okx) echo "OKX" ;; binance) echo "Binance" ;; gate) echo "Gate" ;; *) echo "$1" ;; esac } normalize_exchange_key() { local k k="$(echo "${1:-}" | tr '[:upper:]' '[:lower:]' | xargs)" case "${k}" in okx|binance|gate) echo "${k}" ;; *) return 1 ;; esac } check_http() { local url="$1" local code code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 "${url}" 2>/dev/null || echo "000")" [[ "${code}" == "200" || "${code}" == "302" || "${code}" == "301" ]] } verify_deployment() { local ip="${1:-$(detect_server_ip)}" local mode="${2:-all}" local ok=1 local online total local ex_key="" step "部署验收" if [[ "${mode}" == "all" || -z "${mode}" ]]; then total="${#PM2_APPS[@]}" online="$(pm2_count_online)" if [[ "${online}" -eq "${total}" ]]; then echo " [✓] PM2 进程 ${online}/${total} online" else echo " [✗] PM2 进程 ${online}/${total} online" ok=0 pm2 list 2>/dev/null || true fi local checks=( "中控:http://127.0.0.1:5100/" "Binance:http://127.0.0.1:5001/" "Gate:http://127.0.0.1:5000/" "OKX:http://127.0.0.1:5004/" ) local item label url for item in "${checks[@]}"; do label="${item%%:*}" url="${item#*:}" if check_http "${url}"; then echo " [✓] ${label} 可访问" else echo " [✗] ${label} 不可访问 (${url})" ok=0 fi done else if ! ex_key="$(normalize_exchange_key "${mode}")"; then echo " [✗] 未知验收模式: ${mode}" return 1 fi local pm2_name port label pm2_name="$(exchange_pm2_name "${ex_key}")" port="$(exchange_http_port "${ex_key}")" label="$(exchange_label "${ex_key}")" if pm2_app_exists "${pm2_name}"; then echo " [✓] PM2 ${pm2_name} 已注册" else echo " [✗] PM2 缺少进程 ${pm2_name}" ok=0 pm2 list 2>/dev/null || true fi if check_http "http://127.0.0.1:${port}/"; then echo " [✓] ${label} 可访问 (http://127.0.0.1:${port}/)" else echo " [✗] ${label} 不可访问 (http://127.0.0.1:${port}/)" ok=0 fi fi if [[ "${ok}" -eq 1 ]]; then echo "" log "验收通过: 进程都在 + 页面可打开" return 0 fi echo "" log "验收未完全通过,可执行 pm2 logs <进程名> --lines 30 排查" return 1 } print_post_install_guide() { local ip="${1:-$(detect_server_ip)}" cat <