diff --git a/docs/CABLE_WIZARD_GUIDE.md b/docs/CABLE_WIZARD_GUIDE.md deleted file mode 100644 index e1dea88..0000000 --- a/docs/CABLE_WIZARD_GUIDE.md +++ /dev/null @@ -1,186 +0,0 @@ -# 向导式接线创建功能使用指南 - -## 功能概述 - -新的向导式接线创建功能提供了一种直观、交互式的接线创建流程,帮助用户更高效地管理设备间的物理连接。 - -## 核心特性 - -### 1. 四步向导流程 - -#### 步骤 1: 选择源设备 -- 搜索并选择接线的起点设备 -- 支持设备类型过滤(服务器、交换机、存储设备) -- 点击设备卡片快速选择 - -#### 步骤 2: 选择目标设备 -- 搜索并选择接线的终点设备 -- 实时端口冲突检测 -- 高亮显示已占用端口 - -#### 步骤 3: 线缆配置 -- **线缆类型选择**: - - 🌐 以太网线(适用于1G/10G短距离连接) - - 🔦 光纤(适用于长距离或高带宽需求) - - 🔌 铜缆(适用于电源或特殊设备连接) - -- **线缆长度选择**: - - 提供常用长度选项(1m, 2m, 3m, 5m, 7m, 10m, 15m, 20m, 30m, 50m) - - **自动计算建议长度**:根据源/目标设备的机柜位置自动估算所需长度 - -- **线缆属性设置**: - - 自定义线缆标签(支持自动生成或手动输入) - - 添加备注说明 - -#### 步骤 4: 预览确认 -- 全面展示接线信息 -- 可视化连接预览 -- 确认创建接线 - -### 2. 端口可视化面板 - -- **端口状态颜色编码**: - - 🟦 灰色:空闲端口 - - 🟩 绿色:已连接端口 - - 🟥 红色:故障端口 - -- **端口信息提示**: - - 悬停显示端口详细信息 - - 显示端口类型、速率、VLAN - - 显示已连接的对端设备信息 - -- **智能端口选择**: - - 仅显示可用端口 - - 自动过滤已占用端口 - - 冲突端口高亮显示 - -### 3. 冲突检测 - -- 实时检测端口占用情况 -- 显示冲突的详细信息 -- 提供解决方案建议 - -## 使用方法 - -### 从设备管理页面创建接线 - -1. 进入**设备管理**页面 -2. 在交换机设备卡片上找到 **"添加接线"** 按钮 -3. 点击按钮,打开向导式创建界面 -4. 按照四步向导完成接线创建 - -### 从机柜3D视图创建接线 - -1. 进入**机柜3D可视化**页面 -2. 在3D场景中选择源设备 -3. 点击设备上的端口 -4. 选择目标设备的端口 -5. 确认接线配置 - -## 新增字段说明 - -### Cable 模型新增字段 - -| 字段名 | 类型 | 说明 | 示例 | -|--------|------|------|------| -| `cableLabel` | VARCHAR(255) | 线缆标签/编号 | CABLE-001-002-1234 | -| `cableColor` | VARCHAR(50) | 线缆颜色 | "红色", "蓝色", "黄色" | -| `installedBy` | VARCHAR(100) | 安装人 | "张三" | -| `installedAt` | DATETIME | 安装时间 | "2024-01-15 10:30:00" | -| `lastTestedAt` | DATETIME | 上次测试时间 | "2024-01-15 11:00:00" | - -## 技术实现 - -### 前端组件 - -- **CableWizardModal**: 向导式接线创建主组件 -- **PortPanel**: 端口可视化面板 -- **framer-motion**: 动画效果 - -### 后端 API - -- `POST /api/cables`: 创建接线(支持新字段) -- `POST /api/cables/check-conflict`: 检查端口冲突 -- `GET /api/device-ports/device/:deviceId`: 获取设备端口列表 - -### 数据库迁移 - -运行以下命令添加新字段: - -```bash -cd backend -node scripts/migrate-cable-fields.js -``` - -## 最佳实践 - -### 1. 线缆标签规范 - -建议使用以下格式生成线缆标签: - -``` -CABLE-{源设备ID后4位}-{目标设备ID后4位}-{时间戳后4位} -``` - -示例: -- `CABLE-0001-0002-1234` -- `CABLE-SRV01-SW01-5678` - -### 2. 线缆长度估算 - -系统会根据设备所在机柜的位置自动估算所需长度: - -``` -建议长度 = |源机柜ID - 目标机柜ID| × 0.5 + 3 -``` - -最小长度:3m -最大长度:50m - -### 3. 端口选择建议 - -- 优先选择相邻的端口(便于管理) -- 同一设备的端口尽量连续分配 -- 考虑线缆走线路径,避免交叉 - -## 常见问题 - -### Q1: 如何查看已创建的接线? - -**A**: 在接线管理页面可以查看所有接线,支持按状态、类型、设备等条件筛选。 - -### Q2: 如何编辑已创建的接线? - -**A**: 点击接线记录的"编辑"按钮,修改后保存即可。 - -### Q3: 如何删除接线? - -**A**: 点击接线记录的"删除"按钮,确认后删除。 - -### Q4: 端口冲突如何解决? - -**A**: 系统会高亮显示冲突端口,建议选择其他空闲端口或先删除原有连接。 - -## 未来扩展 - -### 计划功能 - -1. **批量接线创建** - - 支持 Excel 批量导入 - - 支持模板下载 - -2. **3D 线缆渲染** - - 在 3D 场景中显示线缆走向 - - 支持线缆路径追踪 - -3. **接线统计报表** - - 按类型/长度/状态统计 - - 导出 Excel 报表 - -4. **工单联动** - - 接线操作与工单系统集成 - - 支持变更审批流程 - -## 技术支持 - -如有问题,请联系系统管理员或查看项目文档。 diff --git a/docs/install-improvement-plan.md b/docs/install-improvement-plan.md deleted file mode 100644 index 2bbd019..0000000 --- a/docs/install-improvement-plan.md +++ /dev/null @@ -1,661 +0,0 @@ -# install.js 安装脚本改进计划 - -## 概述 - -本文档记录 `install.js` 安装脚本的改进计划,涵盖 **15 项改进**,按优先级分为高/中/低三档。 - ---- - -## 一、高优先级改进 - -### 1. npm 依赖安装网络问题处理 - -**问题描述**: -- 直接执行 `npm install`,无重试机制 -- 国内访问 npmjs.org 超时/速度慢 -- 网络波动导致安装中断无反馈 - -**改进方案**: - -#### 1.1 自动检测并切换镜像源 - -```javascript -async function detectAndSetRegistry() { - const registries = [ - { name: '淘宝镜像', url: 'https://registry.npmmirror.com' }, - { name: '腾讯镜像', url: 'https://mirrors.cloud.tencent.com/npm/' }, - { name: '华为镜像', url: 'https://repo.huaweicloud.com/repository/npm/' }, - { name: '官方源', url: 'https://registry.npmjs.org' }, - ]; - - for (const registry of registries.slice(0, 3)) { - const pingResult = await pingRegistry(registry.url); - if (pingResult.success && pingResult.latency < 3000) { - log.success(`使用 ${registry.name} (${pingResult.latency}ms)`); - return registry.url; - } - } - return null; -} - -async function pingRegistry(url) { - const start = Date.now(); - try { - await new Promise((resolve, reject) => { - const req = require('https').get(`${url}/-/ping`, (res) => { - if (res.statusCode === 200) resolve(); - else reject(new Error('Not 200')); - }); - req.on('error', reject); - req.setTimeout(3000, () => { req.destroy(); reject(new Error('Timeout')); }); - }); - return { success: true, latency: Date.now() - start }; - } catch { - return { success: false, latency: Infinity }; - } -} -``` - -#### 1.2 重试机制与指数退避 - -```javascript -async function installWithRetry(cwd, name, maxRetries = 3) { - const baseDelay = 2000; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - log.info(`安装 ${name} 依赖 (第 ${attempt}/${maxRetries} 次)...`); - const result = await runNpmInstall(cwd); - - if (result.success) { - return { success: true }; - } - - const errorType = analyzeNpmError(result.error); - - if (errorType === 'NETWORK_ERROR' && attempt < maxRetries) { - const delay = baseDelay * Math.pow(2, attempt - 1); - log.warning(`网络错误,${delay/1000} 秒后重试...`); - await new Promise(r => setTimeout(r, delay)); - runCommand('npm cache clean --force', { cwd, silent: true }); - } else if (attempt < maxRetries) { - await new Promise(r => setTimeout(r, baseDelay)); - } - } - return { success: false }; -} - -function analyzeNpmError(error) { - const errorLower = (error || '').toLowerCase(); - if (errorLower.includes('econnrefused') || - errorLower.includes('econnreset') || - errorLower.includes('etimedout') || - errorLower.includes('network')) { - return 'NETWORK_ERROR'; - } - return 'UNKNOWN_ERROR'; -} -``` - -#### 1.3 安装进度显示 - -```javascript -function runNpmInstall(cwd) { - return new Promise((resolve) => { - const spawn = require('child_process').spawn; - const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; - - const child = spawn(npm, ['install', '--loglevel=warn'], { - cwd, shell: true, stdio: ['ignore', 'pipe', 'pipe'] - }); - - let stdout = ''; - let lastProgress = ''; - - child.stdout.on('data', (data) => { - stdout += data; - const progressMatch = data.toString().match(/(\d+\/\d+|\d+%)/); - if (progressMatch) { - const progress = progressMatch[1]; - if (progress !== lastProgress) { - process.stdout.write(`\r 安装进度: ${progress} `); - lastProgress = progress; - } - } - }); - - child.on('close', (code) => { - if (lastProgress) process.stdout.write('\n'); - resolve({ success: code === 0, output: stdout }); - }); - - setTimeout(() => { child.kill(); resolve({ success: false, error: '安装超时' }); }, 10 * 60 * 1000); - }); -} -``` - -#### 1.4 安装后验证 - -```javascript -async function verifyDependencies(cwd, name) { - log.subStep(`验证 ${name} 依赖完整性...`); - - const lsResult = runCommand('npm ls --depth=0 --json', { cwd, silent: true }); - if (!lsResult.success) { - return { success: false, error: '依赖检查失败' }; - } - - try { - const deps = JSON.parse(lsResult.output); - if (deps.problems && deps.problems.length > 0) { - log.warning(`发现 ${deps.problems.length} 个依赖问题`); - return { success: false, problems: deps.problems }; - } - return { success: true }; - } catch { - return { success: false, error: '验证解析失败' }; - } -} -``` - -**实施位置**:`install.js` 第 1782-1804 行 `installDependencies()` 函数 - ---- - -### 2. 健康检查跨平台兼容性 - -**问题描述**: -```javascript -// 当前代码(Windows 无 curl) -execSync('curl -s -o /dev/null -w "%{http_code}" ...'); -``` - -**改进方案**: - -```javascript -async function healthCheck() { - const http = require('http'); - - return new Promise((resolve) => { - const req = http.get(`http://localhost:${config.backendPort}/health`, (res) => { - resolve({ success: res.statusCode === 200, statusCode: res.statusCode }); - }); - req.on('error', () => resolve({ success: false })); - req.setTimeout(5000, () => { req.destroy(); resolve({ success: false }); }); - }); -} -``` - -**实施位置**:`install.js` 第 2039-2073 行 `healthCheck()` 函数 - ---- - -### 3. MySQL 密码安全传递 - -**问题描述**: -```javascript -// 当前代码 - 密码暴露在命令行 -execSync(`mysql -u root -p'${rootPassword}' -e "${sql}"`); -``` - -**改进方案**: - -```javascript -async function createMySQLDatabase(rootPassword) { - const createDbSql = 'CREATE DATABASE IF NOT EXISTS idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'; - - try { - execSync(`mysql -u root -e "${createDbSql}"`, { - env: { ...process.env, MYSQL_PWD: rootPassword }, - stdio: ['pipe', 'pipe', 'pipe'] - }); - log.success('数据库创建成功'); - } catch (error) { - // 错误处理... - } -} -``` - -**实施位置**:`install.js` 第 1052-1086 行 `createMySQLDatabase()` 函数 - ---- - -### 4. 错误处理完善 - -**问题描述**: -- 错误后继续执行,导致级联失败 -- 无重试选项 - -**改进方案**: - -```javascript -async function initDatabase() { - 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) { - const retry = await ask('数据库初始化失败,是否重试? (Y/n)', 'Y'); - if (retry.toLowerCase() === 'y') { - return await initDatabase(); - } - throw new Error('Database initialization failed'); - } - - log.success('数据库初始化完成'); -} -``` - -**实施位置**:`install.js` 第 1811-1833 行 `initDatabase()` 函数 - ---- - -### 5. 回滚机制 - -**问题描述**: -- 安装失败后无清理 -- 残留半成品状态 - -**改进方案**: - -```javascript -const rollbackSteps = []; - -async function installDependencies() { - try { - log.step('安装后端依赖...'); - await installBackend(); - rollbackSteps.push(() => runCommand('rmdir /s /q node_modules', { cwd: backendDir, shell: true })); - - log.step('安装前端依赖...'); - await installFrontend(); - rollbackSteps.push(() => runCommand('rmdir /s /q node_modules', { cwd: frontendDir, shell: true })); - - } catch (error) { - await rollback(); - throw error; - } -} - -async function rollback() { - log.warning('安装失败,正在回滚...'); - for (const step of rollbackSteps.reverse()) { - try { await step(); } catch {} - } -} -``` - -**实施位置**:新增 `rollbackSteps` 数组和 `rollback()` 函数,在 `main()` 中调用 - ---- - -### 6. SQLite 编译依赖替换 - -**已完成 ✅** - -| 文件 | 修改内容 | -|------|---------| -| `backend/package.json` | `sqlite3` → `better-sqlite3` | -| `backend/db.js` | 添加 `dialectModule` 配置 | - ---- - -## 二、中优先级改进 - -### 7. 日志管理优化 - -**问题描述**: -- 每次安装生成新日志,无清理 -- Windows 文件名不兼容冒号 - -**改进方案**: - -```javascript -function initLogFile() { - if (!fs.existsSync(LOG_DIR)) { - fs.mkdirSync(LOG_DIR, { recursive: true }); - } - - // 清理 7 天前的旧日志 - const oldLogs = fs.readdirSync(LOG_DIR) - .filter(f => f.startsWith('install_')) - .filter(f => { - const stat = fs.statSync(path.join(LOG_DIR, f)); - return Date.now() - stat.mtimeMs > 7 * 24 * 60 * 60 * 1000; - }); - oldLogs.forEach(f => fs.unlinkSync(path.join(LOG_DIR, f))); - - // 使用安全的时间格式 - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); - const logFileName = `install_${timestamp}.log`; - - logFileStream = fs.createWriteStream(path.join(LOG_DIR, logFileName), { flags: 'a' }); - - // 重写 console.log 同时输出到文件和终端 - const originalLog = console.log; - console.log = (...args) => { - const message = args.map(arg => String(arg)).join(' '); - logFileStream.write(`[${new Date().toISOString()}] ${message}\n`); - originalLog.apply(console, args); - }; -} -``` - -**实施位置**:`install.js` 第 100-123 行 `initLogFile()` 函数 - ---- - -### 8. 配置持久化与恢复 - -**问题描述**: -- 每次安装需重新配置 - -**改进方案**: - -```javascript -const CONFIG_FILE = path.join(__dirname, '.install-config.json'); - -async function loadSavedConfig() { - if (!fs.existsSync(CONFIG_FILE)) return false; - - const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); - const useSaved = await ask('检测到上次配置,是否使用? (Y/n)', 'Y'); - - if (useSaved.toLowerCase() === 'y') { - Object.assign(config, saved); - return true; - } - return false; -} - -function saveConfig() { - fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); - log.subStep('配置已保存'); -} - -// 在 confirmConfiguration() 成功后调用 saveConfig() -``` - -**实施位置**:新增 `CONFIG_FILE` 常量、`loadSavedConfig()` 和 `saveConfig()` 函数 - ---- - -### 9. 进度显示优化 - -**问题描述**: -- 只有文字输出,无进度条 - -**改进方案**: - -```javascript -const steps = [ - { name: '环境检测', fn: checkEnvironment }, - { name: '数据库配置', fn: configureDatabase }, - { name: '服务配置', fn: configureServices }, - { name: '安装依赖', fn: installDependencies }, - { name: '初始化数据库', fn: initDatabase }, - { name: '构建前端', fn: buildFrontend }, - { name: '启动服务', fn: startServices }, -]; - -async function runWithProgress() { - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const progress = `[${i + 1}/${steps.length}]`; - log.step(`${progress} ${step.name}`); - - const startTime = Date.now(); - await step.fn(); - log.subStep(`耗时 ${((Date.now() - startTime) / 1000).toFixed(1)}s`); - } -} -``` - -**实施位置**:新增 `steps` 数组和 `runWithProgress()` 函数 - ---- - -### 10. Nginx 版本动态获取 - -**问题描述**: -```javascript -// 固定版本 -const nginxUrl = 'https://nginx.org/download/nginx-1.24.0.zip'; -``` - -**改进方案**: - -```javascript -async function getLatestNginxVersion() { - const https = require('https'); - return new Promise((resolve) => { - https.get('https://nginx.org/en/download.html', (res) => { - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => { - const match = data.match(/nginx-(\d+\.\d+\.\d+)\.zip/); - resolve(match ? match[1] : '1.24.0'); - }); - }).on('error', () => resolve('1.24.0')); - }); -} - -async function autoInstallNginx() { - const version = await getLatestNginxVersion(); - const nginxUrl = `https://nginx.org/download/nginx-${version}.zip`; - // 后续下载逻辑... -} -``` - -**实施位置**:`install.js` 第 1430-1483 行 `autoInstallNginx()` 函数 - ---- - -### 11. PM2 serve 本地化 - -**问题描述**: -```javascript -// 全局安装 -runCommand('npm install -g serve'); -``` - -**改进方案**: - -```javascript -// 方案1:使用 npx(推荐) -const frontendResult = runCommand( - `npx serve -s dist -l ${config.frontendPort}`, - { cwd: path.join(__dirname, 'frontend') } -); - -// 方案2:本地安装 -runCommand('npm install serve', { cwd: path.join(__dirname, 'frontend') }); -``` - -**实施位置**:`install.js` 第 1889-1903 行 `startServices()` 函数 - ---- - -## 三、低优先级改进 - -### 12. 代码模块化重构 - -**问题描述**: -- 2100+ 行单文件,难以维护 - -**改进方案**: - -``` -scripts/ -├── install.js # 入口 -├── lib/ -│ ├── logger.js # 日志模块 -│ ├── prompt.js # 交互输入模块 -│ ├── executor.js # 命令执行模块 -│ ├── database.js # 数据库配置模块 -│ ├── nginx.js # Nginx 配置模块 -│ ├── pm2.js # PM2 配置模块 -│ └── health-check.js # 健康检查模块 -└── templates/ - ├── env.template # .env 模板 - ├── pm2.template.js # PM2 配置模板 - └── nginx.template # Nginx 配置模板 -``` - ---- - -### 13. 卸载脚本 - -**改进方案**: - -```javascript -// uninstall.js -async function uninstall() { - log.step('停止服务'); - runCommand('pm2 stop idc-backend idc-frontend'); - runCommand('pm2 delete idc-backend idc-frontend'); - - log.step('清理文件'); - ['node_modules', 'dist', 'logs'].forEach(dir => { - const cmd = process.platform === 'win32' - ? `rmdir /s /q ${dir}` - : `rm -rf ${dir}`; - runCommand(cmd, { cwd: backendDir }); - runCommand(cmd, { cwd: frontendDir }); - }); - - log.step('清理配置'); - ['.env', '.install-config.json'].forEach(file => { - const path = `${backendDir}/${file}`; - if (fs.existsSync(path)) fs.unlinkSync(path); - }); - - log.success('卸载完成'); -} -``` - ---- - -### 14. 配置验证 - -**改进方案**: - -```javascript -async function validateConfig() { - const errors = []; - - // 端口冲突检查 - const usedPorts = await checkUsedPorts([config.backendPort, config.frontendPort]); - if (usedPorts.length > 0) { - errors.push(`端口已被占用: ${usedPorts.join(', ')}`); - } - - // 磁盘空间检查 - const freeSpace = await checkDiskSpace(__dirname); - if (freeSpace < 500 * 1024 * 1024) { - errors.push('磁盘空间不足 500MB'); - } - - // 权限检查 - if (config.frontendPort < 1024 && !isRootUser()) { - errors.push(`端口 ${config.frontendPort} 需要 root 权限`); - } - - return errors; -} -``` - ---- - -### 15. 配置导入导出 - -**改进方案**: - -```javascript -// 导入配置 -// node install.js --import=config.json - -async function importConfig(filePath) { - const content = fs.readFileSync(filePath, 'utf8'); - const loaded = JSON.parse(content); - Object.assign(config, loaded); - log.success(`配置已导入: ${filePath}`); -} - -// 导出配置(在 saveConfig 时可选择) -// node install.js --export=config.json -``` - ---- - -## 四、实施优先级 - -| 优先级 | 编号 | 改进项 | 复杂度 | 状态 | -|--------|------|--------|--------|------| -| 🔴高 | 1 | npm 安装网络处理 | 中 | 待实施 | -| 🔴高 | 2 | 健康检查跨平台 | 低 | 待实施 | -| 🔴高 | 3 | MySQL 密码安全 | 低 | 待实施 | -| 🔴高 | 4 | 错误处理完善 | 中 | 待实施 | -| 🔴高 | 5 | 回滚机制 | 高 | 待实施 | -| 🔴高 | 6 | SQLite 替换 | 中 | ✅ 已完成 | -| 🟡中 | 7 | 日志管理 | 低 | 待实施 | -| 🟡中 | 8 | 配置持久化 | 中 | 待实施 | -| 🟡中 | 9 | 进度显示 | 低 | 待实施 | -| 🟡中 | 10 | Nginx 版本动态 | 低 | 待实施 | -| 🟡中 | 11 | PM2 serve 本地化 | 低 | 待实施 | -| 🟢低 | 12 | 代码模块化 | 高 | 待实施 | -| 🟢低 | 13 | 卸载脚本 | 中 | 待实施 | -| 🟢低 | 14 | 配置验证 | 中 | 待实施 | -| 🟢低 | 15 | 配置导入导出 | 低 | 待实施 | - ---- - -## 五、建议实施顺序 - -### 第一阶段(快速见效) - -1. **Nginx 版本动态** (10) - 改动小,立即生效 -2. **日志管理优化** (7) - 改动小,提升调试体验 -3. **PM2 serve 本地化** (11) - 改动小,避免全局污染 - -### 第二阶段(核心增强) - -4. **npm 安装网络处理** (1) - 提升安装成功率 -5. **健康检查跨平台** (2) - Windows 兼容性 -6. **MySQL 密码安全** (3) - 安全性 - -### 第三阶段(健壮性) - -7. **错误处理完善** (4) - 提升容错能力 -8. **回滚机制** (5) - 失败恢复 -9. **配置持久化** (8) - 复用配置 -10. **进度显示** (9) - 用户体验 - -### 第四阶段(长期) - -11. **代码模块化** (12) - 提升可维护性 -12. **卸载脚本** (13) - 功能完整性 -13. **配置验证** (14) - 提前发现问题 -14. **配置导入导出** (15) - 扩展性 - ---- - -## 六、测试清单 - -每项改进实施后需验证: - -- [ ] Windows 环境 -- [ ] Linux 环境(Ubuntu/Debian) -- [ ] Linux 环境(CentOS/RHEL) -- [ ] macOS 环境 -- [ ] 非交互模式 (`-y`) -- [ ] 配置恢复功能 -- [ ] 日志文件生成 -- [ ] 回滚功能(模拟失败) diff --git a/uninstall.js b/uninstall.js index 3259015..82bc0c8 100644 --- a/uninstall.js +++ b/uninstall.js @@ -346,16 +346,150 @@ async function cleanupNginxConfig(cmdArgs) { const isWindows = process.platform === 'win32'; const isLinux = process.platform === 'linux'; + const isMac = process.platform === 'darwin'; + + /** + * 检测 Nginx 是否已安装 + * + * @returns {boolean} Nginx 是否可用 + */ + function nginxInstalled() { + return commandExists('nginx'); + } + + /** + * 检测 Nginx 服务进程是否在运行 + * + * @returns {boolean} Nginx 是否在运行 + */ + function nginxRunning() { + if (isWindows) { + const result = runCommand('tasklist /FI "IMAGENAME eq nginx.exe" 2>nul', { silent: true }); + return result.success && result.output.includes('nginx.exe'); + } + const result = runCommand('pgrep -x nginx 2>/dev/null', { silent: true }); + return result.success && result.output.trim().length > 0; + } + + /** + * 停止 Nginx 服务 + * + * @returns {boolean} 是否成功停止 + */ + function stopNginx() { + if (isLinux) { + const root = isRootUser(); + const sudoPrefix = root ? '' : 'sudo '; + + // 优先使用 systemctl + if (commandExists('systemctl')) { + const result = runCommand(`${sudoPrefix}systemctl stop nginx`, { silent: true }); + if (result.success) { + log.success('Nginx 服务已停止 (systemctl)'); + return true; + } + } + // 回退到 nginx -s stop + const result = runCommand(`${sudoPrefix}nginx -s stop`, { silent: true }); + if (result.success) { + log.success('Nginx 服务已停止'); + return true; + } + log.warning('Nginx 停止失败,可能需要手动处理'); + return false; + } + + if (isWindows) { + if (!nginxRunning()) { + log.info('Nginx 未在运行'); + return true; + } + const result = runCommand('nginx -s stop', { silent: true }); + if (result.success) { + log.success('Nginx 已停止'); + return true; + } + log.warning('Nginx 停止失败,请手动执行: nginx -s stop'); + return false; + } + + return false; + } + + /** + * 禁用 Nginx 开机自启(Linux) + */ + function disableNginxStartup() { + if (!isLinux || !commandExists('systemctl')) return; + + const root = isRootUser(); + const sudoPrefix = root ? '' : 'sudo '; + const result = runCommand(`${sudoPrefix}systemctl disable nginx`, { silent: true }); + if (result.success) { + log.success('Nginx 开机自启已禁用'); + } + } + + /** + * 清理 Linux 前端部署目录 + */ + function cleanupFrontendDeployDir() { + if (!isLinux) return; + + const webRoot = '/var/www/idc'; + if (!fs.existsSync(webRoot)) return; + + log.info('清理前端部署目录...'); + const root = isRootUser(); + const sudoPrefix = root ? '' : 'sudo '; + const result = runCommand(`${sudoPrefix}rm -rf "${webRoot}"`, { silent: true }); + if (result.success) { + log.success('前端部署目录已删除 (/var/www/idc)'); + deletedItems.push({ type: '目录', name: '/var/www/idc' }); + } else { + log.warning('前端部署目录删除失败,请手动清理'); + } + } if (isWindows) { - // Windows 下提示手动清理 - log.info('Windows 系统请手动清理 Nginx 配置:'); + log.info('Windows Nginx 清理:'); + + if (nginxInstalled() && nginxRunning()) { + log.info('检测到 Nginx 正在运行'); + if (!cmdArgs?.force) { + const stopConfirm = await ask('是否停止 Nginx 服务? (Y/n)', 'Y'); + if (stopConfirm.toLowerCase() === 'y') { + stopNginx(); + } + } else { + stopNginx(); + } + } + + console.log(`\n 请手动完成以下操作:`); console.log(` 1. 删除配置文件: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`); - console.log(` 2. 编辑主配置: 从 ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset} 中移除 include conf.d/*.conf;`); + console.log(` 2. 编辑主配置: 从 ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset} 中移除 include 语句`); console.log(` 3. 停止 Nginx: ${colors.cyan}nginx -s stop${colors.reset}`); + // 检测 Nginx 安装方式,提供卸载指引 + log.info('检测 Nginx 安装方式...'); + const chocoCheck = runCommand('choco list nginx --local-only 2>nul', { silent: true }); + if (chocoCheck.success && chocoCheck.output.includes('nginx')) { + console.log(`\n 检测到 Chocolatey 安装的 Nginx,卸载命令:`); + console.log(` ${colors.cyan}choco uninstall nginx${colors.reset}`); + } else { + const scoopCheck = runCommand('scoop list nginx 2>nul', { silent: true }); + if (scoopCheck.success && scoopCheck.output.includes('nginx')) { + console.log(`\n 检测到 Scoop 安装的 Nginx,卸载命令:`); + console.log(` ${colors.cyan}scoop uninstall nginx${colors.reset}`); + } else if (fs.existsSync('C:/nginx')) { + console.log(`\n 检测到 Nginx 安装在 ${colors.cyan}C:/nginx${colors.reset}`); + console.log(` 如需完整卸载,请手动删除该目录`); + } + } + if (!cmdArgs?.force) { - const confirm = await ask('是否已手动清理 Nginx 配置? (Y/n)', 'Y'); + const confirm = await ask('\n是否已完成以上清理操作? (Y/n)', 'Y'); if (confirm.toLowerCase() !== 'y') { log.warning('请记得手动清理 Nginx 配置'); } @@ -363,11 +497,26 @@ async function cleanupNginxConfig(cmdArgs) { log.info('强制模式:跳过 Nginx 清理确认'); } } else if (isLinux) { - // Linux 下自动清理 const root = isRootUser(); const sudoPrefix = root ? '' : 'sudo '; - // 检测配置文件位置 + // 1. 停止 Nginx 服务 + if (nginxInstalled()) { + log.info('停止 Nginx 服务...'); + if (nginxRunning()) { + stopNginx(); + } else { + log.info('Nginx 未在运行'); + } + } + + // 2. 禁用开机自启 + disableNginxStartup(); + + // 3. 清理前端部署目录 + cleanupFrontendDeployDir(); + + // 4. 检测并清理 Nginx 配置文件 const sitesAvailablePath = '/etc/nginx/sites-available/idc'; const sitesEnabledPath = '/etc/nginx/sites-enabled/idc'; const confDPath = '/etc/nginx/conf.d/idc'; @@ -375,7 +524,6 @@ async function cleanupNginxConfig(cmdArgs) { let configExists = false; - // 删除 sites-available 中的配置 if (fs.existsSync(sitesAvailablePath)) { configExists = true; log.info('删除 sites-available 配置...'); @@ -387,7 +535,6 @@ async function cleanupNginxConfig(cmdArgs) { } } - // 删除 sites-enabled 中的软链接 if (fs.existsSync(sitesEnabledPath)) { configExists = true; log.info('删除 sites-enabled 软链接...'); @@ -399,7 +546,6 @@ async function cleanupNginxConfig(cmdArgs) { } } - // 删除 conf.d 中的配置(无扩展名) if (fs.existsSync(confDPath)) { configExists = true; log.info('删除 conf.d 配置...'); @@ -411,7 +557,6 @@ async function cleanupNginxConfig(cmdArgs) { } } - // 删除 conf.d 中的配置(带 .conf 扩展名) if (fs.existsSync(confDWithExtPath)) { configExists = true; log.info('删除 conf.d 配置(带扩展名)...'); @@ -425,29 +570,60 @@ async function cleanupNginxConfig(cmdArgs) { if (!configExists) { log.info('未找到系统 Nginx 配置文件'); - } else { - // 测试并重载 Nginx + } else if (nginxInstalled()) { log.info('测试 Nginx 配置...'); const testResult = runCommand(`${sudoPrefix}nginx -t`, { silent: true }); if (testResult.success) { - log.success('Nginx 配置测试通过'); - - log.info('重载 Nginx 服务...'); - const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`, { silent: true }); - if (reloadResult.success) { - log.success('Nginx 已重载'); - } else { - log.warning('Nginx 重载失败,可能需要手动重启'); - } + log.success('Nginx 配置测试通过(已无残留配置)'); } else { - log.warning('Nginx 配置测试失败,请手动检查'); + log.success('Nginx 配置已清理完成'); } } - } else { - // macOS 或其他系统 - log.info('请手动清理 Nginx 配置'); + } else if (isMac) { + log.info('macOS Nginx 清理:'); + + if (nginxInstalled() && nginxRunning()) { + log.info('检测到 Nginx 正在运行'); + if (!cmdArgs?.force) { + const stopConfirm = await ask('是否停止 Nginx 服务? (Y/n)', 'Y'); + if (stopConfirm.toLowerCase() === 'y') { + // 优先使用 brew + if (commandExists('brew')) { + const brewResult = runCommand('brew services stop nginx', { silent: true }); + if (brewResult.success) { + log.success('Nginx 已停止 (brew)'); + } else { + runCommand('nginx -s stop', { silent: true }); + } + } else { + runCommand('nginx -s stop', { silent: true }); + } + } + } else { + if (commandExists('brew')) { + runCommand('brew services stop nginx', { silent: true }); + } else { + runCommand('nginx -s stop', { silent: true }); + } + } + } + + console.log(`\n 请手动完成以下操作:`); console.log(` 配置文件可能位于: ${colors.cyan}/usr/local/etc/nginx/servers/${colors.reset}`); console.log(` 或使用: ${colors.cyan}brew services stop nginx${colors.reset}`); + + if (commandExists('brew')) { + console.log(` 如需卸载 Nginx:${colors.cyan}brew uninstall nginx${colors.reset}`); + } + + if (!cmdArgs?.force) { + const confirm = await ask('\n是否已完成以上清理操作? (Y/n)', 'Y'); + if (confirm.toLowerCase() !== 'y') { + log.warning('请记得手动清理 Nginx 配置'); + } + } else { + log.info('强制模式:跳过 Nginx 清理确认'); + } } log.divider(); @@ -787,16 +963,35 @@ function showDryRunPreview(cmdArgs) { // Nginx 配置 console.log(`\n ${colors.bright}2. 清理 Nginx 配置${colors.reset}`); + + // 停止 Nginx 服务 + if (nginxRunning()) { + console.log(` - 停止 Nginx 服务`); + } + + // Linux 下额外操作 if (process.platform === 'linux') { - const paths = ['/etc/nginx/sites-available/idc', '/etc/nginx/sites-enabled/idc', + console.log(` - 禁用 Nginx 开机自启 (systemctl disable nginx)`); + + if (fs.existsSync('/var/www/idc')) { + console.log(` - 删除前端部署目录 (/var/www/idc)`); + } + + const nginxConfigPaths = ['/etc/nginx/sites-available/idc', '/etc/nginx/sites-enabled/idc', '/etc/nginx/conf.d/idc', '/etc/nginx/conf.d/idc.conf']; - paths.forEach(p => { + nginxConfigPaths.forEach(p => { if (fs.existsSync(p)) { - console.log(` - 删除: ${p}`); + console.log(` - 删除 Nginx 配置: ${p}`); } }); - } else { - console.log(` - 提示手动清理(${process.platform === 'win32' ? 'Windows' : 'macOS'})`); + } else if (process.platform === 'win32') { + console.log(` - 删除 Nginx 配置文件 (C:/nginx/conf/conf.d/idc.conf)`); + console.log(` - 提供 Chocolatey/Scoop 卸载指引`); + } else if (process.platform === 'darwin') { + if (commandExists('brew')) { + console.log(` - 停止 Nginx (brew services)`); + } + console.log(` - 删除 Nginx 配置 (/usr/local/etc/nginx/servers/)`); } // 配置文件