Add interactive deploy manager with install, uninstall, and update menus.
Provides curl-friendly manage.sh so production servers can bootstrap without a prior clone, then configure API and risk settings in the browser. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,13 +16,17 @@
|
|||||||
| 进程 | **PM2**(唯一推荐的常驻方式) |
|
| 进程 | **PM2**(唯一推荐的常驻方式) |
|
||||||
|
|
||||||
**环境详解**(Python 3.10+,Node,PM2 安装与启动顺序):**[docs/ubuntu-server.md](./docs/ubuntu-server.md)**
|
**环境详解**(Python 3.10+,Node,PM2 安装与启动顺序):**[docs/ubuntu-server.md](./docs/ubuntu-server.md)**
|
||||||
**一键 venv**:`bash deploy/setup_env.sh` → **[deploy/README.md](./deploy/README.md)**
|
**一键部署管理器**:`curl -fsSL .../deploy/manage.sh | bash` → **[deploy/README.md](./deploy/README.md)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 新服务器(推荐)
|
||||||
|
curl -fsSL https://git.bz121.com/dekun/crypto_monitor/raw/branch/main/deploy/manage.sh | bash
|
||||||
|
|
||||||
|
# 或手动 clone
|
||||||
cd /opt
|
cd /opt
|
||||||
git clone https://git.bz121.com/dekun/crypto_monitor.git crypto_monitor
|
git clone https://git.bz121.com/dekun/crypto_monitor.git crypto_monitor
|
||||||
cd /opt/crypto_monitor
|
cd /opt/crypto_monitor
|
||||||
bash deploy/setup_env.sh --install-system-deps
|
bash deploy/manage.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
配置与运维脚本: **[docs/env-sync-scripts.md](./docs/env-sync-scripts.md)** · **[备份与恢复.md](./备份与恢复.md)**
|
配置与运维脚本: **[docs/env-sync-scripts.md](./docs/env-sync-scripts.md)** · **[备份与恢复.md](./备份与恢复.md)**
|
||||||
|
|||||||
+90
-31
@@ -1,51 +1,117 @@
|
|||||||
# 环境一键部署(Ubuntu / root /opt)
|
# 环境一键部署(Ubuntu / root /opt)
|
||||||
|
|
||||||
在 **`/opt/crypto_monitor`** 下以 **root** 为各子项目创建 Python **`.venv`**,安装依赖,从 `.env.example` 生成 `.env`(不覆盖已有),并可选安装 **PM2**.
|
在 **`/opt/crypto_monitor`** 下以 **root** 部署三所 Flask + 中控 hub/agent,使用 **PM2** 常驻.
|
||||||
|
|
||||||
完整系统要求(Python / Node / PM2 版本,启动顺序)见 **[docs/ubuntu-server.md](../docs/ubuntu-server.md)**.
|
完整系统要求见 **[docs/ubuntu-server.md](../docs/ubuntu-server.md)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一键部署管理器(推荐)
|
||||||
|
|
||||||
|
新服务器**无需先 clone**,一条命令进入菜单:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.bz121.com/dekun/crypto_monitor/raw/branch/main/deploy/manage.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
已安装机器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /opt/crypto_monitor/deploy/manage.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 菜单
|
||||||
|
|
||||||
|
| 选项 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| **1) 一键部署** | 装系统依赖 + Node/PM2 + venv + 自动生成密钥 + 启动 7 进程 |
|
||||||
|
| **2) 一键卸载** | 备份 `.env` / `hub_settings.json` → 停 PM2 → 移走目录 |
|
||||||
|
| **3) 更新** | 快速更新 / 依赖更新 / 深度重装 |
|
||||||
|
| **0) 退出** | |
|
||||||
|
|
||||||
|
### 部署完成后
|
||||||
|
|
||||||
|
脚本自动验收(PM2 7 进程 + 页面可访问),并提示:
|
||||||
|
|
||||||
|
- 登录账号: **admin**
|
||||||
|
- 登录密码: **admin123**
|
||||||
|
- 浏览器配置: 各所 **env 配置**(API,风控) + 中控 **系统设置**
|
||||||
|
|
||||||
|
**无需 SSH 编辑 `.env` 填 API**;密钥由 `bootstrap_deploy_secrets.py` 自动生成.
|
||||||
|
|
||||||
|
| 地址 | 端口 |
|
||||||
|
|------|------|
|
||||||
|
| 中控 | 5100 |
|
||||||
|
| Binance | 5001 |
|
||||||
|
| Gate | 5000 |
|
||||||
|
| OKX | 5004 |
|
||||||
|
|
||||||
|
agent 默认 `127.0.0.1:15200/15201/15202`,由代码内置,一般无需改.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 脚本结构
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/
|
||||||
|
├── manage.sh # 入口(自举 + 菜单)
|
||||||
|
├── lib/
|
||||||
|
│ ├── common.sh # 公共函数,验收
|
||||||
|
│ ├── install.sh # 一键部署
|
||||||
|
│ ├── uninstall.sh # 一键卸载
|
||||||
|
│ └── update.sh # 更新子菜单
|
||||||
|
├── setup_env.sh # venv + 依赖(被 install 调用)
|
||||||
|
├── pm2_start_all.sh # 启动 7 进程
|
||||||
|
├── pull_and_restart.sh # 快速更新(被 update 调用)
|
||||||
|
└── reinstall.sh # 深度重装(被 update 调用)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 前置条件
|
## 前置条件
|
||||||
|
|
||||||
- **Ubuntu 22.04 / 24.04**,用户 **root**
|
- **Ubuntu 22.04 / 24.04**,用户 **root**
|
||||||
- 已安装 **git**,仓库位于 **`/opt/crypto_monitor`**
|
- 能 `git clone` 仓库到 `/opt/crypto_monitor`
|
||||||
|
|
||||||
```bash
|
|
||||||
apt update
|
|
||||||
apt install -y python3 python3-pip python3-venv curl git ca-certificates
|
|
||||||
# Node 20 + PM2 见 docs/ubuntu-server.md §3
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一键执行
|
## 分步安装(仍可用)
|
||||||
|
|
||||||
|
若不使用 `manage.sh`,可手动:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd /opt
|
||||||
|
git clone https://git.bz121.com/dekun/crypto_monitor.git crypto_monitor
|
||||||
cd /opt/crypto_monitor
|
cd /opt/crypto_monitor
|
||||||
bash deploy/setup_env.sh --install-system-deps
|
bash deploy/setup_env.sh --install-system-deps
|
||||||
|
bash deploy/pm2_start_all.sh
|
||||||
|
pm2 save && pm2 startup
|
||||||
```
|
```
|
||||||
|
|
||||||
常用参数:
|
`setup_env.sh` 常用参数:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy/setup_env.sh --only binance,gate # 仅部分子项目
|
bash deploy/setup_env.sh --only binance,gate # 仅部分子项目
|
||||||
bash deploy/setup_env.sh --recreate-venv # 重建虚拟环境
|
bash deploy/setup_env.sh --recreate-venv # 重建虚拟环境
|
||||||
bash deploy/setup_env.sh --skip-pm2 # 不尝试安装 pm2
|
bash deploy/setup_env.sh --skip-pm2 # 不尝试安装 pm2
|
||||||
bash deploy/setup_env.sh --skip-env-copy # 不复制 .env.example
|
bash deploy/setup_env.sh --skip-env-copy # 不复制 .env.example
|
||||||
```
|
```
|
||||||
|
|
||||||
**整目录重装**(保留 `.env`,清库,去脏 PM2)见 **[reinstall-plan-b.md](./reinstall-plan-b.md)**,执行 `bash deploy/reinstall.sh`.与 `setup_env.sh` 独立,不影响首次一键安装.
|
**整目录重装**(保留 `.env`,清库)见 **[reinstall-plan-b.md](./reinstall-plan-b.md)**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/reinstall.sh --yes
|
||||||
|
```
|
||||||
|
|
||||||
若在其它环境编辑过脚本后报 `pipefail` 错误,先转 LF:
|
若在其它环境编辑过脚本后报 `pipefail` 错误,先转 LF:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sed -i 's/\r$//' deploy/setup_env.sh
|
sed -i 's/\r$//' deploy/manage.sh deploy/lib/*.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 脚本会做什么
|
## setup_env.sh 会做什么
|
||||||
|
|
||||||
| 步骤 | 说明 |
|
| 步骤 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -59,21 +125,14 @@ sed -i 's/\r$//' deploy/setup_env.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 部署之后
|
## 环境变量(可选)
|
||||||
|
|
||||||
1. 编辑各子目录 **`.env`**(交易所 API,企业微信等;**AI 请在中控系统设置 → AI 配置**).
|
```bash
|
||||||
2. **仅用 PM2 常驻**(见 [docs/ubuntu-server.md](../docs/ubuntu-server.md) §3):
|
INSTALL_ROOT=/opt/crypto_monitor
|
||||||
|
GIT_URL=https://git.bz121.com/dekun/crypto_monitor.git
|
||||||
```bash
|
GIT_BRANCH=main
|
||||||
cd /opt/crypto_monitor/crypto_monitor_binance && pm2 start ecosystem.config.cjs
|
BACKUP_ROOT=/root/backups
|
||||||
# … 其余三所 …
|
```
|
||||||
cd /opt/crypto_monitor/manual_trading_hub && pm2 start ecosystem.config.cjs
|
|
||||||
pm2 save
|
|
||||||
```
|
|
||||||
|
|
||||||
或一条命令:`bash deploy/pm2_start_all.sh`
|
|
||||||
|
|
||||||
3. 三所 `.env` 同步脚本见 **[docs/env-sync-scripts.md](../docs/env-sync-scripts.md)**.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/lib/common.sh — 部署管理器公共函数
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
if [ -n "${BASH_VERSION:-}" ]; then
|
||||||
|
set -o pipefail
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_monitor}"
|
||||||
|
GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_monitor.git}"
|
||||||
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
||||||
|
BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}"
|
||||||
|
TZ_NAME="${CM_TZ:-Asia/Shanghai}"
|
||||||
|
NODE_MAJOR="${NODE_MAJOR:-20}"
|
||||||
|
|
||||||
|
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
PM2_APPS=(
|
||||||
|
crypto_binance
|
||||||
|
crypto_gate
|
||||||
|
crypto_okx
|
||||||
|
manual-trading-hub
|
||||||
|
manual-agent-binance
|
||||||
|
manual-agent-okx
|
||||||
|
manual-agent-gate
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_PATHS=(
|
||||||
|
crypto_monitor_binance/.env
|
||||||
|
crypto_monitor_okx/.env
|
||||||
|
crypto_monitor_gate/.env
|
||||||
|
manual_trading_hub/.env
|
||||||
|
manual_trading_hub/hub_settings.json
|
||||||
|
)
|
||||||
|
|
||||||
|
INSTANCE_DIRS=(
|
||||||
|
crypto_monitor_binance
|
||||||
|
crypto_monitor_gate
|
||||||
|
crypto_monitor_okx
|
||||||
|
manual_trading_hub
|
||||||
|
)
|
||||||
|
|
||||||
|
log() { printf '[%s] %s\n' "$(TZ="${TZ_NAME}" date '+%Y-%m-%d %H:%M:%S')" "$*"; }
|
||||||
|
step() { echo ""; log "==> $*"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "错误: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
die "请使用 root 执行(推荐: sudo -i 后运行)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_ubuntu() {
|
||||||
|
if [[ ! -f /etc/os-release ]]; then
|
||||||
|
log "警告: 未检测到 /etc/os-release,跳过 Ubuntu 版本检查"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source /etc/os-release
|
||||||
|
if [[ "${ID:-}" != "ubuntu" ]]; then
|
||||||
|
log "警告: 当前系统为 ${ID:-unknown},官方仅测试 Ubuntu 22.04/24.04"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local ver="${VERSION_ID:-}"
|
||||||
|
if [[ "${ver}" != "22.04" && "${ver}" != "24.04" ]]; then
|
||||||
|
log "警告: Ubuntu ${ver} 未在文档中明确测试,继续执行"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_server_ip() {
|
||||||
|
local ip=""
|
||||||
|
if command -v hostname >/dev/null 2>&1; then
|
||||||
|
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||||
|
fi
|
||||||
|
if [[ -z "${ip}" ]]; then
|
||||||
|
ip="127.0.0.1"
|
||||||
|
fi
|
||||||
|
echo "${ip}"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_yes() {
|
||||||
|
local msg="$1"
|
||||||
|
local ans=""
|
||||||
|
read -r -p "${msg} [y/N] " ans
|
||||||
|
[[ "${ans}" == [yY] || "${ans}" == [yY][eE][sS] ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_uninstall() {
|
||||||
|
local ans=""
|
||||||
|
echo "此操作将停止全部 PM2 进程并移走安装目录."
|
||||||
|
read -r -p "输入 UNINSTALL 确认卸载: " ans
|
||||||
|
[[ "${ans}" == "UNINSTALL" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_ready() {
|
||||||
|
[[ -f "${1}/deploy/setup_env.sh" && -f "${1}/deploy/manage.sh" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_repo_root() {
|
||||||
|
if repo_ready "${INSTALL_ROOT}"; then
|
||||||
|
echo "${INSTALL_ROOT}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if repo_ready "${REPO_ROOT}"; then
|
||||||
|
echo "${REPO_ROOT}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_log_file() {
|
||||||
|
local stamp
|
||||||
|
stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)"
|
||||||
|
mkdir -p "${BACKUP_ROOT}/deploy-logs"
|
||||||
|
echo "${BACKUP_ROOT}/deploy-logs/manage-${stamp}.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_configs_to() {
|
||||||
|
local src_root="$1"
|
||||||
|
local dest="$2"
|
||||||
|
mkdir -p "${dest}"
|
||||||
|
local rel copied=0
|
||||||
|
for rel in "${CONFIG_PATHS[@]}"; do
|
||||||
|
local src="${src_root}/${rel}"
|
||||||
|
if [[ -f "${src}" ]]; then
|
||||||
|
mkdir -p "${dest}/$(dirname "${rel}")"
|
||||||
|
cp -a "${src}" "${dest}/${rel}"
|
||||||
|
log "backup ${rel}"
|
||||||
|
copied=$((copied + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "${copied}" -eq 0 ]]; then
|
||||||
|
log "提示: 未找到可备份的配置文件"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_system_packages() {
|
||||||
|
step "安装系统依赖"
|
||||||
|
if ! command -v apt-get >/dev/null 2>&1; then
|
||||||
|
die "未检测到 apt-get,请手动安装 python3-venv git curl"
|
||||||
|
fi
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y python3 python3-pip python3-venv curl git ca-certificates
|
||||||
|
local pyver=""
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
pyver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
|
||||||
|
apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_node_pm2() {
|
||||||
|
step "检查 Node.js 与 PM2"
|
||||||
|
if command -v pm2 >/dev/null 2>&1; then
|
||||||
|
log "PM2 已安装: $(pm2 -v)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
log "安装 Node.js ${NODE_MAJOR}.x ..."
|
||||||
|
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
log "安装 PM2 ..."
|
||||||
|
npm install -g pm2
|
||||||
|
log "PM2: $(pm2 -v)"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_backup_cron_all() {
|
||||||
|
step "安装三所每日备份 cron"
|
||||||
|
local dir
|
||||||
|
for dir in crypto_monitor_binance crypto_monitor_gate crypto_monitor_okx; do
|
||||||
|
local inst="${REPO_ROOT}/${dir}/scripts/install_backup_cron.sh"
|
||||||
|
if [[ -f "${inst}" ]]; then
|
||||||
|
chmod +x "${inst}"
|
||||||
|
bash "${inst}" || log "警告: ${dir} cron 安装失败"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_backup_cron_all() {
|
||||||
|
step "移除三所备份 cron"
|
||||||
|
local tmp removed=0
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
if ! crontab -l 2>/dev/null >"${tmp}"; then
|
||||||
|
rm -f "${tmp}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local filtered
|
||||||
|
filtered="$(grep -vF "backup_data.sh" "${tmp}" || true)"
|
||||||
|
if [[ "${filtered}" != "$(cat "${tmp}")" ]]; then
|
||||||
|
printf '%s\n' "${filtered}" | awk '
|
||||||
|
BEGIN { tz = 0 }
|
||||||
|
/^CRON_TZ=Asia\/Shanghai$/ {
|
||||||
|
if (tz++) next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' | crontab -
|
||||||
|
removed=1
|
||||||
|
fi
|
||||||
|
rm -f "${tmp}"
|
||||||
|
if [[ "${removed}" -eq 1 ]]; then
|
||||||
|
log "已移除 backup_data.sh 相关 cron"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pm2_save_startup() {
|
||||||
|
step "PM2 save & startup"
|
||||||
|
pm2 save 2>/dev/null || true
|
||||||
|
if pm2 startup systemd -u root --hp /root 2>/dev/null | grep -q "sudo"; then
|
||||||
|
pm2 startup systemd -u root --hp /root 2>/dev/null | grep "^sudo" | bash || true
|
||||||
|
else
|
||||||
|
pm2 startup 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pm2_count_online() {
|
||||||
|
local name online=0
|
||||||
|
if ! command -v pm2 >/dev/null 2>&1; then
|
||||||
|
echo "0"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
for name in "${PM2_APPS[@]}"; do
|
||||||
|
if pm2 pid "${name}" >/dev/null 2>&1; then
|
||||||
|
online=$((online + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "${online}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_deployed() {
|
||||||
|
local root="$1"
|
||||||
|
[[ -x "${root}/crypto_monitor_binance/.venv/bin/python" ]] \
|
||||||
|
|| [[ -x "${root}/manual_trading_hub/.venv/bin/python" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
check_http() {
|
||||||
|
local url="$1"
|
||||||
|
local code
|
||||||
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 "${url}" 2>/dev/null || echo "000")"
|
||||||
|
[[ "${code}" == "200" || "${code}" == "302" || "${code}" == "301" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_deployment() {
|
||||||
|
local ip="${1:-$(detect_server_ip)}"
|
||||||
|
local ok=1
|
||||||
|
local online total
|
||||||
|
|
||||||
|
step "部署验收"
|
||||||
|
total="${#PM2_APPS[@]}"
|
||||||
|
online="$(pm2_count_online)"
|
||||||
|
if [[ "${online}" -eq "${total}" ]]; then
|
||||||
|
echo " [✓] PM2 进程 ${online}/${total} online"
|
||||||
|
else
|
||||||
|
echo " [✗] PM2 进程 ${online}/${total} online"
|
||||||
|
ok=0
|
||||||
|
pm2 list 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
local checks=(
|
||||||
|
"中控:http://127.0.0.1:5100/"
|
||||||
|
"Binance:http://127.0.0.1:5001/"
|
||||||
|
"Gate:http://127.0.0.1:5000/"
|
||||||
|
"OKX:http://127.0.0.1:5004/"
|
||||||
|
)
|
||||||
|
local item label url
|
||||||
|
for item in "${checks[@]}"; do
|
||||||
|
label="${item%%:*}"
|
||||||
|
url="${item#*:}"
|
||||||
|
if check_http "${url}"; then
|
||||||
|
echo " [✓] ${label} 可访问"
|
||||||
|
else
|
||||||
|
echo " [✗] ${label} 不可访问 (${url})"
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${ok}" -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
log "验收通过: 进程都在 + 页面可打开"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
log "验收未完全通过,可执行 pm2 logs <进程名> --lines 30 排查"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_post_install_guide() {
|
||||||
|
local ip="${1:-$(detect_server_ip)}"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
✅ 部署完成
|
||||||
|
|
||||||
|
中控: http://${ip}:5100
|
||||||
|
Binance: http://${ip}:5001
|
||||||
|
Gate: http://${ip}:5000
|
||||||
|
OKX: http://${ip}:5004
|
||||||
|
|
||||||
|
登录账号: admin
|
||||||
|
登录密码: admin123
|
||||||
|
(请尽快在「系统设置」中修改)
|
||||||
|
|
||||||
|
下一步(浏览器):
|
||||||
|
1. 中控 → 系统设置 → 确认账户 URL(有域名时改 flask_url)
|
||||||
|
2. 各所 → env 配置 → 填 API、风控、企业微信
|
||||||
|
3. 中控 → AI 配置(如需复盘)
|
||||||
|
4. 各所 env 配置 → 开启实盘下单 → 保存并重启
|
||||||
|
|
||||||
|
agent_url 默认本机 127.0.0.1:15200/15201/15202,一般无需修改
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/lib/install.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"
|
||||||
|
|
||||||
|
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 "修复环境(保留数据与配置)"
|
||||||
|
bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps
|
||||||
|
}
|
||||||
|
|
||||||
|
install_deep() {
|
||||||
|
step "深度重装(保留 .env,清库)"
|
||||||
|
bash "${REPO_ROOT}/deploy/reinstall.sh" --yes
|
||||||
|
verify_deployment "$(detect_server_ip)" || true
|
||||||
|
print_post_install_guide "$(detect_server_ip)"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_install_pipeline() {
|
||||||
|
step "环境部署 setup_env.sh"
|
||||||
|
bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps
|
||||||
|
|
||||||
|
step "启动 PM2 全部进程"
|
||||||
|
bash "${REPO_ROOT}/deploy/pm2_start_all.sh"
|
||||||
|
|
||||||
|
pm2_save_startup
|
||||||
|
install_backup_cron_all
|
||||||
|
|
||||||
|
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) 深度重装(保留 .env,清库,见 reinstall.sh)"
|
||||||
|
local choice=""
|
||||||
|
read -r -p "请选择 [a/b/c]: " choice
|
||||||
|
case "${choice}" in
|
||||||
|
b|B)
|
||||||
|
install_repair
|
||||||
|
if command -v pm2 >/dev/null 2>&1; then
|
||||||
|
bash "${REPO_ROOT}/deploy/pm2_start_all.sh" 2>/dev/null || pm2 restart all 2>/dev/null || true
|
||||||
|
pm2_save_startup
|
||||||
|
fi
|
||||||
|
verify_deployment "$(detect_server_ip)" || true
|
||||||
|
print_post_install_guide "$(detect_server_ip)"
|
||||||
|
;;
|
||||||
|
c|C)
|
||||||
|
install_deep
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log "已取消"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
run_install_pipeline
|
||||||
|
}
|
||||||
|
|
||||||
|
main_install "$@"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/lib/uninstall.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_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}/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"
|
||||||
|
if command -v pm2 >/dev/null 2>&1; then
|
||||||
|
pm2 stop all 2>/dev/null || true
|
||||||
|
pm2 delete all 2>/dev/null || true
|
||||||
|
pm2 save 2>/dev/null || true
|
||||||
|
else
|
||||||
|
log "未安装 pm2,跳过"
|
||||||
|
fi
|
||||||
|
|
||||||
|
remove_backup_cron_all
|
||||||
|
|
||||||
|
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,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/lib/update.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"
|
||||||
|
|
||||||
|
require_installed() {
|
||||||
|
if ! repo_ready "${REPO_ROOT}"; then
|
||||||
|
die "未找到安装,请先执行「1) 一键部署」"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_quick() {
|
||||||
|
step "快速更新"
|
||||||
|
bash "${REPO_ROOT}/deploy/pull_and_restart.sh"
|
||||||
|
verify_deployment "$(detect_server_ip)" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
update_deps() {
|
||||||
|
step "依赖更新"
|
||||||
|
local dir req
|
||||||
|
for dir in crypto_monitor_binance crypto_monitor_gate crypto_monitor_okx; do
|
||||||
|
local proj="${REPO_ROOT}/${dir}"
|
||||||
|
if [[ -x "${proj}/.venv/bin/pip" ]]; then
|
||||||
|
log "pip install ${dir}"
|
||||||
|
"${proj}/.venv/bin/pip" install -r "${REPO_ROOT}/requirements.txt" -q
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
local hub="${REPO_ROOT}/manual_trading_hub"
|
||||||
|
if [[ -x "${hub}/.venv/bin/pip" && -f "${hub}/requirements.txt" ]]; then
|
||||||
|
log "pip install manual_trading_hub"
|
||||||
|
"${hub}/.venv/bin/pip" install -r "${hub}/requirements.txt" -q
|
||||||
|
fi
|
||||||
|
update_quick
|
||||||
|
}
|
||||||
|
|
||||||
|
update_deep() {
|
||||||
|
step "深度重装"
|
||||||
|
if ! confirm_yes "深度重装将清库并保留 .env,确认继续?"; then
|
||||||
|
log "已取消"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
bash "${REPO_ROOT}/deploy/reinstall.sh" --yes
|
||||||
|
verify_deployment "$(detect_server_ip)" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
show_update_menu() {
|
||||||
|
while true; do
|
||||||
|
echo ""
|
||||||
|
echo " 更新选项:"
|
||||||
|
echo " 3-1) 快速更新(git pull + pm2 restart)"
|
||||||
|
echo " 3-2) 依赖更新(含 pip install)"
|
||||||
|
echo " 3-3) 深度重装(保留 .env,清库)"
|
||||||
|
echo " 0) 返回主菜单"
|
||||||
|
local choice=""
|
||||||
|
read -r -p "请选择 [0/3-1/3-2/3-3]: " choice
|
||||||
|
case "${choice}" in
|
||||||
|
3-1|31|1) update_quick; break ;;
|
||||||
|
3-2|32|2) update_deps; break ;;
|
||||||
|
3-3|33|3) update_deep; break ;;
|
||||||
|
0) break ;;
|
||||||
|
*) echo "无效选项" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main_update() {
|
||||||
|
require_root
|
||||||
|
if ! REPO_ROOT="$(resolve_repo_root)"; then
|
||||||
|
die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署」"
|
||||||
|
fi
|
||||||
|
require_installed
|
||||||
|
show_update_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
main_update "$@"
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# crypto_monitor 部署管理器 — 一键部署 / 卸载 / 更新
|
||||||
|
#
|
||||||
|
# 新服务器(免克隆):
|
||||||
|
# curl -fsSL https://git.bz121.com/dekun/crypto_monitor/raw/branch/main/deploy/manage.sh | bash
|
||||||
|
#
|
||||||
|
# 已安装:
|
||||||
|
# bash /opt/crypto_monitor/deploy/manage.sh
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
if [ -n "${BASH_VERSION:-}" ]; then
|
||||||
|
set -o pipefail
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_monitor}"
|
||||||
|
GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_monitor.git}"
|
||||||
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
||||||
|
|
||||||
|
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)"
|
||||||
|
LIB_DIR="${DEPLOY_DIR}/lib"
|
||||||
|
|
||||||
|
repo_ready() {
|
||||||
|
[[ -f "${1}/deploy/setup_env.sh" && -f "${1}/deploy/manage.sh" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap_repo() {
|
||||||
|
if repo_ready "${INSTALL_ROOT}"; then
|
||||||
|
REPO_ROOT="${INSTALL_ROOT}"
|
||||||
|
DEPLOY_DIR="${REPO_ROOT}/deploy"
|
||||||
|
LIB_DIR="${DEPLOY_DIR}/lib"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if repo_ready "${REPO_ROOT}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "crypto_monitor 部署管理器 — 首次自举"
|
||||||
|
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
|
||||||
|
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" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_banner() {
|
||||||
|
local ip path
|
||||||
|
path="${INSTALL_ROOT}"
|
||||||
|
if repo_ready "${REPO_ROOT}"; then
|
||||||
|
path="${REPO_ROOT}"
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
if [[ -f "${LIB_DIR}/common.sh" ]]; then
|
||||||
|
source "${LIB_DIR}/common.sh"
|
||||||
|
ip="$(detect_server_ip)"
|
||||||
|
else
|
||||||
|
ip="?"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "╔══════════════════════════════════════╗"
|
||||||
|
echo "║ crypto_monitor 部署管理器 ║"
|
||||||
|
echo "║ 路径: ${path}"
|
||||||
|
echo "╚══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
show_menu() {
|
||||||
|
echo " 1) 一键部署"
|
||||||
|
echo " 2) 一键卸载"
|
||||||
|
echo " 3) 更新"
|
||||||
|
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=""
|
||||||
|
read -r -p "请选择 [0-3]: " choice
|
||||||
|
case "${choice}" in
|
||||||
|
1)
|
||||||
|
bash "${LIB_DIR}/install.sh"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
bash "${LIB_DIR}/uninstall.sh"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
bash "${LIB_DIR}/update.sh"
|
||||||
|
;;
|
||||||
|
0)
|
||||||
|
echo "再见."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "无效选项,请输入 0-3"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo ""
|
||||||
|
read -r -p "按 Enter 返回菜单..." _
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main_menu "$@"
|
||||||
+12
-3
@@ -38,11 +38,20 @@ apt install -y python3.12-venv
|
|||||||
|
|
||||||
### 2.2 一键创建各目录 venv
|
### 2.2 一键创建各目录 venv
|
||||||
|
|
||||||
|
**推荐**(新服务器一条命令):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.bz121.com/dekun/crypto_monitor/raw/branch/main/deploy/manage.sh | bash
|
||||||
|
# 菜单选 1) 一键部署
|
||||||
|
```
|
||||||
|
|
||||||
|
或已 clone 后:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/crypto_monitor
|
cd /opt/crypto_monitor
|
||||||
|
bash deploy/manage.sh
|
||||||
|
# 或仅建环境:
|
||||||
bash deploy/setup_env.sh --install-system-deps
|
bash deploy/setup_env.sh --install-system-deps
|
||||||
# 或已是 root 且已装 venv 包:
|
|
||||||
bash deploy/setup_env.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
完成后各目录使用 **`.venv/bin/python`** 运行 `app.py` / `hub.py`;**PM2 的 ecosystem 脚本已指向该解释器**.
|
完成后各目录使用 **`.venv/bin/python`** 运行 `app.py` / `hub.py`;**PM2 的 ecosystem 脚本已指向该解释器**.
|
||||||
@@ -163,7 +172,7 @@ curl -sS http://127.0.0.1:5100/api/monitor/board | head
|
|||||||
|
|
||||||
| 文档 | 内容 |
|
| 文档 | 内容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [deploy/README.md](../deploy/README.md) | `setup_env.sh` 参数说明 |
|
| [deploy/README.md](../deploy/README.md) | `manage.sh` 一键部署;`setup_env.sh` 参数 |
|
||||||
| [备份与恢复.md](../备份与恢复.md) | 数据库与 `.env` 备份 |
|
| [备份与恢复.md](../备份与恢复.md) | 数据库与 `.env` 备份 |
|
||||||
| 各 `crypto_monitor_*/部署文档.md` | 交易所 SOCKS,`.env`,PM2 细节 |
|
| 各 `crypto_monitor_*/部署文档.md` | 交易所 SOCKS,`.env`,PM2 细节 |
|
||||||
| [manual_trading_hub/部署文档.md](../manual_trading_hub/部署文档.md) | 中控 PM2,端口,反代 |
|
| [manual_trading_hub/部署文档.md](../manual_trading_hub/部署文档.md) | 中控 PM2,端口,反代 |
|
||||||
|
|||||||
Reference in New Issue
Block a user