import { exec } from 'child_process' import { Client as SSHClient } from 'ssh2' import ora from 'ora' import SftpClient from 'ssh2-sftp-client' import chalk from 'chalk' import fs from 'fs' import { loadEnv } from 'vite' // 存储原始的 接口请求地址 let originalApiUrl = ''; // 创建分隔线 const createSeparator = (text) => { const width = 60; const padding = Math.floor((width - text.length - 2) / 2); const separator = '='.repeat(padding) + ' ' + text + ' ' + '='.repeat(padding); return chalk.blue('\n' + separator + '\n'); }; const spinner = ora('正在构建项目...').start() spinner.stopAndPersist({ text: ' __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ \n' + ' | |\n' + ' | *** 运行发布流程 *** |\n' + ' | 【pigx管理系统】 |\n' + ' | __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ |\n' }) spinner.start() let speed = 20 let time = 20 const count = 400 const getProgress = () => { const done = '█' const undone = '░' let progress = Math.floor((speed / count) * 50) if (progress > 50) { progress = 50 } const progressBar = done.repeat(progress) + undone.repeat(50 - progress) return progressBar } const interval = setInterval(() => { spinner.text = `正在构建项目 ${getProgress()} 进度${((speed / count) * 100).toFixed(1)}% 耗时${(time / 10).toFixed(1)}秒 ` speed++ time++ if (speed > count) { speed = 400 } }, 100) const env = loadEnv('', process.cwd()) const updateProxyConfig = (url) => { try { const envPath = '.env'; let content = fs.readFileSync(envPath, 'utf-8'); // 提取当前的 apiBaseUrl (如果需要) const match = content.match(/VITE_API_URL = (.*)/); if (!originalApiUrl && match && match[1]) { originalApiUrl = match[1]; console.log(chalk.gray(`原始 API 地址已保存: ${originalApiUrl}`)); } // 使用正则表达式替换apiBaseUrl的值,但保留注释 content = content.replace( /VITE_API_URL = (.*)/, `VITE_API_URL = ${url}` ); fs.writeFileSync(envPath, content, 'utf-8'); console.log(chalk.green('✓'), chalk.blue(`已更新 API 地址为: ${url}`)); } catch (error) { console.error(chalk.red('✗'), chalk.red('更新 API 地址失败:'), error); process.exit(1); } }; const command = (list, config, host) => { return new Promise((resolveCommand, rejectCommand) => { const ssh = new SSHClient(); const runCommand = (index) => { if (index >= list.length) { ssh.end(); console.log(chalk.green('✓'), chalk.blue(`端口 ${host} 的远程命令执行完毕`)); resolveCommand(); return; } const cmd = list[index]; console.log(chalk.blue('→'), chalk.gray(`执行命令: ${cmd}`)); ssh.exec(cmd, (err, stream) => { if (err) { console.error(chalk.red('✗'), chalk.red(`命令执行失败: ${err}`)); ssh.end(); rejectCommand(err); return; } let hasError = false; stream.on('close', (code, signal) => { if (hasError && !cmd.includes('docker build')) { console.error(chalk.red('✗'), chalk.red(`命令执行失败,退出码: ${code}`)); ssh.end(); rejectCommand(new Error(`命令 "${cmd}" 失败,退出码: ${code}`)); return; } console.log(chalk.green('✓'), chalk.gray(`命令执行完成: ${cmd}`)); runCommand(index + 1); }).on('data', (data) => { console.log(chalk.gray(data.toString())); // 实时输出命令结果 }).stderr.on('data', (data) => { hasError = true; console.error(chalk.yellow('!'), chalk.yellow(data.toString())); // 实时输出错误信息 }); }); }; ssh.on('ready', () => { console.log(chalk.green('✓'), chalk.blue('SSH 连接成功')); runCommand(0); }).connect(config); // Handle SSH connection errors ssh.on('error', (err) => { console.error(chalk.red('✗'), chalk.red('SSH 连接错误:'), err); rejectCommand(err); }); }); }; const updateFiles = async () => { spinner.text = '正在链接服务器...'; const config = { host: env.VITE_HOST, port: env.VITE_POST, username: env.VITE_USERNAME, password: env.VITE_PASSWORD }; const sftp = new SftpClient(); if (!fs.existsSync('dist')) { console.error(chalk.red('✗'), chalk.red('构建目录不存在')); spinner.fail('构建目录不存在'); spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner return; } try { await sftp.connect(config); spinner.succeed(chalk.green('服务器连接成功')); console.log(chalk.blue('→'), chalk.gray('开始上传文件...')); await sftp.uploadDir('dist_6888', '/data/seo-vue/dist'); console.log(chalk.green('✓'), chalk.blue('6888端口文件上传完成')); await sftp.uploadDir('dist_16888', '/data/seo-vue-test/dist'); console.log(chalk.green('✓'), chalk.blue('16888端口文件上传完成')); sftp.end(); console.log(createSeparator('开始部署 6888 端口')); await command([ "cd /data/seo-vue && pwd", "cd /data/seo-vue && ls -la", "cd /data/seo-vue && docker rm -f seo-vue", "cd /data/seo-vue && docker rmi seo-vue", "cd /data/seo-vue && docker build -t seo-vue .", "cd /data/seo-vue && docker run -d --name seo-vue -p 6888:80 seo-vue" ], config, "6888"); console.log(createSeparator('6888 端口部署完成')); console.log(createSeparator('开始部署 16888 端口')); await command([ "cd /data/seo-vue-test && pwd", "cd /data/seo-vue-test && ls -la", "cd /data/seo-vue-test && docker rm -f seo-vue-test", "cd /data/seo-vue-test && docker rmi seo-vue-test", "cd /data/seo-vue-test && docker build -t seo-vue-test .", "cd /data/seo-vue-test && docker run -d --name seo-vue-test -p 16888:80 seo-vue-test" ], config, "16888"); console.log(createSeparator('16888 端口部署完成')); } catch (err) { console.error(chalk.red('✗'), chalk.red('部署或文件上传失败:'), err); spinner.fail(err); spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner sftp.end(); throw err; // 向上抛出错误,让main函数捕获并处理退出 } }; // 项目构建 console.log(createSeparator('开始构建项目')); // 使用 Promise 包装构建过程 const buildWithProgress = (port, apiUrl) => { return new Promise((resolve, reject) => { console.log(chalk.blue(`\n构建 ${port} 端口版本`)); console.log(chalk.gray(`API 地址: ${apiUrl}`)); updateProxyConfig(apiUrl); // 重置进度条 speed = 20; time = 20; spinner.start(); // 确保每次构建前启动 spinner const buildProcess = exec('npm run build'); let buildOutput = ''; buildProcess.stdout.on('data', (data) => { buildOutput += data; // 根据输出更新进度 if (data.includes('compiled successfully')) { speed = count; } }); buildProcess.stderr.on('data', (data) => { buildOutput += data; }); buildProcess.on('close', async (code) => { if (code === 0) { // 构建成功后,将dist目录复制到对应的端口目录 const distDir = 'dist'; const targetDir = `dist_${port}`; try { // 如果目标目录已存在,先删除 if (fs.existsSync(targetDir)) { fs.rmSync(targetDir, { recursive: true, force: true }); } // 复制dist目录到目标目录 fs.cpSync(distDir, targetDir, { recursive: true }); console.log(chalk.green('✓'), chalk.blue(`已保存 ${port} 端口构建文件到 ${targetDir}`)); } catch (err) { console.error(chalk.red('✗'), chalk.red(`保存构建文件失败: ${err}`)); reject(err); return; } spinner.succeed(chalk.green(`构建成功`)); resolve(buildOutput); } else { spinner.fail(chalk.red(`构建失败`)); reject(new Error(buildOutput)); } }); }); }; // 主构建流程 const main = async () => { spinner.start(); // 在主流程开始时启动 spinner,用于总体的进度指示 try { const envPath = '.env'; const content = fs.readFileSync(envPath, 'utf-8'); const match = content.match(/VITE_API_URL = (.*)/); if (match && match[1]) { originalApiUrl = match[1]; } else { console.warn(chalk.yellow('!'), chalk.yellow('无法从 env 中读取原始 VITE_API_URL,可能无法还原。')); } // 构建 6888 端口版本 await buildWithProgress('6888', 'http://192.168.10.101:9999'); // 构建 16888 端口版本 await buildWithProgress('16888', 'http://192.168.3.17:9999'); // 开始部署 await updateFiles(); // 所有任务完成后,打印最终访问路径并停止 spinner clearInterval(interval); // 在所有任务完成后停止进度条动画 console.log(createSeparator('部署完成')); console.log(chalk.green('🎉'), chalk.cyan('所有环境部署成功!')); console.log(chalk.green('🚀'), chalk.cyan(`6888 端口访问地址: http://${env.VITE_HOST}:6888`)); console.log(chalk.green('🚀'), chalk.cyan(`16888 端口访问地址: http://${env.VITE_HOST}:16888`)); // 还原 VITE_API_URL 到原始值 if (originalApiUrl) { console.log(createSeparator('还原配置')); updateProxyConfig(originalApiUrl); } else { console.warn(chalk.yellow('!'), chalk.yellow('未找到原始 VITE_API_URL,跳过还原。')); } spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner,返回控制台 process.exit(0); // 正常退出,返回控制台 } catch (error) { console.error(chalk.red('✗'), chalk.red('发布流程出错:'), error); spinner.fail(error); spinner.stopAndPersist({ text: '' }); // 停止并清理 spinner,返回控制台 process.exit(1); // 异常退出 }}; // 启动主流程 main();