Files
crypto_monitor_user/deploy/lib/common.sh
T
dekun 53863559f4 Initialize crypto_monitor_user (user edition) from monitor codebase.
Retarget git remote, install path, and deploy docs from crypto_monitor to crypto_monitor_user.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:18:13 +08:00

510 lines
13 KiB
Bash
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
# deploy/lib/common.sh — 部署管理器公共函数
set -e
set -u
if [ -n "${BASH_VERSION:-}" ]; then
set -o pipefail
fi
INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_monitor_user}"
GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_monitor_user.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/tty || true
else
IFS= read -r -p "${__prompt}" __val || true
fi
printf -v "${__var}" '%s' "${__val}"
}
repo_ready() {
[[ -f "${1}/deploy/setup_env.sh" && -f "${1}/deploy/manage.sh" ]]
}
resolve_repo_root() {
if repo_ready "${INSTALL_ROOT}"; then
echo "${INSTALL_ROOT}"
return 0
fi
if repo_ready "${REPO_ROOT}"; then
echo "${REPO_ROOT}"
return 0
fi
return 1
}
install_log_file() {
local stamp
stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)"
mkdir -p "${BACKUP_ROOT}/deploy-logs"
echo "${BACKUP_ROOT}/deploy-logs/manage-${stamp}.log"
}
backup_configs_to() {
local src_root="$1"
local dest="$2"
mkdir -p "${dest}"
local rel copied=0
for rel in "${CONFIG_PATHS[@]}"; do
local src="${src_root}/${rel}"
if [[ -f "${src}" ]]; then
mkdir -p "${dest}/$(dirname "${rel}")"
cp -a "${src}" "${dest}/${rel}"
log "backup ${rel}"
copied=$((copied + 1))
fi
done
if [[ "${copied}" -eq 0 ]]; then
log "提示: 未找到可备份的配置文件"
fi
}
install_system_packages() {
step "安装系统依赖"
if ! command -v apt-get >/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 <<EOF
═══════════════════════════════════════════
✅ 部署完成
中控: http://${ip}:5100
Binance: http://${ip}:5001
Gate: http://${ip}:5000
OKX: http://${ip}:5004
登录账号: admin
登录密码: admin123
(请尽快在「系统设置」中修改)
下一步(浏览器):
1. 中控 → 系统设置 → 确认账户 URL(有域名时改 flask_url
2. 各所 → env 配置 → 填 API、风控、企业微信
3. 中控 → AI 配置(如需复盘)
4. 各所 env 配置 → 开启实盘下单 → 保存并重启
agent_url 默认本机 127.0.0.1:15200/15201/15202,一般无需修改
═══════════════════════════════════════════
EOF
}
print_post_install_guide_instance() {
local ex_key="$1"
local ip="${2:-$(detect_server_ip)}"
local label port
label="$(exchange_label "${ex_key}")"
port="$(exchange_http_port "${ex_key}")"
cat <<EOF
═══════════════════════════════════════════
✅ 单所实例部署完成(不含中控)
${label}: http://${ip}:${port}
登录账号: admin
登录密码: admin123
(请尽快在 env 配置中修改)
下一步(浏览器):
1. ${label} → env 配置 → 填 API、风控、企业微信
2. 开启实盘下单 → 保存并重启该实例进程
说明: 本模式不起中控 / agent;整仓仍在 ${INSTALL_ROOT},仅启动本所 Flask.
═══════════════════════════════════════════
EOF
}