From d7268103204502a5d2605d23a6b70b0f4119ae4a Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 1 Oct 2026 11:03:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=20Ubuntu=20=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E9=83=A8=E7=BD=B2=E4=B8=8E=20PM2=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE=E6=94=B9?= =?UTF-8?q?=E5=AF=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 默认账号改为 admin/admin123,去掉 Windows 部署说明,与客户端同套部署流程。 Co-authored-by: Cursor --- .env.example | 4 +- .gitattributes | 3 + README.md | 14 +- app.py | 75 +++++++- deploy/README.md | 103 +++++++++++ deploy/lib/common.sh | 360 +++++++++++++++++++++++++++++++++++++ deploy/lib/doctor.sh | 23 +++ deploy/lib/install.sh | 109 +++++++++++ deploy/lib/uninstall.sh | 66 +++++++ deploy/lib/update.sh | 60 +++++++ deploy/manage.sh | 139 ++++++++++++++ deploy/pull_and_restart.sh | 106 +++++++++++ deploy/setup_env.sh | 227 +++++++++++++++++++++++ ecosystem.config.cjs | 27 ++- lib/env_lib.py | 55 ++++++ static/hub.css | 6 + templates/base.html | 1 + templates/settings.html | 28 +++ 使用说明.md | 42 ++--- 设计文档.md | 4 +- 部署文档.md | 109 +++++------ 21 files changed, 1456 insertions(+), 105 deletions(-) create mode 100644 .gitattributes create mode 100644 deploy/README.md create mode 100644 deploy/lib/common.sh create mode 100644 deploy/lib/doctor.sh create mode 100644 deploy/lib/install.sh create mode 100644 deploy/lib/uninstall.sh create mode 100644 deploy/lib/update.sh create mode 100644 deploy/manage.sh create mode 100644 deploy/pull_and_restart.sh create mode 100644 deploy/setup_env.sh create mode 100644 lib/env_lib.py create mode 100644 templates/settings.html diff --git a/.env.example b/.env.example index 27939c6..af80059 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ -# crypto_okx_hub — 设计阶段占位;实现时补全 +# crypto_okx_hub APP_HOST=0.0.0.0 APP_PORT=5100 HUB_USERNAME=admin -HUB_PASSWORD=change-me +HUB_PASSWORD=admin123 FLASK_SECRET_KEY=change-me-to-long-random DB_PATH=hub.db HUB_PULL_INTERVAL_SEC=10 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..63f8771 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.sh text eol=lf +deploy/**/*.sh text eol=lf +ecosystem.config.cjs text eol=lf diff --git a/README.md b/README.md index dddce0f..008b2cb 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,16 @@ OKX **期权多机中控**:登录后查看各期权客户端持仓总览,并 |----|------| | 云仓库 | [https://git.bz121.com/dekun/crypto_okx_hub.git](https://git.bz121.com/dekun/crypto_okx_hub.git) | | 关联客户端 | [crypto_okx](https://git.bz121.com/dekun/crypto_okx.git)(期权执行机,公网域名部署) | -| 文档 | [使用说明.md](./使用说明.md) · [部署文档.md](./部署文档.md) · [设计文档.md](./设计文档.md) | +| 文档 | [使用说明.md](./使用说明.md) · [部署文档.md](./部署文档.md) · [设计文档.md](./设计文档.md) · [deploy/README.md](./deploy/README.md) | **第一期范围:** 多机持仓总览 + 内嵌打开客户端(可新标签)。中控不下单、不存交易所密钥。 +## 一键部署(Ubuntu) + ```bash -# 本地开发(实现后) -# python -m venv .venv && .venv\Scripts\activate -# pip install -r requirements.txt -# copy .env.example .env -# python app.py +curl -fsSL https://git.bz121.com/dekun/crypto_okx_hub/raw/branch/main/deploy/manage.sh | bash ``` + +安装到 `/opt/crypto_okx_hub`,PM2 进程名 `crypto_okx_hub`,默认端口 **5100**,默认账号 **admin / admin123**。 + +详见 **[部署文档.md](./部署文档.md)**。 diff --git a/app.py b/app.py index a001454..aa370b3 100644 --- a/app.py +++ b/app.py @@ -22,10 +22,12 @@ ROOT = Path(__file__).resolve().parent if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -load_dotenv(ROOT / ".env") +ENV_PATH = ROOT / ".env" +load_dotenv(ENV_PATH) from lib.bridge_client_lib import pull_all_enabled from lib.db_lib import init_db +from lib.env_lib import apply_env_updates from lib.instances_lib import ( create_instance, delete_instance, @@ -37,10 +39,14 @@ from lib.sso_lib import build_embed_open_url APP_HOST = (os.getenv("APP_HOST") or "0.0.0.0").strip() or "0.0.0.0" APP_PORT = int(os.getenv("APP_PORT") or 5100) -HUB_USERNAME = (os.getenv("HUB_USERNAME") or "admin").strip() or "admin" -HUB_PASSWORD = (os.getenv("HUB_PASSWORD") or "change-me").strip() or "change-me" SECRET = (os.getenv("FLASK_SECRET_KEY") or "hub-dev-secret").strip() +# 可变登录态:系统设置页可改,并立刻生效 +_auth = { + "username": (os.getenv("HUB_USERNAME") or "admin").strip() or "admin", + "password": (os.getenv("HUB_PASSWORD") or "admin123").strip() or "admin123", +} + app = Flask(__name__) app.secret_key = SECRET app.config["TEMPLATES_AUTO_RELOAD"] = True @@ -48,6 +54,23 @@ app.config["TEMPLATES_AUTO_RELOAD"] = True init_db() +def hub_username() -> str: + return _auth["username"] + + +def hub_password() -> str: + return _auth["password"] + + +def set_hub_credentials(username: str, password: str) -> None: + u = (username or "").strip() or "admin" + p = password if password is not None else "" + _auth["username"] = u + _auth["password"] = p + os.environ["HUB_USERNAME"] = u + os.environ["HUB_PASSWORD"] = p + + def login_required(f): @wraps(f) def wrapped(*args, **kwargs): @@ -65,7 +88,7 @@ def login(): if request.method == "POST": u = (request.form.get("username") or "").strip() p = request.form.get("password") or "" - if u == HUB_USERNAME and p == HUB_PASSWORD: + if u == hub_username() and p == hub_password(): session["logged_in"] = True session.permanent = True nxt = (request.args.get("next") or request.form.get("next") or "/").strip() or "/" @@ -125,6 +148,50 @@ def instances_page(): return render_template("instances.html", instances=list_instances()) +@app.route("/settings", methods=["GET", "POST"]) +@login_required +def settings_page(): + if request.method == "POST": + current_pw = request.form.get("current_password") or "" + new_username = (request.form.get("new_username") or "").strip() + new_password = request.form.get("new_password") or "" + confirm = request.form.get("confirm_password") or "" + + if current_pw != hub_password(): + flash("当前密码错误") + return redirect(url_for("settings_page")) + if len(new_username) < 2 or len(new_username) > 64: + flash("用户名长度需为 2–64") + return redirect(url_for("settings_page")) + if any(ch.isspace() for ch in new_username): + flash("用户名不能包含空格") + return redirect(url_for("settings_page")) + + updates: dict[str, str] = {"HUB_USERNAME": new_username} + final_password = hub_password() + if new_password or confirm: + if len(new_password) < 6: + flash("新密码至少 6 位") + return redirect(url_for("settings_page")) + if new_password != confirm: + flash("两次输入的新密码不一致") + return redirect(url_for("settings_page")) + updates["HUB_PASSWORD"] = new_password + final_password = new_password + + try: + apply_env_updates(ENV_PATH, updates) + except OSError as exc: + flash(f"写入 .env 失败: {exc}") + return redirect(url_for("settings_page")) + + set_hub_credentials(new_username, final_password) + flash("账号已更新,立即生效") + return redirect(url_for("settings_page")) + + return render_template("settings.html", current_username=hub_username()) + + @app.route("/desk") @login_required def desk(): diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..d152683 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,103 @@ +# crypto_okx_hub 环境一键部署(Ubuntu / root /opt) + +在 **`/opt/crypto_okx_hub`** 下以 **root** 部署期权多机中控,使用 **PM2** 常驻. + +仓库: [https://git.bz121.com/dekun/crypto_okx_hub.git](https://git.bz121.com/dekun/crypto_okx_hub.git) + +完整步骤见根目录 **[部署文档.md](../部署文档.md)**. + +--- + +## 一键部署管理器(推荐) + +新服务器**无需先 clone**,一条命令进入菜单: + +```bash +curl -fsSL https://git.bz121.com/dekun/crypto_okx_hub/raw/branch/main/deploy/manage.sh | bash +``` + +已安装机器: + +```bash +bash /opt/crypto_okx_hub/deploy/manage.sh +``` + +### 菜单 + +| 选项 | 功能 | +|------|------| +| **1) 一键部署** | 环境识别 + 系统依赖 + Node/PM2 + venv + 启动 `crypto_okx_hub` | +| **2) 一键卸载** | 备份 `.env` / `hub.db` → 停 PM2 → 移走目录 | +| **3) 更新** | 快速更新 / 依赖更新 | +| **4) 仅检测** | 识别 OS / Python / Node / PM2 / 项目状态(不安装) | +| **0) 退出** | | + +### 部署完成后 + +- 访问: `http://<服务器IP>:5100/login` (端口以 `.env` 的 `APP_PORT` 为准) +- 默认账号: **admin** / **admin123**(可在页面「系统设置」修改) +- 路径: `/opt/crypto_okx_hub` +- 日志: `pm2 logs crypto_okx_hub --lines 100` + +--- + +## 脚本结构 + +``` +deploy/ +├── manage.sh # 入口(自举 + 菜单) +├── lib/ +│ ├── common.sh # 公共函数 / 环境识别 / 验收 +│ ├── install.sh # 一键部署 +│ ├── uninstall.sh # 一键卸载 +│ ├── update.sh # 更新子菜单 +│ └── doctor.sh # 仅检测 +├── setup_env.sh # venv + pip(被 install 调用) +└── pull_and_restart.sh # 快速更新 +``` + +--- + +## 前置条件 + +- **Ubuntu 22.04 / 24.04**,用户 **root** +- 能 `git clone` [crypto_okx_hub](https://git.bz121.com/dekun/crypto_okx_hub.git) 到 `/opt/crypto_okx_hub` + +--- + +## 分步安装(仍可用) + +```bash +cd /opt +git clone https://git.bz121.com/dekun/crypto_okx_hub.git crypto_okx_hub +cd /opt/crypto_okx_hub +bash deploy/setup_env.sh --install-system-deps +pm2 start ecosystem.config.cjs +pm2 save && pm2 startup +``` + +`setup_env.sh` 常用参数: + +```bash +bash deploy/setup_env.sh --recreate-venv # 重建虚拟环境 +bash deploy/setup_env.sh --skip-pm2 # 不尝试安装 pm2 +bash deploy/setup_env.sh --skip-env-copy # 不复制 .env.example +``` + +若脚本报 `pipefail` 错误(CRLF 行尾),先转 LF: + +```bash +sed -i 's/\r$//' deploy/manage.sh deploy/lib/*.sh deploy/*.sh +``` + +--- + +## 环境变量(可选) + +```bash +INSTALL_ROOT=/opt/crypto_okx_hub +GIT_URL=https://git.bz121.com/dekun/crypto_okx_hub.git +GIT_BRANCH=main +BACKUP_ROOT=/root/backups +APP_PORT=5100 +``` diff --git a/deploy/lib/common.sh b/deploy/lib/common.sh new file mode 100644 index 0000000..3afcb40 --- /dev/null +++ b/deploy/lib/common.sh @@ -0,0 +1,360 @@ +#!/usr/bin/env bash +# deploy/lib/common.sh — crypto_okx_hub 部署公共函数 (Ubuntu / root / PM2) +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_okx_hub}" +GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_okx_hub.git}" +GIT_BRANCH="${GIT_BRANCH:-main}" +BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}" +TZ_NAME="${CM_TZ:-Asia/Shanghai}" +NODE_MAJOR="${NODE_MAJOR:-20}" +APP_PORT="${APP_PORT:-5100}" +PM2_APP_NAME="${PM2_APP_NAME:-crypto_okx_hub}" + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)" +REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" + +PM2_APPS=( + crypto_okx_hub +) +PM2_APPS_LEGACY=() + +CONFIG_PATHS=( + .env + hub.db +) + +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}" +} + +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_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}安装完成" +} + +pm2_app_exists() { + local name="$1" + if ! command -v pm2 >/dev/null 2>&1; then + return 1 + fi + if ! command -v python3 >/dev/null 2>&1; then + pm2 describe "${name}" 2>/dev/null | grep -q "status" + return $? + fi + pm2 jlist 2>/dev/null | python3 -c ' +import json,sys +name=sys.argv[1] +try: + apps=json.load(sys.stdin) +except Exception: + sys.exit(1) +sys.exit(0 if any(isinstance(a,dict) and a.get("name")==name for a in (apps or [])) else 1) +' "${name}" +} + +pm2_stop_project_apps() { + if ! command -v pm2 >/dev/null 2>&1; then + log "未安装 pm2,跳过" + return 0 + fi + local name + 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 + fi + done +} + +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_start_app() { + step "启动 PM2: ${PM2_APP_NAME}" + cd "${REPO_ROOT}" + [[ -f "${REPO_ROOT}/ecosystem.config.cjs" ]] || die "缺少 ecosystem.config.cjs" + [[ -x "${REPO_ROOT}/.venv/bin/python" ]] || die "缺少 .venv,请先运行 setup_env.sh" + if pm2_app_exists "${PM2_APP_NAME}"; then + log "已存在,执行 restart --update-env" + if ! pm2 restart "${PM2_APP_NAME}" --update-env; then + pm2 start ecosystem.config.cjs + fi + else + log "首次启动: pm2 start ecosystem.config.cjs" + pm2 start ecosystem.config.cjs + 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 +} + +read_app_port() { + local env_file="${REPO_ROOT}/.env" + local port="${APP_PORT}" + if [[ -f "${env_file}" ]]; then + local line + line="$(grep -E '^[[:space:]]*APP_PORT=' "${env_file}" | tail -n1 || true)" + if [[ -n "${line}" ]]; then + port="${line#*=}" + port="$(echo "${port}" | tr -d '"' | tr -d "'" | xargs)" + fi + fi + echo "${port:-5100}" +} + +verify_deployment() { + local ip="${1:-$(detect_server_ip)}" + local port + port="$(read_app_port)" + step "验收" + if command -v pm2 >/dev/null 2>&1 && pm2_app_exists "${PM2_APP_NAME}"; then + log "PM2: ${PM2_APP_NAME} 已注册" + else + log "警告: 未检测到 PM2 进程 ${PM2_APP_NAME}" + return 1 + fi + local code + code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 2 --max-time 3 "http://127.0.0.1:${port}/login" 2>/dev/null || echo "000")" + log "HTTP /login -> ${code}" + log "访问: http://${ip}:${port}/login" + return 0 +} + +print_post_install_guide() { + local ip="${1:-$(detect_server_ip)}" + local port + port="$(read_app_port)" + echo "" + echo "======== 部署完成 ========" + echo " 中控: http://${ip}:${port}/login" + echo " 账号: 见 .env 的 HUB_USERNAME / HUB_PASSWORD" + echo " 路径: ${REPO_ROOT}" + echo " 日志: pm2 logs ${PM2_APP_NAME} --lines 100" + echo " 菜单: bash ${REPO_ROOT}/deploy/manage.sh" + echo " 客户端: https://git.bz121.com/dekun/crypto_okx.git" + echo "==========================" + echo "" +} + +doctor_report() { + step "环境检测" + require_ubuntu || true + log "用户: $(id -un) uid=$(id -u)" + log "路径候选: INSTALL_ROOT=${INSTALL_ROOT}" + if command -v python3 >/dev/null 2>&1; then + log "Python: $(python3 --version 2>&1)" + else + log "Python: 未安装" + fi + if command -v node >/dev/null 2>&1; then + log "Node: $(node -v)" + else + log "Node: 未安装" + fi + if command -v pm2 >/dev/null 2>&1; then + log "PM2: $(pm2 -v)" + else + log "PM2: 未安装" + fi + local root="" + if root="$(resolve_repo_root 2>/dev/null)"; then + log "仓库: ${root} (就绪)" + REPO_ROOT="${root}" + if [[ -x "${root}/.venv/bin/python" ]]; then + log "venv: 存在" + else + log "venv: 缺失" + fi + if [[ -f "${root}/.env" ]]; then + log ".env: 存在" + else + log ".env: 缺失" + fi + if command -v pm2 >/dev/null 2>&1 && pm2_app_exists "${PM2_APP_NAME}"; then + log "PM2 进程: ${PM2_APP_NAME} 已注册" + else + log "PM2 进程: 未注册 ${PM2_APP_NAME}" + fi + else + log "仓库: 未安装到 ${INSTALL_ROOT}" + fi +} diff --git a/deploy/lib/doctor.sh b/deploy/lib/doctor.sh new file mode 100644 index 0000000..cc2e4e5 --- /dev/null +++ b/deploy/lib/doctor.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# deploy/lib/doctor.sh — 仅检测,不安装 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_doctor() { + local root="" + if root="$(resolve_repo_root 2>/dev/null)"; then + REPO_ROOT="${root}" + fi + doctor_report + echo "" + log "检测完成(未修改系统).需要安装请返回菜单选 1." +} + +main_doctor "$@" diff --git a/deploy/lib/install.sh b/deploy/lib/install.sh new file mode 100644 index 0000000..a113347 --- /dev/null +++ b/deploy/lib/install.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# deploy/lib/install.sh — crypto_okx_hub 一键部署 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +install_fresh() { + step "克隆仓库" + if [[ -d "${INSTALL_ROOT}" ]]; then + die "目录已存在: ${INSTALL_ROOT},请选修复环境" + fi + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" +} + +install_repair() { + step "修复环境(保留 .env 与 hub.db)" + bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps --recreate-venv +} + +run_install_pipeline() { + step "环境部署 setup_env.sh" + bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps + + pm2_start_app + pm2_save_startup + + verify_deployment "$(detect_server_ip)" || true + print_post_install_guide "$(detect_server_ip)" +} + +handle_existing_install() { + echo "" + echo "检测到已部署安装: ${INSTALL_ROOT}" + echo " a) 取消" + echo " b) 修复环境(重建 venv,保留 .env 与数据库)" + echo " c) 重新启动 PM2(不改依赖)" + local choice="" + cm_read choice "请选择 [a/b/c]: " + case "${choice}" in + b|B) + install_repair + if command -v pm2 >/dev/null 2>&1; then + pm2_start_app + pm2_save_startup + fi + verify_deployment "$(detect_server_ip)" || true + print_post_install_guide "$(detect_server_ip)" + ;; + c|C) + pm2_start_app + pm2_save_startup + verify_deployment "$(detect_server_ip)" || true + print_post_install_guide "$(detect_server_ip)" + ;; + *) + log "已取消" + ;; + esac +} + +ensure_repo_ready() { + if repo_ready "${INSTALL_ROOT}"; then + REPO_ROOT="${INSTALL_ROOT}" + elif [[ -n "${REPO_ROOT:-}" ]] && repo_ready "${REPO_ROOT}"; then + : + else + REPO_ROOT="" + fi + + if [[ -z "${REPO_ROOT}" ]]; then + install_system_packages + install_node_pm2 + install_fresh + REPO_ROOT="${INSTALL_ROOT}" + else + install_system_packages + install_node_pm2 + fi +} + +main_install() { + require_root + require_ubuntu + + if repo_ready "${INSTALL_ROOT}"; then + REPO_ROOT="${INSTALL_ROOT}" + elif repo_ready "${REPO_ROOT}"; then + : + else + REPO_ROOT="" + fi + + if [[ -n "${REPO_ROOT}" ]] && is_deployed "${REPO_ROOT}"; then + handle_existing_install + return 0 + fi + + ensure_repo_ready + run_install_pipeline +} + +main_install "$@" diff --git a/deploy/lib/uninstall.sh b/deploy/lib/uninstall.sh new file mode 100644 index 0000000..7b048d5 --- /dev/null +++ b/deploy/lib/uninstall.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# deploy/lib/uninstall.sh — crypto_okx_hub 一键卸载 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_uninstall() { + require_root + + local root="" + if ! root="$(resolve_repo_root)"; then + if [[ -d "${INSTALL_ROOT}" ]]; then + root="${INSTALL_ROOT}" + else + die "未找到安装目录 ${INSTALL_ROOT}" + fi + fi + REPO_ROOT="${root}" + + if ! confirm_uninstall; then + log "已取消卸载" + return 0 + fi + + local stamp backup_dir removed_dir + stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)" + backup_dir="${BACKUP_ROOT}/crypto_okx_hub-pre-uninstall-${stamp}" + removed_dir="${INSTALL_ROOT}.removed.${stamp}" + + step "备份配置到 ${backup_dir}" + backup_configs_to "${REPO_ROOT}" "${backup_dir}" + { + echo "created_at=${stamp}" + echo "install_root=${INSTALL_ROOT}" + echo "removed_dir=${removed_dir}" + } >"${backup_dir}/uninstall.manifest" + + step "停止并移除本项目 PM2 进程(不影响其它 PM2)" + pm2_stop_project_apps + pm2_delete_project_apps + + step "移走安装目录" + if [[ -d "${INSTALL_ROOT}" ]]; then + mv "${INSTALL_ROOT}" "${removed_dir}" + log "已移动: ${INSTALL_ROOT} -> ${removed_dir}" + else + log "安装目录不存在,跳过" + fi + + echo "" + echo "卸载完成." + echo " 配置备份: ${backup_dir}" + echo " 旧目录: ${removed_dir}" + echo "" + echo "回滚示例:" + echo " mv ${removed_dir} ${INSTALL_ROOT}" + echo " bash ${INSTALL_ROOT}/deploy/manage.sh # 选 1 修复环境" +} + +main_uninstall "$@" diff --git a/deploy/lib/update.sh b/deploy/lib/update.sh new file mode 100644 index 0000000..09ab087 --- /dev/null +++ b/deploy/lib/update.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# deploy/lib/update.sh — crypto_okx_hub 更新 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +update_quick() { + step "快速更新" + bash "${REPO_ROOT}/deploy/pull_and_restart.sh" + # shellcheck source=common.sh + source "${LIB_DIR}/common.sh" + verify_deployment "$(detect_server_ip)" || true +} + +update_deps() { + step "依赖更新" + if [[ -x "${REPO_ROOT}/.venv/bin/pip" ]]; then + pip_install_requirements "${REPO_ROOT}/.venv/bin/pip" "${REPO_ROOT}/requirements.txt" "crypto_okx_hub 依赖" + else + bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps + fi + update_quick +} + +show_update_menu() { + while true; do + echo "" + echo " 更新选项:" + echo " 3-1) 快速更新(git pull + pm2 restart)" + echo " 3-2) 依赖更新(含 pip install)" + echo " 0) 返回主菜单" + local choice="" + cm_read choice "请选择 [0/3-1/3-2]: " + case "${choice}" in + 3-1|31|1) update_quick; break ;; + 3-2|32|2) update_deps; break ;; + 0) break ;; + *) echo "无效选项" ;; + esac + done +} + +main_update() { + require_root + if ! REPO_ROOT="$(resolve_repo_root)"; then + die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署」" + fi + if ! repo_ready "${REPO_ROOT}"; then + die "未找到安装,请先执行「1) 一键部署」" + fi + show_update_menu +} + +main_update "$@" diff --git a/deploy/manage.sh b/deploy/manage.sh new file mode 100644 index 0000000..a277822 --- /dev/null +++ b/deploy/manage.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# crypto_okx_hub 部署管理器 — 一键部署 / 卸载 / 更新 (Ubuntu / root / PM2) +# +# 新服务器(免克隆): +# curl -fsSL https://git.bz121.com/dekun/crypto_okx_hub/raw/branch/main/deploy/manage.sh | bash +# +# 已安装: +# bash /opt/crypto_okx_hub/deploy/manage.sh +# +set -e +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_okx_hub}" +GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_okx_hub.git}" +GIT_BRANCH="${GIT_BRANCH:-main}" + +_script_src="${BASH_SOURCE[0]:-}" +if [[ -n "${_script_src}" && -f "${_script_src}" ]]; then + DEPLOY_DIR="$(cd "$(dirname "${_script_src}")" && pwd)" + REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" + LIB_DIR="${DEPLOY_DIR}/lib" +else + DEPLOY_DIR="" + REPO_ROOT="" + LIB_DIR="" +fi +unset _script_src + +set -u + +repo_ready() { + [[ -f "${1}/deploy/setup_env.sh" && -f "${1}/deploy/manage.sh" && -f "${1}/app.py" ]] +} + +sync_repo_if_present() { + local root="$1" + if [[ -d "${root}/.git" ]] && command -v git >/dev/null 2>&1; then + git -C "${root}" pull -q --ff-only 2>/dev/null || true + fi +} + +bootstrap_repo() { + if repo_ready "${INSTALL_ROOT}"; then + REPO_ROOT="${INSTALL_ROOT}" + DEPLOY_DIR="${REPO_ROOT}/deploy" + LIB_DIR="${DEPLOY_DIR}/lib" + sync_repo_if_present "${REPO_ROOT}" + return 0 + fi + if [[ -n "${REPO_ROOT}" ]] && repo_ready "${REPO_ROOT}"; then + DEPLOY_DIR="${REPO_ROOT}/deploy" + LIB_DIR="${DEPLOY_DIR}/lib" + return 0 + fi + + echo "crypto_okx_hub 部署管理器 — 首次自举" + echo "将克隆到: ${INSTALL_ROOT}" + if [[ "$(id -u)" -ne 0 ]]; then + echo "错误: 请使用 root 执行" >&2 + exit 1 + fi + if ! command -v git >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y git ca-certificates curl + else + echo "错误: 未找到 git" >&2 + exit 1 + fi + fi + if [[ -d "${INSTALL_ROOT}" ]]; then + echo "错误: ${INSTALL_ROOT} 已存在但不是有效仓库" >&2 + exit 1 + fi + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" + exec bash "${INSTALL_ROOT}/deploy/manage.sh" "$@" /dev/null 2>&1; then + return 1 + fi + if command -v python3 >/dev/null 2>&1; then + pm2 jlist 2>/dev/null | python3 -c ' +import json,sys +name=sys.argv[1] +try: + apps=json.load(sys.stdin) +except Exception: + sys.exit(1) +sys.exit(0 if any(isinstance(a,dict) and a.get("name")==name for a in (apps or [])) else 1) +' "${name}" + return $? + fi + pm2 describe "${name}" 2>/dev/null | grep -qE 'status[[:space:]]*[|:]' +} + +read_port() { + local env_file="${REPO}/.env" + local port="5100" + if [[ -f "${env_file}" ]]; then + local line + line="$(grep -E '^[[:space:]]*APP_PORT=' "${env_file}" | tail -n1 || true)" + if [[ -n "${line}" ]]; then + port="${line#*=}" + port="$(echo "${port}" | tr -d '"' | tr -d "'" | xargs)" + fi + fi + echo "${port:-5100}" +} + +wait_http_ready() { + local port="$1" + local url="http://127.0.0.1:${port}/login" + local i code + echo ">>> 等待 HTTP 就绪 ${url} (最多 30s)" + for i in $(seq 1 30); do + code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 2 --max-time 3 "${url}" 2>/dev/null || echo "000")" + if [[ "${code}" == "200" || "${code}" == "302" || "${code}" == "301" || "${code}" == "401" || "${code}" == "403" ]]; then + echo ">>> HTTP OK (${code}) after ${i}s" + return 0 + fi + if ! pm2_has_app "${PM2_APP_NAME}"; then + echo ">>> PM2 进程已消失" >&2 + return 1 + fi + sleep 1 + done + echo ">>> HTTP 仍未就绪 (最后 code=${code:-000})" >&2 + echo ">>> 请查看: pm2 logs ${PM2_APP_NAME} --lines 50" >&2 + return 1 +} + +cd "${REPO}" +echo ">>> git pull" +git pull + +if [[ "${DRY}" -eq 1 ]]; then + echo "(dry-run, skip pm2 restart)" + exit 0 +fi + +if ! command -v pm2 >/dev/null 2>&1; then + echo "warn: 未安装 pm2" >&2 + exit 1 +fi + +if [[ ! -f ecosystem.config.cjs ]]; then + echo "error: 缺少 ecosystem.config.cjs" >&2 + exit 1 +fi + +if pm2_has_app "${PM2_APP_NAME}"; then + echo ">>> pm2 restart ${PM2_APP_NAME} --update-env" + pm2 restart "${PM2_APP_NAME}" --update-env +else + echo ">>> 未注册 ${PM2_APP_NAME},执行 pm2 start ecosystem.config.cjs" + pm2 start ecosystem.config.cjs +fi + +port="$(read_port)" +wait_http_ready "${port}" || true + +echo "done" diff --git a/deploy/setup_env.sh b/deploy/setup_env.sh new file mode 100644 index 0000000..5609bb8 --- /dev/null +++ b/deploy/setup_env.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# crypto_okx_hub 一键环境部署(Ubuntu / root /opt/crypto_okx_hub) +# +# 用法: +# bash deploy/setup_env.sh +# bash deploy/setup_env.sh --skip-pm2 +# bash deploy/setup_env.sh --recreate-venv +# bash deploy/setup_env.sh --install-system-deps +# +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" +REQ_FILE="${REPO_ROOT}/requirements.txt" +# shellcheck source=lib/common.sh +source "${DEPLOY_DIR}/lib/common.sh" + +SKIP_PM2=0 +SKIP_ENV_COPY=0 +RECREATE_VENV=0 +INSTALL_APT_DEPS=0 +PY="" + +usage() { + sed -n '2,10p' "$0" | sed 's/^# \?//' + exit "${1:-0}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-pm2) SKIP_PM2=1; shift ;; + --skip-env-copy) SKIP_ENV_COPY=1; shift ;; + --recreate-venv) RECREATE_VENV=1; shift ;; + --install-system-deps) INSTALL_APT_DEPS=1; shift ;; + -h|--help) usage 0 ;; + *) echo "未知参数: $1" >&2; usage 1 ;; + esac +done + +find_python() { + if command -v python3 >/dev/null 2>&1; then + echo python3 + return + fi + if command -v python >/dev/null 2>&1; then + echo python + return + fi + echo "未找到 python3/python,请先安装 Python 3.10+" >&2 + exit 1 +} + +check_python_version() { + local py="$1" + local ver + ver="$("${py}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" + local major minor + major="${ver%%.*}" + minor="${ver#*.}" + if [[ "${major}" -lt 3 ]] || [[ "${major}" -eq 3 && "${minor}" -lt 10 ]]; then + echo "需要 Python 3.10+,当前: ${ver}" >&2 + exit 1 + fi + echo "Python: $("${py}" --version 2>&1)" +} + +python_minor_version() { + local py="$1" + "${py}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' +} + +check_venv_available() { + local py="$1" + local tmp + tmp="$(mktemp -d 2>/dev/null || mktemp -d -t cmvenv)" + if "${py}" -m venv "${tmp}" >/dev/null 2>&1 && [[ -x "${tmp}/bin/python" ]]; then + rm -rf "${tmp}" + return 0 + fi + rm -rf "${tmp}" 2>/dev/null || true + return 1 +} + +install_debian_venv_packages() { + local py="$1" + local ver + ver="$(python_minor_version "${py}")" + if ! command -v apt-get >/dev/null 2>&1; then + echo " 未检测到 apt-get,请手动安装 python${ver}-venv 与 python3-pip" >&2 + return 1 + fi + if [[ "$(id -u)" -ne 0 ]]; then + echo " 需要 root 安装系统包,请执行:" >&2 + echo " sudo apt update && sudo apt install -y python${ver}-venv python3-pip curl" >&2 + echo " 或: sudo bash deploy/setup_env.sh --install-system-deps" >&2 + return 1 + fi + step "安装系统依赖 (python${ver}-venv) ..." + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + if ! apt-get install -y "python${ver}-venv" python3-pip curl ca-certificates git; then + apt-get install -y python3-venv python3-pip curl ca-certificates git + fi +} + +ensure_venv_prereqs() { + local py="$1" + if check_venv_available "${py}"; then + return 0 + fi + echo " 当前 Python 无法创建 venv(缺少 ensurepip,常见于未安装 python*-venv)" >&2 + if [[ "${INSTALL_APT_DEPS}" -eq 1 ]] || [[ "$(id -u)" -eq 0 ]]; then + install_debian_venv_packages "${py}" || exit 1 + if check_venv_available "${py}"; then + return 0 + fi + fi + local ver + ver="$(python_minor_version "${py}")" + echo "请安装后重试:" >&2 + echo " apt update && apt install -y python${ver}-venv python3-pip" >&2 + echo " bash deploy/setup_env.sh" >&2 + exit 1 +} + +create_project_venv() { + local py="$1" + if [[ "${RECREATE_VENV}" -eq 1 && -d .venv ]]; then + echo " 删除旧 venv ..." + rm -rf .venv + fi + if [[ -d .venv && ! -x .venv/bin/python ]]; then + echo " 清理未完成的 venv ..." + rm -rf .venv + fi + if [[ -x .venv/bin/python ]]; then + return 0 + fi + echo " 创建 venv ..." + if ! "${py}" -m venv .venv; then + rm -rf .venv 2>/dev/null || true + echo " venv 创建失败" >&2 + exit 1 + fi +} + +ensure_flask_secret() { + local env_file="${REPO_ROOT}/.env" + [[ -f "${env_file}" ]] || return 0 + local line val + line="$(grep -E '^[[:space:]]*FLASK_SECRET_KEY=' "${env_file}" | tail -n1 || true)" + val="${line#*=}" + val="$(echo "${val}" | tr -d '"' | tr -d "'" | xargs)" + if [[ -z "${val}" || "${val}" == "change-me-to-long-random" || "${val}" == "hub-dev-secret" ]]; then + local secret + secret="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))' 2>/dev/null || openssl rand -hex 24)" + if grep -qE '^[[:space:]]*FLASK_SECRET_KEY=' "${env_file}"; then + sed -i "s|^[[:space:]]*FLASK_SECRET_KEY=.*|FLASK_SECRET_KEY=${secret}|" "${env_file}" + else + echo "FLASK_SECRET_KEY=${secret}" >>"${env_file}" + fi + echo " 已生成 FLASK_SECRET_KEY" + else + echo " 保留已有 FLASK_SECRET_KEY" + fi +} + +setup_app() { + step "crypto_okx_hub" + cd "${REPO_ROOT}" + create_project_venv "${PY}" + pip_upgrade_tools ".venv/bin/pip" + pip_install_requirements ".venv/bin/pip" "${REQ_FILE}" "crypto_okx_hub 依赖" + if [[ "${SKIP_ENV_COPY}" -eq 0 ]]; then + if [[ -f .env.example && ! -f .env ]]; then + cp -n .env.example .env 2>/dev/null || cp .env.example .env + echo " 已复制 .env.example -> .env" + elif [[ -f .env ]]; then + echo " 保留已有 .env" + else + echo " 无 .env.example,请手动配置 .env" + fi + fi + ensure_flask_secret + echo " 完成: ${REPO_ROOT}/.venv/bin/python" +} + +install_pm2_optional() { + if [[ "${SKIP_PM2}" -eq 1 ]]; then + return + fi + step "PM2(可选)" + if ! command -v node >/dev/null 2>&1; then + echo " 未检测到 Node.js,跳过.安装后执行: npm install -g pm2" + return + fi + if command -v pm2 >/dev/null 2>&1; then + echo " PM2 已安装: $(pm2 -v)" + return + fi + echo " 正在安装 pm2 ..." + npm install -g pm2 +} + +echo "crypto_okx_hub 环境部署" +echo "仓库根目录: ${REPO_ROOT}" +echo "远端仓库: ${GIT_URL}" + +[[ -f "${REQ_FILE}" ]] || { echo "缺少 ${REQ_FILE}" >&2; exit 1; } + +PY="$(find_python)" +check_python_version "${PY}" +ensure_venv_prereqs "${PY}" + +setup_app +install_pm2_optional + +echo "" +echo "部署完成.下一步:" +echo " 1. 编辑 ${REPO_ROOT}/.env (默认账号 admin / admin123)" +echo " 2. pm2 start ecosystem.config.cjs" +echo " 或: bash deploy/manage.sh # 选 1" +echo "" diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index a6d2314..a601da7 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -1,13 +1,28 @@ +/** + * PM2 进程定义(Ubuntu / Linux). + * + * 使用前:项目根存在 `.venv`,且已 pip install -r requirements.txt + * + * pm2 start ecosystem.config.cjs + * pm2 save && pm2 startup + */ +const path = require("path"); + +const ROOT = __dirname; +const PY = path.join(ROOT, ".venv", "bin", "python"); + module.exports = { apps: [ { name: "crypto_okx_hub", - script: "app.py", - interpreter: "python3", - cwd: __dirname, - env: { - PYTHONUNBUFFERED: "1", - }, + cwd: ROOT, + script: path.join(ROOT, "app.py"), + interpreter: PY, + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: "400M", + env: { PYTHONPATH: ROOT }, }, ], }; diff --git a/lib/env_lib.py b/lib/env_lib.py new file mode 100644 index 0000000..2992a7f --- /dev/null +++ b/lib/env_lib.py @@ -0,0 +1,55 @@ +"""Hub .env 读写(仅中控账号等少量键).""" +from __future__ import annotations + +import re +from pathlib import Path +from typing import Dict, List + +_KEY_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$") + + +def _parse_line(line: str) -> tuple[str | None, str]: + raw = line.rstrip("\n\r") + if not raw.strip() or raw.lstrip().startswith("#"): + return None, raw + m = _KEY_RE.match(raw) + if not m: + return None, raw + return m.group(1), raw + + +def apply_env_updates(path: Path | str, updates: Dict[str, str]) -> List[str]: + """更新或追加 .env 键;返回实际写入的 key 列表.不删其它行.""" + env_path = Path(path) + wanted = {str(k).strip(): str(v) for k, v in updates.items() if str(k).strip()} + if not wanted: + return [] + + lines: list[str] = [] + if env_path.is_file(): + lines = env_path.read_text(encoding="utf-8").splitlines(keepends=True) + + seen: set[str] = set() + out: list[str] = [] + changed: list[str] = [] + + for line in lines: + key, _ = _parse_line(line) + if key and key in wanted: + out.append(f"{key}={wanted[key]}\n") + seen.add(key) + changed.append(key) + else: + if line.endswith("\n") or line.endswith("\r"): + out.append(line if line.endswith("\n") else line + "\n") + else: + out.append(line + "\n") + + for key, val in wanted.items(): + if key not in seen: + out.append(f"{key}={val}\n") + changed.append(key) + + env_path.parent.mkdir(parents=True, exist_ok=True) + env_path.write_text("".join(out), encoding="utf-8") + return changed diff --git a/static/hub.css b/static/hub.css index bc9c862..23b7e6c 100644 --- a/static/hub.css +++ b/static/hub.css @@ -120,6 +120,12 @@ button, .btn { background: #000; } .empty { padding: 40px; color: var(--muted); } +.settings-card { max-width: 480px; } +.settings-form { max-width: 420px; } +input.readonly { + opacity: 0.75; + cursor: default; +} @media (max-width: 800px) { .desk { grid-template-columns: 1fr; } } diff --git a/templates/base.html b/templates/base.html index 28f8a32..083496f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -14,6 +14,7 @@ 总览 实例 工作台 + 系统设置 退出 diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..77c08e9 --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block title %}系统设置 · 期权中控{% endblock %} +{% block body %} +

系统设置

+ +
+

登录账号

+

修改后立即生效,并写入服务器 .env(HUB_USERNAME / HUB_PASSWORD).

+
+ + + + + + +
+
+{% endblock %} diff --git a/使用说明.md b/使用说明.md index c78f854..6d2b1db 100644 --- a/使用说明.md +++ b/使用说明.md @@ -15,11 +15,9 @@ OKX **期权多机中控**:在一个入口查看多台期权客户端的持仓 | **本仓库 crypto_okx_hub** | 中控:登录、实例登记、多机总览、内嵌/新标签打开 | [https://git.bz121.com/dekun/crypto_okx_hub.git](https://git.bz121.com/dekun/crypto_okx_hub.git) | | **crypto_okx** | 期权客户端(执行机):密钥、开平仓、模拟盘、持仓与提醒 | [https://git.bz121.com/dekun/crypto_okx.git](https://git.bz121.com/dekun/crypto_okx.git) | -关系简述: - -- 每个策略 / 账户部署一台(或多进程)**crypto_okx**,挂 **公网域名**。 -- **中控只部署一次**(可局域网电脑或云机),在后台登记各客户端的公网 URL 与 Bridge Token。 -- **交易所 API 密钥只在客户端**;中控不存、不代下单(第一期)。 +- 每个策略 / 账户部署一台 **crypto_okx**,挂 **公网域名**。 +- **中控只部署一次**(Ubuntu + PM2),在后台登记各客户端公网 URL 与 Bridge Token。 +- **交易所 API 密钥只在客户端**;中控不存、不代下单。 --- @@ -28,35 +26,30 @@ OKX **期权多机中控**:在一个入口查看多台期权客户端的持仓 | 功能 | 说明 | |------|------| | 多机持仓总览 | 按客户端卡片展示在线状态、sim/live、浮盈、持仓摘要 | -| 内嵌打开 | 在中控工作台 iframe 中打开该客户端(默认) | +| 内嵌打开 | 在中控工作台 iframe 中打开该客户端 | | 新标签打开 | 浏览器新标签直达同一客户端公网地址 | | 实例管理 | 增删改:名称、公网 URL、Token、标签、是否启用 | +| 系统设置 | 修改中控登录用户名 / 密码 | **不做:** 中控里直接下单、改客户端 env、保管 OKX 密钥。 --- -## 3. 使用流程(实现后) +## 3. 使用流程 -1. 部署并登录中控(账号见中控 `.env`)。 -2. **实例管理** 中添加客户端: - - 名称(如 `ETH-主策略`) - - 公网根地址(如 `https://okx-eth.example.com`) - - Bridge Token(与该客户端 `.env` 的 `HUB_BRIDGE_TOKEN` 一致) -3. 打开 **总览**:确认各机在线且有持仓/空仓数据。 -4. 点 **打开** → 进入内嵌工作台操作期权;需要独立窗口时点 **新标签打开**。 +1. 部署并登录中控(默认 **admin / admin123**)。 +2. **系统设置** 中建议立刻修改密码。 +3. **实例** 中添加客户端:名称、公网根地址、Bridge Token(与客户端 `HUB_BRIDGE_TOKEN` 一致)。 +4. 打开 **总览**:确认各机在线。 +5. 点 **打开** → 内嵌工作台;需要独立窗口时点 **新标签打开**。 --- ## 4. 对客户端的要求 -每台 crypto_okx 需(实现阶段在客户端仓库完成): - -1. 公网 HTTPS(或至少浏览器与中控都能访问的 URL)。 -2. 配置 `HUB_BRIDGE_TOKEN`,并开放中控约定的只读快照 API。 -3. 允许被中控页面 iframe 嵌入(或提供 embed 入口 + 短期登录链)。 - -客户端日常操作仍以该机页面为准;中控是「总台 + 快捷入口」。 +1. 公网 HTTPS(或浏览器与中控都能访问的 URL)。 +2. 配置 `HUB_BRIDGE_TOKEN`,开放只读快照 API。 +3. 允许被中控页面 iframe 嵌入(`/embed` + 短期 SSO)。 --- @@ -67,14 +60,13 @@ OKX **期权多机中控**:在一个入口查看多台期权客户端的持仓 | 总览拉数 | 中控服务器 → 客户端公网 URL | 中控出口能访问客户端 | | 内嵌/新标签 | 操作员浏览器 → 客户端公网 URL | 操作员网络能打开客户端 | -中控放在 **局域网** 时:只要上面两条仍成立即可(常见:客户端公网域名,人在家访问局域网中控)。 - --- ## 6. 文档索引 | 文档 | 内容 | |------|------| -| [设计文档.md](./设计文档.md) | 架构、数据表、API 约定、验收标准 | -| [部署文档.md](./部署文档.md) | 安装、环境变量、与客户端联调 | +| [设计文档.md](./设计文档.md) | 架构、数据表、API 约定 | +| [部署文档.md](./部署文档.md) | Ubuntu 一键部署 / PM2 | +| [deploy/README.md](./deploy/README.md) | 部署脚本说明 | | [README.md](./README.md) | 仓库入口摘要 | diff --git a/设计文档.md b/设计文档.md index c49b4d9..4f710d7 100644 --- a/设计文档.md +++ b/设计文档.md @@ -38,7 +38,7 @@ | **crypto_okx** | 期权客户端 / 执行机 | https://git.bz121.com/dekun/crypto_okx.git | 客户端对外:**公网域名**(如 `https://okx-eth.example.com`)。 -中控可部署在:**局域网机器** 或云服务器(第一期可用局域网)。 +中控部署:**Ubuntu 云机 + PM2**(与客户端同一套一键部署方式,不部署 Windows)。 协议靠约定对齐,不强制 monorepo。客户端侧需提供(实现阶段在 `crypto_okx` 补): @@ -55,7 +55,7 @@ │ │ iframe / 新标签打开 │ 公网域名 + 密钥/DB │ │ ┌───────────────┐ │ └──────────────────────┘ │ │ crypto_okx_hub│ │ 中控服务端拉快照 -│ │ (局域网或云) │──┼────────────────────────────▶┌──────────────────────┐ +│ │ (Ubuntu/PM2) │──┼────────────────────────────▶┌──────────────────────┐ │ └───────────────┘ │ GET /api/hub/options/... │ crypto_okx Worker B │ └─────────────────────┘ Header: X-Hub-Token └──────────────────────┘ ``` diff --git a/部署文档.md b/部署文档.md index 0ef1acd..93edc9c 100644 --- a/部署文档.md +++ b/部署文档.md @@ -1,38 +1,61 @@ -# crypto_okx_hub 部署文档 +# crypto_okx_hub 部署文档(Ubuntu) **仓库:** [https://git.bz121.com/dekun/crypto_okx_hub.git](https://git.bz121.com/dekun/crypto_okx_hub.git) -配套说明:**[使用说明.md](./使用说明.md)** · **[设计文档.md](./设计文档.md)** +配套说明:**[使用说明.md](./使用说明.md)** · **[设计文档.md](./设计文档.md)** · **[deploy/README.md](./deploy/README.md)** 关联客户端仓库:[crypto_okx](https://git.bz121.com/dekun/crypto_okx.git) -> 第一期骨架已可用:登录、登记实例、总览拉快照、工作台内嵌/新标签(SSO)。 +> 中控与客户端一样:**仅 Ubuntu + PM2 一键部署**。不提供 Windows 部署。 --- -## 1. 部署拓扑(推荐) +## 1. 一键部署(推荐) -``` -[局域网 PC 或一台云机] [云服务器 A] [云服务器 B] - crypto_okx_hub ──拉快照──▶ crypto_okx crypto_okx - (中控只一份) 公网域名 A 公网域名 B - HUB_BRIDGE_TOKEN=xxx HUB_BRIDGE_TOKEN=yyy +新服务器(免 clone): + +```bash +curl -fsSL https://git.bz121.com/dekun/crypto_okx_hub/raw/branch/main/deploy/manage.sh | bash ``` -- 客户端:**必须**能被浏览器与中控以 **公网域名(建议 HTTPS)** 访问。 -- 中控:可局域网;需能访问各客户端公网 URL。 +已安装: + +```bash +bash /opt/crypto_okx_hub/deploy/manage.sh +``` + +菜单: + +1. **一键部署** — 系统依赖、Node/PM2、venv、启动 `crypto_okx_hub` +2. **一键卸载** — 备份 `.env` / `hub.db` → 停 PM2 → 移走目录 +3. **更新** — git pull + pm2 restart(或含 pip) +4. **仅检测** — 不安装 + +安装路径:`/opt/crypto_okx_hub`,默认端口 **5100**,默认登录 **admin / admin123**。 --- -## 2. 中控环境变量(草案) +## 2. 部署拓扑 -实现后提供 `.env.example`,核心项预计包括: +``` +[Ubuntu 云机] [云服务器 A] [云服务器 B] + crypto_okx_hub ──拉快照──▶ crypto_okx crypto_okx + (中控只一份,PM2) 公网域名 A 公网域名 B + HUB_BRIDGE_TOKEN=xxx HUB_BRIDGE_TOKEN=yyy +``` + +- 客户端:**必须**能被浏览器与中控以 **公网域名(建议 HTTPS)** 访问。 +- 中控:Ubuntu + PM2;需能访问各客户端公网 URL。 + +--- + +## 3. 中控环境变量 | 变量 | 说明 | |------|------| -| `APP_HOST` / `APP_PORT` | 例如 `0.0.0.0` / `5100` | -| `HUB_USERNAME` / `HUB_PASSWORD` | 中控登录 | -| `FLASK_SECRET_KEY` | Session | -| `DB_PATH` | 注册表 SQLite,默认 `./hub.db` | +| `APP_HOST` / `APP_PORT` | 默认 `0.0.0.0` / `5100` | +| `HUB_USERNAME` / `HUB_PASSWORD` | 默认 `admin` / `admin123`;也可在页面「系统设置」修改 | +| `FLASK_SECRET_KEY` | Session;部署脚本可自动生成 | +| `DB_PATH` | 注册表 SQLite,默认 `hub.db` | | `HUB_PULL_INTERVAL_SEC` | 总览拉取间隔,默认 `10` | | `HUB_PULL_TIMEOUT_SEC` | 单机超时,默认 `5` | @@ -40,58 +63,23 @@ --- -## 3. 客户端侧(crypto_okx)联调清单 - -在每台客户端 `.env`(名称以实现为准): +## 4. 客户端侧(crypto_okx)联调清单 | 变量 | 说明 | |------|------| -| `HUB_BRIDGE_TOKEN` | 与中控登记的 Token 一致,足够长随机串 | +| `HUB_BRIDGE_TOKEN` | 与中控登记的 Token 一致 | | (可选)`HUB_EMBED_FRAME_ANCESTORS` | 允许嵌入的中控 Origin | -并确认: - -1. 公网 DNS → 该机;反向代理 HTTPS。 -2. 快照 API 返回 `ok: true`(见设计文档 §6)。 -3. Embed 路径可打开(默认倾向 `/embed`)。 - -防火墙可选:仅放行中控出口 IP 访问 `/api/hub/*`。 - ---- - -## 4. 安装步骤(实现后) - -本地 / 局域网示意: - -```bash -git clone https://git.bz121.com/dekun/crypto_okx_hub.git -cd crypto_okx_hub -python3 -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\activate -pip install -r requirements.txt -cp .env.example .env # 编辑账号端口 -python app.py -``` - -访问:`http://<中控局域网IP>:` - -生产可用 PM2(实现后提供 `ecosystem.config.cjs`),安装路径建议 `/opt/crypto_okx_hub`。 +并确认公网 DNS、快照 API、`/embed` 可打开。 --- ## 5. 联调验收 -1. 中控添加 2 个客户端(不同公网域名 + Token)。 -2. 总览两卡均在线,持仓/浮盈与客户端页面大致一致。 -3. 「打开」内嵌可见期权页;「新标签」独立打开。 -4. `pm2 stop` 一台客户端后,总览该卡变离线,另一台正常。 - ---- - -## 6. 与客户端部署文档的关系 - -- 客户端安装、PM2、实盘密钥:见 **crypto_okx** 仓库内 [部署文档.md](https://git.bz121.com/dekun/crypto_okx/src/branch/main/部署文档.md) 等。 -- 本仓库只描述 **中控自身** 及如何指向已部署的客户端。 +1. 中控添加 2 个客户端(不同公网域名 + Token)。 +2. 总览两卡均在线。 +3. 「打开」内嵌 / 「新标签」正常。 +4. 系统设置可改用户名密码,改后可用新账号登录。 --- @@ -99,4 +87,5 @@ python app.py | 日期 | 说明 | |------|------| -| 2026-10-01 | 初稿:双仓拓扑、env 草案、联调清单(代码待实现) | +| 2026-10-01 | Ubuntu 一键部署 + PM2;默认 admin/admin123;系统设置改密 | +| 2026-10-01 | 初稿:双仓拓扑、env 草案 |