const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const os = require('os'); const { execSync } = require('child_process'); function getCurrentVersion() { const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); return packageJson.version; } function getPackageSourceDir() { const sourceDirArg = process.argv .slice(2) .find((arg) => arg.startsWith('--source-dir=')); const configuredSourceDir = sourceDirArg ? sourceDirArg.slice('--source-dir='.length) : process.env.DEPLOY_PACKAGE_SOURCE_DIR; if (!configuredSourceDir) { return path.join(__dirname, '../dist/apps/web'); } return path.resolve(process.cwd(), configuredSourceDir); } function preparePackageRoot(sourceDir) { if (path.basename(sourceDir) === 'web') { return { rootDir: path.dirname(sourceDir), packageDirName: 'web', cleanup: () => {}, }; } const stagingRoot = fs.mkdtempSync( path.join(os.tmpdir(), 'opentu-deploy-package-') ); const stagingWebDir = path.join(stagingRoot, 'web'); fs.cpSync(sourceDir, stagingWebDir, { recursive: true, dereference: true, }); return { rootDir: stagingRoot, packageDirName: 'web', cleanup: () => fs.rmSync(stagingRoot, { recursive: true, force: true }), }; } function getTagAttribute(tag, attributeName) { const escapedName = attributeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const match = String(tag).match( new RegExp(`\\s${escapedName}\\s*=\\s*(['"])(.*?)\\1`, 'i') ); return match ? match[2] : ''; } function normalizeEntryAssetPath(value) { let normalized = String(value || '') .split(/[?#]/, 1)[0] .trim(); if (!normalized) { return ''; } if (/^https?:\/\//i.test(normalized)) { try { normalized = new URL(normalized).pathname; } catch (_error) { return ''; } } normalized = normalized .replace(/^\//, '') .replace(/^\.\//, '') .replace(/^npm\/aitu-app@[^/]+\//, '') .replace(/^aitu-app@[^/]+\//, ''); return normalized; } function collectEntryAssetsFromHtml(html) { const assets = new Set(); const scriptTags = String(html).match( /]*\bsrc\s*=\s*(['"]).*?\1[^>]*><\/script>/gi ) || []; for (const tag of scriptTags) { const asset = normalizeEntryAssetPath( getTagAttribute(tag, 'data-local-src') || getTagAttribute(tag, 'src') ); if (asset.startsWith('assets/') && asset.endsWith('.js')) { assets.add(asset); } } const linkTags = String(html).match(/]*>/gi) || []; for (const tag of linkTags) { const rel = getTagAttribute(tag, 'rel').toLowerCase(); if (!rel.split(/\s+/).includes('stylesheet')) { continue; } const asset = normalizeEntryAssetPath( getTagAttribute(tag, 'data-local-href') || getTagAttribute(tag, 'href') ); if (asset.startsWith('assets/') && asset.endsWith('.css')) { assets.add(asset); } } return Array.from(assets); } // 加载 .env 配置文件 function loadEnvConfig() { const envPath = path.join(__dirname, '../.env'); const config = { DEPLOY_HOST: '', DEPLOY_USER: '', DEPLOY_PORT: '22', DEPLOY_SSH_KEY: '', DEPLOY_SSH_PASSWORD: '', DEPLOY_UPLOAD_DIR: '', DEPLOY_SCRIPT_PATH: '', DEPLOY_AUTO_UPLOAD: 'false', DEPLOY_AUTO_DEPLOY: 'false', // 'false', 'true' (prod), 'test' }; if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf8'); envContent.split('\n').forEach((line) => { line = line.trim(); // 跳过注释和空行 if (line && !line.startsWith('#')) { const match = line.match(/^([^=]+)=(.*)$/); if (match) { const key = match[1].trim(); let value = match[2].trim(); // 移除引号 if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } if (config.hasOwnProperty(key)) { config[key] = value; } } } }); } return config; } // 检查 sshpass 是否安装 function checkSshpassInstalled() { try { execSync('which sshpass', { stdio: 'ignore' }); return true; } catch (e) { return false; } } // 执行远程部署脚本 function executeRemoteDeploy( config, tarName, env = 'test', usePassword = false ) { if (!config.DEPLOY_SCRIPT_PATH) { console.error(`\n❌ 未配置部署脚本路径`); console.error(` 请在 .env 文件中配置 DEPLOY_SCRIPT_PATH`); return false; } const deployScriptPath = config.DEPLOY_SCRIPT_PATH; console.log(`\n🚀 开始自动部署到${env === 'test' ? '测试' : '生产'}环境...`); console.log(` 部署脚本: ${deployScriptPath}`); console.log(` 包文件: ${tarName}`); try { // 构建 SSH 命令 let sshCommand = ''; let usePassword = false; // 如果配置了密码,使用 sshpass if (config.DEPLOY_SSH_PASSWORD && !config.DEPLOY_SSH_KEY) { if (!checkSshpassInstalled()) { console.error(`\n❌ 未安装 sshpass,无法执行远程部署`); console.error(` 请安装: brew install hudochenkov/sshpass/sshpass`); return false; } usePassword = true; sshCommand = `sshpass -p "${config.DEPLOY_SSH_PASSWORD}" `; } sshCommand += 'ssh'; // 添加端口 if (config.DEPLOY_PORT && config.DEPLOY_PORT !== '22') { sshCommand += ` -p ${config.DEPLOY_PORT}`; } // 添加 SSH 密钥 if (config.DEPLOY_SSH_KEY) { const sshKeyPath = config.DEPLOY_SSH_KEY.startsWith('/') ? config.DEPLOY_SSH_KEY : path.join( process.env.HOME || '', config.DEPLOY_SSH_KEY.replace(/^~/, '') ); if (fs.existsSync(sshKeyPath)) { sshCommand += ` -i "${sshKeyPath}"`; } } // 禁用严格主机密钥检查 sshCommand += ` -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`; // 构建远程命令 const remoteCommand = `bash ${deployScriptPath} --${env} ${tarName}`; sshCommand += ` ${config.DEPLOY_USER}@${config.DEPLOY_HOST} "${remoteCommand}"`; console.log(`🔄 执行远程部署命令...`); execSync(sshCommand, { stdio: 'inherit' }); console.log(`✅ 部署成功!`); return true; } catch (error) { console.error(`❌ 部署失败:`, error.message); console.error(`\n💡 请检查:`); console.error(` 1. 部署脚本路径是否正确: ${deployScriptPath}`); console.error(` 2. 脚本是否有执行权限`); console.error(` 3. 服务器目录权限是否正确`); return false; } } // 上传文件到远程服务器 function uploadToServer(tarPath, tarName, config) { if (!config.DEPLOY_HOST || !config.DEPLOY_USER) { console.log(`\n⚠️ 未配置远程服务器信息,跳过上传`); console.log(` 请在 .env 文件中配置 DEPLOY_HOST 和 DEPLOY_USER`); return false; } if (!config.DEPLOY_UPLOAD_DIR) { console.error(`\n❌ 未配置上传目录`); console.error(` 请在 .env 文件中配置 DEPLOY_UPLOAD_DIR`); return false; } console.log(`\n🚀 开始上传到远程服务器...`); console.log( ` 服务器: ${config.DEPLOY_USER}@${config.DEPLOY_HOST}:${config.DEPLOY_PORT}` ); console.log(` 目标目录: ${config.DEPLOY_UPLOAD_DIR}`); console.log(` 文件: ${tarName}`); try { // 构建 scp 命令 let scpCommand = ''; let usePassword = false; // 如果配置了密码,使用 sshpass if (config.DEPLOY_SSH_PASSWORD && !config.DEPLOY_SSH_KEY) { if (!checkSshpassInstalled()) { console.error(`\n❌ 未安装 sshpass,无法使用密码认证`); console.error(`\n💡 安装方法:`); console.error(` macOS: brew install hudochenkov/sshpass/sshpass`); console.error( ` Linux: apt-get install sshpass 或 yum install sshpass` ); console.error(`\n 或者配置 SSH 密钥认证(更安全)`); return false; } usePassword = true; scpCommand = `sshpass -p "${config.DEPLOY_SSH_PASSWORD}" `; } scpCommand += 'scp'; // 添加端口 if (config.DEPLOY_PORT && config.DEPLOY_PORT !== '22') { scpCommand += ` -P ${config.DEPLOY_PORT}`; } // 添加 SSH 密钥(如果配置了密钥,优先使用密钥) if (config.DEPLOY_SSH_KEY) { const sshKeyPath = config.DEPLOY_SSH_KEY.startsWith('/') ? config.DEPLOY_SSH_KEY : path.join(process.env.HOME || '', config.DEPLOY_SSH_KEY); if (fs.existsSync(sshKeyPath)) { scpCommand += ` -i "${sshKeyPath}"`; } else { console.warn(`⚠️ SSH 密钥文件不存在: ${sshKeyPath}`); } } // 禁用严格主机密钥检查(可选,用于自动化场景) scpCommand += ` -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`; // 添加源文件和目标 const remotePath = `${config.DEPLOY_USER}@${config.DEPLOY_HOST}:${config.DEPLOY_UPLOAD_DIR}`; scpCommand += ` "${tarPath}" "${remotePath}/"`; console.log(`🔄 执行上传命令...`); if (usePassword) { console.log(` 使用密码认证`); } else if (config.DEPLOY_SSH_KEY) { console.log(` 使用 SSH 密钥认证`); } else { console.log(` 使用默认 SSH 认证`); } execSync(scpCommand, { stdio: 'inherit' }); console.log(`✅ 上传成功!`); console.log(`📦 远程路径: ${config.DEPLOY_UPLOAD_DIR}/${tarName}`); return { success: true, tarName }; } catch (error) { console.error(`❌ 上传失败:`, error.message); console.error(`\n💡 请检查:`); console.error(` 1. 服务器地址和端口是否正确`); if (config.DEPLOY_SSH_PASSWORD) { console.error(` 2. 密码是否正确`); console.error( ` 3. 是否已安装 sshpass (brew install hudochenkov/sshpass/sshpass)` ); } else { console.error(` 2. SSH 密钥是否正确配置`); } console.error(` 4. 服务器目录权限是否正确`); console.error(` 5. 网络连接是否正常`); return false; } return false; } function createDeployPackage() { const version = getCurrentVersion(); const distPath = getPackageSourceDir(); const tarName = `web-${version}.tar.gz`; const tarPath = path.join(__dirname, '../dist/apps', tarName); console.log(`📦 开始创建部署包...`); console.log(`📂 源目录: ${distPath}`); console.log(`📁 目标文件: ${tarPath}`); // 检查源目录是否存在 if (!fs.existsSync(distPath)) { console.error(`❌ 构建目录不存在: ${distPath}`); console.error(`请先运行 npm run build 命令`); process.exit(1); } // 检查目录是否为空 const files = fs.readdirSync(distPath); if (files.length === 0) { console.error(`❌ 构建目录为空: ${distPath}`); process.exit(1); } // 检查 version.json 是否存在 const versionJsonPath = path.join(distPath, 'version.json'); if (!fs.existsSync(versionJsonPath)) { console.error(`❌ version.json 不存在: ${versionJsonPath}`); console.error(`请确保构建过程已正确复制 version.json`); process.exit(1); } console.log(`📝 目录内容 (${files.length} 个文件/目录):`); files.forEach((file) => { const filePath = path.join(distPath, file); const stats = fs.statSync(filePath); const type = stats.isDirectory() ? '📁' : '📄'; const size = stats.isDirectory() ? '' : ` (${(stats.size / 1024).toFixed(1)} KB)`; console.log(` ${type} ${file}${size}`); }); try { fs.mkdirSync(path.dirname(tarPath), { recursive: true }); // 删除可能存在的旧版本 tar.gz 文件 if (fs.existsSync(tarPath)) { fs.unlinkSync(tarPath); console.log(`🗑️ 删除旧版本: ${tarName}`); } // 创建 tar.gz 包 // 包内结构应该是 web/ 目录,这样 deploy.sh 可以从 web/version.json 读取版本 // 进入 dist/apps 目录,打包 web 目录,这样包内路径就是 web/ // 使用 COPYFILE_DISABLE=1 禁用 macOS 扩展属性,确保跨平台兼容性 const packageRoot = preparePackageRoot(distPath); // 切换到 apps 目录,使用相对路径避免路径问题 // 排除 macOS 特定文件,使用 --exclude 选项 console.log(`🔄 执行压缩命令...`); console.log(` 工作目录: ${packageRoot.rootDir}`); console.log(` 打包目录: ${packageRoot.packageDirName}`); console.log(` 输出文件: ${tarPath}`); const env = { ...process.env, COPYFILE_DISABLE: '1' }; // 使用 tar 打包,确保包含所有文件 // COPYFILE_DISABLE=1 环境变量会禁用 macOS 扩展属性 // 只排除系统文件,不排除任何业务文件 try { execSync( `tar -czf "${tarPath}" --exclude='.DS_Store' --exclude='._*' --exclude='.git*' ${packageRoot.packageDirName}`, { cwd: packageRoot.rootDir, stdio: 'inherit', env: env, } ); } finally { packageRoot.cleanup(); } // 验证 tar.gz 文件是否创建成功 if (fs.existsSync(tarPath)) { const tarStats = fs.statSync(tarPath); const tarSizeMB = (tarStats.size / 1024 / 1024).toFixed(2); // 验证包内结构(检查是否能从 web/version.json 读取) try { // 使用 2>/dev/null 忽略扩展属性警告,这些警告不影响功能 const versionCheck = execSync( `tar -xzf "${tarPath}" -O web/version.json 2>/dev/null`, { encoding: 'utf8' } ); const versionInfo = JSON.parse(versionCheck.trim()); if (versionInfo.version === version) { console.log(`✅ 版本验证通过: ${version}`); } else { console.warn( `⚠️ 版本不匹配: 期望 ${version}, 实际 ${versionInfo.version}` ); } } catch (e) { console.warn(`⚠️ 无法验证包内版本信息: ${e.message}`); console.warn( ` 这可能是由于 macOS 扩展属性警告导致的,但不影响包的功能` ); } // 验证关键文件是否存在 try { const fileList = execSync(`tar -tzf "${tarPath}" 2>/dev/null`, { encoding: 'utf8', }); const files = fileList.split('\n').filter((f) => f.trim()); const fileSet = new Set(files); const rootIndexEntry = files.find((f) => f === 'web/index.html') || files.find((f) => /(^|\/)index\.html$/.test(f)); const hasAssets = files.some((f) => f.includes('assets/')); const hasIndex = Boolean(rootIndexEntry); const assetCount = files.filter( (f) => f.includes('assets/') && (f.endsWith('.js') || f.endsWith('.css')) ).length; let entryAssets = []; let missingEntryAssets = []; if (rootIndexEntry) { const indexHtml = execSync( `tar -xOzf "${tarPath}" "${rootIndexEntry}" 2>/dev/null`, { encoding: 'utf8' } ); const rootDir = path.posix.dirname(rootIndexEntry); entryAssets = collectEntryAssetsFromHtml(indexHtml); missingEntryAssets = entryAssets.filter( (asset) => !fileSet.has(path.posix.join(rootDir, asset)) ); } const hasEntryJs = entryAssets.some((asset) => asset.endsWith('.js')); const hasEntryCss = entryAssets.some((asset) => asset.endsWith('.css')); // 检查当前构建产物的关键资源组(文件名哈希是动态的,只检查 chunk 前缀 / manifest 语义) const hasAssetChunk = (pattern) => files.some((f) => f.includes('assets/') && pattern.test(f)); const hasToolWindowsJs = hasAssetChunk( /\/?assets\/tool-windows-[^/]+\.js$/ ); const hasToolWindowsCss = hasAssetChunk( /\/?assets\/tool-windows-[^/]+\.css$/ ); const hasToolRuntimeJs = hasAssetChunk( /\/?assets\/tool-runtime-[^/]+\.js$/ ); const hasToolDrawersJs = hasAssetChunk( /\/?assets\/tool-drawers-[^/]+\.js$/ ); const hasToolDialogsJs = hasAssetChunk( /\/?assets\/tool-dialogs-[^/]+\.js$/ ); const hasExternalSkillsJs = hasAssetChunk( /\/?assets\/external-skills-[^/]+\.js$/ ); const hasAnyAssetJs = files.some( (f) => f.includes('assets/') && f.endsWith('.js') ); const idlePrefetchManifestEntry = files.find((f) => /(^|\/)idle-prefetch-manifest\.json$/.test(f) ); let idlePrefetchManifest = null; let idlePrefetchGroupCounts = {}; let idlePrefetchDefaults = []; if (idlePrefetchManifestEntry) { try { const manifestContent = execSync( `tar -xOzf "${tarPath}" "${idlePrefetchManifestEntry}" 2>/dev/null`, { encoding: 'utf8' } ); idlePrefetchManifest = JSON.parse(manifestContent); idlePrefetchGroupCounts = Object.fromEntries( Object.entries(idlePrefetchManifest.groups || {}).map( ([group, entries]) => [ group, Array.isArray(entries) ? entries.length : 0, ] ) ); idlePrefetchDefaults = Array.isArray(idlePrefetchManifest.defaults) ? idlePrefetchManifest.defaults : []; } catch (e) { console.warn( `⚠️ 无法读取 idle-prefetch-manifest.json: ${e.message}` ); } } const hasRuntimeStaticAssetsGroup = Object.prototype.hasOwnProperty.call( idlePrefetchGroupCounts, 'runtime-static-assets' ); const runtimeStaticAssetsCount = idlePrefetchGroupCounts['runtime-static-assets'] || 0; const hasOfflineStaticAssetsGroup = Object.prototype.hasOwnProperty.call( idlePrefetchGroupCounts, 'offline-static-assets' ); const offlineStaticAssetsCount = idlePrefetchGroupCounts['offline-static-assets'] || 0; const usesCurrentToolChunkLayout = hasToolWindowsJs || hasRuntimeStaticAssetsGroup || idlePrefetchDefaults.includes('tool-windows'); const usesLegacyToolChunkLayout = hasToolRuntimeJs || hasToolDrawersJs || hasToolDialogsJs; const missingCriticalGroups = []; if (!hasEntryJs) missingCriticalGroups.push('entry script'); if (!hasEntryCss) missingCriticalGroups.push('entry stylesheet'); missingCriticalGroups.push(...missingEntryAssets); if (usesCurrentToolChunkLayout) { if (!hasToolWindowsJs) missingCriticalGroups.push('tool-windows-*.js'); if (!hasRuntimeStaticAssetsGroup) { missingCriticalGroups.push('runtime-static-assets(group)'); } } else { if (!hasToolRuntimeJs) missingCriticalGroups.push('tool-runtime-*.js'); if (!hasToolDrawersJs) missingCriticalGroups.push('tool-drawers-*.js'); if (!hasToolDialogsJs) missingCriticalGroups.push('tool-dialogs-*.js'); } console.log(`📊 打包统计:`); console.log(` 总文件数: ${files.length}`); console.log(` Assets 文件数: ${assetCount}`); console.log(` 关键资源组检查:`); console.log( ` - entry script: ${ hasEntryJs ? `✅ (${entryAssets .filter((asset) => asset.endsWith('.js')) .join(', ')})` : '❌' }` ); console.log( ` - entry stylesheet: ${ hasEntryCss ? `✅ (${entryAssets .filter((asset) => asset.endsWith('.css')) .join(', ')})` : '❌' }` ); if (usesCurrentToolChunkLayout) { console.log( ` - tool-windows-*.js: ${hasToolWindowsJs ? '✅' : '❌'}` ); console.log( ` - tool-windows-*.css: ${ hasToolWindowsCss ? '✅' : 'ℹ️ 未生成' }` ); console.log( ` - runtime-static-assets(group): ${ hasRuntimeStaticAssetsGroup ? `✅ (${runtimeStaticAssetsCount} entries)` : '❌' }` ); console.log( ` - offline-static-assets(group): ${ hasOfflineStaticAssetsGroup ? `✅ (${offlineStaticAssetsCount} entries)` : 'ℹ️ 未生成' }` ); if (usesLegacyToolChunkLayout) { console.log( ` - legacy tool-runtime/tool-drawers/tool-dialogs: ℹ️ 兼容存在,不再作为当前布局必需项` ); } } else { console.log( ` - tool-runtime-*.js: ${hasToolRuntimeJs ? '✅' : '❌'}` ); console.log( ` - tool-drawers-*.js: ${hasToolDrawersJs ? '✅' : '❌'}` ); console.log( ` - tool-dialogs-*.js: ${hasToolDialogsJs ? '✅' : '❌'}` ); } console.log( ` - external-skills-*.js: ${ hasExternalSkillsJs ? '✅' : 'ℹ️ 未生成' }` ); if (idlePrefetchManifest) { console.log( ` - idle-prefetch defaults: ${ idlePrefetchDefaults.length > 0 ? idlePrefetchDefaults.join(', ') : 'ℹ️ 空' }` ); } else { console.log(` - idle-prefetch-manifest.json: ℹ️ 未找到`); } if (!hasAssets) { console.warn(`⚠️ 警告: 未找到 assets 目录`); } if (!hasIndex) { console.warn(`⚠️ 警告: 未找到 index.html`); } if (assetCount < 50) { console.warn( `⚠️ 警告: Assets 文件数量较少 (${assetCount}),可能不完整` ); } if (!hasAnyAssetJs) { console.warn( `⚠️ 警告: assets 目录下未找到任何 JS 文件,产物可能不完整` ); } if (missingCriticalGroups.length > 0) { throw new Error(`关键资源缺失: ${missingCriticalGroups.join(', ')}`); } } catch (e) { if ( e.message === '关键文件缺失' || e.message.startsWith('关键资源缺失:') ) { throw e; } console.warn(`⚠️ 无法验证包内文件列表: ${e.message}`); } // 测试解压(在临时目录) console.log(`🧪 测试解压...`); const testDir = fs.mkdtempSync( path.join(os.tmpdir(), 'opentu-test-extract-') ); try { execSync( `tar -xzf "${tarPath}" -C "${testDir}" --strip-components=1 2>/dev/null`, { stdio: 'ignore' } ); // 验证解压后的关键文件(只检查必须存在的文件) const requiredFiles = ['version.json', 'index.html', 'assets']; let allFilesExist = true; for (const testFile of requiredFiles) { const testPath = path.join(testDir, testFile); if (!fs.existsSync(testPath)) { console.error(`❌ 测试解压失败: 未找到 ${testFile}`); allFilesExist = false; } } // 检查 assets 目录中是否有 JS 文件 const assetsDir = path.join(testDir, 'assets'); if (fs.existsSync(assetsDir)) { const assetFiles = fs .readdirSync(assetsDir) .filter((f) => f.endsWith('.js')); if (assetFiles.length === 0) { console.error(`❌ 测试解压失败: assets 目录为空`); allFilesExist = false; } else { console.log(`✅ assets 目录包含 ${assetFiles.length} 个 JS 文件`); } } if (allFilesExist) { console.log(`✅ 解压测试通过: 所有关键文件都能正确解压`); } else { throw new Error('解压测试失败'); } } catch (e) { if (e.message === '解压测试失败') { throw e; } console.warn(`⚠️ 解压测试跳过: ${e.message}`); } finally { fs.rmSync(testDir, { recursive: true, force: true }); } // 计算文件哈希 console.log(`🔐 计算文件哈希...`); const fileBuffer = fs.readFileSync(tarPath); const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); console.log(`✅ 文件哈希: ${hash.substring(0, 16)}...`); console.log(`✅ 部署包创建成功!`); console.log(`📦 文件: ${tarName}`); console.log(`📏 大小: ${tarSizeMB} MB`); console.log(`📍 路径: ${tarPath}`); console.log(`🔐 SHA256: ${hash}`); // 显示相对路径,更友好 const relativePath = path.relative(process.cwd(), tarPath); console.log(`🎉 相对路径: ${relativePath}`); // 返回 tar 文件路径和名称,供上传使用 return { tarPath, tarName, hash }; } else { throw new Error('TAR.GZ 文件创建失败'); } } catch (error) { console.error(`❌ 创建部署包失败:`, error.message); // 提供备用方案 console.log(`\n💡 手动创建方案:`); console.log(` cd ${path.dirname(distPath)}`); console.log(` tar -czf ${tarName} -C web .`); process.exit(1); } } // 主函数 function main() { // 解析命令行参数 const args = process.argv.slice(2); console.log(`🚀 Opentu 部署包创建工具`); console.log(`⏰ 时间: ${new Date().toLocaleString()}`); console.log(`───────────────────────────────────`); // 加载环境配置 const config = loadEnvConfig(); // 创建部署包 const result = createDeployPackage(); console.log(`───────────────────────────────────`); console.log(`🎊 部署包创建完成!`); // 自动上传(如果配置了服务器信息) let uploadSuccess = false; let deploySuccess = false; // 兼容 DEPLOY_AUTO_UPLOAD 配置(如果设置了,也设置 DEPLOY_AUTO_DEPLOY) // 如果 DEPLOY_AUTO_UPLOAD 设置了且不为 'false',且 DEPLOY_AUTO_DEPLOY 未设置或为 'false',则使用 DEPLOY_AUTO_UPLOAD 的值 if (config.DEPLOY_AUTO_UPLOAD && config.DEPLOY_AUTO_UPLOAD !== 'false') { if (!config.DEPLOY_AUTO_DEPLOY || config.DEPLOY_AUTO_DEPLOY === 'false') { config.DEPLOY_AUTO_DEPLOY = config.DEPLOY_AUTO_UPLOAD; console.log( `ℹ️ 检测到 DEPLOY_AUTO_UPLOAD=${ config.DEPLOY_AUTO_UPLOAD },将自动部署到${ config.DEPLOY_AUTO_UPLOAD === 'test' ? '测试' : '生产' }环境` ); } } if (config.DEPLOY_HOST && config.DEPLOY_USER) { // 默认自动上传,除非明确禁用 const shouldSkipUpload = args.includes('--no-upload'); if (!shouldSkipUpload) { // 使用打包时计算的哈希 const uploadResult = uploadToServer( result.tarPath, result.tarName, config, result.hash ); uploadSuccess = uploadResult && uploadResult.success; // 如果上传成功且配置了自动部署,执行远程部署脚本 if ( uploadSuccess && (config.DEPLOY_AUTO_DEPLOY === 'true' || config.DEPLOY_AUTO_DEPLOY === 'test') ) { const deployEnv = config.DEPLOY_AUTO_DEPLOY === 'test' ? 'test' : 'prod'; deploySuccess = executeRemoteDeploy( config, result.tarName, deployEnv, uploadResult.usePassword ); } else if (uploadSuccess) { console.log(`\n💡 上传成功,但未配置自动部署`); console.log( ` 如需自动部署,请在 .env 中设置 DEPLOY_AUTO_DEPLOY=test 或 DEPLOY_AUTO_DEPLOY=true` ); } } else { console.log(`\n💡 已跳过上传(使用 --no-upload 参数)`); console.log( ` 手动上传: scp ${result.tarPath} ${config.DEPLOY_USER}@${config.DEPLOY_HOST}:${config.DEPLOY_UPLOAD_DIR}/` ); } } else { console.log(`\n💡 未配置远程服务器信息,跳过上传`); console.log(` 请在 .env 文件中配置 DEPLOY_HOST 和 DEPLOY_USER`); } const version = getCurrentVersion(); const tarName = `web-${version}.tar.gz`; console.log(`\n💡 使用方法:`); if (uploadSuccess) { console.log(` ✅ 文件已上传到 ${config.DEPLOY_UPLOAD_DIR}/${tarName}`); if (deploySuccess) { console.log( ` ✅ 已自动部署到${ config.DEPLOY_AUTO_DEPLOY === 'test' ? '测试' : '生产' }环境` ); } else { if (config.DEPLOY_SCRIPT_PATH) { console.log( ` 在服务器上运行: ${config.DEPLOY_SCRIPT_PATH} --test ${tarName}` ); } else { console.log(` 在服务器上运行部署脚本 --test ${tarName}`); } } } else { console.log(` 本地部署: ./deploy.sh --test ${tarName}`); if (config.DEPLOY_HOST && config.DEPLOY_USER) { console.log( ` 手动上传: scp ${result.tarPath} ${config.DEPLOY_USER}@${config.DEPLOY_HOST}:${config.DEPLOY_UPLOAD_DIR}/` ); } } } main();