diff --git a/AGENTS.md b/AGENTS.md index f5a87fdf..55299ff8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,8 +80,8 @@ Nexus/ | 心跳数据流 | Agent→Redis→前端直读→10min→MySQL | 前端实时, MySQL只存历史 | | 告警推送 | WebSocket→前端 + Telegram | 即时感知 | | Python守护 | 3层: Supervisor + Python自检(30s) + Shell(cron 1min) | 全覆盖 | -| 安装向导 | install.html + FastAPI API | D2: 替代install.php | -| 配置源 | 安装向导→config.php(PHP兼容)+settings表(MySQL)+.env(Python) | 三写一致 | +| 安装向导 | install.html + FastAPI API | 纯Python,无PHP依赖 | +| 配置源 | 安装向导→config.json+settings表(MySQL)+.env(Python) | 三写一致 | | Agent告警心跳 | CPU/内存>80% 或变化>10% 时主动上报 | 智能监控 | | 仓库统一 | 所有代码统一在Nexus仓库 | 不再用两个仓库 | @@ -111,7 +111,7 @@ Nexus/ | E12 /health端点 | server/api/health.py | Shell health_monitor.sh checks this | | **技术债务** | | | | D1 ✅ paramiko删除 | — | pool.py已删除, 全部迁移asyncssh | -| D2 ✅ install迁移 | server/api/install.py | install.php→install.html+API | +| D2 ✅ install迁移 | server/api/install.py | PHP→install.html+API | | D3 ✅ WebSSH前端 | web/app/terminal.html | xterm.js+Koko协议+全屏 | | **安全增强** | | | | S1 JWT全局保护 | server/api/*.py | 所有业务API都有JWT验证 | @@ -159,7 +159,7 @@ Nexus/ ## 配置三写机制 安装向导 Step 3 POST处理同时写入: -1. `web/data/config.php` — PHP兼容配置 (API_BASE_URL, DB_*, API_KEY) +1. `web/data/config.json` — 共享配置 (API_BASE_URL, DB_*, API_KEY) 2. `.env` — Python后端启动必需 (NEXUS_前缀) 3. `settings` MySQL表 — 共享动态配置 (api_key, thresholds, Telegram等) diff --git a/CLAUDE.md b/CLAUDE.md index b03f5fe7..cc622ba6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,8 +50,8 @@ Nexus/ | 心跳数据流 | Agent→Redis→前端直读→10min→MySQL | 前端实时, MySQL只存历史 | | 告警推送 | WebSocket→前端 + Telegram | 即时感知 | | Python守护 | 3层: Supervisor + Python自检(30s) + Shell(cron 1min) | 全覆盖 | -| 安装向导 | install.html + FastAPI API | D2: 替代install.php | -| 配置源 | 安装向导→config.php(PHP兼容)+settings表(MySQL)+.env(Python) | 三写一致 | +| 安装向导 | install.html + FastAPI API | 纯Python,无PHP依赖 | +| 配置源 | 安装向导→config.json+settings表(MySQL)+.env(Python) | 三写一致 | | Agent告警心跳 | CPU/内存>80% 或变化>10% 时主动上报 | 智能监控 | | 仓库统一 | 所有代码统一在Nexus仓库 | 不再用两个仓库 | @@ -81,7 +81,7 @@ Nexus/ | E12 /health端点 | server/api/health.py | Shell health_monitor.sh checks this | | **技术债务** | | | | D1 ✅ paramiko删除 | — | pool.py已删除, 全部迁移asyncssh | -| D2 ✅ install迁移 | server/api/install.py | install.php→install.html+API | +| D2 ✅ install迁移 | server/api/install.py | PHP→install.html+API | | D3 ✅ WebSSH前端 | web/app/terminal.html | xterm.js+Koko协议+全屏 | | **安全增强** | | | | S1 JWT全局保护 | server/api/*.py | 所有业务API都有JWT验证 | @@ -129,7 +129,7 @@ Nexus/ ## 配置三写机制 安装向导 Step 3 POST处理同时写入: -1. `web/data/config.php` — PHP兼容配置 (API_BASE_URL, DB_*, API_KEY) +1. `web/data/config.json` — 共享配置 (API_BASE_URL, DB_*, API_KEY) 2. `.env` — Python后端启动必需 (NEXUS_前缀) 3. `settings` MySQL表 — 共享动态配置 (api_key, thresholds, Telegram等) diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100644 index 00000000..87af45e4 --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,526 @@ +#!/bin/bash +# ================================================================ +# Nexus 6.0 — One-Click Install Script +# +# Usage: +# curl -fsSL https://YOUR_DOMAIN/deploy/install.sh | sudo bash -s -- --domain nexus.example.com +# sudo bash install.sh --domain nexus.example.com +# +# Supports: Ubuntu 20.04+, Debian 11+, CentOS 7/8, Rocky 8/9, AlmaLinux 8/9 +# Auto-detects BT Panel (宝塔面板) and adapts paths accordingly. +# ================================================================ + +set -euo pipefail + +# ── Colors ── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; } + +# ── Helper: Idempotent config write ── +write_config() { + local target="$1" content="$2" + if [ -f "$target" ]; then + local existing + existing=$(cat "$target" 2>/dev/null || true) + if [ "$existing" = "$content" ]; then + info " [skip] $target (unchanged)" + return 0 + fi + info " [update] $target (content changed)" + else + info " [create] $target" + fi + echo "$content" > "$target" +} + +# ── Defaults ── +DOMAIN="" +DEPLOY_DIR="" +PORT=8600 +SKIP_NGINX=false +SKIP_SUPERVISOR=false +BT_MODE=false +NEXUS_REPO="" + +# ── Parse Arguments ── +while [[ $# -gt 0 ]]; do + case $1 in + --domain) DOMAIN="$2"; shift 2 ;; + --deploy-dir) DEPLOY_DIR="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + --skip-nginx) SKIP_NGINX=true; shift ;; + --skip-supervisor) SKIP_SUPERVISOR=true; shift ;; + --repo) NEXUS_REPO="$2"; shift 2 ;; + -h|--help) + echo "Usage: sudo bash install.sh --domain DOMAIN [options]" + echo "" + echo "Options:" + echo " --domain DOMAIN Required. Domain or IP for Nginx server_name" + echo " --deploy-dir PATH Installation directory (auto-detected)" + echo " --port PORT uvicorn port (default: 8600)" + echo " --skip-nginx Skip Nginx configuration" + echo " --skip-supervisor Skip Supervisor configuration" + echo " --repo URL Git repository URL (optional, for cloning)" + exit 0 ;; + *) shift ;; + esac +done + +# ── Banner ── +echo "" +echo "==========================================" +echo " Nexus 6.0 Install Script" +echo "==========================================" +echo "" + +# ================================================================ +# Phase 1: Pre-flight Checks +# ================================================================ + +info "Phase 1/7: Pre-flight checks..." + +# 1.1 Must run as root +if [ "$(id -u)" -ne 0 ]; then + error "This script must be run as root. Use: sudo bash install.sh" + exit 1 +fi + +# 1.2 Domain is required +if [ -z "$DOMAIN" ]; then + error "--domain is required. Usage: sudo bash install.sh --domain nexus.example.com" + exit 1 +fi + +# 1.3 OS detection +if [ ! -f /etc/os-release ]; then + error "Cannot detect OS. /etc/os-release not found." + exit 1 +fi + +OS_ID=$(grep '^ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"' | cut -d'-' -f1) +OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"') + +case "$OS_ID" in + ubuntu|debian) PKG_MANAGER="apt" ;; + centos|rocky|almalinux|rhel) PKG_MANAGER="yum" ;; + *) + warn "Unsupported OS: $OS_ID. Script may still work but is untested." + PKG_MANAGER="apt" + ;; +esac +info " OS: $OS_ID $OS_VERSION (pkg: $PKG_MANAGER)" + +# 1.4 BT Panel detection +if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then + BT_MODE=true + info " BT Panel: detected" +else + info " BT Panel: not detected (standard mode)" +fi + +# 1.5 Set deploy directory +if [ -z "$DEPLOY_DIR" ]; then + if [ "$BT_MODE" = true ]; then + DEPLOY_DIR="/www/wwwroot/$DOMAIN" + else + DEPLOY_DIR="/opt/nexus" + fi +fi +VENV_DIR="$DEPLOY_DIR/venv" + +# Set log paths early (used in multiple phases) +if [ "$BT_MODE" = true ]; then + LOG_DIR="/www/wwwlogs" +else + LOG_DIR="/var/log/nexus" +fi + +info " Deploy dir: $DEPLOY_DIR" +info " Domain: $DOMAIN" +info " Port: $PORT" + +# ================================================================ +# Phase 2: System Dependencies +# ================================================================ + +info "Phase 2/7: Installing system dependencies..." + +if [ "$PKG_MANAGER" = "apt" ]; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq \ + python3 python3-venv python3-pip python3-dev \ + build-essential libssl-dev libffi-dev default-libmysqlclient-dev \ + git curl >/dev/null 2>&1 || warn "Some apt packages failed to install (may already exist)" +elif [ "$PKG_MANAGER" = "yum" ]; then + if command -v dnf >/dev/null 2>&1; then + dnf install -y -q \ + python3 python3-devel python3-pip \ + gcc openssl-devel libffi-devel mysql-devel \ + git curl >/dev/null 2>&1 || warn "Some dnf packages failed to install" + else + yum install -y -q \ + python3 python3-devel python3-pip \ + gcc openssl-devel libffi-devel mysql-devel \ + git curl >/dev/null 2>&1 || warn "Some yum packages failed to install" + fi +fi + +# ================================================================ +# Phase 3: Python 3.10+ Resolution +# ================================================================ + +info "Phase 3/7: Resolving Python 3.10+..." + +PYTHON="" +CANDIDATES="python3.12 python3.11 python3.10 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 python3" + +# Add BT Panel Python paths +if [ "$BT_MODE" = true ]; then + for p in /www/server/python-manager/versions/*/bin/python3.*; do + [ -x "$p" ] && CANDIDATES="$CANDIDATES $p" + done +fi + +for candidate in $CANDIDATES; do + if command -v "$candidate" >/dev/null 2>&1 || [ -x "$candidate" ]; then + ver=$("$candidate" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true) + if [ -n "$ver" ]; then + major=$(echo "$ver" | cut -d'.' -f1) + minor=$(echo "$ver" | cut -d'.' -f2) + if [ "$major" -eq 3 ] && [ "${minor:-0}" -ge 10 ] 2>/dev/null; then + PYTHON="$candidate" + break + fi + fi + fi +done + +# Fallback: install Python 3.12 on Ubuntu via deadsnakes PPA +if [ -z "$PYTHON" ] && [ "$OS_ID" = "ubuntu" ]; then + warn " Python 3.10+ not found. Adding deadsnakes PPA..." + export DEBIAN_FRONTEND=noninteractive + apt-get install -y -qq software-properties-common >/dev/null 2>&1 + add-apt-repository -y ppa:deadsnakes/ppa >/dev/null 2>&1 + apt-get update -qq + apt-get install -y -qq python3.12 python3.12-venv python3.12-dev >/dev/null 2>&1 + if command -v python3.12 >/dev/null 2>&1; then + PYTHON="python3.12" + fi +fi + +if [ -z "$PYTHON" ]; then + error "Python 3.10+ is required but not found." + error "Install Python 3.10+ manually and re-run." + exit 1 +fi + +PYTHON_VERSION=$("$PYTHON" --version 2>&1) +info " Using: $PYTHON ($PYTHON_VERSION)" + +# ================================================================ +# Phase 4: Application Deployment +# ================================================================ + +info "Phase 4/7: Deploying application..." + +# 4.1 Create deploy directory +mkdir -p "$DEPLOY_DIR" + +# 4.2 Download / Clone Nexus +if [ -d "$DEPLOY_DIR/server" ] && [ -d "$DEPLOY_DIR/web" ]; then + info " Application files already exist in $DEPLOY_DIR (skipping download)" + if [ -d "$DEPLOY_DIR/.git" ]; then + info " Git repo detected, pulling latest..." + (cd "$DEPLOY_DIR" && git pull --ff-only 2>/dev/null || warn " git pull failed (may have local changes)") + fi +elif [ -n "$NEXUS_REPO" ]; then + info " Cloning from $NEXUS_REPO..." + git clone "$NEXUS_REPO" "$DEPLOY_DIR" 2>/dev/null || { + error "git clone failed. Check the repository URL." + exit 1 + } +else + error "No application files found in $DEPLOY_DIR and no --repo URL provided." + error "Either:" + error " 1. Copy Nexus files to $DEPLOY_DIR manually and re-run" + error " 2. Use --repo https://your-gitea-url/Nexus.git" + exit 1 +fi + +# 4.3 Create Python venv +if [ -d "$VENV_DIR/bin" ] && "$VENV_DIR/bin/python" -c 'import sys; v=sys.version_info; assert v.major==3 and v.minor>=10' 2>/dev/null; then + info " venv already exists with Python 3.10+ (skipping)" +else + info " Creating Python venv..." + rm -rf "$VENV_DIR" + "$PYTHON" -m venv "$VENV_DIR" +fi + +# 4.4 Install pip dependencies +info " Installing Python dependencies (this may take a few minutes)..." +"$VENV_DIR/bin/pip" install -q --upgrade pip +"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || { + warn " pip install had some warnings, but continuing..." +} + +# 4.5 Create log directory +mkdir -p "$LOG_DIR" + +# 4.6 Create backup directory +mkdir -p "$DEPLOY_DIR/backups" + +# ================================================================ +# Phase 5: Supervisor Configuration +# ================================================================ + +if [ "$SKIP_SUPERVISOR" = true ]; then + warn "Phase 5/7: Skipping Supervisor (per --skip-supervisor)" +else + info "Phase 5/7: Configuring Supervisor..." + + # Calculate worker count + WORKERS=$(nproc 2>/dev/null || echo 2) + [ "$WORKERS" -gt 4 ] && WORKERS=4 + [ "$WORKERS" -lt 1 ] && WORKERS=1 + + # Set Supervisor config path + if [ "$BT_MODE" = true ]; then + SUPERVISOR_CONF_DIR="/www/server/panel/plugin/supervisor/profile" + CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.ini" + else + SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d" + CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.conf" + fi + + # Generate Supervisor config + SUPERVISOR_CONF="[program:nexus] +command=$VENV_DIR/bin/uvicorn server.main:app --host 0.0.0.0 --port $PORT --workers $WORKERS --loop uvloop --http httptools --log-level info +directory=$DEPLOY_DIR +user=root +autostart=true +autorestart=true +startretries=10 +startsecs=3 +stopwaitsecs=10 +stopsignal=INT +environment=PATH=\"$VENV_DIR/bin:%(ENV_PATH)s\",HOME=\"/root\",NEXUS_WORKERS=\"$WORKERS\" +stderr_logfile=$LOG_DIR/nexus_error.log +stdout_logfile=$LOG_DIR/nexus_access.log +stderr_logfile_maxbytes=50MB +stdout_logfile_maxbytes=50MB +stderr_logfile_backups=5 +stdout_logfile_backups=5" + + # Write config (idempotent) + mkdir -p "$SUPERVISOR_CONF_DIR" + write_config "$CONF_FILE" "$SUPERVISOR_CONF" + + # Ensure Supervisor is running + if ! supervisorctl status >/dev/null 2>&1; then + if [ "$BT_MODE" = true ]; then + if [ -f /etc/init.d/supervisor ]; then + /etc/init.d/supervisor start >/dev/null 2>&1 || true + fi + else + systemctl start supervisor >/dev/null 2>&1 || true + fi + sleep 2 + fi + + # Reload Supervisor config + supervisorctl reread >/dev/null 2>&1 || true + supervisorctl update >/dev/null 2>&1 || true + info " Supervisor config written to $CONF_FILE (workers=$WORKERS)" +fi + +# ================================================================ +# Phase 6: Nginx Configuration +# ================================================================ + +if [ "$SKIP_NGINX" = true ]; then + warn "Phase 6/7: Skipping Nginx (per --skip-nginx)" +else + info "Phase 6/7: Configuring Nginx..." + + if [ "$BT_MODE" = true ]; then + NGINX_CONF_FILE="/www/server/panel/vhost/nginx/$DOMAIN.conf" + NGINX_RELOAD_CMD="/www/server/nginx/sbin/nginx -s reload" + NGINX_TEST_CMD="/www/server/nginx/sbin/nginx -t" + NGINX_ACCESS_LOG="/www/wwwlogs/$DOMAIN.log" + NGINX_ERROR_LOG="/www/wwwlogs/$DOMAIN.error.log" + else + NGINX_CONF_FILE="/etc/nginx/sites-available/nexus" + NGINX_RELOAD_CMD="systemctl reload nginx" + NGINX_TEST_CMD="nginx -t" + NGINX_ACCESS_LOG="/var/log/nginx/nexus_access.log" + NGINX_ERROR_LOG="/var/log/nginx/nexus_error.log" + fi + + NGINX_CONF="# Nexus 6.0 — Nginx Reverse Proxy +# Auto-generated by install.sh +upstream nexus_api { + server 127.0.0.1:$PORT; + keepalive 32; +} + +server { + listen 80; + server_name $DOMAIN; + + # Reverse proxy to Nexus (uvicorn) + location / { + proxy_pass http://nexus_api; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # WebSocket support (terminal, alerts) + location /ws/ { + proxy_pass http://nexus_api; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"upgrade\"; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_read_timeout 3600s; + } + + # Static assets caching + location /app/vendor/ { + proxy_pass http://nexus_api; + expires 7d; + add_header Cache-Control \"public, immutable\"; + } + + # Block sensitive files + location ~ ^/(\\.env|\\.git|\\.svn) { + return 404; + } + + # Block config.json access + location ~ /data/config\\.json$ { + return 404; + } + + # Upload limit + client_max_body_size 100m; + + # Well-known for SSL cert validation + location ~ \\.well-known { + allow all; + } + + access_log $NGINX_ACCESS_LOG; + error_log $NGINX_ERROR_LOG; +}" + + # Write Nginx config + if [ "$BT_MODE" = true ]; then + write_config "$NGINX_CONF_FILE" "$NGINX_CONF" + else + mkdir -p /etc/nginx/sites-available + write_config "$NGINX_CONF_FILE" "$NGINX_CONF" + if [ ! -L /etc/nginx/sites-enabled/nexus ]; then + ln -sf "$NGINX_CONF_FILE" /etc/nginx/sites-enabled/nexus + fi + fi + + # Test and reload Nginx + if $NGINX_TEST_CMD >/dev/null 2>&1; then + $NGINX_RELOAD_CMD >/dev/null 2>&1 || warn " Nginx reload failed" + info " Nginx config written to $NGINX_CONF_FILE" + else + warn " Nginx config test failed. Please check $NGINX_CONF_FILE manually" + fi +fi + +# ================================================================ +# Phase 7: Start Application + Verify +# ================================================================ + +info "Phase 7/7: Starting application..." + +# Start via Supervisor +if [ "$SKIP_SUPERVISOR" = false ]; then + supervisorctl start nexus >/dev/null 2>&1 || supervisorctl restart nexus >/dev/null 2>&1 || true +fi + +# Wait for health check +HEALTH_OK=false +for i in $(seq 1 15); do + if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then + HEALTH_OK=true + break + fi + sleep 2 +done + +# ── Print Results ── +echo "" +echo "==========================================" + +if [ "$HEALTH_OK" = true ]; then + echo -e " ${GREEN}Nexus installed successfully!${NC}" +else + echo -e " ${YELLOW}Nexus process started but health check pending.${NC}" + echo -e " ${YELLOW}It may need a few more seconds to start up.${NC}" +fi + +echo "==========================================" +echo "" +echo " Deploy: $DEPLOY_DIR" +echo " Domain: $DOMAIN" +echo " Port: $PORT" +echo " Workers: ${WORKERS:-N/A}" +echo " BT Panel: $BT_MODE" +echo "" + +if [ "$HEALTH_OK" = true ]; then + echo -e " ${GREEN}Next step:${NC} Open your browser and complete the web install wizard:" + echo "" + echo " http://$DOMAIN/app/install.html" + echo "" + echo " The wizard will guide you through:" + echo " Step 1: Welcome & prerequisites" + echo " Step 2: Environment check" + echo " Step 3: MySQL + Redis configuration" + echo " Step 4: Admin account + brand" + echo " Step 5: Complete" +else + echo " Troubleshooting:" + echo " supervisorctl status nexus" + echo " tail -f $LOG_DIR/nexus_error.log" + echo " curl http://127.0.0.1:$PORT/health" +fi + +echo "" +echo " Post-install:" +echo " 1. Configure SSL (HTTPS)" +if [ "$BT_MODE" = true ]; then + echo " BT Panel -> Website -> $DOMAIN -> SSL" +else + echo " certbot --nginx -d $DOMAIN" +fi +echo " 2. Set up health monitor crontab:" +echo " * * * * * $DEPLOY_DIR/deploy/health_monitor.sh" +echo " 3. Set up database backup crontab:" +echo " 0 3 * * * $DEPLOY_DIR/deploy/db_backup.sh" +echo "" +echo " Management:" +echo " supervisorctl status nexus" +echo " supervisorctl restart nexus" +echo " supervisorctl stop nexus" +echo "" diff --git a/deploy/nginx_http.conf b/deploy/nginx_http.conf index ae8218ce..d56a302e 100644 --- a/deploy/nginx_http.conf +++ b/deploy/nginx_http.conf @@ -61,7 +61,7 @@ server { location ~ /\. { deny all; } - location ~ /data/config\.php$ { + location ~ /data/config\.json$ { deny all; } diff --git a/deploy/nginx_https.conf b/deploy/nginx_https.conf index 3b9fa257..e931796d 100644 --- a/deploy/nginx_https.conf +++ b/deploy/nginx_https.conf @@ -89,7 +89,7 @@ server { location ~ /\. { deny all; } - location ~ /data/config\.php$ { + location ~ /data/config\.json$ { deny all; } diff --git a/deploy/uninstall.sh b/deploy/uninstall.sh new file mode 100644 index 00000000..42ac002d --- /dev/null +++ b/deploy/uninstall.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# ================================================================ +# Nexus 6.0 — Uninstall Script +# +# Usage: +# sudo bash uninstall.sh # interactive +# sudo bash uninstall.sh --deploy-dir /opt/nexus # specify deploy dir +# sudo bash uninstall.sh --purge # also remove deploy dir +# +# Safe: stops services, removes configs. Database is NEVER deleted automatically. +# ================================================================ + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; } + +DEPLOY_DIR="" +PURGE=false + +while [[ $# -gt 0 ]]; do + case $1 in + --deploy-dir) DEPLOY_DIR="$2"; shift 2 ;; + --purge) PURGE=true; shift ;; + -h|--help) + echo "Usage: sudo bash uninstall.sh [--deploy-dir PATH] [--purge]" + echo "" + echo " --deploy-dir PATH Specify Nexus installation directory" + echo " --purge Also remove the deploy directory and venv" + echo "" + echo " Note: MySQL database is NEVER deleted automatically." + exit 0 ;; + *) shift ;; + esac +done + +# Auto-detect deploy directory +if [ -z "$DEPLOY_DIR" ]; then + for d in /opt/nexus /www/wwwroot/*/; do + if [ -d "$d/server" ] && [ -d "$d/web" ]; then + DEPLOY_DIR="$d" + break + fi + done +fi + +if [ -z "$DEPLOY_DIR" ] || [ ! -d "$DEPLOY_DIR/server" ]; then + error "Cannot find Nexus installation. Use --deploy-dir to specify." + exit 1 +fi + +# Detect BT Panel +BT_MODE=false +if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then + BT_MODE=true +fi + +echo "" +echo "==========================================" +echo " Nexus 6.0 Uninstall" +echo "==========================================" +echo " Deploy: $DEPLOY_DIR" +echo " BT Panel: $BT_MODE" +echo " Purge files: $PURGE" +echo "" + +# Confirm +read -rp "Are you sure you want to uninstall Nexus? [y/N] " confirm +if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "Cancelled." + exit 0 +fi + +# 1. Stop Supervisor process +info "Step 1/5: Stopping Nexus process..." +supervisorctl stop nexus >/dev/null 2>&1 || true + +# 2. Remove Supervisor config +info "Step 2/5: Removing Supervisor config..." +if [ "$BT_MODE" = true ]; then + SUPERVISOR_CONF="/www/server/panel/plugin/supervisor/profile/nexus.ini" +else + SUPERVISOR_CONF="/etc/supervisor/conf.d/nexus.conf" +fi +if [ -f "$SUPERVISOR_CONF" ]; then + rm -f "$SUPERVISOR_CONF" + supervisorctl reread >/dev/null 2>&1 || true + supervisorctl update >/dev/null 2>&1 || true + info " Removed $SUPERVISOR_CONF" +else + info " Supervisor config not found (already removed)" +fi + +# 3. Remove Nginx config +info "Step 3/5: Removing Nginx config..." +if [ "$BT_MODE" = true ]; then + # Find the domain from deploy dir + DOMAIN=$(basename "$DEPLOY_DIR") + NGINX_CONF="/www/server/panel/vhost/nginx/$DOMAIN.conf" +else + NGINX_CONF="/etc/nginx/sites-available/nexus" + NGINX_SYMLINK="/etc/nginx/sites-enabled/nexus" +fi + +if [ -f "$NGINX_CONF" ]; then + rm -f "$NGINX_CONF" + [ -L "${NGINX_SYMLINK:-}" ] && rm -f "$NGINX_SYMLINK" + if [ "$BT_MODE" = true ]; then + /www/server/nginx/sbin/nginx -s reload >/dev/null 2>&1 || true + else + nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true + fi + info " Removed $NGINX_CONF" +else + info " Nginx config not found (already removed)" +fi + +# 4. Remove crontab entries +info "Step 4/5: Removing crontab entries..." +crontab -l 2>/dev/null | grep -v "health_monitor.sh" | grep -v "db_backup.sh" | crontab - 2>/dev/null || true +info " Crontab entries removed" + +# 5. Optionally remove deploy directory +if [ "$PURGE" = true ]; then + info "Step 5/5: Removing deploy directory (--purge)..." + rm -rf "$DEPLOY_DIR" + info " Removed $DEPLOY_DIR" +else + info "Step 5/5: Keeping deploy directory (use --purge to remove)" + info " To remove manually: rm -rf $DEPLOY_DIR" +fi + +echo "" +echo -e " ${GREEN}Nexus uninstalled successfully.${NC}" +echo "" +echo " Note: MySQL database was NOT deleted." +echo " To drop the database manually:" +echo " mysql -u root -e 'DROP DATABASE IF EXISTS nexus;'" +echo "" diff --git a/deploy/upgrade.sh b/deploy/upgrade.sh new file mode 100644 index 00000000..39bec7df --- /dev/null +++ b/deploy/upgrade.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# ================================================================ +# Nexus 6.0 — Upgrade Script +# +# Usage: +# sudo bash upgrade.sh # auto-detect deploy dir +# sudo bash upgrade.sh --deploy-dir /opt/nexus # specify deploy dir +# +# Safe: git pull + pip install + restart. Database is NOT touched. +# ================================================================ + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; } + +DEPLOY_DIR="" + +while [[ $# -gt 0 ]]; do + case $1 in + --deploy-dir) DEPLOY_DIR="$2"; shift 2 ;; + -h|--help) + echo "Usage: sudo bash upgrade.sh [--deploy-dir PATH]" + exit 0 ;; + *) shift ;; + esac +done + +# Auto-detect deploy directory +if [ -z "$DEPLOY_DIR" ]; then + # Check common locations + for d in /opt/nexus /www/wwwroot/*/; do + if [ -d "$d/server" ] && [ -d "$d/web" ]; then + DEPLOY_DIR="$d" + break + fi + done + # Try from Supervisor config + if [ -z "$DEPLOY_DIR" ]; then + DEPLOY_DIR=$(supervisorctl status nexus 2>/dev/null | grep -oP 'cwd=\K[^ ,]+' || true) + fi +fi + +if [ -z "$DEPLOY_DIR" ] || [ ! -d "$DEPLOY_DIR/server" ]; then + error "Cannot find Nexus installation. Use --deploy-dir to specify." + exit 1 +fi + +VENV_DIR="$DEPLOY_DIR/venv" +ENV_FILE="$DEPLOY_DIR/.env" + +echo "" +echo "==========================================" +echo " Nexus 6.0 Upgrade" +echo "==========================================" +echo " Deploy: $DEPLOY_DIR" +echo "" + +# 1. Pull latest code +info "Step 1/4: Updating source code..." +if [ -d "$DEPLOY_DIR/.git" ]; then + cd "$DEPLOY_DIR" + OLD_REV=$(git rev-parse HEAD 2>/dev/null || echo "unknown") + git pull --ff-only 2>/dev/null || { + error "git pull failed. You may have local changes." + error "Resolve conflicts manually: cd $DEPLOY_DIR && git status" + exit 1 + } + NEW_REV=$(git rev-parse HEAD 2>/dev/null || echo "unknown") + if [ "$OLD_REV" = "$NEW_REV" ]; then + info " Already up to date (no new commits)" + else + info " Updated: ${OLD_REV:0:8}..${NEW_REV:0:8}" + fi +else + warn " Not a git repo. Skip git pull (update files manually)" +fi + +# 2. Update Python dependencies +info "Step 2/4: Updating Python dependencies..." +if [ -d "$VENV_DIR/bin" ]; then + "$VENV_DIR/bin/pip" install -q --upgrade pip + "$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || { + warn " pip install had some warnings, but continuing..." + } +else + error " venv not found at $VENV_DIR. Run install.sh first." + exit 1 +fi + +# 3. Restart application +info "Step 3/4: Restarting Nexus..." +supervisorctl restart nexus >/dev/null 2>&1 || { + warn " supervisorctl restart failed. Try manually: supervisorctl restart nexus" +} + +# 4. Health check +info "Step 4/4: Verifying health..." + +# Read port from .env or default +PORT=8600 +if [ -f "$ENV_FILE" ]; then + PORT_FROM_ENV=$(grep '^NEXUS_PORT=' "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || true) + [ -n "$PORT_FROM_ENV" ] && PORT="$PORT_FROM_ENV" +fi + +HEALTH_OK=false +for i in $(seq 1 15); do + if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then + HEALTH_OK=true + break + fi + sleep 2 +done + +echo "" +if [ "$HEALTH_OK" = true ]; then + echo -e " ${GREEN}Upgrade complete! Nexus is healthy.${NC}" +else + echo -e " ${YELLOW}Health check pending. Verify manually:${NC}" + echo " curl http://127.0.0.1:$PORT/health" + echo " supervisorctl status nexus" +fi +echo "" diff --git a/docs/install-guide.md b/docs/install-guide.md new file mode 100644 index 00000000..70b87751 --- /dev/null +++ b/docs/install-guide.md @@ -0,0 +1,410 @@ +# Nexus 6.0 安装指南 + +## 系统要求 + +| 组件 | 最低版本 | 推荐 | +|------|----------|------| +| 操作系统 | Ubuntu 20.04 / Debian 11 / CentOS 7 | Ubuntu 22.04+ | +| Python | 3.10 | 3.12 | +| MySQL | 8.0 | 8.4 | +| Redis | 6.0 | 7.0 | +| Nginx | 1.18 | 最新稳定版 | +| 内存 | 1GB | 2GB+ | +| 磁盘 | 5GB | 20GB+ | + +--- + +## 快速安装 + +### 一键脚本安装(推荐) + +```bash +# 1. 下载并运行安装脚本 +sudo bash install.sh --domain nexus.example.com + +# 2. 打开浏览器完成 Web 安装向导 +# http://nexus.example.com/app/install.html +``` + +脚本会自动检测环境、安装依赖、配置 Supervisor 和 Nginx。 + +### 参数说明 + +``` +--domain DOMAIN 必填,域名或 IP +--deploy-dir PATH 安装目录(默认自动检测) +--port PORT uvicorn 端口(默认 8600) +--skip-nginx 跳过 Nginx 配置 +--skip-supervisor 跳过 Supervisor 配置 +--repo URL Git 仓库地址(可选) +``` + +--- + +## 宝塔面板安装指南 + +### 1. 安装宝塔面板(如未安装) + +```bash +curl -sSO https://raw.githubusercontent.com/zhucaidan/btpanel-v7.0.3/main/install_panel.sh && bash install_panel.sh +``` + +### 2. 通过宝塔安装 MySQL 和 Redis + +1. 宝塔面板 → 软件商店 → 搜索 **MySQL 8.0** → 安装 +2. 软件商店 → 搜索 **Redis** → 安装 +3. 数据库 → 添加数据库: + - 数据库名:`nexus` + - 用户名:`nexus` + - 密码:自动生成或自定义 + +### 3. 创建网站 + +1. 网站 → 添加站点 + - 域名:`nexus.example.com` + - 根目录:`/www/wwwroot/nexus.example.com` + - PHP版本:纯静态(不需要 PHP) +2. 记录网站根目录路径 + +### 4. 运行安装脚本 + +```bash +# 将 Nexus 文件上传到网站根目录后运行 +cd /www/wwwroot/nexus.example.com +sudo bash deploy/install.sh --domain nexus.example.com --deploy-dir /www/wwwroot/nexus.example.com +``` + +或者如果文件还没上传: + +```bash +sudo bash deploy/install.sh --domain nexus.example.com --repo https://your-gitea-url/Nexus.git +``` + +脚本会自动检测宝塔面板,配置写入正确的路径。 + +### 5. 配置 Supervisor + +如果脚本未自动配置: + +1. 宝塔面板 → 软件商店 → 搜索 **Supervisor管理器** → 安装 +2. Supervisor管理器 → 添加守护进程: + - 名称:`nexus` + - 启动命令:`/www/wwwroot/nexus.example.com/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port 8600` + - 运行目录:`/www/wwwroot/nexus.example.com` + +### 6. 配置 Nginx + +1. 网站 → nexus.example.com → 设置 → 配置文件 +2. 在 `#PHP-INFO-END` 后面粘贴: + +```nginx + # Nexus Python API + WebSocket + location ^~ /api/ { + proxy_pass http://127.0.0.1:8600; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location ^~ /health { + proxy_pass http://127.0.0.1:8600; + } + location ^~ /ws/ { + proxy_pass http://127.0.0.1:8600; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +``` + +3. 保存并重载 + +### 7. 配置 SSL + +1. 网站 → nexus.example.com → SSL → Let's Encrypt +2. 申请证书 → 开启强制 HTTPS + +### 8. 完成 Web 安装向导 + +打开浏览器访问 `https://nexus.example.com/app/install.html`,按向导完成: + +- Step 2:环境检测 +- Step 3:填写 MySQL 和 Redis 连接信息 +- Step 4:创建管理员账号 +- Step 5:安装完成 + +--- + +## 手动安装指南 + +### 1. 安装系统依赖 + +**Ubuntu / Debian:** + +```bash +sudo apt update +sudo apt install -y python3 python3-venv python3-pip python3-dev \ + build-essential libssl-dev libffi-dev default-libmysqlclient-dev \ + git curl nginx supervisor +``` + +**CentOS / Rocky / AlmaLinux:** + +```bash +sudo dnf install -y python3 python3-devel python3-pip \ + gcc openssl-devel libffi-devel mysql-devel \ + git curl nginx supervisor +``` + +### 2. 安装 MySQL 8.0 + +```bash +# Ubuntu +sudo apt install -y mysql-server +sudo mysql_secure_installation + +# 创建数据库 +sudo mysql -e "CREATE DATABASE nexus CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" +sudo mysql -e "CREATE USER 'nexus'@'localhost' IDENTIFIED BY 'YOUR_PASSWORD';" +sudo mysql -e "GRANT ALL PRIVILEGES ON nexus.* TO 'nexus'@'localhost';" +sudo mysql -e "FLUSH PRIVILEGES;" +``` + +### 3. 安装 Redis + +```bash +# Ubuntu +sudo apt install -y redis-server +sudo systemctl enable redis-server +sudo systemctl start redis-server + +# 验证 +redis-cli ping # 应返回 PONG +``` + +### 4. 部署 Nexus + +```bash +# 创建目录 +sudo mkdir -p /opt/nexus +cd /opt/nexus + +# 克隆代码(或从发布包解压) +sudo git clone https://your-gitea-url/Nexus.git . + +# 创建 Python 虚拟环境 +python3.12 -m venv venv +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 5. 配置 Supervisor + +```bash +sudo tee /etc/supervisor/conf.d/nexus.conf << 'EOF' +[program:nexus] +command=/opt/nexus/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port 8600 --workers 2 --loop uvloop --http httptools --log-level info +directory=/opt/nexus +user=root +autostart=true +autorestart=true +startretries=10 +startsecs=3 +stopwaitsecs=10 +stopsignal=INT +environment=PATH="/opt/nexus/venv/bin:%(ENV_PATH)s",HOME="/root" +stderr_logfile=/var/log/nexus/error.log +stdout_logfile=/var/log/nexus/access.log +stderr_logfile_maxbytes=50MB +stdout_logfile_maxbytes=50MB +EOF + +# 创建日志目录 +sudo mkdir -p /var/log/nexus + +# 重载 Supervisor +sudo supervisorctl reread +sudo supervisorctl update +sudo supervisorctl start nexus +``` + +### 6. 配置 Nginx + +```bash +sudo tee /etc/nginx/sites-available/nexus << 'EOF' +upstream nexus_api { + server 127.0.0.1:8600; + keepalive 32; +} + +server { + listen 80; + server_name nexus.example.com; + + location / { + proxy_pass http://nexus_api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } + + location /ws/ { + proxy_pass http://nexus_api; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 3600s; + } + + location ~ ^/(\.env|\.git) { + return 404; + } + + client_max_body_size 100m; + + access_log /var/log/nginx/nexus_access.log; + error_log /var/log/nginx/nexus_error.log; +} +EOF + +# 启用站点 +sudo ln -sf /etc/nginx/sites-available/nexus /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +``` + +### 7. 完成 Web 安装向导 + +打开浏览器访问 `http://nexus.example.com/app/install.html` + +### 8. 配置 SSL + +```bash +sudo apt install -y certbot python3-certbot-nginx +sudo certbot --nginx -d nexus.example.com +``` + +--- + +## 安装后配置 + +### 健康检查 Crontab(第3层守护) + +```bash +# 编辑 crontab +sudo crontab -e + +# 添加(每分钟检查一次) +* * * * * /opt/nexus/deploy/health_monitor.sh +``` + +### 数据库备份 Crontab + +```bash +# 每天凌晨3点自动备份,保留30天 +0 3 * * * /opt/nexus/deploy/db_backup.sh +``` + +### 防火墙配置 + +```bash +# 只开放必要端口 +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw enable + +# 8600 端口不需要对外开放(Nginx 反向代理) +``` + +### 安全收尾清单 + +- [ ] 安装向导已自动锁定 +- [ ] `.env` 文件包含 SECRET_KEY/API_KEY/ENCRYPTION_KEY — 安装后不可修改 +- [ ] SSL 证书已配置,强制 HTTPS +- [ ] 8600 端口未对外开放 +- [ ] 管理员密码已修改(非默认弱密码) +- [ ] Telegram 告警已配置(Settings 页面) + +--- + +## 升级 + +```bash +sudo bash /opt/nexus/deploy/upgrade.sh +``` + +升级脚本会:git pull → pip install → supervisorctl restart → 健康检查。 + +--- + +## 卸载 + +```bash +sudo bash /opt/nexus/deploy/uninstall.sh # 保留文件 +sudo bash /opt/nexus/deploy/uninstall.sh --purge # 同时删除部署目录 +``` + +> 注意:MySQL 数据库不会被自动删除。需手动 `DROP DATABASE nexus;` + +--- + +## 故障排查 + +### 端口 8600 无响应 + +```bash +supervisorctl status nexus # 检查进程状态 +tail -f /var/log/nexus/error.log # 查看错误日志 +curl http://127.0.0.1:8600/health # 本地健康检查 +``` + +### Nginx 502 Bad Gateway + +- 检查 uvicorn 是否运行:`supervisorctl status nexus` +- 检查端口匹配:Nginx upstream 端口和 uvicorn 端口是否一致 +- 检查 Nginx 错误日志:`tail -f /var/log/nginx/nexus_error.log` + +### 数据库连接失败 + +```bash +# 检查 MySQL 是否运行 +sudo systemctl status mysql + +# 测试连接 +mysql -u nexus -p -h 127.0.0.1 nexus +``` + +### Redis 连接失败 + +```bash +# 检查 Redis 是否运行 +sudo systemctl status redis-server +redis-cli ping +``` + +### Supervisor 启动失败 + +```bash +# 检查配置语法 +supervisorctl reread + +# 查看详细日志 +tail -f /var/log/nexus/error.log + +# 手动启动测试 +cd /opt/nexus && source venv/bin/activate +uvicorn server.main:app --host 0.0.0.0 --port 8600 +``` + +### 安装向导无法访问 + +- 检查 `.env` 文件是否存在:如果存在,安装向导会被锁定 +- 删除 `.env` 可重新进入安装模式:`rm /opt/nexus/.env && supervisorctl restart nexus` diff --git a/mcp/firefox_server.py b/mcp/firefox_server.py index 07844a67..b14ae397 100644 --- a/mcp/firefox_server.py +++ b/mcp/firefox_server.py @@ -26,7 +26,7 @@ def _get_driver(): # T1: 导航 # ================================================================ @server.tool() -async def firefox_navigate(url: str = "https://192.168.124.10/login.php"): +async def firefox_navigate(url: str = "https://192.168.124.10/app/login.html"): """Firefox 打开指定 URL,返回页面标题和 URL""" try: d = _get_driver() diff --git a/server/api/install.py b/server/api/install.py index 4045e3f8..47753a74 100644 --- a/server/api/install.py +++ b/server/api/install.py @@ -29,10 +29,9 @@ router = APIRouter(prefix="/api/install", tags=["install"]) ROOT_DIR = Path(__file__).resolve().parent.parent.parent ENV_FILE = ROOT_DIR / ".env" -LOCK_FILE = ROOT_DIR / "web" / "install.php.locked" # Legacy compat — primary lock is .install_locked INSTALL_LOCK = ROOT_DIR / ".install_locked" -CONFIG_PHP_DIR = ROOT_DIR / "web" / "data" -CONFIG_PHP = CONFIG_PHP_DIR / "config.php" +CONFIG_DIR = ROOT_DIR / "web" / "data" +CONFIG_JSON = CONFIG_DIR / "config.json" STATE_FILE = ROOT_DIR / ".install_state.json" @@ -41,7 +40,7 @@ def _is_installed() -> bool: def _is_locked() -> bool: - return INSTALL_LOCK.exists() or LOCK_FILE.exists() + return INSTALL_LOCK.exists() # ── Pydantic Models ── @@ -76,16 +75,6 @@ class CreateAdminRequest(BaseModel): # ── Helpers ── -def _escape_php_string(s: str) -> str: - """Escape a string for safe insertion into a PHP single-quoted string. - - In PHP single-quoted strings, only ``\\`` and ``'`` are special escapes. - A user value containing ``'`` could break out of the define() and inject - arbitrary PHP code (RCE). This function neutralises that vector. - """ - return s.replace("\\", "\\\\").replace("'", "\\'") - - def _make_db_url(req: InitDbRequest | CreateAdminRequest) -> str: return ( f"mysql+aiomysql://{req.db_user}:{quote_plus(req.db_pass)}" @@ -171,42 +160,48 @@ def _write_env(req: InitDbRequest, secret_key: str, api_key: str, encryption_key logger.info(f".env written to {ENV_FILE}") -def _write_config_php(req: InitDbRequest, api_key: str, site_url: str) -> None: - CONFIG_PHP_DIR.mkdir(parents=True, exist_ok=True) +def _write_config_json(req: InitDbRequest, api_key: str, site_url: str) -> None: + """Write shared configuration as JSON (replaces legacy config.php).""" + import json + CONFIG_DIR.mkdir(parents=True, exist_ok=True) install_dir = str(ROOT_DIR) - now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - lines = [ - " bool: + """Detect if BT Panel (宝塔面板) is installed on this system.""" + return Path("/www/server/panel/class/common.py").exists() def _configure_guardian(install_dir: str, api_port: str) -> list[str]: """Best-effort: configure Supervisor, crontab, health_monitor.sh""" results: list[str] = [] + bt_panel = _detect_bt_panel() # 1. Log directory - log_dir = Path("/var/log/nexus") + if bt_panel: + log_dir = Path("/www/wwwlogs") + else: + log_dir = Path("/var/log/nexus") try: log_dir.mkdir(parents=True, exist_ok=True) results.append(f"✓ 日志目录已创建 {log_dir}") @@ -214,6 +209,11 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]: results.append(f"✗ 日志目录创建失败(需 root 权限)") # 2. Supervisor config + if bt_panel: + conf_path = Path("/www/server/panel/plugin/supervisor/profile/nexus.ini") + else: + conf_path = Path("/etc/supervisor/conf.d/nexus.conf") + supervisor_conf = ( f"[program:nexus]\n" f"command={install_dir}/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port {api_port}\n" @@ -226,18 +226,18 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]: f"stopwaitsecs=10\n" f"stopsignal=INT\n" f"environment=PATH=\"{install_dir}/venv/bin:/usr/local/bin:%(ENV_PATH)s\",HOME=\"/root\"\n" - f"stderr_logfile=/var/log/nexus/error.log\n" - f"stdout_logfile=/var/log/nexus/access.log\n" + f"stderr_logfile={log_dir}/nexus_error.log\n" + f"stdout_logfile={log_dir}/nexus_access.log\n" f"stderr_logfile_maxbytes=50MB\n" f"stdout_logfile_maxbytes=50MB\n" f"stderr_logfile_backups=5\n" f"stdout_logfile_backups=5\n" ) - conf_path = Path("/etc/supervisor/conf.d/nexus.conf") try: conf_path.parent.mkdir(parents=True, exist_ok=True) conf_path.write_text(supervisor_conf) - results.append(f"✓ Supervisor 配置已写入 {conf_path}") + mode_label = "宝塔" if bt_panel else "标准" + results.append(f"✓ Supervisor 配置已写入 {conf_path}({mode_label}模式)") except OSError: results.append("✗ Supervisor 配置写入失败(需 root 权限)") @@ -379,7 +379,7 @@ async def env_check(): }) # Write permissions - data_writable = CONFIG_PHP_DIR.exists() and os.access(CONFIG_PHP_DIR, os.W_OK) or \ + data_writable = CONFIG_DIR.exists() and os.access(CONFIG_DIR, os.W_OK) or \ os.access(ROOT_DIR / "web", os.W_OK) root_writable = os.access(ROOT_DIR, os.W_OK) checks.append({ @@ -498,8 +498,8 @@ async def init_db(req: InitDbRequest): site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}" _write_env(req, secret_key, api_key, encryption_key, pool_size, max_overflow, redis_url, site_url) - # Write config.php - _write_config_php(req, api_key, site_url) + # Write config.json + _write_config_json(req, api_key, site_url) # Configure process guardian (best-effort) install_dir = str(ROOT_DIR) @@ -530,6 +530,7 @@ async def init_db(req: InitDbRequest): "max_overflow": max_overflow, "tables_created": 14, "guardian_results": guardian_results, + "bt_panel": _detect_bt_panel(), } @@ -588,16 +589,15 @@ async def create_admin(req: CreateAdminRequest): except Exception as e: raise HTTPException(400, f"创建管理员失败: {e}") - # Update config.php with brand - if CONFIG_PHP.exists(): - import re - content = CONFIG_PHP.read_text() - content = re.sub( - r"define\('APP_NAME', '.*?'\);", - f"define('APP_NAME', '{_escape_php_string(req.system_name)}');", - content, - ) - CONFIG_PHP.write_text(content) + # Update config.json with brand + if CONFIG_JSON.exists(): + import json + try: + config = json.loads(CONFIG_JSON.read_text()) + config["app_name"] = req.system_name + CONFIG_JSON.write_text(json.dumps(config, indent=2, ensure_ascii=False)) + except Exception: + pass # Update .env with brand if ENV_FILE.exists(): @@ -654,17 +654,11 @@ async def lock_install(): logger.warning("Failed to verify admin account before locking: %s", e) # If DB is unreachable, allow lock (admin may have been created but DB is temporarily down) - # Create lock marker files + # Create lock marker file INSTALL_LOCK.write_text( f"Nexus installation locked at {datetime.now(timezone.utc).isoformat()}\n" ) - # Also create the legacy lock file (install.php.locked) for compatibility - try: - LOCK_FILE.write_text("locked\n") - except OSError as e: - logger.warning("Failed to write legacy lock file: %s", e) - # Clean up state file if STATE_FILE.exists(): try: diff --git a/server/config.py b/server/config.py index d5e92d90..204aea2e 100644 --- a/server/config.py +++ b/server/config.py @@ -39,10 +39,10 @@ class Settings(BaseSettings): # Database (immutable — must be set in .env) DATABASE_URL: str = "mysql+aiomysql://root:password@127.0.0.1:3306/nexus" - # Pool params: install.php auto-calculates from MySQL max_connections + # Pool params: install wizard auto-calculates from MySQL max_connections # Formula: pool_size = max(20, maxConn * 0.4), overflow = max(20, maxConn * 0.3) # MySQL max_connections=400 → pool=160, overflow=120, max=280 - # These defaults match install.php; override via .env or MySQL settings table + # These defaults match install wizard; override via .env or MySQL settings table DB_POOL_SIZE: Optional[int] = 160 DB_MAX_OVERFLOW: Optional[int] = 120 diff --git a/server/infrastructure/database/crypto.py b/server/infrastructure/database/crypto.py index 9ad9ab94..8bd6d614 100644 --- a/server/infrastructure/database/crypto.py +++ b/server/infrastructure/database/crypto.py @@ -1,12 +1,9 @@ -"""Nexus — Encryption Utilities (Fernet + AES) +"""Nexus — Encryption Utilities (Fernet) Shared crypto module — used by SSH pool, DB credentials, password presets. """ -import base64 -import hashlib import logging from cryptography.fernet import Fernet -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from server.config import settings @@ -39,14 +36,6 @@ def get_fernet() -> Fernet: return _fernet_instance -def _derive_aes_key() -> bytes: - """Unified key derivation: must match PHP _encryptPw() - PHP uses hash('sha256', API_KEY, true). - This uses API_KEY first, falls back to SECRET_KEY.""" - key = settings.API_KEY or settings.SECRET_KEY - return hashlib.sha256(key.encode()).digest() - - def encrypt_value(plaintext: str) -> str: """Encrypt a string using Fernet""" if not plaintext: @@ -55,29 +44,11 @@ def encrypt_value(plaintext: str) -> str: def decrypt_value(ciphertext: str) -> str: - """Decrypt a string — supports both Fernet and PHP AES format""" + """Decrypt a Fernet-encrypted string""" if not ciphertext: return ciphertext - # PHP-compatible AES format - if ciphertext.startswith("aes:"): - try: - key = _derive_aes_key() - data = base64.b64decode(ciphertext[4:]) - iv = data[:16] - enc = data[16:] - cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) - decryptor = cipher.decryptor() - plain = decryptor.update(enc) + decryptor.finalize() - pad_len = plain[-1] - if 1 <= pad_len <= 16: - plain = plain[:-pad_len] - return plain.decode() - except Exception as e: - logger.warning(f"AES decryption failed (key mismatch?): {e}") - raise ValueError("Failed to decrypt credential (AES)") from e - # Fernet format try: return get_fernet().decrypt(ciphertext.encode()).decode() except Exception as e: logger.warning(f"Fernet decryption failed (key mismatch?): {e}") - raise ValueError("Failed to decrypt credential (Fernet)") from e \ No newline at end of file + raise ValueError("Failed to decrypt credential") from e \ No newline at end of file diff --git a/server/infrastructure/database/migrations.py b/server/infrastructure/database/migrations.py index 223140c2..0f0e6a7b 100644 --- a/server/infrastructure/database/migrations.py +++ b/server/infrastructure/database/migrations.py @@ -144,7 +144,7 @@ async def run_schema_migrations(): "ALTER TABLE servers ADD COLUMN connectivity VARCHAR(20) DEFAULT 'unknown' COMMENT '连接状态'", "ALTER TABLE servers ADD COLUMN last_checked_at DATETIME NULL COMMENT '上次连接测试时间'", "ALTER TABLE servers ADD COLUMN ssh_key_configured TINYINT(1) DEFAULT 0 COMMENT '是否已配置SSH Key'", - # Fix: ssh_key columns were VARCHAR(500) in install.php — change to TEXT for large encrypted keys + # Fix: ssh_key columns were VARCHAR(500) in legacy schema — change to TEXT for large encrypted keys "ALTER TABLE servers MODIFY COLUMN ssh_key_private TEXT NULL COMMENT '加密后的私钥内容(Fernet)'", "ALTER TABLE servers MODIFY COLUMN ssh_key_public TEXT NULL COMMENT '公钥内容'", ] diff --git a/server/main.py b/server/main.py index 38fa7493..b6b01c1c 100644 --- a/server/main.py +++ b/server/main.py @@ -14,7 +14,7 @@ Lifespan startup (no .env — install mode): - All other API routes return 503 "System not configured" - User completes the install wizard at /app/install.html -D2: install.php → install.html migration (conditional lifespan) +D2: install wizard (install.html + FastAPI API) D7: DB session leak fix — middleware auto-manages session lifecycle per request. """ diff --git a/web/app/install.html b/web/app/install.html index 71f35f26..c1a332b6 100644 --- a/web/app/install.html +++ b/web/app/install.html @@ -76,7 +76,7 @@

本向导将引导您完成系统安装配置。
安装完成后将自动生成:.env (Python后端) + - config.php (PHP兼容) + + config.json (共享配置) + MySQL settings表 (共享配置)
整个过程大约 3 分钟。

@@ -232,7 +232,7 @@
⚠ 注意:初始化将自动:① 连接数据库并建表 ② 生成 SECRET_KEY/API_KEY - ③ 写入 .env + config.php + settings表 ④ 自动计算连接池大小 + ③ 写入 .env + config.json + settings表 ④ 自动计算连接池大小
@@ -337,24 +337,52 @@ Supervisor 进程守护
-

安装: sudo apt install -y supervisor

-

配置文件复制到: /etc/supervisor/conf.d/nexus.conf

-
[program:nexus]
-command=/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port 
-directory=
-user=root
-autostart=true
-autorestart=true
-startretries=10
-startsecs=3
-stopwaitsecs=10
-stopsignal=INT
-environment=PATH="/venv/bin:%(ENV_PATH)s"
-stderr_logfile=/var/log/nexus/error.log
-stdout_logfile=/var/log/nexus/access.log
-stderr_logfile_maxbytes=50MB
-stdout_logfile_maxbytes=50MB
-

启动: sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start nexus

+ + + +
✓ 已自动配置 — Supervisor 配置已写入,服务已重载
@@ -380,8 +408,11 @@ stdout_logfile_maxbytes=50MB Nginx 反向代理 (API + WebSocket)
-

宝塔 → 网站 → 配置文件 → #PHP-INFO-END 后面粘贴:

-
    # Nexus Python API + WebSocket
+              
+              
+              
+              
             
@@ -428,7 +494,15 @@ location / { 5 SSL证书 (强制HTTPS) -
宝塔 → 网站 → SSL → Let's Encrypt → 一键申请 → 开启强制HTTPS
+
+ + +
@@ -491,6 +565,7 @@ function installWizard() { envAllPass: false, initResult: null, installDir: '/opt/nexus', + btPanel: false, form: { db_host: 'localhost', @@ -582,6 +657,7 @@ function installWizard() { if (r.ok && d.success) { this.success = '数据库初始化成功!已创建 ' + (d.tables_created || 14) + ' 张表。'; this.initResult = d; + this.btPanel = !!d.bt_panel; this.step = 4; } else { this.error = d.detail || '初始化失败'; diff --git a/web/install.php b/web/install.php deleted file mode 100644 index 481657d9..00000000 --- a/web/install.php +++ /dev/null @@ -1,1260 +0,0 @@ - -

⚠ 安装已锁定

-

系统已完成安装,install.php 已重命名为 install.php.locked。

-

如需重新安装,请删除 install.php.locked 文件。

- 前往登录 → - '); -} - -if (file_exists($envFile)) { - die('
-

⚠ .env 已存在

-

检测到 .env 配置文件,系统可能已安装。

-

如需重新安装,请先删除 .envweb/data/config.php

- 前往登录 → -
'); -} - -$step = intval($_GET['step'] ?? 1); -$action = $_POST['action'] ?? ''; -$error = ''; -$success = ''; - -// ============================================================ -// POST Processing -// ============================================================ - -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - - // ── Step 3: DB + Redis + API Configuration ── - if ($action === 'init_db') { - $dbHost = $_POST['db_host'] ?? 'localhost'; - $dbPort = $_POST['db_port'] ?? '3306'; - - $dbName = $_POST['db_name'] ?? 'Nexus'; - $dbUser = $_POST['db_user'] ?? 'Nexus'; - $dbPass = $_POST['db_pass'] ?? ''; - - $redisHost = $_POST['redis_host'] ?? '127.0.0.1'; - $redisPort = $_POST['redis_port'] ?? '6379'; - $redisDb = $_POST['redis_db'] ?? '0'; - $redisPass = $_POST['redis_pass'] ?? ''; - - $apiPort = $_POST['api_port'] ?? '8600'; - $timezone = $_POST['timezone'] ?? 'Asia/Shanghai'; - - // ── Connect to Nexus database ── - try { - $pdo = new PDO( - "mysql:host=$dbHost;port=$dbPort;dbname=$dbName;charset=utf8mb4", - $dbUser, $dbPass, - [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5] - ); - - // ── Create tables in dependency order (referenced tables first) ── - $tables = [ - "platforms" => " - CREATE TABLE IF NOT EXISTS platforms ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL UNIQUE COMMENT '平台名称: Linux服务器/Windows/MySQL', - category VARCHAR(50) NOT NULL COMMENT '分类: host/database/device', - type VARCHAR(50) NOT NULL COMMENT '类型: linux/windows/mysql/switch', - default_protocols JSON COMMENT '默认协议: [{name:\"ssh\",port:22,primary:true}]', - charset VARCHAR(20) DEFAULT 'utf-8' COMMENT '默认字符集', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "nodes" => " - CREATE TABLE IF NOT EXISTS nodes ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL COMMENT '节点名称', - parent_id INT COMMENT '父节点ID', - sort_order INT DEFAULT 0 COMMENT '排序权重', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (parent_id) REFERENCES nodes(id) ON DELETE SET NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "admins" => " - CREATE TABLE IF NOT EXISTS admins ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(100) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL, - email VARCHAR(255), - totp_secret VARCHAR(64), - totp_enabled TINYINT(1) DEFAULT 0, - is_active TINYINT(1) DEFAULT 1, - jwt_refresh_token VARCHAR(500) COMMENT 'JWT刷新令牌', - jwt_token_expires DATETIME COMMENT '令牌过期时间', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_login DATETIME - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "servers" => " - CREATE TABLE IF NOT EXISTS servers ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL UNIQUE COMMENT '服务器名称', - domain VARCHAR(255) NOT NULL COMMENT '域名或IP', - port INT DEFAULT 22 COMMENT 'SSH端口', - username VARCHAR(100) DEFAULT 'root' COMMENT 'SSH用户名', - auth_method VARCHAR(20) DEFAULT 'key' COMMENT '认证方式: key/password', - password VARCHAR(255) COMMENT 'SSH密码(加密存储)', - ssh_key_path VARCHAR(500) COMMENT 'SSH私钥路径', - ssh_key_private VARCHAR(500) COMMENT '加密后的私钥内容', - ssh_key_public VARCHAR(500) COMMENT '公钥内容', - ssh_key_configured TINYINT(1) DEFAULT 0 COMMENT '是否已配置SSH Key', - agent_port INT DEFAULT 8601 COMMENT 'Agent API端口', - agent_api_key VARCHAR(255) COMMENT 'Agent API密钥', - description TEXT COMMENT '备注说明', - target_path VARCHAR(500) COMMENT '推送目标路径', - category VARCHAR(100) COMMENT '服务器分类(旧字段,保留兼容)', - platform_id INT COMMENT '平台类型ID', - node_id INT COMMENT '所属节点ID', - protocols JSON COMMENT '实例级协议覆盖', - extra_attrs JSON COMMENT '扩展属性', - connectivity VARCHAR(20) DEFAULT 'unknown' COMMENT '连接状态: ok/err/unknown', - is_online TINYINT(1) DEFAULT 0 COMMENT '是否在线', - last_heartbeat DATETIME COMMENT '最后心跳时间', - system_info TEXT COMMENT '系统信息JSON(Agent上报)', - agent_version VARCHAR(20) COMMENT 'Agent版本', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (platform_id) REFERENCES platforms(id), - FOREIGN KEY (node_id) REFERENCES nodes(id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "sync_logs" => " - CREATE TABLE IF NOT EXISTS sync_logs ( - id INT AUTO_INCREMENT PRIMARY KEY, - server_id INT NOT NULL, - source_path VARCHAR(500) NOT NULL COMMENT '源目录', - target_path VARCHAR(500) NOT NULL COMMENT '目标目录', - trigger_type VARCHAR(20) COMMENT '触发类型: manual/schedule/batch', - operator VARCHAR(100) COMMENT '操作人用户名', - status VARCHAR(20) COMMENT 'pending/running/success/failed', - sync_mode VARCHAR(20) DEFAULT 'incremental' COMMENT '同步模式: incremental/full/overwrite/checksum', - files_total INT DEFAULT 0, - files_transferred INT DEFAULT 0, - files_skipped INT DEFAULT 0, - bytes_transferred INT DEFAULT 0, - duration_seconds INT DEFAULT 0, - diff_summary TEXT, - error_message TEXT, - started_at DATETIME DEFAULT CURRENT_TIMESTAMP, - finished_at DATETIME, - FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "login_attempts" => " - CREATE TABLE IF NOT EXISTS login_attempts ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(100) NOT NULL, - ip_address VARCHAR(45) NOT NULL, - attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP, - success TINYINT(1) DEFAULT 0 - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "settings" => " - CREATE TABLE IF NOT EXISTS settings ( - `key` VARCHAR(100) PRIMARY KEY, - `value` TEXT, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "password_presets" => " - CREATE TABLE IF NOT EXISTS password_presets ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL COMMENT '预设名称', - encrypted_pw VARCHAR(500) NOT NULL COMMENT '加密后的密码', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "push_schedules" => " - CREATE TABLE IF NOT EXISTS push_schedules ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL COMMENT '调度名称', - source_path VARCHAR(500) NOT NULL, - server_ids TEXT NOT NULL COMMENT 'JSON数组: 目标服务器ID列表', - cron_expr VARCHAR(100) NOT NULL COMMENT 'cron表达式: 分 时 日 月 周', - enabled TINYINT(1) DEFAULT 1, - last_run_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "push_retry_jobs" => " - CREATE TABLE IF NOT EXISTS push_retry_jobs ( - id INT AUTO_INCREMENT PRIMARY KEY, - server_id INT NOT NULL, - server_name VARCHAR(100) COMMENT '冗余字段便于列表显示', - operator VARCHAR(100) COMMENT '操作人用户名', - source_path VARCHAR(500) NOT NULL, - target_path VARCHAR(500) NOT NULL, - status VARCHAR(20) DEFAULT 'pending', - retry_count INT DEFAULT 0, - max_tries INT DEFAULT 100, - next_retry_at DATETIME, - last_error TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "audit_logs" => " - CREATE TABLE IF NOT EXISTS audit_logs ( - id INT AUTO_INCREMENT PRIMARY KEY, - admin_username VARCHAR(100) COMMENT '操作人用户名', - action VARCHAR(50) NOT NULL COMMENT '操作类型', - target_type VARCHAR(50) COMMENT '目标类型', - target_id INT COMMENT '目标ID', - detail TEXT COMMENT '操作详情JSON', - ip_address VARCHAR(45) COMMENT '操作IP', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "scripts" => " - CREATE TABLE IF NOT EXISTS scripts ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL COMMENT '脚本名称', - category VARCHAR(50) DEFAULT 'ops' COMMENT '分类: ops/deploy/check/cleanup', - content TEXT NOT NULL COMMENT 'Shell命令内容', - description TEXT COMMENT '说明', - created_by VARCHAR(100) COMMENT '创建人', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "script_executions" => " - CREATE TABLE IF NOT EXISTS script_executions ( - id INT AUTO_INCREMENT PRIMARY KEY, - script_id INT COMMENT '关联脚本(手动输入时为null)', - command TEXT NOT NULL COMMENT '实际执行的命令', - server_ids TEXT NOT NULL COMMENT 'JSON数组: 目标服务器ID列表', - status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/running/completed/failed', - results TEXT COMMENT 'JSON: 每台服务器执行结果', - operator VARCHAR(100) COMMENT '操作人用户名', - started_at DATETIME DEFAULT CURRENT_TIMESTAMP, - completed_at DATETIME, - FOREIGN KEY (script_id) REFERENCES scripts(id) ON DELETE SET NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "db_credentials" => " - CREATE TABLE IF NOT EXISTS db_credentials ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL COMMENT '凭据名称', - db_type VARCHAR(20) DEFAULT 'mysql' COMMENT '数据库类型', - host VARCHAR(255) DEFAULT 'localhost' COMMENT '数据库主机', - port INT DEFAULT 3306 COMMENT '数据库端口', - username VARCHAR(100) NOT NULL COMMENT '数据库用户名', - encrypted_password VARCHAR(500) NOT NULL COMMENT '加密后的密码', - database VARCHAR(100) COMMENT '数据库库名', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "ssh_sessions" => " - CREATE TABLE IF NOT EXISTS ssh_sessions ( - id VARCHAR(36) PRIMARY KEY COMMENT 'UUID', - server_id INT NOT NULL COMMENT '服务器ID', - admin_id INT COMMENT '操作人ID', - remote_addr VARCHAR(45) COMMENT '客户端IP', - status VARCHAR(20) DEFAULT 'active' COMMENT 'active/closed', - started_at DATETIME DEFAULT CURRENT_TIMESTAMP, - closed_at DATETIME, - FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, - FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE SET NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "command_logs" => " - CREATE TABLE IF NOT EXISTS command_logs ( - id INT AUTO_INCREMENT PRIMARY KEY, - session_id VARCHAR(36) COMMENT 'SSH会话ID', - server_id INT NOT NULL COMMENT '服务器ID', - admin_id INT COMMENT '操作人ID', - command VARCHAR(2000) NOT NULL COMMENT '执行的命令', - remote_addr VARCHAR(45) COMMENT '客户端IP', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES ssh_sessions(id) ON DELETE CASCADE, - FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, - FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE SET NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - ]; - - foreach ($tables as $name => $sql) { - $pdo->exec($sql); - } - - // ── Create indexes ── - $indexes = [ - "ALTER TABLE servers ADD INDEX idx_servers_is_online (is_online)", - "ALTER TABLE servers ADD INDEX idx_servers_category (category)", - "ALTER TABLE servers ADD INDEX idx_servers_platform_id (platform_id)", - "ALTER TABLE servers ADD INDEX idx_servers_node_id (node_id)", - "ALTER TABLE sync_logs ADD INDEX idx_sync_logs_srv_start (server_id, started_at)", - "ALTER TABLE login_attempts ADD INDEX idx_login_attempts_username_ip (username, ip_address)", - "ALTER TABLE ssh_sessions ADD INDEX idx_ssh_sessions_server_id (server_id)", - "ALTER TABLE ssh_sessions ADD INDEX idx_ssh_sessions_admin_id (admin_id)", - "ALTER TABLE command_logs ADD INDEX idx_cmdlog_srv_time (server_id, created_at)", - "ALTER TABLE command_logs ADD INDEX idx_cmdlog_session_id (session_id)", - ]; - foreach ($indexes as $sql) { - try { $pdo->exec($sql); } catch (PDOException $e) { - if ($e->getCode() != '42S01') throw $e; - } - } - - // ── Auto-calculate pool_size from max_connections ── - try { - $poolStmt = $pdo->query("SHOW VARIABLES LIKE 'max_connections'"); - $poolRow = $poolStmt->fetch(); - $maxConn = max(100, intval($poolRow['Value'] ?? 100)); - $poolSize = max(20, intval($maxConn * 0.4)); - $maxOverflow = max(20, intval($maxConn * 0.3)); - } catch (PDOException $e) { - $poolSize = 40; - $maxOverflow = 60; - } - - // ── Generate keys ── - $secretKey = bin2hex(random_bytes(32)); - $apiKey = bin2hex(random_bytes(16)); - - // ── Write web/data/config.php (PHP frontend) ── - $configDir = __DIR__ . '/data'; - if (!is_dir($configDir)) mkdir($configDir, 0755, true); - $configPath = $configDir . '/config.php'; - - $redisUrl = "redis://"; - if ($redisPass) $redisUrl .= addslashes($redisPass) . "@"; - $redisUrl .= "$redisHost:$redisPort/$redisDb"; - - // Auto-detect install directory + site URL - $installDir = dirname(__DIR__); - $siteUrl = trim($_POST['site_url'] ?? ''); - if (empty($siteUrl)) { - $detectScheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; - $detectHost = $_SERVER['SERVER_ADDR'] ?? $_SERVER['HTTP_HOST'] ?? '127.0.0.1'; - $siteUrl = $detectScheme . '://' . $detectHost; - } - $siteUrl = rtrim($siteUrl, '/'); - $apiBaseUrl = $siteUrl; - $internalApiUrl = 'http://127.0.0.1:' . $apiPort; - - $configContent = " $apiKey, - 'secret_key' => $secretKey, - 'system_name' => 'Nexus', - 'system_title' => 'Nexus — 服务器运维管理平台', - 'db_pool_size' => (string)$poolSize, - 'db_max_overflow' => (string)$maxOverflow, - 'redis_url' => $redisUrl, - 'heartbeat_timeout' => '60', - 'cpu_alert_threshold' => '80', - 'mem_alert_threshold' => '80', - 'disk_alert_threshold' => '80', - 'telegram_bot_token' => '', - 'telegram_chat_id' => '', - ]; - - $stmtKV = $pdo->prepare("INSERT INTO `settings` (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)"); - foreach ($settingsKV as $k => $v) { - $stmtKV->execute([$k, $v]); - } - - // ── Save DB credentials to session for Step 4 ── - $_SESSION['db_host'] = $dbHost; - $_SESSION['db_port'] = $dbPort; - $_SESSION['db_name'] = $dbName; - $_SESSION['db_user'] = $dbUser; - $_SESSION['db_pass'] = $dbPass; - $_SESSION['installed'] = true; - $_SESSION['pool_size'] = $poolSize; - $_SESSION['max_overflow'] = $maxOverflow; - $_SESSION['install_dir'] = $installDir; - $_SESSION['site_url'] = $siteUrl; - $_SESSION['api_port'] = $apiPort; - - // ── Auto-configure process guardian ── - $guardianResults = []; - - // 1. Create log directory - $logDir = '/var/log/nexus'; - if (!is_dir($logDir)) { - @mkdir($logDir, 0755, true); - $guardianResults[] = '✓ 日志目录已创建 ' . $logDir; - } else { - $guardianResults[] = '✓ 日志目录已存在 ' . $logDir; - } - - // 2. Write Supervisor config - $supervisorConf = "[program:nexus]\n"; - $supervisorConf .= "command={$installDir}/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port {$apiPort}\n"; - $supervisorConf .= "directory={$installDir}\n"; - $supervisorConf .= "user=root\n"; - $supervisorConf .= "autostart=true\n"; - $supervisorConf .= "autorestart=true\n"; - $supervisorConf .= "startretries=10\n"; - $supervisorConf .= "startsecs=3\n"; - $supervisorConf .= "stopwaitsecs=10\n"; - $supervisorConf .= "stopsignal=INT\n"; - $supervisorConf .= "environment=PATH=\"{$installDir}/venv/bin:/usr/local/bin:%(ENV_PATH)s\",HOME=\"/root\"\n"; - $supervisorConf .= "stderr_logfile=/var/log/nexus/error.log\n"; - $supervisorConf .= "stdout_logfile=/var/log/nexus/access.log\n"; - $supervisorConf .= "stderr_logfile_maxbytes=50MB\n"; - $supervisorConf .= "stdout_logfile_maxbytes=50MB\n"; - $supervisorConf .= "stderr_logfile_backups=5\n"; - $supervisorConf .= "stdout_logfile_backups=5\n"; - - $confPath = '/etc/supervisor/conf.d/nexus.conf'; - $confWritten = @file_put_contents($confPath, $supervisorConf); - if ($confWritten !== false) { - $guardianResults[] = '✓ Supervisor 配置已写入 ' . $confPath; - } else { - $guardianResults[] = '✗ Supervisor 配置写入失败(需 root 权限)'; - } - - // 3. Update health_monitor.sh INSTALL_DIR - $healthSh = $installDir . '/deploy/health_monitor.sh'; - if (file_exists($healthSh)) { - $shContent = file_get_contents($healthSh); - $shContent = preg_replace( - '/INSTALL_DIR="[^"]*"/', - 'INSTALL_DIR="' . $installDir . '"', - $shContent - ); - if ($shContent !== null) { - @file_put_contents($healthSh, $shContent); - @chmod($healthSh, 0755); - $guardianResults[] = '✓ health_monitor.sh INSTALL_DIR 已更新'; - } - } else { - $guardianResults[] = '⚠ health_monitor.sh 未找到,跳过'; - } - - // 4. Set up crontab for health_monitor.sh - $cronEntry = "* * * * * {$installDir}/deploy/health_monitor.sh"; - $currentCron = shell_exec('crontab -l 2>/dev/null') ?? ''; - if ($currentCron === null || strpos($currentCron, 'health_monitor.sh') === false) { - $newCron = rtrim($currentCron ?? '') . "\n" . $cronEntry . "\n"; - $tempFile = tempnam(sys_get_temp_dir(), 'cron_'); - if ($tempFile) { - file_put_contents($tempFile, $newCron); - $cronResult = shell_exec("crontab {$tempFile} 2>&1"); - @unlink($tempFile); - if ($cronResult === null || strpos($cronResult, 'error') === false) { - $guardianResults[] = '✓ Crontab 已配置(每分钟健康检查)'; - } else { - $guardianResults[] = '✗ Crontab 配置失败 — 请手动添加'; - } - } - } else { - $guardianResults[] = '✓ Crontab 已存在 health_monitor 条目'; - } - - // 5. Reload Supervisor - $supervisorReread = shell_exec('supervisorctl reread 2>&1'); - $supervisorUpdate = shell_exec('supervisorctl update 2>&1'); - if ($supervisorReread !== null || $supervisorUpdate !== null) { - $guardianResults[] = '✓ Supervisor 已重载配置'; - } else { - $guardianResults[] = '⚠ Supervisor 未运行 — 请手动执行 supervisorctl reread && supervisorctl update'; - } - - $_SESSION['guardian_results'] = $guardianResults; - - $success = "数据库初始化成功!已创建 {$dbName} 数据库和所有表。"; - - // Don't auto-redirect — show success message, user clicks next - $step = 3; // Stay on step 3 to show success - - } catch (PDOException $e) { - $error = "数据库创建失败: " . $e->getMessage(); - $step = 3; - } - } - - // ── Step 4: Admin Account + Brand ── - if ($action === 'create_admin') { - $dbHost = $_SESSION['db_host'] ?? '127.0.0.1'; - $dbPort = $_SESSION['db_port'] ?? '3306'; - $dbName = $_SESSION['db_name'] ?? 'Nexus'; - $dbUser = $_SESSION['db_user'] ?? 'Nexus'; - $dbPass = $_SESSION['db_pass'] ?? ''; - - $adminUser = $_POST['admin_username'] ?? 'admin'; - $adminPass = $_POST['admin_password'] ?? ''; - $adminEmail = $_POST['admin_email'] ?? ''; - $systemName = $_POST['system_name'] ?? 'Nexus'; - $systemTitle = $_POST['system_title'] ?? 'Nexus — 服务器运维管理平台'; - - if (empty($adminPass) || strlen($adminPass) < 6) { - $error = "密码长度至少6位"; - $step = 4; - } else { - try { - $pdo = new PDO( - "mysql:host=$dbHost;port=$dbPort;dbname=$dbName;charset=utf8mb4", - $dbUser, $dbPass, - [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] - ); - - // Insert admin - $hash = password_hash($adminPass, PASSWORD_DEFAULT); - $stmt = $pdo->prepare("INSERT INTO admins (username, password_hash, email) VALUES (?, ?, ?) - ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash), email = VALUES(email)"); - $stmt->execute([$adminUser, $hash, $adminEmail]); - - // Update brand in settings table - $stmtBrand = $pdo->prepare("INSERT INTO `settings` (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)"); - $stmtBrand->execute(['system_name', $systemName]); - $stmtBrand->execute(['system_title', $systemTitle]); - - // Also update config.php with brand - $configPath = __DIR__ . '/data/config.php'; - if (file_exists($configPath)) { - $configContent = file_get_contents($configPath); - $configContent = preg_replace("/define\('APP_NAME', '.*?'\);/", "define('APP_NAME', '" . addslashes($systemName) . "');", $configContent); - file_put_contents($configPath, $configContent); - } - - // Update .env with brand - if (file_exists($envFile)) { - $envContent = file_get_contents($envFile); - $envContent = preg_replace("/NEXUS_SYSTEM_NAME=.*/", "NEXUS_SYSTEM_NAME=" . addslashes($systemName), $envContent); - $envContent = preg_replace("/NEXUS_SYSTEM_TITLE=.*/", "NEXUS_SYSTEM_TITLE=" . addslashes($systemTitle), $envContent); - file_put_contents($envFile, $envContent); - } - - header('Location: install.php?step=5'); - exit; - - } catch (PDOException $e) { - $error = "创建管理员失败: " . $e->getMessage(); - $step = 4; - } - } - } -} - -// ============================================================ -// Environment Detection -// ============================================================ -function checkEnvironment() { - $checks = []; - - $checks[] = [ - 'name' => 'PHP 版本', - 'required' => '8.0+', - 'current' => PHP_VERSION, - 'pass' => version_compare(PHP_VERSION, '8.0.0', '>='), - ]; - - $checks[] = [ - 'name' => 'PDO MySQL 扩展', - 'required' => '已启用', - 'current' => extension_loaded('pdo_mysql') ? '✓ 已启用' : '✗ 未启用', - 'pass' => extension_loaded('pdo_mysql'), - ]; - - $checks[] = [ - 'name' => 'cURL 扩展', - 'required' => '已启用', - 'current' => extension_loaded('curl') ? '✓ 已启用' : '✗ 未启用', - 'pass' => extension_loaded('curl'), - ]; - - $checks[] = [ - 'name' => 'JSON 扩展', - 'required' => '已启用', - 'current' => extension_loaded('json') ? '✓ 已启用' : '✗ 未启用', - 'pass' => extension_loaded('json'), - ]; - - // Redis - $redisOk = false; - $redisMsg = '未安装'; - if (extension_loaded('redis')) { - try { - $r = new Redis(); - if ($r->connect('127.0.0.1', 6379, 0.5)) { - $r->ping(); - $redisOk = true; - $redisMsg = '✓ 已连接 127.0.0.1:6379'; - } else { - $redisMsg = '无法连接 127.0.0.1:6379'; - } - } catch (Exception $e) { - $redisMsg = '连接失败: ' . $e->getMessage(); - } - } else { - $redisMsg = 'PHP Redis 扩展未安装'; - } - $checks[] = [ - 'name' => 'Redis', - 'required' => '已连接', - 'current' => $redisMsg, - 'pass' => $redisOk, - ]; - - // MySQL — 不检测 root,用户在步骤3自行填写数据库凭据 - $checks[] = [ - 'name' => 'MySQL 数据库', - 'required' => '步骤3配置', - 'current' => '— 请在步骤3填写数据库连接信息', - 'pass' => true, - ]; - - // Write permissions - $dataDir = __DIR__ . '/data'; - $writable = is_dir($dataDir) ? is_writable($dataDir) : is_writable(__DIR__); - $rootWritable = is_writable(dirname(__DIR__)); - $checks[] = [ - 'name' => 'web/data 写入权限', - 'required' => '可写', - 'current' => $writable ? '✓ 可写' : '✗ 不可写', - 'pass' => $writable, - ]; - $checks[] = [ - 'name' => '根目录写入权限 (.env)', - 'required' => '可写', - 'current' => $rootWritable ? '✓ 可写' : '✗ 不可写', - 'pass' => $rootWritable, - ]; - - return $checks; -} - -$envChecks = ($step == 2) ? checkEnvironment() : []; -$allPass = empty(array_filter($envChecks, fn($c) => !$c['pass'])); -?> - - - - - - Nexus 6.0 安装向导 - - - -
-
-

Nexus 6.0 安装向导

-

服务器运维管理平台 — 心跳监控 + 智能告警 + 3层守护

-
- -
-
1
-
-
2
-
-
3
-
-
4
-
-
5
-
- -
- - -
- - - -
- - - - -
-

欢迎使用 Nexus 6.0

-

- 本向导将引导您完成系统安装配置。
- 安装完成后将自动生成:config.php (PHP前端) + .env (Python后端) + MySQL settings表 (共享配置)
- 整个过程大约 3 分钟。 -

-
-

📋 安装前准备

-
    -
  • PHP 8.0+ (pdo_mysql, cURL, Redis 扩展)
  • -
  • MySQL 8.0+ (需提前创建数据库和用户)
  • -
  • Redis 6+ (心跳缓冲和实时数据)
  • -
  • Python 3.12+ (后端 FastAPI + uvicorn)
  • -
  • Web 目录和根目录可写权限
  • -
-
- 开始安装 → -
- - - -

🔍 环境检测

-
- -
-
- - 需要: -
- - - -
- -
- - -
✓ 所有环境检测通过,可以继续安装。
- - -
部分环境检测未通过,请先解决上述问题。
- - - - - -

⚙ 数据库 + Redis + API 配置

- -
- 说明:请提前创建好数据库和用户,安装向导将自动建表并写入配置。 - SECRET_KEY 和 API_KEY 将自动生成,安装后不可修改(加密一致性)。 -
- -
- - - -
-
🗄 MySQL 数据库
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - 写入 .env + config.php - -
-
- - -
-
⚡ Redis 配置
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
🌐 网站地址 + API 服务 + 时区
-
- - 自动检测 — 用于 Agent 上报和前端 API 调用,可手动修改 - - -
-
-
- - uvicorn 监听端口 - -
-
- - -
-
-
- -
- ⚠ 注意:初始化将自动:① 连接数据库并建表 ② 生成 SECRET_KEY/API_KEY - ③ 写入 config.php + .env + settings表 ④ 自动计算连接池大小 -
- -
- - ← 上一步 -
-
- - - -

👤 管理员账号 + 系统名称

- - -
- ✓ 步骤 3 配置完成 -
- 数据库: - 用户: - 连接池: - 溢出池: -
-
- -
- - -
-
系统品牌
-
- - 显示在浏览器标题和后台标题栏 - -
-
- - -
-
- -
-
管理员账号
-
- - -
-
- - 至少6位字符 - -
-
- - -
-
- -
- - ← 上一步 -
-
- - - -
-
-

安装完成!

-

Nexus 6.0 已成功安装。

-

install.php 已重命名为 install.php.locked — 防止重复安装

- - - -
-

━━━ 后续配置清单(必须完成)━━━

- - - -
-
- -
-
- -
- -
-
- - - -
-
1 Supervisor 进程守护
- -
✓ 已自动配置 — Supervisor 配置已写入,服务已重载
- -
- 安装: sudo apt install -y supervisor
- 配置文件复制到: /etc/supervisor/conf.d/nexus.conf -
-
- -
[program:nexus]
-command=/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port 
-
-directory=
-
-user=root
-autostart=true
-autorestart=true
-startretries=10
-startsecs=3
-stopwaitsecs=10
-stopsignal=INT
-environment=PATH="/venv/bin:%(ENV_PATH)s"
-stderr_logfile=/var/log/nexus/error.log
-stdout_logfile=/var/log/nexus/access.log
-stderr_logfile_maxbytes=50MB
-stdout_logfile_maxbytes=50MB
-
-
启动: sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start nexus
- -
- - -
-
2 Python 虚拟环境 + 依赖
-
- cd
- python3.12 -m venv venv
- source venv/bin/activate
- pip install -r requirements.txt -
-
- - -
-
3 Nginx 反向代理 (API + WebSocket + PHP)
-
宝塔 → 网站 → 配置文件 → #PHP-INFO-END 后面粘贴:
-
- -
    # Nexus Python API + WebSocket
-    location ^~ /api/ {
-        proxy_pass http://127.0.0.1:;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto $scheme;
-    }
-    location ^~ /health {
-        proxy_pass http://127.0.0.1:;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-    }
-    location ^~ /ws/ {
-        proxy_pass http://127.0.0.1:;
-        proxy_http_version 1.1;
-        proxy_set_header Upgrade $http_upgrade;
-        proxy_set_header Connection "upgrade";
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-    }
-
-
- - -
-
4 Nginx 伪静态
-
宝塔 → 网站 → 伪静态 → 粘贴:
-
- -
location /app/ {
-    try_files $uri $uri/ /app/index.html;
-}
-location / {
-    try_files $uri $uri/ =404;
-}
-
-
- - -
-
5 SSL证书 (强制HTTPS)
-
宝塔 → 网站 → SSL → Let's Encrypt → 一键申请 → 开启强制HTTPS
-
- - -
-
6 Shell 健康检查 (外部守护 + Telegram告警)
- -
- ✓ 已自动配置 — health_monitor.sh INSTALL_DIR 已更新,Crontab 已添加 -
- -
- 复制 health_monitor.sh 到: /deploy/
- 配置 crontab: -
-
- -
* * * * * /deploy/health_monitor.sh
-
- -
- 3层守护机制:
- Layer 1: Supervisor — Python崩溃自动重启
- Layer 2: Python self_monitor — 每30s检查Redis/MySQL/WebSocket
- Layer 3: Shell脚本 — 每分钟HTTP检查,连续3次失败→Supervisor重启+Telegram告警 -
-
- - -
-
7 安全收尾
-
- ✓ install.php 已自动重命名为 install.php.locked
- ✓ .env 包含 SECRET_KEY/API_KEY — 安装后不可修改(加密一致性)
- ✓ 防火墙限制端口 (仅允许本机和子服务器IP)
- ✓ 修改管理员默认密码 -
-
-
- - -
- - - - -
-
- - \ No newline at end of file