支持 Ubuntu 一键部署与 PM2,并增加系统设置改密。

默认账号改为 admin/admin123,去掉 Windows 部署说明,与客户端同套部署流程。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-01 11:03:29 +08:00
parent d20acaf852
commit d726810320
21 changed files with 1456 additions and 105 deletions
+2 -2
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
*.sh text eol=lf
deploy/**/*.sh text eol=lf
ecosystem.config.cjs text eol=lf
+8 -6
View File
@@ -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)**。
+71 -4
View File
@@ -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():
+103
View File
@@ -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
```
+360
View File
@@ -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
}
+23
View File
@@ -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 "$@"
+109
View File
@@ -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 "$@"
+66
View File
@@ -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 "$@"
+60
View File
@@ -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 "$@"
+139
View File
@@ -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 "$@"
+106
View File
@@ -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"
+227
View File
@@ -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
View File
@@ -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 },
},
],
};
+55
View File
@@ -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
+6
View File
@@ -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; }
}
+1
View File
@@ -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>
+28
View File
@@ -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 %}
+17 -25
View File
@@ -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) | 仓库入口摘要 |
+2 -2
View File
@@ -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 └──────────────────────┘
```
+45 -56
View File
@@ -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. 一键部署(推荐)
新服务器(免 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. **更新** — git pull + pm2 restart(或含 pip)
4. **仅检测** — 不安装
安装路径:`/opt/crypto_okx_hub`,默认端口 **5100**,默认登录 **admin / admin123**。
---
## 2. 部署拓扑
```
[局域网 PC 或一台云机] [云服务器 A] [云服务器 B]
[Ubuntu 云机] [云服务器 A] [云服务器 B]
crypto_okx_hub ──拉快照──▶ crypto_okx crypto_okx
(中控只一份) 公网域名 A 公网域名 B
(中控只一份,PM2) 公网域名 A 公网域名 B
HUB_BRIDGE_TOKEN=xxx HUB_BRIDGE_TOKEN=yyy
```
- 客户端:**必须**能被浏览器与中控以 **公网域名(建议 HTTPS)** 访问。
- 中控:可局域网;需能访问各客户端公网 URL。
- 中控:Ubuntu + PM2;需能访问各客户端公网 URL。
---
## 2. 中控环境变量(草案)
实现后提供 `.env.example`,核心项预计包括:
## 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) 等。
- 本仓库只描述 **中控自身** 及如何指向已部署的客户端。
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 草案 |