feat(install): 新增安装脚本模块化重构
This commit is contained in:
@@ -112,3 +112,4 @@ backend/config/remote-backup-configs.json
|
||||
.gitignore
|
||||
.vercelignore
|
||||
docs/install-improvement-plan.md
|
||||
docs/install-split-plan.md
|
||||
|
||||
+4
-2492
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { SAVED_CONFIG_PATH, SCRIPT_VERSION } = require('./constants');
|
||||
const { colors, log } = require('./logger');
|
||||
|
||||
const config = {
|
||||
dbType: 'sqlite',
|
||||
dbConfig: {},
|
||||
backendPort: 8000,
|
||||
nodeEnv: 'production',
|
||||
frontendDeploy: 'nginx',
|
||||
frontendPort: 80,
|
||||
domain: 'localhost'
|
||||
};
|
||||
|
||||
function saveConfig() {
|
||||
const deployDir = path.dirname(SAVED_CONFIG_PATH);
|
||||
if (!fs.existsSync(deployDir)) {
|
||||
fs.mkdirSync(deployDir, { recursive: true });
|
||||
}
|
||||
|
||||
const savedConfig = {
|
||||
dbType: config.dbType,
|
||||
dbConfig: {
|
||||
host: config.dbConfig.host,
|
||||
port: config.dbConfig.port,
|
||||
username: config.dbConfig.username,
|
||||
database: config.dbConfig.database,
|
||||
},
|
||||
backendPort: config.backendPort,
|
||||
nodeEnv: config.nodeEnv,
|
||||
frontendDeploy: config.frontendDeploy,
|
||||
frontendPort: config.frontendPort,
|
||||
domain: config.domain,
|
||||
savedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
fs.writeFileSync(SAVED_CONFIG_PATH, JSON.stringify(savedConfig, null, 2));
|
||||
log.subStep('配置已保存到 deploy/install-config.json');
|
||||
}
|
||||
|
||||
function loadSavedConfig() {
|
||||
try {
|
||||
if (!fs.existsSync(SAVED_CONFIG_PATH)) {
|
||||
return null;
|
||||
}
|
||||
const content = fs.readFileSync(SAVED_CONFIG_PATH, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function applySavedConfig(saved) {
|
||||
if (!saved) return;
|
||||
|
||||
if (saved.dbType) config.dbType = saved.dbType;
|
||||
if (saved.dbConfig) {
|
||||
config.dbConfig = { ...config.dbConfig, ...saved.dbConfig };
|
||||
}
|
||||
if (saved.backendPort) config.backendPort = saved.backendPort;
|
||||
if (saved.nodeEnv) config.nodeEnv = saved.nodeEnv;
|
||||
if (saved.frontendDeploy) config.frontendDeploy = saved.frontendDeploy;
|
||||
if (saved.frontendPort) config.frontendPort = saved.frontendPort;
|
||||
if (saved.domain) config.domain = saved.domain;
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
return {
|
||||
nonInteractive: args.includes('--non-interactive') || args.includes('-y'),
|
||||
skipNginx: args.includes('--skip-nginx'),
|
||||
skipBuild: args.includes('--skip-build'),
|
||||
dbType: args.find(a => a.startsWith('--db='))?.split('=')[1],
|
||||
backendPort: args.find(a => a.startsWith('--port='))?.split('=')[1],
|
||||
help: args.includes('--help') || args.includes('-h')
|
||||
};
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
${colors.bright}IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION}${colors.reset}
|
||||
|
||||
用法: node install.js [选项]
|
||||
|
||||
选项:
|
||||
-y, --non-interactive 非交互式安装(使用默认配置)
|
||||
--skip-nginx 跳过 Nginx 配置
|
||||
--skip-build 跳过前端构建
|
||||
--db=<type> 数据库类型 (sqlite/mysql)
|
||||
--port=<port> 后端端口
|
||||
-h, --help 显示帮助信息
|
||||
|
||||
示例:
|
||||
node install.js # 交互式安装
|
||||
node install.js -y # 使用默认配置快速安装
|
||||
node install.js -y --db=mysql # 使用 MySQL 数据库
|
||||
node install.js -y --port=3000 # 指定后端端口
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
config,
|
||||
saveConfig,
|
||||
loadSavedConfig,
|
||||
applySavedConfig,
|
||||
parseArgs,
|
||||
showHelp,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
const path = require('path');
|
||||
|
||||
const SCRIPT_VERSION = '2.0.0';
|
||||
const MIN_NODE_VERSION = 16;
|
||||
const LOG_DIR = path.join(__dirname, '..', 'logs');
|
||||
|
||||
const INSTALL_STEPS = [
|
||||
'环境检测',
|
||||
'数据库配置',
|
||||
'服务配置',
|
||||
'生成配置文件',
|
||||
'安装依赖',
|
||||
'数据库初始化',
|
||||
'构建前端',
|
||||
'启动服务',
|
||||
'配置 Nginx',
|
||||
'健康检查',
|
||||
];
|
||||
|
||||
const NPM_MIRRORS = [
|
||||
{ name: 'npm 官方', registry: 'https://registry.npmjs.org' },
|
||||
{ name: '淘宝镜像', registry: 'https://registry.npmmirror.com' },
|
||||
{ name: '腾讯镜像', registry: 'https://mirrors.tencent.com/npm' },
|
||||
];
|
||||
|
||||
const SAVED_CONFIG_PATH = path.join(__dirname, '..', 'deploy', 'install-config.json');
|
||||
|
||||
module.exports = {
|
||||
SCRIPT_VERSION,
|
||||
MIN_NODE_VERSION,
|
||||
LOG_DIR,
|
||||
INSTALL_STEPS,
|
||||
NPM_MIRRORS,
|
||||
SAVED_CONFIG_PATH,
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
const { colors, log } = require('./logger');
|
||||
const { ask, askPassword, select } = require('./ui');
|
||||
const { config } = require('./config');
|
||||
const { detectLinuxDistro } = require('./env-check');
|
||||
|
||||
async function configureDatabase(cmdArgs) {
|
||||
log.step('数据库配置');
|
||||
|
||||
if (cmdArgs?.nonInteractive && cmdArgs?.dbType) {
|
||||
config.dbType = cmdArgs.dbType;
|
||||
log.info(`使用命令行参数: 数据库类型 = ${config.dbType}`);
|
||||
} else if (cmdArgs?.nonInteractive) {
|
||||
config.dbType = 'sqlite';
|
||||
log.info('使用默认配置: 数据库类型 = sqlite');
|
||||
} else {
|
||||
config.dbType = await select('选择数据库类型:', [
|
||||
{ label: 'SQLite(零配置,适合开发/小规模)', value: 'sqlite' },
|
||||
{ label: 'MySQL(生产环境推荐)', value: 'mysql' }
|
||||
]);
|
||||
}
|
||||
|
||||
if (config.dbType === 'mysql') {
|
||||
if (cmdArgs?.nonInteractive) {
|
||||
config.dbConfig.host = process.env.MYSQL_HOST || 'localhost';
|
||||
config.dbConfig.port = process.env.MYSQL_PORT || '3306';
|
||||
config.dbConfig.username = process.env.MYSQL_USER || 'root';
|
||||
config.dbConfig.password = process.env.MYSQL_PASSWORD || '';
|
||||
config.dbConfig.database = process.env.MYSQL_DATABASE || 'idc_management';
|
||||
log.info(`MySQL 配置: ${config.dbConfig.host}:${config.dbConfig.port}/${config.dbConfig.database}`);
|
||||
} else {
|
||||
const mysqlOption = await select('MySQL 配置方式:', [
|
||||
{ label: '已有数据库(填写现有 MySQL 连接信息)', value: 'existing' },
|
||||
{ label: '自动安装 MySQL(Linux 系统支持自动安装配置)', value: 'install' }
|
||||
]);
|
||||
|
||||
if (mysqlOption === 'install') {
|
||||
const installResult = await installMySQL();
|
||||
if (installResult.success) {
|
||||
config.dbConfig = { ...installResult.config };
|
||||
} else {
|
||||
log.error('MySQL 自动安装失败');
|
||||
const fallback = await ask('是否改为手动配置现有 MySQL? (Y/n)', 'Y');
|
||||
if (fallback.toLowerCase() === 'y') {
|
||||
await configureExistingMySQL();
|
||||
} else {
|
||||
log.info('已取消 MySQL 配置,将使用 SQLite');
|
||||
config.dbType = 'sqlite';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await configureExistingMySQL();
|
||||
}
|
||||
}
|
||||
|
||||
if (config.dbType === 'mysql') {
|
||||
const testResult = await testMySQLConnection();
|
||||
if (!testResult.success) {
|
||||
log.error(`MySQL 连接测试失败: ${testResult.error}`);
|
||||
const retry = cmdArgs?.nonInteractive ? false : await ask('是否重新配置? (Y/n)', 'Y');
|
||||
if (retry.toLowerCase() === 'y') {
|
||||
return await configureDatabase(cmdArgs);
|
||||
}
|
||||
log.warning('将继续部署,但数据库可能无法正常工作');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.divider();
|
||||
}
|
||||
|
||||
async function configureExistingMySQL() {
|
||||
console.log('\n' + colors.yellow + '请确保 MySQL 服务已启动并创建了数据库' + colors.reset);
|
||||
console.log(colors.gray + '创建数据库命令: CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset + '\n');
|
||||
|
||||
config.dbConfig.host = await ask('MySQL 主机地址', 'localhost');
|
||||
config.dbConfig.port = await ask('MySQL 端口', '3306');
|
||||
config.dbConfig.username = await ask('MySQL 用户名', 'root');
|
||||
config.dbConfig.password = await askPassword('MySQL 密码');
|
||||
config.dbConfig.database = await ask('数据库名称', 'idc_management');
|
||||
}
|
||||
|
||||
async function installMySQL() {
|
||||
log.subStep('检测系统环境...');
|
||||
|
||||
const platform = process.platform;
|
||||
|
||||
if (platform === 'win32') {
|
||||
log.warning('Windows 系统暂不支持自动安装 MySQL');
|
||||
console.log('\n' + colors.cyan + '请手动安装 MySQL:' + colors.reset);
|
||||
console.log(' 1. 下载 MySQL: https://dev.mysql.com/downloads/mysql/');
|
||||
console.log(' 2. 或使用 XAMPP/WAMP 等集成环境');
|
||||
console.log(' 3. 安装后创建数据库:');
|
||||
console.log(colors.gray + ' CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
if (platform !== 'linux') {
|
||||
log.warning('当前系统暂不支持自动安装 MySQL');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const distro = detectLinuxDistro();
|
||||
log.info(`检测到 Linux 发行版: ${distro}`);
|
||||
|
||||
const mysqlRootPassword = await askPassword('设置 MySQL root 密码(用于新安装的 MySQL)');
|
||||
if (!mysqlRootPassword) {
|
||||
log.error('密码不能为空');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
log.subStep('开始安装 MySQL...');
|
||||
|
||||
try {
|
||||
switch (distro) {
|
||||
case 'ubuntu':
|
||||
case 'debian':
|
||||
await installMySQLDebian(mysqlRootPassword);
|
||||
break;
|
||||
case 'centos':
|
||||
case 'rhel':
|
||||
case 'rocky':
|
||||
case 'almalinux':
|
||||
await installMySQLRHEL(mysqlRootPassword);
|
||||
break;
|
||||
default:
|
||||
log.warning(`暂不支持 ${distro} 自动安装 MySQL`);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
config.dbConfig.host = 'localhost';
|
||||
config.dbConfig.port = '3306';
|
||||
config.dbConfig.username = 'root';
|
||||
config.dbConfig.password = mysqlRootPassword;
|
||||
config.dbConfig.database = 'idc_management';
|
||||
|
||||
log.subStep('创建数据库...');
|
||||
await createMySQLDatabase(mysqlRootPassword);
|
||||
|
||||
log.success('MySQL 安装配置完成');
|
||||
return {
|
||||
success: true,
|
||||
config: {
|
||||
host: 'localhost',
|
||||
port: '3306',
|
||||
username: 'root',
|
||||
password: mysqlRootPassword,
|
||||
database: 'idc_management'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`MySQL 安装失败: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function installMySQLDebian(rootPassword) {
|
||||
log.info('安装 MySQL (Ubuntu/Debian)...');
|
||||
|
||||
execSync('export DEBIAN_FRONTEND=noninteractive', { shell: '/bin/bash' });
|
||||
execSync('apt-get update -qq', { shell: '/bin/bash', stdio: 'inherit' });
|
||||
execSync(`debconf-set-selections <<< "mysql-server mysql-server/root_password password ${rootPassword}"`, { shell: '/bin/bash' });
|
||||
execSync(`debconf-set-selections <<< "mysql-server mysql-server/root_password_again password ${rootPassword}"`, { shell: '/bin/bash' });
|
||||
execSync('apt-get install -y -qq mysql-server', { shell: '/bin/bash', stdio: 'inherit' });
|
||||
execSync('systemctl start mysql', { shell: '/bin/bash' });
|
||||
execSync('systemctl enable mysql', { shell: '/bin/bash' });
|
||||
|
||||
log.success('MySQL 安装完成');
|
||||
}
|
||||
|
||||
async function installMySQLRHEL(rootPassword) {
|
||||
log.info('安装 MySQL (CentOS/RHEL)...');
|
||||
|
||||
execSync('yum install -y -q epel-release', { shell: '/bin/bash', stdio: 'inherit' });
|
||||
execSync('yum install -y -q mysql-server', { shell: '/bin/bash', stdio: 'inherit' });
|
||||
execSync('systemctl start mysqld', { shell: '/bin/bash' });
|
||||
execSync('systemctl enable mysqld', { shell: '/bin/bash' });
|
||||
|
||||
try {
|
||||
execSync(`mysqladmin -u root password "${rootPassword}"`, { shell: '/bin/bash' });
|
||||
} catch {
|
||||
log.info('root 密码可能已设置,跳过');
|
||||
}
|
||||
|
||||
log.success('MySQL 安装完成');
|
||||
}
|
||||
|
||||
async function createMySQLDatabase(rootPassword) {
|
||||
log.subStep('创建数据库...');
|
||||
|
||||
const createDbSql = 'CREATE DATABASE IF NOT EXISTS idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;';
|
||||
|
||||
try {
|
||||
execSync(`mysql -u root -p'${rootPassword}' -e "${createDbSql}"`, {
|
||||
shell: '/bin/bash',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
log.success('数据库 idc_management 创建成功');
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMsg = error.stderr ? error.stderr.toString() : error.message;
|
||||
|
||||
if (errorMsg.includes('Access denied') || errorMsg.includes('ERROR 1045')) {
|
||||
log.warning('MySQL root 密码验证失败,尝试无密码连接...');
|
||||
try {
|
||||
execSync(`mysql -u root -e "${createDbSql}"`, {
|
||||
shell: '/bin/bash',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
log.success('数据库 idc_management 创建成功');
|
||||
return;
|
||||
} catch (e2) {
|
||||
log.warning(`无密码连接也失败: ${e2.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
log.warning(`数据库自动创建失败: ${errorMsg.trim()}`);
|
||||
log.info('请手动创建数据库:');
|
||||
console.log(colors.cyan + ' mysql -u root -p' + colors.reset);
|
||||
console.log(colors.cyan + ' CREATE DATABASE idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' + colors.reset);
|
||||
}
|
||||
}
|
||||
|
||||
async function testMySQLConnection() {
|
||||
log.subStep('测试 MySQL 连接...');
|
||||
|
||||
try {
|
||||
execSync(`mysql -h ${config.dbConfig.host} -P ${config.dbConfig.port} -u ${config.dbConfig.username} -p'${config.dbConfig.password}' -e "SELECT 1" ${config.dbConfig.database}`, {
|
||||
shell: '/bin/bash',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 5000
|
||||
});
|
||||
log.success('MySQL 连接测试成功');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMsg = error.stderr ? error.stderr.toString() : error.message;
|
||||
|
||||
if (errorMsg.includes('Access denied') || errorMsg.includes('ERROR 1045')) {
|
||||
return { success: false, error: '用户名或密码错误' };
|
||||
}
|
||||
if (errorMsg.includes('Unknown database')) {
|
||||
return { success: false, error: `数据库 ${config.dbConfig.database} 不存在` };
|
||||
}
|
||||
if (errorMsg.includes('Connection refused') || errorMsg.includes("Can't connect")) {
|
||||
return { success: false, error: `无法连接到 ${config.dbConfig.host}:${config.dbConfig.port}` };
|
||||
}
|
||||
|
||||
return { success: false, error: errorMsg.trim() || '连接失败' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configureDatabase,
|
||||
configureExistingMySQL,
|
||||
installMySQL,
|
||||
installMySQLDebian,
|
||||
installMySQLRHEL,
|
||||
createMySQLDatabase,
|
||||
testMySQLConnection,
|
||||
};
|
||||
@@ -0,0 +1,426 @@
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
const { MIN_NODE_VERSION } = require('./constants');
|
||||
const { colors, log } = require('./logger');
|
||||
const { runCommand, commandExists } = require('./utils');
|
||||
const { ask } = require('./ui');
|
||||
|
||||
function checkNodeVersion() {
|
||||
const version = process.version;
|
||||
const major = parseInt(version.slice(1).split('.')[0]);
|
||||
return major >= MIN_NODE_VERSION;
|
||||
}
|
||||
|
||||
function isNginxInstalled() {
|
||||
try {
|
||||
execSync('nginx -v', { stdio: 'pipe', shell: true });
|
||||
return true;
|
||||
} catch {
|
||||
if (process.platform === 'win32') {
|
||||
const commonPaths = [
|
||||
'C:\\nginx\\nginx.exe',
|
||||
'C:\\Program Files\\nginx\\nginx.exe',
|
||||
'C:\\Program Files (x86)\\nginx\\nginx.exe'
|
||||
];
|
||||
for (const p of commonPaths) {
|
||||
if (fs.existsSync(p)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkNodeAndNpm() {
|
||||
let nodeInstalled = false;
|
||||
let npmInstalled = false;
|
||||
let version = '';
|
||||
|
||||
try {
|
||||
version = process.version;
|
||||
nodeInstalled = true;
|
||||
} catch {
|
||||
nodeInstalled = false;
|
||||
}
|
||||
|
||||
npmInstalled = commandExists('npm');
|
||||
|
||||
return { nodeInstalled, npmInstalled, version };
|
||||
}
|
||||
|
||||
function detectLinuxDistro() {
|
||||
try {
|
||||
if (fs.existsSync('/etc/os-release')) {
|
||||
const content = fs.readFileSync('/etc/os-release', 'utf8');
|
||||
const idMatch = content.match(/^ID=(.*)$/m);
|
||||
if (idMatch) {
|
||||
const id = idMatch[1].replace(/"/g, '').toLowerCase();
|
||||
if (['ubuntu', 'debian'].includes(id)) return 'ubuntu';
|
||||
if (['centos', 'rhel', 'rocky', 'almalinux', 'fedora'].includes(id)) return 'centos';
|
||||
return id;
|
||||
}
|
||||
}
|
||||
if (fs.existsSync('/etc/redhat-release')) return 'centos';
|
||||
if (fs.existsSync('/etc/debian_version')) return 'ubuntu';
|
||||
} catch {}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function isRootUser() {
|
||||
return process.getuid && process.getuid() === 0;
|
||||
}
|
||||
|
||||
function showNodeInstallGuide() {
|
||||
const platform = process.platform;
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform === 'linux';
|
||||
|
||||
console.log('\n' + colors.bright + 'Node.js 安装指引:' + colors.reset);
|
||||
|
||||
if (isLinux) {
|
||||
const distro = detectLinuxDistro();
|
||||
|
||||
console.log('\n' + colors.green + '【Linux 推荐安装方式】' + colors.reset);
|
||||
|
||||
switch (distro) {
|
||||
case 'ubuntu':
|
||||
case 'debian':
|
||||
console.log('\n' + colors.yellow + `Ubuntu/Debian (${distro}):` + colors.reset);
|
||||
console.log(` ${colors.cyan}# 使用 NodeSource 源安装 Node.js 20.x` + colors.reset);
|
||||
console.log(` ${colors.cyan}curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -${colors.reset}`);
|
||||
console.log(` ${colors.cyan}sudo apt-get install -y nodejs${colors.reset}`);
|
||||
break;
|
||||
|
||||
case 'centos':
|
||||
case 'rhel':
|
||||
case 'fedora':
|
||||
console.log('\n' + colors.yellow + `CentOS/RHEL/Fedora (${distro}):` + colors.reset);
|
||||
console.log(` ${colors.cyan}# 使用 NodeSource 源安装 Node.js 20.x` + colors.reset);
|
||||
console.log(` ${colors.cyan}curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -${colors.reset}`);
|
||||
console.log(` ${colors.cyan}sudo yum install -y nodejs${colors.reset}`);
|
||||
break;
|
||||
|
||||
case 'arch':
|
||||
console.log('\n' + colors.yellow + 'Arch Linux:' + colors.reset);
|
||||
console.log(` ${colors.cyan}sudo pacman -S nodejs npm${colors.reset}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('\n' + colors.yellow + '通用 Linux 安装方式:' + colors.reset);
|
||||
console.log(` ${colors.cyan}# Ubuntu/Debian${colors.reset}`);
|
||||
console.log(` curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -`);
|
||||
console.log(` sudo apt-get install -y nodejs`);
|
||||
console.log(`\n ${colors.cyan}# CentOS/RHEL/Fedora${colors.reset}`);
|
||||
console.log(` curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -`);
|
||||
console.log(` sudo yum install -y nodejs`);
|
||||
}
|
||||
|
||||
console.log('\n' + colors.yellow + '【开发者推荐】使用 nvm(Node Version Manager):' + colors.reset);
|
||||
console.log(` ${colors.cyan}# 1. 安装 nvm${colors.reset}`);
|
||||
console.log(` curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash`);
|
||||
console.log(` ${colors.cyan}# 2. 加载 nvm(或重新打开终端)${colors.reset}`);
|
||||
console.log(` export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"`);
|
||||
console.log(` ${colors.cyan}# 3. 安装并使用 Node.js 20${colors.reset}`);
|
||||
console.log(` nvm install 20`);
|
||||
console.log(` nvm use 20`);
|
||||
console.log(` ${colors.cyan}# 4. 验证安装${colors.reset}`);
|
||||
console.log(` node -v && npm -v`);
|
||||
|
||||
console.log('\n' + colors.yellow + '【Docker 方式】' + colors.reset);
|
||||
console.log(` ${colors.cyan}# 使用官方 Node 镜像${colors.reset}`);
|
||||
console.log(` docker run -it --rm node:20-alpine node -v`);
|
||||
|
||||
} else if (isWindows) {
|
||||
console.log('\n' + colors.green + '【Windows 安装方式】' + colors.reset);
|
||||
|
||||
console.log('\n' + colors.yellow + '方式一:使用官方安装包(推荐新手)' + colors.reset);
|
||||
console.log(` 1. 访问: ${colors.cyan}https://nodejs.org/${colors.reset}`);
|
||||
console.log(` 2. 下载 LTS 版本(推荐 ${colors.cyan}v20.x${colors.reset})`);
|
||||
console.log(` 3. 运行安装包,按向导完成安装`);
|
||||
console.log(` 4. 安装完成后,${colors.yellow}重新打开终端${colors.reset}并再次运行此脚本`);
|
||||
|
||||
console.log('\n' + colors.yellow + '方式二:使用 nvm-windows(推荐开发者)' + colors.reset);
|
||||
console.log(` 1. 下载安装 nvm-windows: ${colors.cyan}https://github.com/coreybutler/nvm-windows/releases${colors.reset}`);
|
||||
console.log(` 2. 打开新的 PowerShell 或 CMD 窗口`);
|
||||
console.log(` 3. 执行: ${colors.cyan}nvm install 20${colors.reset}`);
|
||||
console.log(` 4. 执行: ${colors.cyan}nvm use 20${colors.reset}`);
|
||||
console.log(` 5. 验证: ${colors.cyan}node -v${colors.reset}`);
|
||||
|
||||
console.log('\n' + colors.yellow + '方式三:使用 Winget(Windows 10/11 自带)' + colors.reset);
|
||||
console.log(` ${colors.cyan}winget install OpenJS.NodeJS.LTS${colors.reset}`);
|
||||
|
||||
} else if (isMac) {
|
||||
console.log('\n' + colors.green + '【macOS 安装方式】' + colors.reset);
|
||||
|
||||
console.log('\n' + colors.yellow + '方式一:使用 Homebrew(推荐)' + colors.reset);
|
||||
console.log(` ${colors.cyan}brew install node@${colors.reset}`);
|
||||
|
||||
console.log('\n' + colors.yellow + '方式二:使用官方安装包' + colors.reset);
|
||||
console.log(` 1. 访问: ${colors.cyan}https://nodejs.org/${colors.reset}`);
|
||||
console.log(` 2. 下载 macOS 安装包(.pkg)`);
|
||||
console.log(` 3. 双击安装包按向导完成安装`);
|
||||
|
||||
console.log('\n' + colors.yellow + '方式三:使用 nvm(推荐开发者)' + colors.reset);
|
||||
console.log(` ${colors.cyan}# 1. 安装 nvm${colors.reset}`);
|
||||
console.log(` curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash`);
|
||||
console.log(` ${colors.cyan}# 2. 重新加载 shell 配置${colors.reset}`);
|
||||
console.log(` source ~/.bashrc # 或 source ~/.zshrc`);
|
||||
console.log(` ${colors.cyan}# 3. 安装 Node.js 20${colors.reset}`);
|
||||
console.log(` nvm install 20`);
|
||||
console.log(` nvm use 20`);
|
||||
}
|
||||
|
||||
console.log('\n' + colors.green + '【验证安装】' + colors.reset);
|
||||
console.log(` 安装完成后,运行以下命令验证:`);
|
||||
console.log(` ${colors.cyan}node -v${colors.reset} # 应显示 v20.x.x 或更高版本`);
|
||||
console.log(` ${colors.cyan}npm -v${colors.reset} # 应显示 10.x.x 或更高版本`);
|
||||
|
||||
console.log('\n' + colors.gray + '安装完成后,请重新运行此部署脚本。' + colors.reset);
|
||||
}
|
||||
|
||||
async function autoInstallNodeLinux() {
|
||||
const distro = detectLinuxDistro();
|
||||
|
||||
log.step('自动安装 Node.js');
|
||||
log.info(`检测到 Linux 发行版: ${distro}`);
|
||||
|
||||
try {
|
||||
switch (distro) {
|
||||
case 'ubuntu':
|
||||
case 'debian':
|
||||
log.info('使用 NodeSource 源安装 Node.js 20.x...');
|
||||
log.info('执行: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -');
|
||||
const debianSetup = runCommand('curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -');
|
||||
if (!debianSetup.success) {
|
||||
log.error('NodeSource 源配置失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info('执行: sudo apt-get install -y nodejs');
|
||||
const debianInstall = runCommand('sudo apt-get install -y nodejs');
|
||||
if (!debianInstall.success) {
|
||||
log.error('Node.js 安装失败');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'centos':
|
||||
case 'rhel':
|
||||
case 'fedora':
|
||||
log.info('使用 NodeSource 源安装 Node.js 20.x...');
|
||||
log.info('执行: curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -');
|
||||
const rpmSetup = runCommand('curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -');
|
||||
if (!rpmSetup.success) {
|
||||
log.error('NodeSource 源配置失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info('执行: sudo yum install -y nodejs');
|
||||
const rpmInstall = runCommand('sudo yum install -y nodejs');
|
||||
if (!rpmInstall.success) {
|
||||
log.error('Node.js 安装失败');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'arch':
|
||||
log.info('执行: sudo pacman -S nodejs npm');
|
||||
const archInstall = runCommand('sudo pacman -S --noconfirm nodejs npm');
|
||||
if (!archInstall.success) {
|
||||
log.error('Node.js 安装失败');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
log.info('未知发行版,尝试使用 nvm 安装...');
|
||||
return await installNodeViaNvm();
|
||||
}
|
||||
|
||||
const checkResult = runCommand('node -v', { silent: true });
|
||||
if (checkResult.success) {
|
||||
log.success(`Node.js 安装成功: ${checkResult.output.trim()}`);
|
||||
const npmCheck = runCommand('npm -v', { silent: true });
|
||||
if (npmCheck.success) {
|
||||
log.success(`npm 安装成功: ${npmCheck.output.trim()}`);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
log.error('Node.js 安装后验证失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log.error(`自动安装失败: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function installNodeViaNvm() {
|
||||
log.info('使用 nvm 安装 Node.js...');
|
||||
|
||||
try {
|
||||
const nvmCheck = runCommand('source ~/.nvm/nvm.sh && nvm --version', { silent: true });
|
||||
|
||||
if (!nvmCheck.success) {
|
||||
log.info('安装 nvm...');
|
||||
const nvmInstall = runCommand('curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash');
|
||||
if (!nvmInstall.success) {
|
||||
log.error('nvm 安装失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
log.info('使用 nvm 安装 Node.js 20...');
|
||||
const nodeInstall = runCommand('export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && nvm install 20');
|
||||
if (!nodeInstall.success) {
|
||||
log.error('Node.js 安装失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info('设置 Node.js 20 为默认版本...');
|
||||
runCommand('export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && nvm alias default 20', { silent: true });
|
||||
|
||||
log.success('Node.js 安装成功');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
log.error(`nvm 安装失败: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNodeNotInstalledLinux() {
|
||||
log.error('未检测到 Node.js');
|
||||
|
||||
console.log('\n' + colors.yellow + '您可以选择:' + colors.reset);
|
||||
console.log(` ${colors.cyan}1.${colors.reset} 自动安装 Node.js 20.x(推荐,需要 sudo 权限)`);
|
||||
console.log(` ${colors.cyan}2.${colors.reset} 显示手动安装指引,退出脚本`);
|
||||
|
||||
const choice = await ask('请选择', '1');
|
||||
|
||||
if (choice === '1') {
|
||||
const installed = await autoInstallNodeLinux();
|
||||
if (installed) {
|
||||
log.success('Node.js 和 npm 安装完成!');
|
||||
log.info('请重新运行此脚本以继续部署');
|
||||
console.log(colors.cyan + ' node install.js' + colors.reset);
|
||||
|
||||
const rerun = await ask('\n是否立即重新运行部署脚本? (Y/n)', 'Y');
|
||||
if (rerun.toLowerCase() === 'y') {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
log.error('自动安装失败,请尝试手动安装');
|
||||
showNodeInstallGuide();
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
showNodeInstallGuide();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkEnvironment() {
|
||||
log.step('环境检测');
|
||||
|
||||
const { nodeInstalled, npmInstalled, version } = checkNodeAndNpm();
|
||||
const isLinux = process.platform === 'linux';
|
||||
|
||||
if (!nodeInstalled) {
|
||||
if (isLinux) {
|
||||
const shouldContinue = await handleNodeNotInstalledLinux();
|
||||
if (shouldContinue) {
|
||||
return await checkEnvironment();
|
||||
}
|
||||
} else {
|
||||
log.error('未检测到 Node.js,请先安装');
|
||||
showNodeInstallGuide();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!npmInstalled) {
|
||||
if (isLinux && nodeInstalled) {
|
||||
log.warning('未检测到 npm,尝试修复...');
|
||||
const fixed = await autoInstallNodeLinux();
|
||||
if (!fixed) {
|
||||
log.error('npm 修复失败,请手动安装');
|
||||
showNodeInstallGuide();
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
log.error('未检测到 npm,请重新安装 Node.js(npm 会随 Node.js 一起安装)');
|
||||
showNodeInstallGuide();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkNodeVersion()) {
|
||||
log.success(`Node.js ${version}`);
|
||||
} else {
|
||||
log.error(`Node.js 版本过低 (${version}),需要 >= 14.0.0`);
|
||||
|
||||
if (isLinux) {
|
||||
console.log('\n' + colors.yellow + '您可以选择:' + colors.reset);
|
||||
console.log(` ${colors.cyan}1.${colors.reset} 自动升级到 Node.js 20.x`);
|
||||
console.log(` ${colors.cyan}2.${colors.reset} 显示手动升级指引,退出脚本`);
|
||||
|
||||
const upgradeChoice = await ask('请选择', '1');
|
||||
if (upgradeChoice === '1') {
|
||||
const upgraded = await autoInstallNodeLinux();
|
||||
if (upgraded) {
|
||||
log.success('Node.js 升级完成!请重新运行此脚本');
|
||||
} else {
|
||||
log.error('自动升级失败');
|
||||
showNodeInstallGuide();
|
||||
}
|
||||
} else {
|
||||
console.log('\n' + colors.yellow + '建议升级到 Node.js 20 LTS 版本:' + colors.reset);
|
||||
showNodeInstallGuide();
|
||||
}
|
||||
} else {
|
||||
console.log('\n' + colors.yellow + '建议升级到 Node.js 20 LTS 版本:' + colors.reset);
|
||||
showNodeInstallGuide();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const npmResult = runCommand('npm --version', { silent: true });
|
||||
if (npmResult.success) {
|
||||
log.success(`npm ${npmResult.output.trim()}`);
|
||||
}
|
||||
|
||||
if (commandExists('pm2')) {
|
||||
log.success('PM2 已安装');
|
||||
} else {
|
||||
log.warning('未检测到 PM2,将自动安装');
|
||||
const result = runCommand('npm install -g pm2');
|
||||
if (result.success) {
|
||||
log.success('PM2 安装完成');
|
||||
} else {
|
||||
log.error('PM2 安装失败,请手动执行: npm install -g pm2');
|
||||
}
|
||||
}
|
||||
|
||||
if (commandExists('nginx') || commandExists('nginx.exe')) {
|
||||
log.success('Nginx 已安装');
|
||||
} else {
|
||||
log.warning('未检测到 Nginx(可选,仅在使用Nginx部署时需要)');
|
||||
}
|
||||
|
||||
log.divider();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkNodeVersion,
|
||||
isNginxInstalled,
|
||||
checkNodeAndNpm,
|
||||
detectLinuxDistro,
|
||||
isRootUser,
|
||||
showNodeInstallGuide,
|
||||
autoInstallNodeLinux,
|
||||
installNodeViaNvm,
|
||||
handleNodeNotInstalledLinux,
|
||||
checkEnvironment,
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { colors, log } = require('./logger');
|
||||
const { generateSecretKey } = require('./utils');
|
||||
const { config } = require('./config');
|
||||
const { ask } = require('./ui');
|
||||
|
||||
function generateBackendEnv() {
|
||||
const envPath = path.join(__dirname, '..', 'backend', '.env');
|
||||
|
||||
try {
|
||||
const jwtSecret = generateSecretKey(64);
|
||||
|
||||
let envContent = `# IDC设备管理系统 - 环境配置
|
||||
# 生成时间: ${new Date().toISOString()}
|
||||
|
||||
# ==============================================
|
||||
# 服务器配置
|
||||
# ==============================================
|
||||
|
||||
# 服务器端口
|
||||
PORT=${config.backendPort}
|
||||
|
||||
# 运行环境: development 或 production
|
||||
NODE_ENV=${config.nodeEnv}
|
||||
|
||||
# ==============================================
|
||||
# 安全配置
|
||||
# ==============================================
|
||||
|
||||
# JWT 密钥(自动生成,请妥善保管)
|
||||
JWT_SECRET=${jwtSecret}
|
||||
|
||||
# ==============================================
|
||||
# 数据库配置
|
||||
# ==============================================
|
||||
|
||||
# 数据库类型: sqlite 或 mysql
|
||||
DB_TYPE=${config.dbType}
|
||||
`;
|
||||
|
||||
if (config.dbType === 'sqlite') {
|
||||
envContent += `
|
||||
# SQLite 配置
|
||||
DB_PATH=./idc_management.db
|
||||
`;
|
||||
} else {
|
||||
envContent += `
|
||||
# MySQL 配置
|
||||
MYSQL_HOST=${config.dbConfig.host}
|
||||
MYSQL_PORT=${config.dbConfig.port}
|
||||
MYSQL_USERNAME=${config.dbConfig.username}
|
||||
MYSQL_PASSWORD=${config.dbConfig.password}
|
||||
MYSQL_DATABASE=${config.dbConfig.database}
|
||||
`;
|
||||
}
|
||||
|
||||
fs.writeFileSync(envPath, envContent);
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
fs.chmodSync(envPath, 0o600);
|
||||
log.success('.env 文件权限已设为 600(仅所有者可读写)');
|
||||
} catch {
|
||||
log.warning('.env 文件权限设置失败,建议手动执行: chmod 600 backend/.env');
|
||||
}
|
||||
}
|
||||
|
||||
log.success('后端环境变量文件已生成 (.env)');
|
||||
log.info(`JWT_SECRET 已自动生成 (${jwtSecret.length}位)`);
|
||||
log.warning('请妥善保管 .env 文件,切勿提交到版本控制系统');
|
||||
} catch (error) {
|
||||
log.error(`后端环境变量文件生成失败: ${error.message}`);
|
||||
throw new Error('Backend env generation failed');
|
||||
}
|
||||
}
|
||||
|
||||
function generatePM2Config() {
|
||||
const deployDir = path.join(__dirname, '..', 'deploy');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(deployDir)) {
|
||||
fs.mkdirSync(deployDir, { recursive: true });
|
||||
}
|
||||
|
||||
const pm2Config = {
|
||||
apps: [{
|
||||
name: 'idc-backend',
|
||||
script: './server.js',
|
||||
cwd: path.join(__dirname, '..', 'backend'),
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: config.backendPort
|
||||
},
|
||||
max_memory_restart: '1G',
|
||||
log_file: path.join(__dirname, '..', 'backend', 'logs', 'combined.log'),
|
||||
out_file: path.join(__dirname, '..', 'backend', 'logs', 'out.log'),
|
||||
error_file: path.join(__dirname, '..', 'backend', 'logs', 'error.log'),
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
||||
merge_logs: true,
|
||||
autorestart: true
|
||||
}]
|
||||
};
|
||||
|
||||
if (config.frontendDeploy === 'pm2') {
|
||||
pm2Config.apps.push({
|
||||
name: 'idc-frontend',
|
||||
script: 'serve',
|
||||
cwd: path.join(__dirname, '..', 'frontend'),
|
||||
args: `-s dist -l ${config.frontendPort}`,
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: { NODE_ENV: 'production' },
|
||||
max_memory_restart: '500M',
|
||||
autorestart: true
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(deployDir, 'ecosystem.config.js'),
|
||||
`module.exports = ${JSON.stringify(pm2Config, null, 2)};`
|
||||
);
|
||||
log.success('PM2 配置文件已生成 (deploy/ecosystem.config.js)');
|
||||
} catch (error) {
|
||||
log.error(`PM2 配置文件生成失败: ${error.message}`);
|
||||
throw new Error('PM2 config generation failed');
|
||||
}
|
||||
}
|
||||
|
||||
function generateNginxConfig() {
|
||||
if (config.frontendDeploy !== 'nginx') return;
|
||||
|
||||
const deployDir = path.join(__dirname, '..', 'deploy');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(deployDir)) {
|
||||
fs.mkdirSync(deployDir, { recursive: true });
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
let frontendPath;
|
||||
if (isWindows) {
|
||||
frontendPath = path.join(__dirname, '..', 'frontend', 'dist').replace(/\\/g, '/');
|
||||
} else {
|
||||
frontendPath = '/var/www/idc';
|
||||
}
|
||||
|
||||
const nginxConfig = `# IDC设备管理系统 - Nginx配置
|
||||
# 生成时间: ${new Date().toISOString()}
|
||||
|
||||
server {
|
||||
listen ${config.frontendPort};
|
||||
server_name ${config.domain};
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
# 前端静态文件目录
|
||||
root "${frontendPath}";
|
||||
index index.html;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/x-javascript image/svg+xml;
|
||||
|
||||
# 静态资源缓存(1年)
|
||||
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|hdr)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# API代理到后端
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:${config.backendPort}/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# 文件上传代理
|
||||
location /uploads/ {
|
||||
proxy_pass http://127.0.0.1:${config.backendPort}/uploads/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
client_max_body_size 100M;
|
||||
}
|
||||
|
||||
# 前端路由支持(SPA单页应用,必须放在最后)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(deployDir, 'nginx-idc.conf'), nginxConfig);
|
||||
log.success('Nginx 配置文件已生成 (deploy/nginx-idc.conf)');
|
||||
|
||||
if (isWindows) {
|
||||
log.info('Windows Nginx 配置路径示例:');
|
||||
console.log(` 将配置文件复制到: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`);
|
||||
console.log(` 或修改主配置 include: ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Nginx 配置文件生成失败: ${error.message}`);
|
||||
throw new Error('Nginx config generation failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmConfiguration() {
|
||||
log.step('配置确认');
|
||||
|
||||
console.log('\n' + colors.bright + '部署配置摘要:' + colors.reset);
|
||||
console.log(` 数据库类型: ${colors.cyan}${config.dbType}${colors.reset}`);
|
||||
if (config.dbType === 'mysql') {
|
||||
console.log(` MySQL主机: ${colors.cyan}${config.dbConfig.host}:${config.dbConfig.port}${colors.reset}`);
|
||||
console.log(` 数据库名: ${colors.cyan}${config.dbConfig.database}${colors.reset}`);
|
||||
console.log(` 用户名: ${colors.cyan}${config.dbConfig.username}${colors.reset}`);
|
||||
}
|
||||
console.log(` 后端端口: ${colors.cyan}${config.backendPort}${colors.reset}`);
|
||||
console.log(` 运行环境: ${colors.cyan}${config.nodeEnv}${colors.reset}`);
|
||||
console.log(` 前端部署: ${colors.cyan}${config.frontendDeploy}${colors.reset}`);
|
||||
console.log(` 前端端口: ${colors.cyan}${config.frontendPort}${colors.reset}`);
|
||||
if (config.frontendDeploy === 'nginx') {
|
||||
console.log(` 域名: ${colors.cyan}${config.domain}${colors.reset}`);
|
||||
}
|
||||
|
||||
const confirm = await ask('\n确认以上配置并开始部署? (Y/n)', 'Y');
|
||||
if (confirm.toLowerCase() !== 'y') {
|
||||
log.info('已取消部署');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
log.divider();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateBackendEnv,
|
||||
generatePM2Config,
|
||||
generateNginxConfig,
|
||||
confirmConfiguration,
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
const { log } = require('./logger');
|
||||
const { config } = require('./config');
|
||||
|
||||
function httpHealthCheck(host, port, path = '/health', timeout = 5000) {
|
||||
return new Promise((resolve) => {
|
||||
const http = require('http');
|
||||
const req = http.get({ hostname: host, port, path, timeout }, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => { body += chunk; });
|
||||
res.on('end', () => {
|
||||
resolve({ success: res.statusCode >= 200 && res.statusCode < 400, statusCode: res.statusCode });
|
||||
});
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve({ success: false, error: 'timeout' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function healthCheck() {
|
||||
log.step('健康检查');
|
||||
log.subStep('检查后端服务状态...');
|
||||
|
||||
const maxRetries = 5;
|
||||
const retryDelay = 3000;
|
||||
|
||||
for (let i = 1; i <= maxRetries; i++) {
|
||||
const result = await httpHealthCheck('localhost', parseInt(config.backendPort));
|
||||
|
||||
if (result.success) {
|
||||
log.success('后端服务健康检查通过');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const detail = result.statusCode ? `HTTP ${result.statusCode}` : (result.error || '无响应');
|
||||
if (i < maxRetries) {
|
||||
log.subStep(`第 ${i} 次检查失败 (${detail}),${retryDelay/1000}秒后重试...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
}
|
||||
|
||||
log.warning('健康检查未通过,请手动验证服务状态');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
httpHealthCheck,
|
||||
healthCheck,
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
const { SCRIPT_VERSION, LOG_DIR } = require('./constants');
|
||||
const { colors, log, initLogFile, closeLogFile } = require('./logger');
|
||||
const { ask, closeReadline } = require('./ui');
|
||||
const { config, saveConfig, loadSavedConfig, applySavedConfig, parseArgs, showHelp } = require('./config');
|
||||
const { checkEnvironment } = require('./env-check');
|
||||
const { configureDatabase } = require('./database');
|
||||
const { configureServices, autoConfigureNginx } = require('./nginx');
|
||||
const { generateBackendEnv, generatePM2Config, generateNginxConfig, confirmConfiguration } = require('./generator');
|
||||
const { installDependencies, initDatabase, buildFrontend, startServices } = require('./installer');
|
||||
const { healthCheck } = require('./health');
|
||||
const { rollback } = require('./rollback');
|
||||
|
||||
let installStartTime = Date.now();
|
||||
|
||||
function printSummary() {
|
||||
const duration = ((Date.now() - installStartTime) / 1000).toFixed(1);
|
||||
|
||||
console.log(`
|
||||
${colors.bright}${colors.magenta}
|
||||
╔══════════════════════════════════════════════════════════╗
|
||||
║ 安装摘要 ║
|
||||
╚══════════════════════════════════════════════════════════╝${colors.reset}`);
|
||||
|
||||
console.log(`\n ${colors.cyan}安装耗时:${colors.reset} ${duration} 秒`);
|
||||
console.log(` ${colors.cyan}数据库类型:${colors.reset} ${config.dbType}`);
|
||||
console.log(` ${colors.cyan}后端端口:${colors.reset} ${config.backendPort}`);
|
||||
console.log(` ${colors.cyan}前端部署:${colors.reset} ${config.frontendDeploy}`);
|
||||
|
||||
console.log(`\n${colors.bright}服务管理命令:${colors.reset}`);
|
||||
console.log(` ${colors.cyan}pm2 status${colors.reset} 查看服务状态`);
|
||||
console.log(` ${colors.cyan}pm2 logs idc-backend${colors.reset} 查看后端日志`);
|
||||
console.log(` ${colors.cyan}pm2 restart idc-backend${colors.reset} 重启后端`);
|
||||
console.log(` ${colors.cyan}pm2 stop idc-backend${colors.reset} 停止后端`);
|
||||
|
||||
console.log(`\n${colors.bright}访问地址:${colors.reset}`);
|
||||
console.log(` 后端API: ${colors.cyan}http://localhost:${config.backendPort}/api${colors.reset}`);
|
||||
if (config.frontendDeploy === 'nginx') {
|
||||
console.log(` 前端页面: ${colors.cyan}http://${config.domain}:${config.frontendPort}${colors.reset}`);
|
||||
} else {
|
||||
console.log(` 前端页面: ${colors.cyan}http://localhost:${config.frontendPort}${colors.reset}`);
|
||||
}
|
||||
|
||||
console.log(`\n${colors.bright}更新升级:${colors.reset}`);
|
||||
console.log(` ${colors.cyan}node update.js${colors.reset} 一键更新`);
|
||||
|
||||
log.divider();
|
||||
log.success('安装部署完成!');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
installStartTime = Date.now();
|
||||
const cmdArgs = parseArgs();
|
||||
|
||||
if (cmdArgs.help) {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
initLogFile(LOG_DIR);
|
||||
|
||||
console.log(`
|
||||
${colors.bright}${colors.cyan}
|
||||
╔══════════════════════════════════════════════════════════╗
|
||||
║ IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION} ║
|
||||
║ Interactive Installation & Deployment Script ║
|
||||
╚══════════════════════════════════════════════════════════╝
|
||||
${colors.reset}`);
|
||||
|
||||
if (cmdArgs.nonInteractive) {
|
||||
log.info('运行模式: 非交互式(使用默认配置)');
|
||||
}
|
||||
|
||||
const savedConfig = loadSavedConfig();
|
||||
if (savedConfig) {
|
||||
log.info(`检测到上次安装配置 (${savedConfig.savedAt || '未知时间'})`);
|
||||
if (!cmdArgs.nonInteractive) {
|
||||
const useSaved = await ask('是否使用上次配置作为默认值? (Y/n)', 'Y');
|
||||
if (useSaved.toLowerCase() === 'y') {
|
||||
applySavedConfig(savedConfig);
|
||||
log.success('已加载上次配置');
|
||||
}
|
||||
} else {
|
||||
applySavedConfig(savedConfig);
|
||||
log.success('已加载上次配置');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEnvironment();
|
||||
await configureDatabase(cmdArgs);
|
||||
await configureServices(cmdArgs);
|
||||
|
||||
if (!cmdArgs.nonInteractive) {
|
||||
await confirmConfiguration();
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
|
||||
generateBackendEnv();
|
||||
generatePM2Config();
|
||||
generateNginxConfig();
|
||||
log.step('生成配置文件');
|
||||
log.success('所有配置文件已生成');
|
||||
|
||||
await installDependencies();
|
||||
await initDatabase();
|
||||
|
||||
if (!cmdArgs.skipBuild) {
|
||||
await buildFrontend();
|
||||
} else {
|
||||
log.info('已跳过前端构建');
|
||||
}
|
||||
|
||||
await startServices();
|
||||
|
||||
if (process.platform !== 'win32' && config.frontendDeploy === 'nginx') {
|
||||
await autoConfigureNginx();
|
||||
}
|
||||
|
||||
await healthCheck();
|
||||
printSummary();
|
||||
|
||||
} catch (error) {
|
||||
log.error(`部署失败: ${error.message}`);
|
||||
console.error(error);
|
||||
await rollback();
|
||||
process.exit(1);
|
||||
} finally {
|
||||
closeReadline();
|
||||
closeLogFile();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,242 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { log } = require('./logger');
|
||||
const { runCommand } = require('./utils');
|
||||
const { config } = require('./config');
|
||||
const { rollbackSteps } = require('./rollback');
|
||||
const { NPM_MIRRORS } = require('./constants');
|
||||
|
||||
function checkRegistryReachable(registry, timeout = 10000) {
|
||||
return new Promise((resolve) => {
|
||||
const url = new URL(registry);
|
||||
const http = url.protocol === 'https:' ? require('https') : require('http');
|
||||
const req = http.get(`${registry}/-/ping`, { timeout }, (res) => {
|
||||
res.resume();
|
||||
resolve(res.statusCode >= 200 && res.statusCode < 500);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
req.on('timeout', () => { req.destroy(); resolve(false); });
|
||||
});
|
||||
}
|
||||
|
||||
async function detectAvailableRegistry() {
|
||||
for (const mirror of NPM_MIRRORS) {
|
||||
log.subStep(`检测 ${mirror.name} (${mirror.registry})...`);
|
||||
const reachable = await checkRegistryReachable(mirror.registry);
|
||||
if (reachable) {
|
||||
log.success(`${mirror.name} 可用`);
|
||||
return mirror.registry;
|
||||
}
|
||||
log.subStep(`${mirror.name} 不可达`);
|
||||
}
|
||||
return NPM_MIRRORS[0].registry;
|
||||
}
|
||||
|
||||
async function npmInstallWithRetry(cwd, label) {
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 3000;
|
||||
|
||||
const availableRegistry = await detectAvailableRegistry();
|
||||
const isDefaultRegistry = availableRegistry === NPM_MIRRORS[0].registry;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
if (attempt > 1) {
|
||||
const delay = baseDelay * Math.pow(2, attempt - 2);
|
||||
log.subStep(`第 ${attempt}/${maxRetries} 次重试,${delay / 1000}秒后...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
let command = 'npm install';
|
||||
if (!isDefaultRegistry) {
|
||||
command += ` --registry=${availableRegistry}`;
|
||||
}
|
||||
|
||||
log.info(`安装${label}依赖${attempt > 1 ? ` (第${attempt}次尝试)` : ''}...`);
|
||||
const result = runCommand(command, { cwd });
|
||||
|
||||
if (result.success) {
|
||||
const nodeModulesPath = path.join(cwd, 'node_modules');
|
||||
if (fs.existsSync(nodeModulesPath)) {
|
||||
return { success: true };
|
||||
}
|
||||
log.subStep(`${label} node_modules 不存在,安装可能未完成`);
|
||||
} else {
|
||||
log.subStep(`${label}依赖安装失败: ${result.error || '未知错误'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, error: `${label}依赖安装失败(已重试${maxRetries}次)` };
|
||||
}
|
||||
|
||||
async function installDependencies() {
|
||||
log.step('安装依赖');
|
||||
|
||||
const backendDir = path.join(__dirname, '..', 'backend');
|
||||
const frontendDir = path.join(__dirname, '..', 'frontend');
|
||||
|
||||
log.info('安装后端依赖...');
|
||||
const backendResult = await npmInstallWithRetry(backendDir, '后端');
|
||||
if (backendResult.success) {
|
||||
log.success('后端依赖安装完成');
|
||||
rollbackSteps.push(() => {
|
||||
const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules';
|
||||
runCommand(rmCmd, { cwd: backendDir, silent: true });
|
||||
});
|
||||
} else {
|
||||
log.error(backendResult.error);
|
||||
throw new Error('Backend install failed');
|
||||
}
|
||||
|
||||
log.info('安装前端依赖...');
|
||||
const frontendResult = await npmInstallWithRetry(frontendDir, '前端');
|
||||
if (frontendResult.success) {
|
||||
log.success('前端依赖安装完成');
|
||||
rollbackSteps.push(() => {
|
||||
const rmCmd = process.platform === 'win32' ? 'rmdir /s /q node_modules' : 'rm -rf node_modules';
|
||||
runCommand(rmCmd, { cwd: frontendDir, silent: true });
|
||||
});
|
||||
} else {
|
||||
log.error(frontendResult.error);
|
||||
throw new Error('Frontend install failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function initDatabase() {
|
||||
log.step('数据库初始化');
|
||||
|
||||
const initScript = path.join(__dirname, '..', 'backend', 'scripts', 'init-database.js');
|
||||
|
||||
if (!fs.existsSync(initScript)) {
|
||||
log.warning('未找到数据库初始化脚本,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('正在初始化数据库...');
|
||||
const result = runCommand('node scripts/init-database.js', {
|
||||
cwd: path.join(__dirname, '..', 'backend')
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
log.success('数据库初始化完成');
|
||||
} else {
|
||||
log.error('数据库初始化失败');
|
||||
log.info('请检查数据库配置并手动运行: node backend/scripts/init-database.js');
|
||||
throw new Error('Database initialization failed');
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailableMemoryMB() {
|
||||
try {
|
||||
const os = require('os');
|
||||
return Math.round(os.freemem() / 1024 / 1024);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function buildFrontend() {
|
||||
log.step('构建前端');
|
||||
|
||||
const frontendDir = path.join(__dirname, '..', 'frontend');
|
||||
|
||||
const freeMemMB = getAvailableMemoryMB();
|
||||
let buildCommand = 'npm run build';
|
||||
|
||||
if (freeMemMB !== null) {
|
||||
if (freeMemMB < 512) {
|
||||
log.warning(`系统可用内存较低 (${freeMemMB}MB),前端构建可能失败`);
|
||||
log.info('建议增加内存或创建 swap 分区后再试');
|
||||
} else if (freeMemMB < 1024) {
|
||||
log.info(`可用内存 ${freeMemMB}MB,限制 Node 堆栈为 512MB...`);
|
||||
buildCommand = 'node --max_old_space_size=512 node_modules/vite/bin/vite.js build';
|
||||
}
|
||||
}
|
||||
|
||||
log.info('执行前端构建...');
|
||||
const result = runCommand(buildCommand, { cwd: frontendDir });
|
||||
|
||||
if (result.success) {
|
||||
log.success('前端构建完成');
|
||||
rollbackSteps.push(() => {
|
||||
const distDir = path.join(frontendDir, 'dist');
|
||||
if (fs.existsSync(distDir)) {
|
||||
const rmCmd = process.platform === 'win32' ? 'rmdir /s /q dist' : 'rm -rf dist';
|
||||
runCommand(rmCmd, { cwd: frontendDir, silent: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.error('前端构建失败');
|
||||
throw new Error('Frontend build failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function startServices() {
|
||||
log.step('启动服务');
|
||||
|
||||
const logDir = path.join(__dirname, '..', 'backend', 'logs');
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.info('停止已有服务...');
|
||||
const stopCmd = process.platform === 'win32'
|
||||
? 'pm2 stop idc-backend idc-frontend 2>nul || exit 0'
|
||||
: 'pm2 stop idc-backend idc-frontend 2>/dev/null || true';
|
||||
runCommand(stopCmd, { silent: true });
|
||||
|
||||
log.info('启动后端服务...');
|
||||
const backendResult = runCommand(
|
||||
`pm2 start server.js --name "idc-backend" --env ${config.nodeEnv}`,
|
||||
{ cwd: path.join(__dirname, '..', 'backend') }
|
||||
);
|
||||
|
||||
if (backendResult.success) {
|
||||
log.success(`后端服务已启动 (端口: ${config.backendPort})`);
|
||||
rollbackSteps.push(() => {
|
||||
runCommand('pm2 stop idc-backend', { silent: true });
|
||||
runCommand('pm2 delete idc-backend', { silent: true });
|
||||
});
|
||||
} else {
|
||||
log.error('后端服务启动失败');
|
||||
log.info('请检查后端日志: pm2 logs idc-backend');
|
||||
throw new Error('Backend service start failed');
|
||||
}
|
||||
|
||||
if (config.frontendDeploy === 'pm2') {
|
||||
log.info('安装 serve 包...');
|
||||
runCommand('npm install -g serve', { silent: true });
|
||||
|
||||
log.info('启动前端服务...');
|
||||
const frontendResult = runCommand(
|
||||
`pm2 start serve --name "idc-frontend" -- -s dist -l ${config.frontendPort}`,
|
||||
{ cwd: path.join(__dirname, '..', 'frontend') }
|
||||
);
|
||||
|
||||
if (frontendResult.success) {
|
||||
log.success(`前端服务已启动 (端口: ${config.frontendPort})`);
|
||||
rollbackSteps.push(() => {
|
||||
runCommand('pm2 stop idc-frontend', { silent: true });
|
||||
runCommand('pm2 delete idc-frontend', { silent: true });
|
||||
});
|
||||
} else {
|
||||
log.error('前端服务启动失败');
|
||||
log.info('请检查前端日志: pm2 logs idc-frontend');
|
||||
throw new Error('Frontend service start failed');
|
||||
}
|
||||
}
|
||||
|
||||
runCommand('pm2 save', { silent: true });
|
||||
|
||||
log.divider();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkRegistryReachable,
|
||||
detectAvailableRegistry,
|
||||
npmInstallWithRetry,
|
||||
installDependencies,
|
||||
initDatabase,
|
||||
getAvailableMemoryMB,
|
||||
buildFrontend,
|
||||
startServices,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
const { INSTALL_STEPS } = require('./constants');
|
||||
|
||||
let currentStepIndex = 0;
|
||||
let logFileStream = null;
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
gray: '\x1b[90m',
|
||||
magenta: '\x1b[35m'
|
||||
};
|
||||
|
||||
const log = {
|
||||
info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`),
|
||||
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
||||
warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
|
||||
error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
|
||||
step: (msg) => {
|
||||
const idx = INSTALL_STEPS.indexOf(msg);
|
||||
if (idx >= 0) {
|
||||
currentStepIndex = idx;
|
||||
}
|
||||
const progress = currentStepIndex < INSTALL_STEPS.length
|
||||
? `${colors.gray}[${currentStepIndex + 1}/${INSTALL_STEPS.length}]${colors.reset} `
|
||||
: '';
|
||||
console.log(`\n${progress}${colors.bright}${colors.cyan}▶ ${msg}${colors.reset}`);
|
||||
},
|
||||
divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`),
|
||||
subStep: (msg) => console.log(` ${colors.gray}└${colors.reset} ${msg}`)
|
||||
};
|
||||
|
||||
function initLogFile(LOG_DIR) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
if (!fs.existsSync(LOG_DIR)) {
|
||||
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
}
|
||||
const logFileName = `install_${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
|
||||
const logFilePath = path.join(LOG_DIR, logFileName);
|
||||
logFileStream = fs.createWriteStream(logFilePath, { flags: 'a' });
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
console.log = (...args) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'string' ? arg.replace(/\x1b\[[0-9;]*m/g, '') : String(arg)
|
||||
).join(' ');
|
||||
logFileStream.write(`[${timestamp}] ${message}\n`);
|
||||
originalConsoleLog.apply(console, args);
|
||||
};
|
||||
}
|
||||
|
||||
function closeLogFile() {
|
||||
if (logFileStream) {
|
||||
logFileStream.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
colors,
|
||||
log,
|
||||
initLogFile,
|
||||
closeLogFile,
|
||||
};
|
||||
@@ -0,0 +1,404 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { colors, log } = require('./logger');
|
||||
const { ask, select } = require('./ui');
|
||||
const { config } = require('./config');
|
||||
const { runCommand } = require('./utils');
|
||||
const { isNginxInstalled, detectLinuxDistro, isRootUser } = require('./env-check');
|
||||
|
||||
function showNginxInstallGuideLinux() {
|
||||
const distro = detectLinuxDistro();
|
||||
const root = isRootUser();
|
||||
const sudoPrefix = root ? '' : 'sudo ';
|
||||
|
||||
console.log('\n' + colors.bright + 'Nginx 安装命令:' + colors.reset);
|
||||
if (root) {
|
||||
console.log(colors.gray + '(当前以 root 用户运行,无需 sudo)' + colors.reset);
|
||||
}
|
||||
|
||||
switch (distro) {
|
||||
case 'ubuntu':
|
||||
case 'debian':
|
||||
console.log(` ${colors.cyan}${sudoPrefix}apt update${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}apt install -y nginx${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}systemctl start nginx${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}systemctl enable nginx${colors.reset}`);
|
||||
break;
|
||||
|
||||
case 'centos':
|
||||
case 'rhel':
|
||||
case 'fedora':
|
||||
console.log(` ${colors.cyan}${sudoPrefix}yum install -y epel-release${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}yum install -y nginx${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}systemctl start nginx${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}systemctl enable nginx${colors.reset}`);
|
||||
break;
|
||||
|
||||
case 'arch':
|
||||
console.log(` ${colors.cyan}${sudoPrefix}pacman -S --noconfirm nginx${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}systemctl start nginx${colors.reset}`);
|
||||
console.log(` ${colors.cyan}${sudoPrefix}systemctl enable nginx${colors.reset}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(` ${colors.cyan}# Ubuntu/Debian${colors.reset}`);
|
||||
console.log(` ${sudoPrefix}apt update && ${sudoPrefix}apt install -y nginx`);
|
||||
console.log(`\n ${colors.cyan}# CentOS/RHEL/Fedora${colors.reset}`);
|
||||
console.log(` ${sudoPrefix}yum install -y epel-release && ${sudoPrefix}yum install -y nginx`);
|
||||
console.log(`\n ${colors.cyan}# Arch Linux${colors.reset}`);
|
||||
console.log(` ${sudoPrefix}pacman -S nginx`);
|
||||
}
|
||||
|
||||
console.log('\n' + colors.gray + '安装完成后,请重新运行此部署脚本。' + colors.reset);
|
||||
}
|
||||
|
||||
async function autoInstallNginxLinux() {
|
||||
const distro = detectLinuxDistro();
|
||||
const root = isRootUser();
|
||||
const sudoPrefix = root ? '' : 'sudo ';
|
||||
|
||||
log.step('自动安装 Nginx');
|
||||
log.info(`检测到 Linux 发行版: ${distro}`);
|
||||
if (root) {
|
||||
log.info('当前以 root 用户运行,无需 sudo');
|
||||
}
|
||||
|
||||
try {
|
||||
let installCommand = '';
|
||||
let startCommand = `${sudoPrefix}systemctl start nginx`;
|
||||
let enableCommand = `${sudoPrefix}systemctl enable nginx`;
|
||||
|
||||
switch (distro) {
|
||||
case 'ubuntu':
|
||||
case 'debian':
|
||||
log.info('使用 apt 安装 Nginx...');
|
||||
log.info(`执行: ${sudoPrefix}apt update`);
|
||||
const updateResult = runCommand(`${sudoPrefix}apt update`);
|
||||
if (!updateResult.success) {
|
||||
log.warning('apt update 失败,尝试继续安装...');
|
||||
}
|
||||
|
||||
log.info(`执行: ${sudoPrefix}apt install -y nginx`);
|
||||
installCommand = `${sudoPrefix}apt install -y nginx`;
|
||||
break;
|
||||
|
||||
case 'centos':
|
||||
case 'rhel':
|
||||
case 'fedora':
|
||||
log.info('使用 yum 安装 Nginx...');
|
||||
log.info(`执行: ${sudoPrefix}yum install -y epel-release`);
|
||||
const epelResult = runCommand(`${sudoPrefix}yum install -y epel-release`);
|
||||
if (!epelResult.success) {
|
||||
log.warning('epel-release 安装失败,尝试继续...');
|
||||
}
|
||||
|
||||
log.info(`执行: ${sudoPrefix}yum install -y nginx`);
|
||||
installCommand = `${sudoPrefix}yum install -y nginx`;
|
||||
break;
|
||||
|
||||
case 'arch':
|
||||
log.info('使用 pacman 安装 Nginx...');
|
||||
log.info(`执行: ${sudoPrefix}pacman -S --noconfirm nginx`);
|
||||
installCommand = `${sudoPrefix}pacman -S --noconfirm nginx`;
|
||||
break;
|
||||
|
||||
default:
|
||||
log.error('未知的 Linux 发行版,无法自动安装');
|
||||
return false;
|
||||
}
|
||||
|
||||
const installResult = runCommand(installCommand);
|
||||
if (!installResult.success) {
|
||||
log.error('Nginx 安装失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
log.success('Nginx 安装完成');
|
||||
|
||||
log.info('启动 Nginx 服务...');
|
||||
const startResult = runCommand(startCommand);
|
||||
if (!startResult.success) {
|
||||
log.warning('Nginx 启动失败,可能需要手动启动');
|
||||
} else {
|
||||
log.success('Nginx 服务已启动');
|
||||
}
|
||||
|
||||
log.info('设置开机自启...');
|
||||
runCommand(enableCommand, { silent: true });
|
||||
|
||||
const checkResult = runCommand('nginx -v', { silent: true });
|
||||
if (checkResult.success) {
|
||||
log.success(`Nginx 安装成功: ${checkResult.output.trim()}`);
|
||||
return true;
|
||||
} else {
|
||||
log.error('Nginx 安装验证失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log.error(`自动安装失败: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function autoInstallNginx() {
|
||||
log.step('自动安装 Nginx');
|
||||
log.info('正在下载 Nginx...');
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const nginxUrl = 'https://nginx.org/download/nginx-1.24.0.zip';
|
||||
const downloadPath = path.join(__dirname, '..', 'nginx-1.24.0.zip');
|
||||
const installPath = 'C:\\nginx';
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(downloadPath);
|
||||
https.get(nginxUrl, (response) => {
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
log.success('Nginx 下载完成');
|
||||
|
||||
log.info('正在解压...');
|
||||
execSync(`powershell -Command "Expand-Archive -Path '${downloadPath}' -DestinationPath 'C:\\\\' -Force"`, { stdio: 'inherit' });
|
||||
|
||||
if (fs.existsSync('C:\\nginx-1.24.0')) {
|
||||
if (fs.existsSync(installPath)) {
|
||||
fs.rmSync(installPath, { recursive: true });
|
||||
}
|
||||
fs.renameSync('C:\\nginx-1.24.0', installPath);
|
||||
}
|
||||
|
||||
fs.unlinkSync(downloadPath);
|
||||
|
||||
log.success(`Nginx 已安装到 ${installPath}`);
|
||||
log.info('安装后操作:');
|
||||
console.log(` 1. 将配置文件复制到: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`);
|
||||
console.log(` 2. 修改主配置: 在 C:/nginx/conf/nginx.conf 的 http 段添加: ${colors.cyan}include conf.d/*.conf;${colors.reset}`);
|
||||
console.log(` 3. 启动 Nginx: ${colors.cyan}C:/nginx/nginx.exe${colors.reset}`);
|
||||
|
||||
} catch (error) {
|
||||
log.error('Nginx 自动安装失败: ' + error.message);
|
||||
log.info('请手动下载安装: https://nginx.org/en/download.html');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function autoConfigureNginx() {
|
||||
log.step('配置 Nginx');
|
||||
|
||||
if (!isNginxInstalled()) {
|
||||
log.warning('Nginx 未安装,尝试自动安装...');
|
||||
if (process.platform === 'linux') {
|
||||
const installed = await autoInstallNginxLinux();
|
||||
if (!installed) {
|
||||
log.error('Nginx 自动安装失败');
|
||||
log.info('请手动安装 Nginx 后执行:');
|
||||
console.log(` ${colors.cyan}sudo cp deploy/nginx-idc.conf /etc/nginx/sites-available/idc${colors.reset}`);
|
||||
console.log(` ${colors.cyan}sudo ln -sf /etc/nginx/sites-available/idc /etc/nginx/sites-enabled/idc${colors.reset}`);
|
||||
console.log(` ${colors.cyan}sudo nginx -t && sudo systemctl restart nginx${colors.reset}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
log.error('当前平台不支持自动安装 Nginx,请手动安装');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const root = isRootUser();
|
||||
const sudoPrefix = root ? '' : 'sudo ';
|
||||
|
||||
const wwwDir = '/var/www/idc';
|
||||
const distDir = path.join(__dirname, '..', 'frontend', 'dist');
|
||||
|
||||
if (fs.existsSync(distDir)) {
|
||||
log.info(`复制前端文件到 ${wwwDir}...`);
|
||||
runCommand(`${sudoPrefix}mkdir -p ${wwwDir}`, { silent: true });
|
||||
const copyResult = runCommand(`${sudoPrefix}cp -r ${distDir}/* ${wwwDir}/`, { silent: true });
|
||||
if (copyResult.success) {
|
||||
runCommand(`${sudoPrefix}chmod -R 755 ${wwwDir}`, { silent: true });
|
||||
log.success('前端文件部署完成');
|
||||
} else {
|
||||
log.warning('前端文件复制失败,Nginx 可能无法访问前端页面');
|
||||
}
|
||||
} else {
|
||||
log.warning(`前端构建产物不存在: ${distDir}`);
|
||||
log.info('请先完成前端构建后再配置 Nginx');
|
||||
}
|
||||
|
||||
const configSource = path.join(__dirname, '..', 'deploy', 'nginx-idc.conf');
|
||||
|
||||
if (!fs.existsSync(configSource)) {
|
||||
log.error('Nginx 配置文件不存在,请先运行安装脚本');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let configDir = '';
|
||||
let useSitesAvailable = false;
|
||||
|
||||
if (fs.existsSync('/etc/nginx/sites-available')) {
|
||||
configDir = '/etc/nginx/sites-available';
|
||||
useSitesAvailable = true;
|
||||
} else if (fs.existsSync('/etc/nginx/conf.d')) {
|
||||
configDir = '/etc/nginx/conf.d';
|
||||
useSitesAvailable = false;
|
||||
} else {
|
||||
log.warning('未找到标准 Nginx 配置目录,请手动配置');
|
||||
return;
|
||||
}
|
||||
|
||||
const configDest = path.join(configDir, 'idc');
|
||||
log.info(`复制配置到 ${configDest}...`);
|
||||
|
||||
const copyResult = runCommand(`${sudoPrefix}cp "${configSource}" "${configDest}"`);
|
||||
if (!copyResult.success) {
|
||||
log.error('配置文件复制失败');
|
||||
return;
|
||||
}
|
||||
|
||||
if (useSitesAvailable) {
|
||||
log.info('创建站点软链接...');
|
||||
runCommand(`${sudoPrefix}ln -sf "${configDest}" /etc/nginx/sites-enabled/idc`);
|
||||
|
||||
if (fs.existsSync('/etc/nginx/sites-enabled/default')) {
|
||||
log.info('删除默认站点配置...');
|
||||
runCommand(`${sudoPrefix}rm -f /etc/nginx/sites-enabled/default`, { silent: true });
|
||||
}
|
||||
}
|
||||
|
||||
log.info('测试 Nginx 配置...');
|
||||
const testResult = runCommand(`${sudoPrefix}nginx -t`);
|
||||
if (!testResult.success) {
|
||||
log.error('Nginx 配置测试失败');
|
||||
log.info('请检查配置文件: ' + configDest);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('重启 Nginx 服务...');
|
||||
const restartResult = runCommand(`${sudoPrefix}systemctl restart nginx`);
|
||||
if (restartResult.success) {
|
||||
log.success('Nginx 配置完成并已生效');
|
||||
} else {
|
||||
log.warning('systemctl 重启失败,尝试直接重载...');
|
||||
const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`);
|
||||
if (reloadResult.success) {
|
||||
log.success('Nginx 配置完成并已生效');
|
||||
} else {
|
||||
log.warning('Nginx 重载失败,请手动重启: sudo systemctl restart nginx');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log.error(`自动配置失败: ${error.message}`);
|
||||
log.info('请手动配置 Nginx');
|
||||
}
|
||||
}
|
||||
|
||||
async function configureServices(cmdArgs) {
|
||||
log.step('服务配置');
|
||||
|
||||
if (cmdArgs?.nonInteractive && cmdArgs?.backendPort) {
|
||||
config.backendPort = cmdArgs.backendPort;
|
||||
log.info(`使用命令行参数: 后端端口 = ${config.backendPort}`);
|
||||
} else if (cmdArgs?.nonInteractive) {
|
||||
config.backendPort = '8000';
|
||||
log.info('使用默认配置: 后端端口 = 8000');
|
||||
} else {
|
||||
config.backendPort = await ask('后端服务端口', '8000');
|
||||
}
|
||||
|
||||
if (cmdArgs?.nonInteractive) {
|
||||
config.nodeEnv = 'production';
|
||||
log.info('使用默认配置: 运行环境 = production');
|
||||
} else {
|
||||
config.nodeEnv = await select('选择运行环境:', [
|
||||
{ label: 'production(生产模式,性能优化,推荐正式部署)', value: 'production' },
|
||||
{ label: 'development(开发模式,详细日志,便于调试)', value: 'development' }
|
||||
]);
|
||||
}
|
||||
|
||||
if (cmdArgs?.nonInteractive || cmdArgs?.skipNginx) {
|
||||
config.frontendDeploy = 'pm2';
|
||||
config.frontendPort = '3000';
|
||||
log.info('使用默认配置: 前端部署 = PM2 serve (端口 3000)');
|
||||
} else {
|
||||
config.frontendDeploy = await select('前端部署方式:', [
|
||||
{ label: 'Nginx(性能最优,推荐生产环境)', value: 'nginx' },
|
||||
{ label: 'PM2 serve(简单快捷,无需额外安装)', value: 'pm2' }
|
||||
]);
|
||||
}
|
||||
|
||||
if (config.frontendDeploy === 'nginx') {
|
||||
const nginxInstalled = isNginxInstalled();
|
||||
|
||||
if (!nginxInstalled) {
|
||||
log.warning('未检测到 Nginx 安装');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
if (!cmdArgs?.nonInteractive) {
|
||||
const autoInstall = await ask('是否自动下载并安装 Nginx? (Y/n)', 'Y');
|
||||
if (autoInstall.toLowerCase() === 'y') {
|
||||
await autoInstallNginx();
|
||||
} else {
|
||||
log.info('已跳过自动安装,请手动安装 Nginx 后启动服务');
|
||||
console.log(` 下载地址: ${colors.cyan}https://nginx.org/en/download.html${colors.reset}`);
|
||||
console.log(` 安装路径建议: ${colors.cyan}C:/nginx${colors.reset}`);
|
||||
}
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (!cmdArgs?.nonInteractive) {
|
||||
const autoInstall = await ask('是否自动安装 Nginx? (Y/n)', 'Y');
|
||||
if (autoInstall.toLowerCase() === 'y') {
|
||||
const installed = await autoInstallNginxLinux();
|
||||
if (!installed) {
|
||||
log.error('Nginx 自动安装失败');
|
||||
log.info('请手动安装后重新运行脚本');
|
||||
showNginxInstallGuideLinux();
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
log.info('已跳过自动安装');
|
||||
showNginxInstallGuideLinux();
|
||||
const continueDeploy = await ask('是否继续部署(后端将启动,前端需手动配置 Nginx)? (Y/n)', 'Y');
|
||||
if (continueDeploy.toLowerCase() !== 'y') {
|
||||
log.info('已取消部署,请安装 Nginx 后重新运行');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info('请使用 Homebrew 安装 Nginx:');
|
||||
console.log(` ${colors.cyan}brew install nginx${colors.reset}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cmdArgs?.nonInteractive) {
|
||||
config.frontendPort = await ask('Nginx 监听端口', '80');
|
||||
config.domain = await ask('域名(没有则填localhost)', 'localhost');
|
||||
} else {
|
||||
config.frontendPort = '80';
|
||||
config.domain = 'localhost';
|
||||
}
|
||||
} else {
|
||||
if (!cmdArgs?.nonInteractive) {
|
||||
config.frontendPort = await ask('前端服务端口', '3000');
|
||||
}
|
||||
}
|
||||
|
||||
log.divider();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
showNginxInstallGuideLinux,
|
||||
autoInstallNginxLinux,
|
||||
autoInstallNginx,
|
||||
autoConfigureNginx,
|
||||
configureServices,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
const { log } = require('./logger');
|
||||
const { runCommand } = require('./utils');
|
||||
|
||||
const rollbackSteps = [];
|
||||
|
||||
function addRollbackStep(fn) {
|
||||
rollbackSteps.push(fn);
|
||||
}
|
||||
|
||||
async function rollback() {
|
||||
if (rollbackSteps.length === 0) {
|
||||
log.info('无需回滚');
|
||||
return;
|
||||
}
|
||||
|
||||
log.step('回滚');
|
||||
log.warning('安装失败,正在清理已安装的内容...');
|
||||
|
||||
const totalSteps = rollbackSteps.length;
|
||||
let completedSteps = 0;
|
||||
|
||||
for (const step of rollbackSteps.reverse()) {
|
||||
try {
|
||||
await step();
|
||||
completedSteps++;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const configFiles = [
|
||||
path.join(__dirname, '..', 'backend', '.env'),
|
||||
path.join(__dirname, '..', 'ecosystem.config.js'),
|
||||
path.join(__dirname, '..', 'deploy', 'nginx-idc.conf'),
|
||||
];
|
||||
|
||||
for (const file of configFiles) {
|
||||
try {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
runCommand('pm2 save', { silent: true });
|
||||
} catch {
|
||||
}
|
||||
|
||||
log.info(`已回滚 ${completedSteps}/${totalSteps} 个步骤`);
|
||||
log.info('如需重新安装,请再次运行: node install.js');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
rollbackSteps,
|
||||
addRollbackStep,
|
||||
rollback,
|
||||
};
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
const readline = require('readline');
|
||||
const { log } = require('./logger');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.on('close', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
rl.on('error', (err) => {
|
||||
log.error(`输入错误: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function ask(question, defaultValue = '') {
|
||||
return new Promise((resolve) => {
|
||||
const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
||||
rl.question(prompt, (answer) => {
|
||||
resolve(answer.trim() || defaultValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function askPassword(question) {
|
||||
return new Promise((resolve) => {
|
||||
const prompt = `${question}: `;
|
||||
process.stdout.write(prompt);
|
||||
|
||||
let password = '';
|
||||
const stdin = process.stdin;
|
||||
const stdout = process.stdout;
|
||||
|
||||
const cleanup = () => {
|
||||
if (stdin.isTTY) {
|
||||
try {
|
||||
stdin.setRawMode(false);
|
||||
} catch {}
|
||||
}
|
||||
stdin.removeListener('data', onData);
|
||||
};
|
||||
|
||||
const onData = (data) => {
|
||||
const char = data.toString();
|
||||
const code = char.charCodeAt(0);
|
||||
|
||||
if (code === 10 || code === 13) {
|
||||
cleanup();
|
||||
stdout.write('\n');
|
||||
resolve(password);
|
||||
} else if (code === 3) {
|
||||
cleanup();
|
||||
stdout.write('\n已取消\n');
|
||||
process.exit(0);
|
||||
} else if (code === 127 || code === 8) {
|
||||
if (password.length > 0) {
|
||||
password = password.slice(0, -1);
|
||||
stdout.write('\b \b');
|
||||
}
|
||||
} else if (code >= 32) {
|
||||
password += char;
|
||||
stdout.write('*');
|
||||
}
|
||||
};
|
||||
|
||||
if (stdin.isTTY) {
|
||||
stdin.setRawMode(true);
|
||||
stdin.resume();
|
||||
stdin.on('data', onData);
|
||||
} else {
|
||||
rl.question('', (answer) => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function select(question, options) {
|
||||
const { colors } = require('./logger');
|
||||
console.log(`\n${question}`);
|
||||
options.forEach((opt, idx) => {
|
||||
console.log(` ${colors.cyan}${idx + 1}.${colors.reset} ${opt.label}`);
|
||||
});
|
||||
|
||||
const answer = await ask('请选择', '1');
|
||||
const index = parseInt(answer) - 1;
|
||||
|
||||
return options[index]?.value || options[0].value;
|
||||
}
|
||||
|
||||
function closeReadline() {
|
||||
rl.close();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ask,
|
||||
askPassword,
|
||||
select,
|
||||
closeReadline,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
const { execSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function runCommand(command, options = {}) {
|
||||
try {
|
||||
const result = execSync(command, {
|
||||
encoding: 'utf8',
|
||||
stdio: options.silent ? 'pipe' : 'inherit',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
shell: true
|
||||
});
|
||||
return { success: true, output: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function commandExists(command) {
|
||||
try {
|
||||
execSync(`${command} --version`, { stdio: 'pipe', shell: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function generateSecretKey(length = 64) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runCommand,
|
||||
commandExists,
|
||||
generateSecretKey,
|
||||
};
|
||||
Reference in New Issue
Block a user