支持 Ubuntu 一键部署与 PM2,并增加系统设置改密。
默认账号改为 admin/admin123,去掉 Windows 部署说明,与客户端同套部署流程。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
*.sh text eol=lf
|
||||
deploy/**/*.sh text eol=lf
|
||||
ecosystem.config.cjs text eol=lf
|
||||
@@ -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)**。
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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/tty || true
|
||||
else
|
||||
IFS= read -r -p "${__prompt}" __val || true
|
||||
fi
|
||||
printf -v "${__var}" '%s' "${__val}"
|
||||
}
|
||||
|
||||
confirm_uninstall() {
|
||||
local ans=""
|
||||
echo "此操作将停止 crypto_okx_hub PM2 进程并移走安装目录."
|
||||
cm_read ans "输入 UNINSTALL 确认卸载: "
|
||||
[[ "${ans}" == "UNINSTALL" ]]
|
||||
}
|
||||
|
||||
repo_ready() {
|
||||
[[ -f "${1}/deploy/setup_env.sh" && -f "${1}/deploy/manage.sh" && -f "${1}/app.py" ]]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
is_deployed() {
|
||||
local root="$1"
|
||||
[[ -f "${root}/.env" && -x "${root}/.venv/bin/python" && -f "${root}/ecosystem.config.cjs" ]]
|
||||
}
|
||||
|
||||
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_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
|
||||
}
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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/tty
|
||||
}
|
||||
|
||||
show_banner() {
|
||||
local ip path
|
||||
path="${INSTALL_ROOT}"
|
||||
if repo_ready "${REPO_ROOT:-}"; then
|
||||
path="${REPO_ROOT}"
|
||||
fi
|
||||
if [[ -f "${LIB_DIR}/common.sh" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "${LIB_DIR}/common.sh"
|
||||
ip="$(detect_server_ip)"
|
||||
else
|
||||
ip="?"
|
||||
fi
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ crypto_okx_hub 部署管理器 ║"
|
||||
echo "║ 仓库: git.bz121.com/dekun/crypto_okx_hub"
|
||||
echo "║ 路径: ${path}"
|
||||
echo "║ IP: ${ip}"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
echo " 1) 一键部署(检测环境 + 依赖 + PM2)"
|
||||
echo " 2) 一键卸载"
|
||||
echo " 3) 更新"
|
||||
echo " 4) 仅检测环境/依赖(不安装)"
|
||||
echo " 0) 退出"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main_menu() {
|
||||
bootstrap_repo
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${LIB_DIR}/common.sh"
|
||||
REPO_ROOT="$(resolve_repo_root || echo "${INSTALL_ROOT}")"
|
||||
|
||||
while true; do
|
||||
show_banner
|
||||
show_menu
|
||||
local choice=""
|
||||
cm_read choice "请选择 [0-4]: "
|
||||
case "${choice}" in
|
||||
1) bash "${LIB_DIR}/install.sh" ;;
|
||||
2) bash "${LIB_DIR}/uninstall.sh" ;;
|
||||
3) bash "${LIB_DIR}/update.sh" ;;
|
||||
4) bash "${LIB_DIR}/doctor.sh" ;;
|
||||
0) echo "再见."; exit 0 ;;
|
||||
*) echo "无效选项,请输入 0-4" ;;
|
||||
esac
|
||||
echo ""
|
||||
cm_read _ "按 Enter 返回菜单..."
|
||||
done
|
||||
}
|
||||
|
||||
main_menu "$@"
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# 服务器上拉代码并重启 PM2.
|
||||
# 用法(/opt/crypto_okx_hub 下 root):
|
||||
# bash deploy/pull_and_restart.sh
|
||||
# bash deploy/pull_and_restart.sh --dry-run
|
||||
set -e
|
||||
set -u
|
||||
if [ -n "${BASH_VERSION:-}" ]; then
|
||||
set -o pipefail
|
||||
fi
|
||||
|
||||
REPO="${REPO:-/opt/crypto_okx_hub}"
|
||||
PM2_APP_NAME="${PM2_APP_NAME:-crypto_okx_hub}"
|
||||
DRY=0
|
||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||
DRY=1
|
||||
echo "(dry-run mode)"
|
||||
fi
|
||||
|
||||
pm2_has_app() {
|
||||
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 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"
|
||||
@@ -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 ""
|
||||
+21
-6
@@ -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 },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<a href="/" class="{% if request.path == '/' %}active{% endif %}">总览</a>
|
||||
<a href="/instances" class="{% if request.path.startswith('/instances') %}active{% endif %}">实例</a>
|
||||
<a href="/desk" class="{% if request.path.startswith('/desk') %}active{% endif %}">工作台</a>
|
||||
<a href="/settings" class="{% if request.path.startswith('/settings') %}active{% endif %}">系统设置</a>
|
||||
<a href="/logout">退出</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}系统设置 · 期权中控{% endblock %}
|
||||
{% block body %}
|
||||
<section class="page-head"><h1>系统设置</h1></section>
|
||||
|
||||
<section class="panel settings-card">
|
||||
<h2>登录账号</h2>
|
||||
<p class="muted">修改后立即生效,并写入服务器 <code>.env</code>(<code>HUB_USERNAME</code> / <code>HUB_PASSWORD</code>).</p>
|
||||
<form method="post" class="stack settings-form" autocomplete="off">
|
||||
<label>当前用户名
|
||||
<input type="text" value="{{ current_username }}" readonly class="readonly">
|
||||
</label>
|
||||
<label>当前密码
|
||||
<input type="password" name="current_password" required autocomplete="current-password" placeholder="验证身份">
|
||||
</label>
|
||||
<label>新用户名
|
||||
<input type="text" name="new_username" value="{{ current_username }}" required autocomplete="username" minlength="2" maxlength="64">
|
||||
</label>
|
||||
<label>新密码
|
||||
<input type="password" name="new_password" autocomplete="new-password" minlength="6" placeholder="至少 6 位;不改密码可留空">
|
||||
</label>
|
||||
<label>确认新密码
|
||||
<input type="password" name="confirm_password" autocomplete="new-password" minlength="6" placeholder="与新密码一致;不改密码可留空">
|
||||
</label>
|
||||
<button type="submit">保存账号</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -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) | 仓库入口摘要 |
|
||||
|
||||
@@ -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 └──────────────────────┘
|
||||
```
|
||||
|
||||
@@ -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>:<APP_PORT>`
|
||||
|
||||
生产可用 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 草案 |
|
||||
|
||||
Reference in New Issue
Block a user