2026-05-20 17:24:21 +08:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Nexus 6.0 — 5-Step Installation Wizard
|
|
|
|
|
*
|
|
|
|
|
* Step 1: Welcome + lock check
|
2026-05-21 05:42:55 +08:00
|
|
|
* 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)
|
2026-05-20 17:24:21 +08:00
|
|
|
* 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>
|
2026-05-22 00:02:46 +08:00
|
|
|
<a href="/app/login.html" style="color:#3b82f6;">前往登录 →</a>
|
2026-05-20 17:24:21 +08:00
|
|
|
</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>
|
2026-05-22 00:02:46 +08:00
|
|
|
<a href="/app/login.html" style="color:#3b82f6;">前往登录 →</a>
|
2026-05-20 17:24:21 +08:00
|
|
|
</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') {
|
2026-05-21 05:42:55 +08:00
|
|
|
$dbHost = $_POST['db_host'] ?? 'localhost';
|
|
|
|
|
$dbPort = $_POST['db_port'] ?? '3306';
|
2026-05-20 17:24:21 +08:00
|
|
|
|
|
|
|
|
$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';
|
|
|
|
|
|
2026-05-21 05:42:55 +08:00
|
|
|
// ── Connect to Nexus database ──
|
2026-05-20 17:24:21 +08:00
|
|
|
try {
|
|
|
|
|
$pdo = new PDO(
|
2026-05-21 05:42:55 +08:00
|
|
|
"mysql:host=$dbHost;port=$dbPort;dbname=$dbName;charset=utf8mb4",
|
|
|
|
|
$dbUser, $dbPass,
|
2026-05-20 17:24:21 +08:00
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-21 23:11:30 +08:00
|
|
|
// ── Create tables in dependency order (referenced tables first) ──
|
2026-05-20 17:24:21 +08:00
|
|
|
$tables = [
|
2026-05-21 23:11:30 +08:00
|
|
|
"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",
|
2026-05-20 17:24:21 +08:00
|
|
|
"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 '推送目标路径',
|
2026-05-21 23:11:30 +08:00
|
|
|
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',
|
2026-05-20 17:24:21 +08:00
|
|
|
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,
|
2026-05-21 23:11:30 +08:00
|
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
|
FOREIGN KEY (platform_id) REFERENCES platforms(id),
|
|
|
|
|
FOREIGN KEY (node_id) REFERENCES nodes(id)
|
2026-05-20 17:24:21 +08:00
|
|
|
) 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",
|
2026-05-21 23:11:30 +08:00
|
|
|
"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",
|
2026-05-20 17:24:21 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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)",
|
2026-05-21 23:11:30 +08:00
|
|
|
"ALTER TABLE servers ADD INDEX idx_servers_platform_id (platform_id)",
|
|
|
|
|
"ALTER TABLE servers ADD INDEX idx_servers_node_id (node_id)",
|
2026-05-20 17:24:21 +08:00
|
|
|
"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)",
|
2026-05-21 23:11:30 +08:00
|
|
|
"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)",
|
2026-05-20 17:24:21 +08:00
|
|
|
];
|
|
|
|
|
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";
|
|
|
|
|
|
2026-05-21 05:42:55 +08:00
|
|
|
// 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;
|
2026-05-20 17:24:21 +08:00
|
|
|
|
|
|
|
|
$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";
|
2026-05-21 05:42:55 +08:00
|
|
|
$configContent .= "define('DB_HOST', '" . addslashes($dbHost) . "');\n";
|
|
|
|
|
$configContent .= "define('DB_PORT', '" . addslashes($dbPort) . "');\n";
|
2026-05-20 17:24:21 +08:00
|
|
|
$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";
|
2026-05-20 17:24:21 +08:00
|
|
|
$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";
|
2026-05-21 05:42:55 +08:00
|
|
|
$envContent .= "NEXUS_DATABASE_URL=mysql+aiomysql://" . addslashes($dbUser) . ":" . addslashes($dbPass) . "@" . addslashes($dbHost) . ":" . addslashes($dbPort) . "/" . addslashes($dbName) . "\n";
|
2026-05-20 17:24:21 +08:00
|
|
|
$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";
|
2026-05-20 17:24:21 +08:00
|
|
|
$envContent .= "# SSH\n";
|
|
|
|
|
$envContent .= "NEXUS_SSH_STRICT_HOST_CHECKING=false\n\n";
|
|
|
|
|
$envContent .= "# Health Check\n";
|
2026-05-21 05:42:55 +08:00
|
|
|
$envContent .= "NEXUS_HEALTH_CHECK_INTERVAL=60\n";
|
|
|
|
|
$envContent .= "NEXUS_API_BASE_URL=$siteUrl\n\n";
|
2026-05-20 17:24:21 +08:00
|
|
|
$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 ──
|
2026-05-21 05:42:55 +08:00
|
|
|
$_SESSION['db_host'] = $dbHost;
|
|
|
|
|
$_SESSION['db_port'] = $dbPort;
|
2026-05-20 17:24:21 +08:00
|
|
|
$_SESSION['db_name'] = $dbName;
|
|
|
|
|
$_SESSION['db_user'] = $dbUser;
|
|
|
|
|
$_SESSION['db_pass'] = $dbPass;
|
|
|
|
|
$_SESSION['installed'] = true;
|
|
|
|
|
$_SESSION['pool_size'] = $poolSize;
|
|
|
|
|
$_SESSION['max_overflow'] = $maxOverflow;
|
2026-05-21 05:42:55 +08:00
|
|
|
$_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;
|
2026-05-20 17:24:21 +08:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-21 05:42:55 +08:00
|
|
|
// MySQL — 不检测 root,用户在步骤3自行填写数据库凭据
|
2026-05-20 17:24:21 +08:00
|
|
|
$checks[] = [
|
2026-05-21 05:42:55 +08:00
|
|
|
'name' => 'MySQL 数据库',
|
|
|
|
|
'required' => '步骤3配置',
|
|
|
|
|
'current' => '— 请在步骤3填写数据库连接信息',
|
|
|
|
|
'pass' => true,
|
2026-05-20 17:24:21 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 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>
|
2026-05-21 05:42:55 +08:00
|
|
|
<li>MySQL 8.0+ (需提前创建数据库和用户)</li>
|
2026-05-20 17:24:21 +08:00
|
|
|
<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">
|
2026-05-21 05:42:55 +08:00
|
|
|
<b>说明:</b>请提前创建好数据库和用户,安装向导将自动建表并写入配置。
|
2026-05-20 17:24:21 +08:00
|
|
|
SECRET_KEY 和 API_KEY 将自动生成,安装后不可修改(加密一致性)。
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form method="POST">
|
|
|
|
|
<input type="hidden" name="action" value="init_db">
|
|
|
|
|
|
2026-05-21 05:42:55 +08:00
|
|
|
<!-- MySQL section -->
|
2026-05-20 17:24:21 +08:00
|
|
|
<div class="form-section">
|
2026-05-21 05:42:55 +08:00
|
|
|
<div class="form-section-title">🗄 MySQL 数据库</div>
|
2026-05-20 17:24:21 +08:00
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="form-group">
|
2026-05-21 05:42:55 +08:00
|
|
|
<label>主机</label>
|
|
|
|
|
<input type="text" name="db_host" class="form-control" value="localhost">
|
2026-05-20 17:24:21 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
2026-05-21 05:42:55 +08:00
|
|
|
<label>端口</label>
|
|
|
|
|
<input type="text" name="db_port" class="form-control" value="3306">
|
2026-05-20 17:24:21 +08:00
|
|
|
</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">
|
2026-05-21 05:42:55 +08:00
|
|
|
<label>用户名</label>
|
2026-05-20 17:24:21 +08:00
|
|
|
<input type="text" name="db_user" class="form-control" value="Nexus">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
2026-05-21 05:42:55 +08:00
|
|
|
<label>密码</label>
|
|
|
|
|
<small>写入 .env + config.php</small>
|
|
|
|
|
<input type="password" name="db_pass" class="form-control" placeholder="数据库密码" required>
|
2026-05-20 17:24:21 +08:00
|
|
|
</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>
|
|
|
|
|
|
2026-05-21 05:42:55 +08:00
|
|
|
<!-- API + Site URL + Timezone -->
|
2026-05-20 17:24:21 +08:00
|
|
|
<div class="form-section">
|
2026-05-21 05:42:55 +08:00
|
|
|
<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>
|
2026-05-20 17:24:21 +08:00
|
|
|
<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;">
|
2026-05-21 05:42:55 +08:00
|
|
|
<b>⚠ 注意:</b>初始化将自动:① 连接数据库并建表 ② 生成 SECRET_KEY/API_KEY
|
|
|
|
|
③ 写入 config.php + .env + settings表 ④ 自动计算连接池大小
|
2026-05-20 17:24:21 +08:00
|
|
|
</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>
|
|
|
|
|
|
2026-05-21 05:42:55 +08:00
|
|
|
<?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; }
|
|
|
|
|
}
|
|
|
|
|
?>
|
|
|
|
|
|
2026-05-20 17:24:21 +08:00
|
|
|
<div class="post-checklist">
|
|
|
|
|
<h3 style="font-size:16px; margin-bottom:12px;">━━━ 后续配置清单(必须完成)━━━</h3>
|
|
|
|
|
|
2026-05-21 05:42:55 +08:00
|
|
|
<?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; ?>
|
|
|
|
|
|
2026-05-20 17:24:21 +08:00
|
|
|
<!-- ① Supervisor -->
|
|
|
|
|
<div class="post-item">
|
|
|
|
|
<div class="post-item-title"><span class="post-item-num">1</span> Supervisor 进程守护</div>
|
2026-05-21 05:42:55 +08:00
|
|
|
<?php if (!empty($guardianResults) && $guardianAllOk): ?>
|
|
|
|
|
<div class="post-item-desc" style="color: #16a34a;">✓ 已自动配置 — Supervisor 配置已写入,服务已重载</div>
|
|
|
|
|
<?php else: ?>
|
2026-05-20 17:24:21 +08:00
|
|
|
<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]
|
2026-05-21 05:42:55 +08:00
|
|
|
command=<?= htmlspecialchars($installDir) ?>/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port <?= htmlspecialchars($apiPort) ?>
|
|
|
|
|
|
|
|
|
|
directory=<?= htmlspecialchars($installDir) ?>
|
|
|
|
|
|
2026-05-20 17:24:21 +08:00
|
|
|
user=root
|
|
|
|
|
autostart=true
|
|
|
|
|
autorestart=true
|
|
|
|
|
startretries=10
|
|
|
|
|
startsecs=3
|
|
|
|
|
stopwaitsecs=10
|
|
|
|
|
stopsignal=INT
|
2026-05-21 05:42:55 +08:00
|
|
|
environment=PATH="<?= htmlspecialchars($installDir) ?>/venv/bin:%(ENV_PATH)s"
|
2026-05-20 17:24:21 +08:00
|
|
|
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>
|
2026-05-21 05:42:55 +08:00
|
|
|
<?php endif; ?>
|
2026-05-20 17:24:21 +08:00
|
|
|
</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">
|
2026-05-21 05:42:55 +08:00
|
|
|
<code>cd <?= htmlspecialchars($installDir) ?></code><br>
|
2026-05-20 17:24:21 +08:00
|
|
|
<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/ {
|
2026-05-21 05:42:55 +08:00
|
|
|
proxy_pass http://127.0.0.1:<?= htmlspecialchars($apiPort) ?>;
|
2026-05-20 17:24:21 +08:00
|
|
|
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 {
|
2026-05-21 05:42:55 +08:00
|
|
|
proxy_pass http://127.0.0.1:<?= htmlspecialchars($apiPort) ?>;
|
2026-05-20 17:24:21 +08:00
|
|
|
proxy_set_header Host $host;
|
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
|
}
|
|
|
|
|
location ^~ /ws/ {
|
2026-05-21 05:42:55 +08:00
|
|
|
proxy_pass http://127.0.0.1:<?= htmlspecialchars($apiPort) ?>;
|
2026-05-20 17:24:21 +08:00
|
|
|
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>
|
2026-05-22 00:02:46 +08:00
|
|
|
<pre id="nginxRewrite">location /app/ {
|
|
|
|
|
try_files $uri $uri/ /app/index.html;
|
|
|
|
|
}
|
|
|
|
|
location / {
|
|
|
|
|
try_files $uri $uri/ =404;
|
|
|
|
|
}</pre>
|
2026-05-20 17:24:21 +08:00
|
|
|
</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>
|
2026-05-21 05:42:55 +08:00
|
|
|
<?php if (!empty($guardianResults) && $guardianAllOk): ?>
|
|
|
|
|
<div class="post-item-desc" style="color: #16a34a;">
|
|
|
|
|
✓ 已自动配置 — health_monitor.sh INSTALL_DIR 已更新,Crontab 已添加
|
|
|
|
|
</div>
|
|
|
|
|
<?php else: ?>
|
2026-05-20 17:24:21 +08:00
|
|
|
<div class="post-item-desc">
|
2026-05-21 05:42:55 +08:00
|
|
|
复制 health_monitor.sh 到: <code><?= htmlspecialchars($installDir) ?>/deploy/</code><br>
|
2026-05-20 17:24:21 +08:00
|
|
|
配置 crontab:
|
|
|
|
|
</div>
|
|
|
|
|
<div class="code-wrap">
|
|
|
|
|
<button class="copy-btn" onclick="copyBlock('crontab')">复制</button>
|
2026-05-21 05:42:55 +08:00
|
|
|
<pre id="crontab">* * * * * <?= htmlspecialchars($installDir) ?>/deploy/health_monitor.sh</pre>
|
2026-05-20 17:24:21 +08:00
|
|
|
</div>
|
2026-05-21 05:42:55 +08:00
|
|
|
<?php endif; ?>
|
2026-05-20 17:24:21 +08:00
|
|
|
<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>
|
2026-05-21 05:42:55 +08:00
|
|
|
✓ 防火墙限制端口 <?= htmlspecialchars($apiPort) ?> (仅允许本机和子服务器IP)<br>
|
2026-05-20 17:24:21 +08:00
|
|
|
✓ 修改管理员默认密码
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="btn-group" style="justify-content:center; margin-top:24px;">
|
2026-05-22 00:02:46 +08:00
|
|
|
<a href="/app/index.html" class="btn btn-success" style="font-size:16px; padding:14px 32px;">进入管理面板 →</a>
|
2026-05-20 17:24:21 +08:00
|
|
|
</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>
|