⚠ 安装已锁定

系统已完成安装,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)
✓ 修改管理员默认密码