From aab0b9bdf661a777918444ec9b8470134ad8d786 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 1 Aug 2026 23:33:41 +0800 Subject: [PATCH] Support up to 3 strategy slots on one host with auto ports. Co-authored-by: Cursor --- deploy/ecosystem.config.cjs | 1 + deploy/lib/common.sh | 164 +++++++++++++++++++ deploy/lib/install.sh | 24 ++- deploy/lib/uninstall.sh | 51 +++--- deploy/lib/update.sh | 48 +++++- deploy/manage.sh | 57 ++++++- deploy/pull_and_restart.sh | 43 +++-- docs/中控Fleet说明.md | 315 +++++++++++++++++++----------------- 8 files changed, 503 insertions(+), 200 deletions(-) diff --git a/deploy/ecosystem.config.cjs b/deploy/ecosystem.config.cjs index 7ac71f6..eaf4880 100644 --- a/deploy/ecosystem.config.cjs +++ b/deploy/ecosystem.config.cjs @@ -1,3 +1,4 @@ +// 默认槽位 1 模板;安装/更新时由 deploy/lib/common.sh#write_ecosystem_config 按目录重写。 module.exports = { apps: [ { diff --git a/deploy/lib/common.sh b/deploy/lib/common.sh index 92cc08c..3bfeeb1 100755 --- a/deploy/lib/common.sh +++ b/deploy/lib/common.sh @@ -15,6 +15,7 @@ TZ_NAME="${EHS_TZ:-Asia/Shanghai}" NODE_MAJOR="${NODE_MAJOR:-20}" PM2_APP_NAME="${PM2_APP_NAME:-eth-hedge-api}" API_PORT="${API_PORT:-5155}" +INSTANCE_SLOT="${INSTANCE_SLOT:-1}" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)" @@ -28,6 +29,167 @@ die() { exit 1 } +# ---------- 同机多套槽位(1/2/3) ---------- + +infer_slot_from_root() { + local root="${1%/}" + case "${root}" in + */eth_hedge_sim_2) echo 2 ;; + */eth_hedge_sim_3) echo 3 ;; + *) echo 1 ;; + esac +} + +# 设置 INSTANCE_SLOT / INSTALL_ROOT / API_PORT / PM2_APP_NAME +resolve_slot() { + local slot="${1:-${INSTANCE_SLOT:-1}}" + case "${slot}" in + 1|2|3) ;; + *) die "无效槽位: ${slot}(仅支持 1/2/3)" ;; + esac + INSTANCE_SLOT="${slot}" + case "${slot}" in + 1) + INSTALL_ROOT="/opt/eth_hedge_sim" + API_PORT="5155" + PM2_APP_NAME="eth-hedge-api" + ;; + 2) + INSTALL_ROOT="/opt/eth_hedge_sim_2" + API_PORT="5156" + PM2_APP_NAME="eth-hedge-api-2" + ;; + 3) + INSTALL_ROOT="/opt/eth_hedge_sim_3" + API_PORT="5157" + PM2_APP_NAME="eth-hedge-api-3" + ;; + esac + export INSTANCE_SLOT INSTALL_ROOT API_PORT PM2_APP_NAME +} + +# 按实际仓库目录对齐槽位(pull/update 时以目录为准) +apply_slot_for_root() { + local root="${1%/}" + [[ -n "${root}" ]] || die "apply_slot_for_root: 空目录" + resolve_slot "$(infer_slot_from_root "${root}")" + INSTALL_ROOT="${root}" + REPO_ROOT="${root}" + # 已有 .env 时保留用户改过的端口 + if [[ -f "${root}/.env" ]]; then + local p="" + p="$(grep -E '^API_PORT=' "${root}/.env" 2>/dev/null | head -n1 | cut -d= -f2- | tr -d '\r' || true)" + if [[ -n "${p}" && "${p}" =~ ^[0-9]+$ ]]; then + API_PORT="${p}" + fi + fi + export INSTANCE_SLOT INSTALL_ROOT API_PORT PM2_APP_NAME REPO_ROOT +} + +is_allowed_install_root() { + local root="${1%/}" + case "${root}" in + /opt/eth_hedge_sim|/opt/eth_hedge_sim_2|/opt/eth_hedge_sim_3) return 0 ;; + *) return 1 ;; + esac +} + +port_is_listening() { + local port="$1" + if command -v ss >/dev/null 2>&1; then + ss -ltn 2>/dev/null | grep -qE ":${port}[[:space:]]" && return 0 + fi + if command -v lsof >/dev/null 2>&1; then + lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 && return 0 + fi + return 1 +} + +# 全新安装前:端口已被占用且不是本槽位 PM2 → 失败 +assert_port_free_for_install() { + local port="${1:-${API_PORT}}" + local name="${2:-${PM2_APP_NAME}}" + if command -v pm2 >/dev/null 2>&1; then + if pm2 describe "${name}" >/dev/null 2>&1; then + return 0 + fi + fi + if port_is_listening "${port}"; then + die "端口 ${port} 已被占用(槽位 ${INSTANCE_SLOT} / ${name})。请释放端口或换槽位" + fi +} + +write_ecosystem_config() { + local root="${1:-${INSTALL_ROOT}}" + local name="${2:-${PM2_APP_NAME}}" + local port="${3:-${API_PORT}}" + local out="${root}/deploy/ecosystem.config.cjs" + mkdir -p "${root}/deploy" + cat >"${out}" </dev/null 2>&1; then + openssl rand -hex 24 + return 0 + fi + head -c 32 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48 + echo "" +} + +# SKIP_SLOT_PROMPT=1 时用环境变量 INSTANCE_SLOT(默认 1),供「全部更新」等非交互调用 +prompt_instance_slot() { + local line="" + local slot="" + if [[ "${SKIP_SLOT_PROMPT:-0}" == "1" ]]; then + resolve_slot "${INSTANCE_SLOT:-1}" + log "槽位 ${INSTANCE_SLOT}: ${INSTALL_ROOT} port=${API_PORT} pm2=${PM2_APP_NAME}" + return 0 + fi + echo "" + echo "策略机槽位(同机最多 3 套,互不影响):" + echo " 1 → /opt/eth_hedge_sim :5155 eth-hedge-api" + echo " 2 → /opt/eth_hedge_sim_2 :5156 eth-hedge-api-2" + echo " 3 → /opt/eth_hedge_sim_3 :5157 eth-hedge-api-3" + cm_read line "请选择槽位 [1/2/3,默认1]: " + slot="${line:-1}" + resolve_slot "${slot}" + log "槽位 ${INSTANCE_SLOT}: ${INSTALL_ROOT} port=${API_PORT} pm2=${PM2_APP_NAME}" +} + +list_installed_slots() { + local s root + for s in 1 2 3; do + case "${s}" in + 1) root="/opt/eth_hedge_sim" ;; + 2) root="/opt/eth_hedge_sim_2" ;; + 3) root="/opt/eth_hedge_sim_3" ;; + esac + if repo_ready "${root}"; then + echo "${s}" + fi + done +} + require_root() { if [[ "$(id -u)" -ne 0 ]]; then die "请使用 root 执行(推荐: sudo -i 后运行)" @@ -360,11 +522,13 @@ print_post_install_guide() { echo "" echo "══════════════════════════════════════" echo " 比特骆驼自动化对冲系统 部署完成" + echo " 槽位: ${INSTANCE_SLOT:-1}" echo " 目录: ${INSTALL_ROOT}" echo " 本机: http://${ip}:${API_PORT}/health" echo " 进程: pm2 status → ${PM2_APP_NAME}" echo " 管理: bash ${INSTALL_ROOT}/deploy/manage.sh" echo " 配置: ${INSTALL_ROOT}/.env" + echo " 同机多套: 菜单选槽位 2/3;实盘请只用槽位 1" echo "══════════════════════════════════════" echo "" } diff --git a/deploy/lib/install.sh b/deploy/lib/install.sh index 9b088c3..76e587f 100755 --- a/deploy/lib/install.sh +++ b/deploy/lib/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# deploy/lib/install.sh — 一键部署策略机 +# deploy/lib/install.sh — 一键部署策略机(支持槽位 1/2/3) set -e set -u if [ -n "${BASH_VERSION:-}" ]; then @@ -13,10 +13,12 @@ source "${LIB_DIR}/common.sh" run_pipeline() { local root="$1" REPO_ROOT="${root}" - step "构建并启动策略机 (pull_and_restart.sh)" + INSTALL_ROOT="${root}" + apply_slot_for_root "${root}" + step "构建并启动策略机 (pull_and_restart.sh) slot=${INSTANCE_SLOT}" bash "${REPO_ROOT}/deploy/pull_and_restart.sh" pm2_save_startup - verify_health || true + verify_health "${API_PORT}" || true print_post_install_guide } @@ -24,7 +26,8 @@ install_fresh() { require_root step "一键部署策略机 — 环境检测与依赖" ensure_system_deps - step "克隆仓库 → ${INSTALL_ROOT}" + assert_port_free_for_install "${API_PORT}" "${PM2_APP_NAME}" + step "克隆仓库 → ${INSTALL_ROOT}(槽位 ${INSTANCE_SLOT} / :${API_PORT})" if [[ -d "${INSTALL_ROOT}" ]]; then die "目录已存在: ${INSTALL_ROOT},请先卸载或选修复" fi @@ -40,13 +43,13 @@ install_repair() { if ! REPO_ROOT="$(resolve_repo_root)"; then die "未找到安装目录 ${INSTALL_ROOT}" fi - step "修复环境(保留 .env 与数据库)" + step "修复环境(保留 .env 与数据库) slot=${INSTANCE_SLOT}" run_pipeline "${REPO_ROOT}" } handle_existing() { echo "" - echo "检测到已部署: ${INSTALL_ROOT}" + echo "检测到已部署: ${INSTALL_ROOT}(槽位 ${INSTANCE_SLOT})" echo " a) 取消" echo " b) 修复/重装环境(保留 .env 与 SQLite)" local choice="" @@ -59,6 +62,15 @@ handle_existing() { main_install() { require_root + # manage.sh 已 prompt;直接调用时按 INSTANCE_SLOT / INSTALL_ROOT 对齐 + if [[ "${INSTALL_ROOT}" == "/opt/eth_hedge_sim_2" ]]; then + resolve_slot 2 + elif [[ "${INSTALL_ROOT}" == "/opt/eth_hedge_sim_3" ]]; then + resolve_slot 3 + else + resolve_slot "${INSTANCE_SLOT:-1}" + fi + if repo_ready "${INSTALL_ROOT}"; then handle_existing else diff --git a/deploy/lib/uninstall.sh b/deploy/lib/uninstall.sh index fd67007..5ff4866 100755 --- a/deploy/lib/uninstall.sh +++ b/deploy/lib/uninstall.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# deploy/lib/uninstall.sh — 一键卸载(仅本项目 PM2,删除 /opt 安装目录) +# deploy/lib/uninstall.sh — 一键卸载指定槽位(仅该槽位 PM2 + 目录) set -e set -u if [ -n "${BASH_VERSION:-}" ]; then @@ -13,6 +13,8 @@ source "${LIB_DIR}/common.sh" main_uninstall() { require_root + resolve_slot "${INSTANCE_SLOT:-1}" + local root="" if ! root="$(resolve_repo_root)"; then if [[ -d "${INSTALL_ROOT}" ]]; then @@ -22,13 +24,15 @@ main_uninstall() { fi fi REPO_ROOT="${root}" + INSTALL_ROOT="${root}" + apply_slot_for_root "${root}" echo "" - echo "将卸载 比特骆驼自动化对冲系统 (eth_hedge_sim):" + echo "将卸载 比特骆驼自动化对冲系统 槽位 ${INSTANCE_SLOT}:" echo " - 停止并删除 PM2 进程: ${PM2_APP_NAME}" echo " - 删除安装目录: ${INSTALL_ROOT}" echo " - 备份 .env / 数据库到 ${BACKUP_ROOT}" - echo " - 不影响其它 PM2 应用 / 不卸载系统 Python、Node、PM2" + echo " - 不影响其它槽位 / 其它 PM2 / 系统 Python、Node、PM2" echo "" if ! confirm_yes "确认卸载并删除 ${INSTALL_ROOT}?"; then log "已取消卸载" @@ -37,7 +41,7 @@ main_uninstall() { local stamp backup_dir stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)" - backup_dir="${BACKUP_ROOT}/pre-uninstall-${stamp}" + backup_dir="${BACKUP_ROOT}/pre-uninstall-slot${INSTANCE_SLOT}-${stamp}" mkdir -p "${backup_dir}" step "备份配置与数据" @@ -49,7 +53,9 @@ main_uninstall() { fi { echo "created_at=${stamp}" + echo "instance_slot=${INSTANCE_SLOT}" echo "install_root=${INSTALL_ROOT}" + echo "api_port=${API_PORT}" echo "pm2_app=${PM2_APP_NAME}" echo "action=rm_rf_install_root" } >"${backup_dir}/uninstall.manifest" @@ -60,28 +66,23 @@ main_uninstall() { step "删除安装目录 ${INSTALL_ROOT}" if [[ -d "${INSTALL_ROOT}" ]]; then - # 安全:只允许删除约定安装路径(默认 /opt/eth_hedge_sim) - case "${INSTALL_ROOT}" in - /opt/eth_hedge_sim|/opt/eth_hedge_sim/) - rm -rf "${INSTALL_ROOT}" - log "已删除: ${INSTALL_ROOT}" - ;; - *) - if [[ "${ALLOW_UNSAFE_UNINSTALL:-}" == "1" ]]; then - rm -rf "${INSTALL_ROOT}" - log "已删除(ALLOW_UNSAFE_UNINSTALL=1): ${INSTALL_ROOT}" - else - die "拒绝删除非默认路径 ${INSTALL_ROOT};若确认,设置 ALLOW_UNSAFE_UNINSTALL=1" - fi - ;; - esac + if is_allowed_install_root "${INSTALL_ROOT}"; then + rm -rf "${INSTALL_ROOT}" + log "已删除: ${INSTALL_ROOT}" + elif [[ "${ALLOW_UNSAFE_UNINSTALL:-}" == "1" ]]; then + rm -rf "${INSTALL_ROOT}" + log "已删除(ALLOW_UNSAFE_UNINSTALL=1): ${INSTALL_ROOT}" + else + die "拒绝删除非约定路径 ${INSTALL_ROOT};允许: /opt/eth_hedge_sim[_2|_3];或 ALLOW_UNSAFE_UNINSTALL=1" + fi else log "安装目录不存在,跳过删除" fi - # 清理历史「移走未删」残留 - local leftover - for leftover in /opt/eth_hedge_sim.removed.* /opt/eth_hedge_sim.old.*; do + # 清理历史「移走未删」残留(仅当前槽位前缀) + local leftover base + base="${INSTALL_ROOT}" + for leftover in "${base}.removed."* "${base}.old."*; do if [[ -e "${leftover}" ]]; then rm -rf "${leftover}" log "已清理残留: ${leftover}" @@ -90,11 +91,13 @@ main_uninstall() { echo "" echo "卸载完成." + echo " 槽位: ${INSTANCE_SLOT}" echo " 配置/数据备份: ${backup_dir}" echo " 安装目录已删除: ${INSTALL_ROOT}" echo "" - echo "重新部署:" - echo " curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash" + echo "重新部署该槽位:" + echo " bash /opt/eth_hedge_sim/deploy/manage.sh # 选 1 → 槽位 ${INSTANCE_SLOT}" + echo " 或: INSTANCE_SLOT=${INSTANCE_SLOT} SKIP_SLOT_PROMPT=1 bash …/deploy/lib/install.sh" } main_uninstall "$@" diff --git a/deploy/lib/update.sh b/deploy/lib/update.sh index 5efa13c..6a85e8b 100755 --- a/deploy/lib/update.sh +++ b/deploy/lib/update.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# deploy/lib/update.sh — 更新(仅 git pull,禁止 scp) +# deploy/lib/update.sh — 更新(仅 git pull,禁止 scp);支持单槽位或全部串行 set -e set -u if [ -n "${BASH_VERSION:-}" ]; then @@ -10,22 +10,58 @@ LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=common.sh source "${LIB_DIR}/common.sh" +update_one_slot() { + local slot="$1" + resolve_slot "${slot}" + if ! repo_ready "${INSTALL_ROOT}"; then + log "槽位 ${slot} 未安装(${INSTALL_ROOT}),跳过" + return 0 + fi + step "更新槽位 ${slot}: ${INSTALL_ROOT} :${API_PORT} ${PM2_APP_NAME}" + ensure_system_deps + bash "${INSTALL_ROOT}/deploy/pull_and_restart.sh" + verify_health "${API_PORT}" || true + log "槽位 ${slot} 更新完成" +} + +update_all_slots() { + local s + local any=0 + step "串行更新全部已安装策略机槽位(避免同机并行 npm build)" + for s in $(list_installed_slots); do + any=1 + update_one_slot "${s}" + done + if [[ "${any}" -eq 0 ]]; then + die "未找到已安装的策略机(槽位 1/2/3)" + fi + echo "" + log "全部槽位更新完成" +} + main_update() { require_root + local mode="${EHS_UPDATE_MODE:-one}" + if [[ "${mode}" == "all" ]]; then + update_all_slots + return 0 + fi + + resolve_slot "${INSTANCE_SLOT:-1}" if ! REPO_ROOT="$(resolve_repo_root)"; then - die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署策略机」" + die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署策略机」并选择该槽位" fi if ! repo_ready "${REPO_ROOT}"; then - die "安装不完整,请先执行「1) 一键部署策略机」" + die "安装不完整: ${REPO_ROOT}" fi step "更新 — 环境检测 (缺则装,有则跳过)" ensure_system_deps - step "更新 比特骆驼自动化对冲系统 (git pull + 构建 + pm2 reload)" + step "更新槽位 ${INSTANCE_SLOT} (git pull + 构建 + pm2 reload)" bash "${REPO_ROOT}/deploy/pull_and_restart.sh" - verify_health || true + verify_health "${API_PORT}" || true echo "" - log "更新完成" + log "更新完成 slot=${INSTANCE_SLOT} http://127.0.0.1:${API_PORT}/health" } main_update "$@" diff --git a/deploy/manage.sh b/deploy/manage.sh index df15cad..584e845 100755 --- a/deploy/manage.sh +++ b/deploy/manage.sh @@ -182,13 +182,15 @@ show_banner() { } show_menu() { - echo " 1) 一键部署策略机" + echo " 1) 一键部署策略机(可选槽位 1/2/3)" echo " 2) 一键部署中控机" - echo " 3) 一键卸载" - echo " 4) 更新策略机" + echo " 3) 一键卸载策略机(选槽位)" + echo " 4) 更新策略机(选槽位 / 全部)" echo " 5) 更新中控机" echo " 0) 退出" echo "" + echo " 槽位: 1→:5155 2→:5156 3→:5157(同机最多三套,实盘请只用 1)" + echo "" } # 交互读入(兼容 curl|bash) @@ -224,6 +226,42 @@ deploy_control_node() { log "中控部署完成 → http://<本机IP>:5160 默认账号 admin / admin123" } +run_with_slot_env() { + # 子脚本继承当前槽位变量 + env \ + INSTANCE_SLOT="${INSTANCE_SLOT}" \ + INSTALL_ROOT="${INSTALL_ROOT}" \ + API_PORT="${API_PORT}" \ + PM2_APP_NAME="${PM2_APP_NAME}" \ + SKIP_SLOT_PROMPT=1 \ + EHS_FROM_MANAGE=1 \ + "$@" +} + +prompt_update_target() { + local line="" + echo "" + echo "更新目标:" + echo " 1/2/3) 指定槽位" + echo " a) 全部已安装槽位(串行,适合 2G 测机)" + cm_read line "请选择 [1/2/3/a,默认1]: " + line="${line:-1}" + case "${line}" in + a|A|all|ALL) + EHS_UPDATE_MODE=all + ;; + 1|2|3) + EHS_UPDATE_MODE=one + resolve_slot "${line}" + ;; + *) + echo "无效选择,使用槽位 1" + EHS_UPDATE_MODE=one + resolve_slot 1 + ;; + esac +} + main_menu() { bootstrap_repo # shellcheck source=lib/common.sh @@ -237,16 +275,23 @@ main_menu() { cm_read choice "请选择 [0-5]: " case "${choice}" in 1) - bash "${LIB_DIR}/install.sh" + prompt_instance_slot + run_with_slot_env bash "${LIB_DIR}/install.sh" ;; 2) deploy_control_node ;; 3) - bash "${LIB_DIR}/uninstall.sh" + prompt_instance_slot + run_with_slot_env bash "${LIB_DIR}/uninstall.sh" ;; 4) - bash "${LIB_DIR}/update.sh" + prompt_update_target + if [[ "${EHS_UPDATE_MODE}" == "all" ]]; then + env EHS_UPDATE_MODE=all SKIP_SLOT_PROMPT=1 bash "${LIB_DIR}/update.sh" + else + run_with_slot_env bash "${LIB_DIR}/update.sh" + fi ;; 5) deploy_control_node diff --git a/deploy/pull_and_restart.sh b/deploy/pull_and_restart.sh index 1837206..d13d81b 100755 --- a/deploy/pull_and_restart.sh +++ b/deploy/pull_and_restart.sh @@ -2,41 +2,57 @@ # 一键更新/部署:仅 git pull,禁止 scp 传代码。 # 用法: # bash /opt/eth_hedge_sim/deploy/pull_and_restart.sh -# 或通过 manage.sh 选 1/3 +# bash /opt/eth_hedge_sim_2/deploy/pull_and_restart.sh +# 或通过 manage.sh 选槽位后部署/更新 set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" -# 有 root 时:检测 Python / Node / PM2,缺则装、有则跳过(pull 前用当前脚本) -if [[ -f "$ROOT/deploy/lib/common.sh" ]] && [[ "$(id -u)" -eq 0 ]]; then +_COMMON="$ROOT/deploy/lib/common.sh" +if [[ -f "$_COMMON" ]]; then # shellcheck source=lib/common.sh - source "$ROOT/deploy/lib/common.sh" + source "$_COMMON" + apply_slot_for_root "$ROOT" +fi + +# 有 root 时:检测 Python / Node / PM2,缺则装、有则跳过(pull 前用当前脚本) +if [[ -f "$_COMMON" ]] && [[ "$(id -u)" -eq 0 ]]; then ensure_system_deps fi -echo "[比特骆驼] pull @ $ROOT" +echo "[比特骆驼] pull @ $ROOT (slot=${INSTANCE_SLOT:-?} port=${API_PORT:-?})" if [[ -d "$ROOT/.git" ]]; then git fetch --all --prune git pull --ff-only fi # pull 后再检测一次,确保用上仓库里最新的 python3-venv / ensurepip 逻辑 -if [[ -f "$ROOT/deploy/lib/common.sh" ]] && [[ "$(id -u)" -eq 0 ]]; then +if [[ -f "$_COMMON" ]]; then # shellcheck source=lib/common.sh - source "$ROOT/deploy/lib/common.sh" + source "$_COMMON" + apply_slot_for_root "$ROOT" +fi +if [[ -f "$_COMMON" ]] && [[ "$(id -u)" -eq 0 ]]; then ensure_system_deps fi if [[ ! -f "$ROOT/.env" ]]; then echo "[比特骆驼] missing .env — copy from example" cp "$ROOT/.env.example" "$ROOT/.env" - # 云上测试机默认直连 OKX + # 云上测试机默认直连 OKX;端口按槽位 + local_port="${API_PORT:-5155}" sed -i 's/^OKX_HTTP_PROXY=.*/OKX_HTTP_PROXY=/' "$ROOT/.env" || true - sed -i 's/^API_PORT=.*/API_PORT=5155/' "$ROOT/.env" || true + sed -i "s/^API_PORT=.*/API_PORT=${local_port}/" "$ROOT/.env" || true sed -i 's/^ENV_NAME=.*/ENV_NAME=test/' "$ROOT/.env" || true sed -i 's/^EXCHANGE=.*/EXCHANGE=okx/' "$ROOT/.env" || true - echo "[比特骆驼] wrote .env — please review AUTH_* secrets" + if [[ -f "$_COMMON" ]]; then + secret="$(random_auth_secret)" + if [[ -n "${secret}" ]]; then + sed -i "s/^AUTH_SECRET=.*/AUTH_SECRET=${secret}/" "$ROOT/.env" || true + fi + fi + echo "[比特骆驼] wrote .env — API_PORT=${local_port};请按需修改 AUTH_*" fi if ! command -v python3 >/dev/null 2>&1; then @@ -93,8 +109,13 @@ else echo "[比特骆驼] pm2 already present, skip install" fi +# 按本目录槽位重写 PM2 配置(避免 git pull 盖回槽位 1 硬编码) +if [[ -f "$_COMMON" ]]; then + write_ecosystem_config "$ROOT" "${PM2_APP_NAME}" "${API_PORT}" +fi + # 仅 reload 本项目进程,禁止 pm2 restart all pm2 startOrReload "$ROOT/deploy/ecosystem.config.cjs" --update-env pm2 save -echo "[比特骆驼] done. health: http://127.0.0.1:5155/health" +echo "[比特骆驼] done. slot=${INSTANCE_SLOT:-1} health: http://127.0.0.1:${API_PORT:-5155}/health" diff --git a/docs/中控Fleet说明.md b/docs/中控Fleet说明.md index e12e731..251bf94 100644 --- a/docs/中控Fleet说明.md +++ b/docs/中控Fleet说明.md @@ -1,147 +1,168 @@ -# 中控(Fleet Control)使用说明 - -中控与策略机在**同一仓库**(`eth_hedge_sim`),**独立进程**部署。中控通常跑在**本地局域网服务器**,**不访问交易所**;只通过专用 API Token 对多台策略机做:状态监控、启停、远程更新代码、免密进入策略页。 - -建议策略机与中控使用**同一 git 版本**(同 `main` / 同 tag),避免 `/api/fleet/*` 接口漂移。 - ---- - -## 1. 架构 - -```text -本地中控 (5160) 云端策略机 × N (5155) -┌─────────────────┐ ┌──────────────────┐ -│ control 前端/后端 │──X-Fleet-Token──│ eth-hedge-api │ -│ 不连交易所 │──SSH 无需主路径──│ OKX/币安交易密钥 │ -└─────────────────┘ └──────────────────┘ -``` - -| 组件 | 端口 | PM2 名 | 代码目录 | 配置 | -|------|------|--------|----------|------| -| 策略机 | 5155 | `eth-hedge-api` | `backend/` + `frontend/` | 仓库根 `.env` | -| 中控 | 5160 | `eth-hedge-control` | `control/backend` + `control/frontend` | 仓库根 `.env.control` | - -中控浏览器**不直连**策略机 API(避免跨域与密钥进前端);一律由中控后端代理。 - ---- - -## 2. 一键部署(manage.sh) - -目标机 Ubuntu 22.04: - -```bash -curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash -``` - -交互菜单: - -| 选项 | 作用 | -|------|------| -| 1) 一键部署策略机 | 克隆/构建,启动 `eth-hedge-api` :5155 | -| 2) 一键部署中控机 | 构建中控,启动 `eth-hedge-control` :5160;自动补全 `.env.control`(**已有非空值不覆盖**) | -| 3) 一键卸载 | 卸载策略相关(按现有 uninstall 逻辑) | -| 4) 更新策略机 | `git pull` + 构建 + reload 策略机 | -| 5) 更新中控机 | `control/deploy/update.sh` | -| 0) 退出 | — | - -也可本机已安装后: - -```bash -bash /opt/eth_hedge_sim/deploy/manage.sh -# 或仅更新中控 -bash /opt/eth_hedge_sim/control/deploy/update.sh -# 开发机 SSH 触发中控(可选) -# CONTROL_HOST=... CONTROL_PASS=... python scripts/deploy_control.py -``` - ---- - -## 3. 中控登录与系统设置 - -### 3.1 默认账号 - -- 首次:`admin` / `admin123`(一键部署写入 `.env.control`,已有值不覆盖)。 -- **改密后**:登录页与设置页**不再显示**默认账号提示。 -- 登录框**不预填用户名**,并关闭浏览器自动填充。 - -### 3.2 系统设置(三个 Tab) - -1. **中控登录账户** - - 修改用户名/密码(改后旧会话作废)。 - - **本地局域网免登录**:开关;开启后,从私网 IP(如 `192.168.x` / `10.x` / `172.16–31.x` / 本机)访问可自动进入监控区,无需输密码。 -2. **添加策略机** — 名称 + 公网 Base URL(如 `https://dc.hyf2.cc`)。 -3. **策略机列表** — 生成/轮换 Token、删除。 - -### 3.3 `.env.control` 常用项 - -见仓库 [`.env.control.example`](../.env.control.example): - -| 变量 | 说明 | -|------|------| -| `CONTROL_AUTH_USERNAME` / `PASSWORD` | 中控登录 | -| `CONTROL_AUTH_SECRET` | Token 加密与签名密钥(部署自动填默认值,有值不覆盖) | -| `CONTROL_LAN_AUTH_BYPASS` | `1`=局域网免登录,`0`=关闭 | -| `CONTROL_SSE_INTERVAL_SEC` | 监控 SSE 推送间隔(默认 1 秒) | -| `CONTROL_SSE_PROBE_TIMEOUT_SEC` | 单次探测策略机超时(默认 2.5 秒) | -| `CONTROL_POLL_INTERVAL_SEC` | 兼容保留(旧前端轮询;现以 SSE 为主) | - -**勿提交** `.env.control` 到 git。 - ---- - -## 4. 策略机配对(API Token) - -1. 中控 **策略机列表** → **生成 Token**(明文只显示一次,中控侧加密保存)。 -2. 登录该策略机 → **系统设置 → 登录账户 → 中控 API Token** → 粘贴保存(策略机存哈希)。 -3. 中控监控区显示「已配对」;若 Token 不一致则「配对失败」并提示重新生成保存。 - -### 策略机 Fleet 接口(头:`X-Fleet-Token`,也可 `Authorization: Fleet `) - -| 方法 | 路径 | 作用 | -|------|------|------| -| GET | `/api/fleet/status` | 状态摘要 + 持仓/策略详情 | -| POST | `/api/fleet/start` | 启动策略(LIVE 仍过策略机门禁) | -| POST | `/api/fleet/pause` | 停止/暂停 | -| POST | `/api/fleet/update` | 本机 `deploy/lib/update.sh`(reload,不自动 start) | -| POST | `/api/fleet/issue-login` | 签发一次性免密登录票 | - -免密进策略页:中控拿到 ticket 后打开 -`https://<策略机>/fleet-login?ticket=...` -→ `POST /api/auth/fleet-exchange` 兑换普通会话 → `/plan`。 -票约 **60 秒、一次性**;长期 Token **不进**浏览器地址栏。 - ---- - -## 5. 监控区 - -- **实时推送**:浏览器通过 SSE(`GET /api/nodes/status/stream`)接收状态;中控约每秒**并行**拉取各策略机 `/api/fleet/status`,有变化才推送;断线自动重连。工具栏显示「实时 · 1s」。 -- **刷新**:仍可手动走 `/api/nodes/status/all` 拉一次。 -- **卡片**:在线/离线、SIM/LIVE、阶段、持仓、轮次、行情、Token 状态。 -- **运行中**:卡片绿色;底部按钮显示「运行中」且不可点启动。 -- **点击卡片**:放大弹层 — 策略详情 + 持仓腿表;**净浮盈 / 浮盈** 正绿负红加粗。 -- **登录策略机**:免密新标签打开策略页。 -- **更新代码 / 勾选更新**:**二次确认**后执行;会 reload 进程,**不会**自动 start。 - -中控**不做**:改策略参数、代开平仓、资金划转、直连交易所。 - ---- - -## 6. 安全边界 - -- 中控无交易所 API Key;交易密钥只在各策略机 `.env`。 -- Fleet Token 权限含启停/更新/签发登录票 → 泄露后两边立即轮换。 -- 局域网免登录仅私网 IP;勿对公网暴露中控端口。 -- 策略机 LIVE 启停仍走本机 `live_ready()` 等门禁,中控不能绕过。 - ---- - -## 7. 相关文档与入口 - -| 文档 | 内容 | -|------|------| -| 本文 | 中控安装、配对、监控、安全 | -| [更新说明](./更新说明.md) | 发版变更记录 | -| [代码结构](./代码结构.md) | 仓库目录(含 `control/`) | -| [README](../README.md) | 总览与部署入口 | - -产品文档索引(README「文档」一节)含本页链接。 +# 中控(Fleet Control)使用说明 + +中控与策略机在**同一仓库**(`eth_hedge_sim`),**独立进程**部署。中控通常跑在**本地局域网服务器**,**不访问交易所**;只通过专用 API Token 对多台策略机做:状态监控、启停、远程更新代码、免密进入策略页。 + +建议策略机与中控使用**同一 git 版本**(同 `main` / 同 tag),避免 `/api/fleet/*` 接口漂移。 + +--- + +## 1. 架构 + +```text +本地中控 (5160) 云端策略机 × N (5155) +┌─────────────────┐ ┌──────────────────┐ +│ control 前端/后端 │──X-Fleet-Token──│ eth-hedge-api │ +│ 不连交易所 │──SSH 无需主路径──│ OKX/币安交易密钥 │ +└─────────────────┘ └──────────────────┘ +``` + +| 组件 | 端口 | PM2 名 | 代码目录 | 配置 | +|------|------|--------|----------|------| +| 策略机(槽位 1) | 5155 | `eth-hedge-api` | `/opt/eth_hedge_sim` | 该目录 `.env` | +| 策略机(槽位 2,可选) | 5156 | `eth-hedge-api-2` | `/opt/eth_hedge_sim_2` | 独立 `.env` / SQLite | +| 策略机(槽位 3,可选) | 5157 | `eth-hedge-api-3` | `/opt/eth_hedge_sim_3` | 独立 `.env` / SQLite | +| 中控 | 5160 | `eth-hedge-control` | `control/backend` + `control/frontend` | 仓库根 `.env.control` | + +中控浏览器**不直连**策略机 API(避免跨域与密钥进前端);一律由中控后端代理。 + +### 同机多套策略机(测试用) + +- **实盘**:一机一套,只用**槽位 1**。 +- **测试**:同一台机可再装槽位 2/3(完整独立克隆 + 独立进程),菜单选槽位即可;端口/PM2 名自动分配。 +- 中控登记多套时,每套一条「策略机」:Base URL 带不同端口,例如 `http://x.x.x.x:5155`、`:5156`、`:5157`(或各自反代域名)。 +- 更新「全部」为**串行**(避免 2G 内存并行 `npm build`)。 + +| 槽位 | 目录 | 端口 | PM2 | +|------|------|------|-----| +| 1 | `/opt/eth_hedge_sim` | 5155 | `eth-hedge-api` | +| 2 | `/opt/eth_hedge_sim_2` | 5156 | `eth-hedge-api-2` | +| 3 | `/opt/eth_hedge_sim_3` | 5157 | `eth-hedge-api-3` | + +非交互示例: + +```bash +INSTANCE_SLOT=2 SKIP_SLOT_PROMPT=1 bash /opt/eth_hedge_sim/deploy/lib/install.sh +``` + +--- + +## 2. 一键部署(manage.sh) + +目标机 Ubuntu 22.04: + +```bash +curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash +``` + +交互菜单: + +| 选项 | 作用 | +|------|------| +| 1) 一键部署策略机 | 选槽位 1/2/3 → 克隆/构建,启动对应端口 | +| 2) 一键部署中控机 | 构建中控,启动 `eth-hedge-control` :5160;自动补全 `.env.control`(**已有非空值不覆盖**) | +| 3) 一键卸载 | 选槽位,只卸该套(不影响其它槽位) | +| 4) 更新策略机 | 选槽位,或 `a` 全部串行更新 | +| 5) 更新中控机 | `control/deploy/update.sh` | +| 0) 退出 | — | + +也可本机已安装后: + +```bash +bash /opt/eth_hedge_sim/deploy/manage.sh +# 或仅更新中控 +bash /opt/eth_hedge_sim/control/deploy/update.sh +# 开发机 SSH 触发中控(可选) +# CONTROL_HOST=... CONTROL_PASS=... python scripts/deploy_control.py +``` + +--- + +## 3. 中控登录与系统设置 + +### 3.1 默认账号 + +- 首次:`admin` / `admin123`(一键部署写入 `.env.control`,已有值不覆盖)。 +- **改密后**:登录页与设置页**不再显示**默认账号提示。 +- 登录框**不预填用户名**,并关闭浏览器自动填充。 + +### 3.2 系统设置(三个 Tab) + +1. **中控登录账户** + - 修改用户名/密码(改后旧会话作废)。 + - **本地局域网免登录**:开关;开启后,从私网 IP(如 `192.168.x` / `10.x` / `172.16–31.x` / 本机)访问可自动进入监控区,无需输密码。 +2. **添加策略机** — 名称 + 公网 Base URL(如 `https://dc.hyf2.cc`)。 +3. **策略机列表** — 生成/轮换 Token、删除。 + +### 3.3 `.env.control` 常用项 + +见仓库 [`.env.control.example`](../.env.control.example): + +| 变量 | 说明 | +|------|------| +| `CONTROL_AUTH_USERNAME` / `PASSWORD` | 中控登录 | +| `CONTROL_AUTH_SECRET` | Token 加密与签名密钥(部署自动填默认值,有值不覆盖) | +| `CONTROL_LAN_AUTH_BYPASS` | `1`=局域网免登录,`0`=关闭 | +| `CONTROL_SSE_INTERVAL_SEC` | 监控 SSE 推送间隔(默认 1 秒) | +| `CONTROL_SSE_PROBE_TIMEOUT_SEC` | 单次探测策略机超时(默认 2.5 秒) | +| `CONTROL_POLL_INTERVAL_SEC` | 兼容保留(旧前端轮询;现以 SSE 为主) | + +**勿提交** `.env.control` 到 git。 + +--- + +## 4. 策略机配对(API Token) + +1. 中控 **策略机列表** → **生成 Token**(明文只显示一次,中控侧加密保存)。 +2. 登录该策略机 → **系统设置 → 登录账户 → 中控 API Token** → 粘贴保存(策略机存哈希)。 +3. 中控监控区显示「已配对」;若 Token 不一致则「配对失败」并提示重新生成保存。 + +### 策略机 Fleet 接口(头:`X-Fleet-Token`,也可 `Authorization: Fleet `) + +| 方法 | 路径 | 作用 | +|------|------|------| +| GET | `/api/fleet/status` | 状态摘要 + 持仓/策略详情 | +| POST | `/api/fleet/start` | 启动策略(LIVE 仍过策略机门禁) | +| POST | `/api/fleet/pause` | 停止/暂停 | +| POST | `/api/fleet/update` | 本机 `deploy/lib/update.sh`(reload,不自动 start) | +| POST | `/api/fleet/issue-login` | 签发一次性免密登录票 | + +免密进策略页:中控拿到 ticket 后打开 +`https://<策略机>/fleet-login?ticket=...` +→ `POST /api/auth/fleet-exchange` 兑换普通会话 → `/plan`。 +票约 **60 秒、一次性**;长期 Token **不进**浏览器地址栏。 + +--- + +## 5. 监控区 + +- **实时推送**:浏览器通过 SSE(`GET /api/nodes/status/stream`)接收状态;中控约每秒**并行**拉取各策略机 `/api/fleet/status`,有变化才推送;断线自动重连。工具栏显示「实时 · 1s」。 +- **刷新**:仍可手动走 `/api/nodes/status/all` 拉一次。 +- **卡片**:在线/离线、SIM/LIVE、阶段、持仓、轮次、行情、Token 状态。 +- **运行中**:卡片绿色;底部按钮显示「运行中」且不可点启动。 +- **点击卡片**:放大弹层 — 策略详情 + 持仓腿表;**净浮盈 / 浮盈** 正绿负红加粗。 +- **登录策略机**:免密新标签打开策略页。 +- **更新代码 / 勾选更新**:**二次确认**后执行;会 reload 进程,**不会**自动 start。 + +中控**不做**:改策略参数、代开平仓、资金划转、直连交易所。 + +--- + +## 6. 安全边界 + +- 中控无交易所 API Key;交易密钥只在各策略机 `.env`。 +- Fleet Token 权限含启停/更新/签发登录票 → 泄露后两边立即轮换。 +- 局域网免登录仅私网 IP;勿对公网暴露中控端口。 +- 策略机 LIVE 启停仍走本机 `live_ready()` 等门禁,中控不能绕过。 + +--- + +## 7. 相关文档与入口 + +| 文档 | 内容 | +|------|------| +| 本文 | 中控安装、配对、监控、安全 | +| [更新说明](./更新说明.md) | 发版变更记录 | +| [代码结构](./代码结构.md) | 仓库目录(含 `control/`) | +| [README](../README.md) | 总览与部署入口 | + +产品文档索引(README「文档」一节)含本页链接。