Files
Nexus/web/install.php
T

1260 lines
65 KiB
PHP
Raw Normal View History

<?php
/**
* Nexus 6.0 — 5-Step Installation Wizard
*
* Step 1: Welcome + lock check
* Step 2: Environment detection (PHP 8+, PDO MySQL, cURL, Redis, write permissions)
* Step 3: DB + Redis + API config (connect to existing database + create tables + generate keys + write config.php/.env/settings)
* Step 4: Admin account + brand name
* Step 5: Installation complete → show post-install checklist + rename install.php → install.php.locked
*/
session_start();
error_reporting(E_ALL);
ini_set('display_errors', 0);
// ── Install lock: prevent re-installation ──
$lockFile = __DIR__ . '/install.php.locked';
$envFile = dirname(__DIR__) . '/.env';
if (file_exists($lockFile)) {
die('<div style="max-width:500px;margin:80px auto;font-family:sans-serif;text-align:center;">
<h2 style="color:#dc2626;">⚠ 安装已锁定</h2>
<p>系统已完成安装,install.php 已重命名为 install.php.locked。</p>
<p>如需重新安装,请删除 <code>install.php.locked</code> 文件。</p>
<a href="/app/login.html" style="color:#3b82f6;">前往登录 →</a>
</div>');
}
if (file_exists($envFile)) {
die('<div style="max-width:500px;margin:80px auto;font-family:sans-serif;text-align:center;">
<h2 style="color:#f59e0b;">⚠ .env 已存在</h2>
<p>检测到 .env 配置文件,系统可能已安装。</p>
<p>如需重新安装,请先删除 <code>.env</code> 和 <code>web/data/config.php</code>。</p>
<a href="/app/login.html" style="color:#3b82f6;">前往登录 →</a>
</div>');
}
$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 = "<?php\n";
$configContent .= "// Nexus Configuration — Auto-generated by installer\n";
$configContent .= "// " . date('Y-m-d H:i:s') . "\n\n";
$configContent .= "define('API_BASE_URL', '$apiBaseUrl');\n";
$configContent .= "define('API_KEY', '$apiKey');\n";
$configContent .= "define('DB_HOST', '" . addslashes($dbHost) . "');\n";
$configContent .= "define('DB_PORT', '" . addslashes($dbPort) . "');\n";
$configContent .= "define('DB_NAME', '" . addslashes($dbName) . "');\n";
$configContent .= "define('DB_USER', '" . addslashes($dbUser) . "');\n";
$configContent .= "define('DB_PASS', '" . addslashes($dbPass) . "');\n";
$configContent .= "define('APP_NAME', 'Nexus');\n";
$configContent .= "define('APP_VERSION', '6.0.0');\n";
$configContent .= "define('SESSION_LIFETIME', 28800);\n";
2026-05-22 08:19:56 +08:00
$configContent .= "define('DEPLOY_ROOT', '" . addslashes(dirname(__DIR__)) . "');\n";
$configContent .= "\ndate_default_timezone_set('" . addslashes($timezone) . "');\n";
file_put_contents($configPath, $configContent);
// ── Write .env (Python backend bootstrap) ──
$envContent = "# Nexus .env — Auto-generated by installer\n";
$envContent .= "# " . date('Y-m-d H:i:s') . "\n\n";
$envContent .= "NEXUS_SYSTEM_NAME=Nexus\n";
$envContent .= "NEXUS_SYSTEM_TITLE=Nexus — 服务器运维管理平台\n\n";
$envContent .= "NEXUS_HOST=0.0.0.0\n";
$envContent .= "NEXUS_PORT=$apiPort\n\n";
$envContent .= "# Database (MySQL 8.4 + aiomysql async driver)\n";
$envContent .= "NEXUS_DATABASE_URL=mysql+aiomysql://" . addslashes($dbUser) . ":" . addslashes($dbPass) . "@" . addslashes($dbHost) . ":" . addslashes($dbPort) . "/" . addslashes($dbName) . "\n";
$envContent .= "NEXUS_DB_POOL_SIZE=$poolSize\n";
$envContent .= "NEXUS_DB_MAX_OVERFLOW=$maxOverflow\n\n";
$envContent .= "# Security — PRODUCTION KEYS (immutable after install)\n";
$envContent .= "NEXUS_SECRET_KEY=$secretKey\n";
$envContent .= "NEXUS_API_KEY=$apiKey\n";
$envContent .= "NEXUS_ENCRYPTION_KEY=\n\n";
$envContent .= "# Redis\n";
$envContent .= "NEXUS_REDIS_URL=$redisUrl\n\n";
2026-05-22 08:19:56 +08:00
$envContent .= "# Deployment\n";
$envContent .= "NEXUS_DEPLOY_PATH=" . addslashes(dirname(__DIR__)) . "\n";
$envContent .= "NEXUS_CORS_ORIGINS=$siteUrl,http://localhost:8600\n\n";
$envContent .= "# SSH\n";
$envContent .= "NEXUS_SSH_STRICT_HOST_CHECKING=false\n\n";
$envContent .= "# Health Check\n";
$envContent .= "NEXUS_HEALTH_CHECK_INTERVAL=60\n";
$envContent .= "NEXUS_API_BASE_URL=$siteUrl\n\n";
$envContent .= "# Alert Thresholds\n";
$envContent .= "NEXUS_CPU_ALERT_THRESHOLD=80\n";
$envContent .= "NEXUS_MEM_ALERT_THRESHOLD=80\n";
$envContent .= "NEXUS_DISK_ALERT_THRESHOLD=80\n\n";
$envContent .= "# Telegram Alerts (optional — configure in Settings UI)\n";
$envContent .= "NEXUS_TELEGRAM_BOT_TOKEN=\n";
$envContent .= "NEXUS_TELEGRAM_CHAT_ID=\n";
file_put_contents($envFile, $envContent);
chmod($envFile, 0644);
// ── Write MySQL settings table (shared config for PHP + Python) ──
$settingsKV = [
'api_key' => $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']));
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus 6.0 安装向导</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f172a;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.installer {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 100%;
max-width: 760px;
overflow: hidden;
}
.installer-header {
background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%);
color: white;
padding: 28px 32px;
text-align: center;
}
.installer-header h1 { font-size: 26px; margin-bottom: 8px; font-weight: 700; }
.installer-header p { color: #c4b5fd; font-size: 14px; }
/* Steps indicator */
.steps { display: flex; justify-content: center; padding: 20px 32px; gap: 4px; }
.step-dot {
width: 34px; height: 34px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 600;
border: 2px solid #e2e8f0; color: #94a3b8; transition: all 0.3s;
}
.step-dot.active { background: #3b82f6; border-color: #3b82f6; color: white; }
.step-dot.done { background: #22c55e; border-color: #22c55e; color: white; }
.step-line { width: 32px; height: 2px; background: #e2e8f0; align-self: center; }
.step-line.done { background: #22c55e; }
.installer-body { padding: 32px; }
/* Form */
.form-group { margin-bottom: 18px; }
.form-group label { display: block; font-weight: 600; margin-bottom: 4px; font-size: 14px; color: #1e293b; }
.form-group small { display: block; color: #64748b; font-size: 12px; margin-bottom: 4px; }
.form-control {
width: 100%; padding: 10px 14px; border: 2px solid #e2e8f0;
border-radius: 8px; font-size: 14px; transition: border-color 0.2s;
}
.form-control:focus { outline: none; border-color: #3b82f6; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-section { border-top: 2px solid #e2e8f0; padding-top: 16px; margin-top: 20px; }
.form-section-title { font-size: 15px; font-weight: 700; color: #1e293b; margin-bottom: 12px; }
/* Buttons */
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 24px; border: none; border-radius: 8px;
font-size: 15px; font-weight: 600; cursor: pointer;
transition: all 0.2s; text-decoration: none;
}
.btn:hover { transform: translateY(-1px); }
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.btn-success { background: #22c55e; color: white; }
.btn-success:hover { background: #16a34a; }
.btn-secondary { background: #94a3b8; color: white; }
.btn-secondary:hover { background: #64748b; }
.btn-group { display: flex; gap: 12px; margin-top: 24px; }
/* Checks */
.check-item {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px; border-bottom: 1px solid #f1f5f9;
}
.check-name { font-weight: 500; }
.check-status { font-weight: 600; font-size: 13px; }
.check-status.pass { color: #22c55e; }
.check-status.fail { color: #ef4444; }
/* Alerts */
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
.alert-success { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
.alert-info { background: #dbeafe; color: #1e40af; border: 1px solid #bfdbfe; }
.alert-warn { background: #fef3c7; color: #92400e; border: 1px solid #fde68a; }
/* Success page */
.success-page { text-align: center; }
.success-icon {
width: 72px; height: 72px; border-radius: 50%;
background: #dcfce7; color: #22c55e; font-size: 36px;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 16px;
}
/* Post-install checklist */
.post-checklist { margin-top: 24px; text-align: left; }
.post-item {
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;
padding: 16px; margin-bottom: 12px;
}
.post-item-num {
display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px; border-radius: 50%;
background: #3b82f6; color: white; font-size: 12px; font-weight: 700;
margin-right: 8px;
}
.post-item-title { font-weight: 700; font-size: 14px; color: #1e293b; }
.post-item-desc { font-size: 13px; color: #475569; margin-top: 6px; }
.post-item-desc code { background: #1e293b; color: #e2e8f0; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
/* Code copy block */
.code-wrap { position: relative; background: #1e293b; border-radius: 6px; margin: 8px 0; }
.code-wrap pre { color: #e2e8f0; padding: 12px; margin: 0; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
.copy-btn {
position: absolute; top: 6px; right: 6px;
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
color: #94a3b8; padding: 4px 10px; border-radius: 4px;
cursor: pointer; font-size: 12px; z-index: 1;
}
.copy-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
.copy-btn.copied { background: #22c55e; color: #fff; border-color: #22c55e; }
</style>
</head>
<body>
<div class="installer">
<div class="installer-header">
<h1>Nexus 6.0 安装向导</h1>
<p>服务器运维管理平台 — 心跳监控 + 智能告警 + 3层守护</p>
</div>
<div class="steps">
<div class="step-dot <?= $step >= 1 ? ($step > 1 ? 'done' : 'active') : '' ?>">1</div>
<div class="step-line <?= $step > 1 ? 'done' : '' ?>"></div>
<div class="step-dot <?= $step >= 2 ? ($step > 2 ? 'done' : 'active') : '' ?>">2</div>
<div class="step-line <?= $step > 2 ? 'done' : '' ?>"></div>
<div class="step-dot <?= $step >= 3 ? ($step > 3 ? 'done' : 'active') : '' ?>">3</div>
<div class="step-line <?= $step > 3 ? 'done' : '' ?>"></div>
<div class="step-dot <?= $step >= 4 ? ($step > 4 ? 'done' : 'active') : '' ?>">4</div>
<div class="step-line <?= $step > 4 ? 'done' : '' ?>"></div>
<div class="step-dot <?= $step >= 5 ? 'active' : '' ?>">5</div>
</div>
<div class="installer-body">
<?php if ($error): ?>
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<!-- ========== Step 1: Welcome ========== -->
<?php if ($step == 1): ?>
<div style="text-align:center; padding: 20px 0;">
<h2 style="margin-bottom:16px; font-size:22px;">欢迎使用 Nexus 6.0</h2>
<p style="color:#64748b; margin-bottom:24px; line-height:1.6;">
本向导将引导您完成系统安装配置。<br>
安装完成后将自动生成:<code>config.php</code> (PHP前端) + <code>.env</code> (Python后端) + <code>MySQL settings表</code> (共享配置)<br>
整个过程大约 3 分钟。
</p>
<div style="background:#f8fafc; border-radius:8px; padding:20px; text-align:left; margin-bottom:24px;">
<h3 style="margin-bottom:12px; font-size:15px;">📋 安装前准备</h3>
<ul style="color:#475569; font-size:14px; margin-left:20px; line-height:1.8;">
<li>PHP 8.0+ (pdo_mysql, cURL, Redis 扩展)</li>
<li>MySQL 8.0+ (需提前创建数据库和用户)</li>
<li>Redis 6+ (心跳缓冲和实时数据)</li>
<li>Python 3.12+ (后端 FastAPI + uvicorn)</li>
<li>Web 目录和根目录可写权限</li>
</ul>
</div>
<a href="install.php?step=2" class="btn btn-primary">开始安装 →</a>
</div>
<!-- ========== Step 2: Environment Check ========== -->
<?php elseif ($step == 2): ?>
<h2 style="margin-bottom:20px;">🔍 环境检测</h2>
<div style="background:#f8fafc; border-radius:8px; margin-bottom:20px;">
<?php foreach ($envChecks as $check): ?>
<div class="check-item">
<div>
<span class="check-name"><?= $check['name'] ?></span>
<small style="color:#94a3b8; margin-left:8px;">需要: <?= $check['required'] ?></small>
</div>
<span class="check-status <?= $check['pass'] ? 'pass' : 'fail' ?>">
<?= $check['pass'] ? '✓ 通过' : '✗ ' . $check['current'] ?>
</span>
</div>
<?php endforeach; ?>
</div>
<?php if ($allPass): ?>
<div class="alert alert-success">✓ 所有环境检测通过,可以继续安装。</div>
<div class="btn-group">
<a href="install.php?step=3" class="btn btn-primary">下一步:数据库配置 →</a>
<a href="install.php?step=2" class="btn btn-secondary">重新检测</a>
</div>
<?php else: ?>
<div class="alert alert-error">部分环境检测未通过,请先解决上述问题。</div>
<div class="btn-group">
<a href="install.php?step=2" class="btn btn-secondary">重新检测</a>
<a href="install.php?step=3" class="btn btn-primary">跳过,继续 →</a>
</div>
<?php endif; ?>
<!-- ========== Step 3: DB + Redis + API Config ========== -->
<?php elseif ($step == 3): ?>
<h2 style="margin-bottom:20px;">⚙ 数据库 + Redis + API 配置</h2>
<div class="alert alert-info">
<b>说明:</b>请提前创建好数据库和用户,安装向导将自动建表并写入配置。
SECRET_KEY 和 API_KEY 将自动生成,安装后不可修改(加密一致性)。
</div>
<form method="POST">
<input type="hidden" name="action" value="init_db">
<!-- MySQL section -->
<div class="form-section">
<div class="form-section-title">🗄 MySQL 数据库</div>
<div class="form-row">
<div class="form-group">
<label>主机</label>
<input type="text" name="db_host" class="form-control" value="localhost">
</div>
<div class="form-group">
<label>端口</label>
<input type="text" name="db_port" class="form-control" value="3306">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>数据库名</label>
<input type="text" name="db_name" class="form-control" value="Nexus">
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" name="db_user" class="form-control" value="Nexus">
</div>
</div>
<div class="form-group">
<label>密码</label>
<small>写入 .env + config.php</small>
<input type="password" name="db_pass" class="form-control" placeholder="数据库密码" required>
</div>
</div>
<!-- Redis section -->
<div class="form-section">
<div class="form-section-title">⚡ Redis 配置</div>
<div class="form-row">
<div class="form-group">
<label>Redis 主机</label>
<input type="text" name="redis_host" class="form-control" value="127.0.0.1">
</div>
<div class="form-group">
<label>Redis 端口</label>
<input type="text" name="redis_port" class="form-control" value="6379">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Redis 数据库号</label>
<input type="text" name="redis_db" class="form-control" value="0">
</div>
<div class="form-group">
<label>Redis 密码(可选)</label>
<input type="password" name="redis_pass" class="form-control" placeholder="无密码留空">
</div>
</div>
</div>
<!-- API + Site URL + Timezone -->
<div class="form-section">
<div class="form-section-title">🌐 网站地址 + API 服务 + 时区</div>
<div class="form-group">
<label>网站地址</label>
<small>自动检测 — 用于 Agent 上报和前端 API 调用,可手动修改</small>
<?php
$detectScheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$detectHost = $_SERVER['SERVER_ADDR'] ?? $_SERVER['HTTP_HOST'] ?? '127.0.0.1';
$autoSiteUrl = $detectScheme . '://' . $detectHost;
?>
<input type="text" name="site_url" class="form-control" value="<?= htmlspecialchars($autoSiteUrl) ?>">
</div>
<div class="form-row">
<div class="form-group">
<label>Python API 端口</label>
<small>uvicorn 监听端口</small>
<input type="text" name="api_port" class="form-control" value="8600">
</div>
<div class="form-group">
<label>时区</label>
<select name="timezone" class="form-control">
<option value="Asia/Shanghai" selected>Asia/Shanghai (中国标准时间)</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
<option value="America/New_York">America/New_York</option>
<option value="Europe/London">Europe/London</option>
<option value="UTC">UTC</option>
</select>
</div>
</div>
</div>
<div class="alert alert-warn" style="margin-top:16px;">
<b>⚠ 注意:</b>初始化将自动:① 连接数据库并建表 ② 生成 SECRET_KEY/API_KEY
③ 写入 config.php + .env + settings表 ④ 自动计算连接池大小
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary">初始化数据库 →</button>
<a href="install.php?step=2" class="btn btn-secondary">← 上一步</a>
</div>
</form>
<!-- ========== Step 4: Admin + Brand ========== -->
<?php elseif ($step == 4): ?>
<h2 style="margin-bottom:20px;">👤 管理员账号 + 系统名称</h2>
<!-- Step 3 config summary -->
<div style="background:#f8fafc;padding:16px;border-radius:8px;margin-bottom:20px;font-size:13px;">
<b style="color:#22c55e;">✓ 步骤 3 配置完成</b>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px 16px;margin-top:8px;">
<span>数据库: <code><?= htmlspecialchars($_SESSION['db_name'] ?? '-') ?></code></span>
<span>用户: <code><?= htmlspecialchars($_SESSION['db_user'] ?? '-') ?></code></span>
<span>连接池: <code><?= htmlspecialchars($_SESSION['pool_size'] ?? '-') ?></code></span>
<span>溢出池: <code><?= htmlspecialchars($_SESSION['max_overflow'] ?? '-') ?></code></span>
</div>
</div>
<form method="POST">
<input type="hidden" name="action" value="create_admin">
<div class="form-section">
<div class="form-section-title">系统品牌</div>
<div class="form-group">
<label>系统名称</label>
<small>显示在浏览器标题和后台标题栏</small>
<input type="text" name="system_name" class="form-control" value="Nexus">
</div>
<div class="form-group">
<label>系统标题</label>
<input type="text" name="system_title" class="form-control" value="Nexus — 服务器运维管理平台">
</div>
</div>
<div class="form-section">
<div class="form-section-title">管理员账号</div>
<div class="form-group">
<label>管理员用户名</label>
<input type="text" name="admin_username" class="form-control" value="admin" required>
</div>
<div class="form-group">
<label>管理员密码</label>
<small>至少6位字符</small>
<input type="password" name="admin_password" class="form-control" required minlength="6" placeholder="请输入安全密码">
</div>
<div class="form-group">
<label>邮箱(可选)</label>
<input type="email" name="admin_email" class="form-control" placeholder="admin@example.com">
</div>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary">创建账号 →</button>
<a href="install.php?step=3" class="btn btn-secondary">← 上一步</a>
</div>
</form>
<!-- ========== Step 5: Installation Complete ========== -->
<?php elseif ($step == 5):
// Rename install.php → install.php.locked
@rename(__FILE__, $lockFile);
?>
<div class="success-page">
<div class="success-icon">✓</div>
<h2 style="font-size:22px;">安装完成!</h2>
<p style="color:#64748b;">Nexus 6.0 已成功安装。</p>
<p style="color:#94a3b8; font-size:13px;">install.php 已重命名为 install.php.locked — 防止重复安装</p>
<?php
$installDir = $_SESSION['install_dir'] ?? dirname(__DIR__);
$siteUrl = $_SESSION['site_url'] ?? 'http://127.0.0.1';
$apiPort = $_SESSION['api_port'] ?? '8600';
$guardianResults = $_SESSION['guardian_results'] ?? [];
$guardianAllOk = true;
foreach ($guardianResults as $r) {
if (strpos($r, '✗') !== false) { $guardianAllOk = false; break; }
if (strpos($r, '⚠') !== false) { $guardianAllOk = false; break; }
}
?>
<div class="post-checklist">
<h3 style="font-size:16px; margin-bottom:12px;">━━━ 后续配置清单(必须完成)━━━</h3>
<?php if (!empty($guardianResults)): ?>
<!-- 守护进程自动配置结果 -->
<div class="post-item" style="background: <?= $guardianAllOk ? '#f0fdf4' : '#fffbeb' ?>; border-left: 3px solid <?= $guardianAllOk ? '#22c55e' : '#f59e0b' ?>; padding: 12px 16px; border-radius: 8px; margin-bottom: 12px;">
<div class="post-item-title" style="color: <?= $guardianAllOk ? '#16a34a' : '#d97706' ?>;">
<?= $guardianAllOk ? '✓ 进程守护已自动配置' : '⚠ 进程守护部分配置失败' ?>
</div>
<div class="post-item-desc" style="font-size: 13px; line-height: 1.8;">
<?php foreach ($guardianResults as $result): ?>
<?= htmlspecialchars($result) ?><br>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- ① Supervisor -->
<div class="post-item">
<div class="post-item-title"><span class="post-item-num">1</span> Supervisor 进程守护</div>
<?php if (!empty($guardianResults) && $guardianAllOk): ?>
<div class="post-item-desc" style="color: #16a34a;">✓ 已自动配置 — Supervisor 配置已写入,服务已重载</div>
<?php else: ?>
<div class="post-item-desc">
安装: <code>sudo apt install -y supervisor</code><br>
配置文件复制到: <code>/etc/supervisor/conf.d/nexus.conf</code>
</div>
<div class="code-wrap">
<button class="copy-btn" onclick="copyBlock('supervisorConf')">复制配置</button>
<pre id="supervisorConf">[program:nexus]
command=<?= htmlspecialchars($installDir) ?>/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port <?= htmlspecialchars($apiPort) ?>
directory=<?= htmlspecialchars($installDir) ?>
user=root
autostart=true
autorestart=true
startretries=10
startsecs=3
stopwaitsecs=10
stopsignal=INT
environment=PATH="<?= htmlspecialchars($installDir) ?>/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</pre>
</div>
<div class="post-item-desc">启动: <code>sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start nexus</code></div>
<?php endif; ?>
</div>
<!-- ② Python venv + deps -->
<div class="post-item">
<div class="post-item-title"><span class="post-item-num">2</span> Python 虚拟环境 + 依赖</div>
<div class="post-item-desc">
<code>cd <?= htmlspecialchars($installDir) ?></code><br>
<code>python3.12 -m venv venv</code><br>
<code>source venv/bin/activate</code><br>
<code>pip install -r requirements.txt</code>
</div>
</div>
<!-- ③ Nginx反向代理 -->
<div class="post-item">
<div class="post-item-title"><span class="post-item-num">3</span> Nginx 反向代理 (API + WebSocket + PHP)</div>
<div class="post-item-desc">宝塔 → 网站 → 配置文件 → <code>#PHP-INFO-END</code> 后面粘贴:</div>
<div class="code-wrap">
<button class="copy-btn" onclick="copyBlock('nginxConf')">复制配置</button>
<pre id="nginxConf"> # Nexus Python API + WebSocket
location ^~ /api/ {
proxy_pass http://127.0.0.1:<?= htmlspecialchars($apiPort) ?>;
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:<?= htmlspecialchars($apiPort) ?>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location ^~ /ws/ {
proxy_pass http://127.0.0.1:<?= htmlspecialchars($apiPort) ?>;
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;
}</pre>
</div>
</div>
<!-- ④ Nginx伪静态 -->
<div class="post-item">
<div class="post-item-title"><span class="post-item-num">4</span> Nginx 伪静态</div>
<div class="post-item-desc">宝塔 → 网站 → 伪静态 → 粘贴:</div>
<div class="code-wrap">
<button class="copy-btn" onclick="copyBlock('nginxRewrite')">复制</button>
<pre id="nginxRewrite">location /app/ {
try_files $uri $uri/ /app/index.html;
}
location / {
try_files $uri $uri/ =404;
}</pre>
</div>
</div>
<!-- ⑤ SSL -->
<div class="post-item">
<div class="post-item-title"><span class="post-item-num">5</span> SSL证书 (强制HTTPS)</div>
<div class="post-item-desc">宝塔 → 网站 → SSL → Let's Encrypt → 一键申请 → 开启强制HTTPS</div>
</div>
<!-- ⑥ Shell健康检查 -->
<div class="post-item">
<div class="post-item-title"><span class="post-item-num">6</span> Shell 健康检查 (外部守护 + Telegram告警)</div>
<?php if (!empty($guardianResults) && $guardianAllOk): ?>
<div class="post-item-desc" style="color: #16a34a;">
✓ 已自动配置 — health_monitor.sh INSTALL_DIR 已更新,Crontab 已添加
</div>
<?php else: ?>
<div class="post-item-desc">
复制 health_monitor.sh 到: <code><?= htmlspecialchars($installDir) ?>/deploy/</code><br>
配置 crontab:
</div>
<div class="code-wrap">
<button class="copy-btn" onclick="copyBlock('crontab')">复制</button>
<pre id="crontab">* * * * * <?= htmlspecialchars($installDir) ?>/deploy/health_monitor.sh</pre>
</div>
<?php endif; ?>
<div class="post-item-desc" style="margin-top:6px;">
<b>3层守护机制:</b><br>
Layer 1: Supervisor — Python崩溃自动重启<br>
Layer 2: Python self_monitor — 每30s检查Redis/MySQL/WebSocket<br>
Layer 3: Shell脚本 — 每分钟HTTP检查,连续3次失败→Supervisor重启+Telegram告警
</div>
</div>
<!-- ⑦ 安全 -->
<div class="post-item">
<div class="post-item-title"><span class="post-item-num">7</span> 安全收尾</div>
<div class="post-item-desc">
✓ <code>install.php</code> 已自动重命名为 <code>install.php.locked</code><br>
✓ <code>.env</code> 包含 SECRET_KEY/API_KEY — <b>安装后不可修改</b>(加密一致性)<br>
✓ 防火墙限制端口 <?= htmlspecialchars($apiPort) ?> (仅允许本机和子服务器IP)<br>
✓ 修改管理员默认密码
</div>
</div>
</div>
<div class="btn-group" style="justify-content:center; margin-top:24px;">
<a href="/app/index.html" class="btn btn-success" style="font-size:16px; padding:14px 32px;">进入管理面板 →</a>
</div>
</div>
<script>
function copyBlock(id) {
var text = document.getElementById(id).textContent;
var btn = event.target;
navigator.clipboard.writeText(text).then(function() {
btn.textContent = '已复制 ✓';
btn.classList.add('copied');
setTimeout(function() { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000);
});
}
</script>
<?php endif; ?>
</div>
</div>
</body>
</html>