// .build/deploy-android-to-server.js - Android APK 部署脚本 const { NodeSSH } = require('node-ssh'); const path = require('path'); const fs = require('fs'); /** * 部署 Android APK 到远程服务器 * @param {Object} options 部署选项 * @param {string} options.privateKey SSH 私钥内容 * @param {string} options.host 服务器地址 * @param {string} options.username 用户名 * @param {string} options.remotePath H5 部署路径(会自动转换为 android-releases) * @param {string} options.apkPath 本地 APK 文件路径 * @param {string} options.appVersion 应用版本号(如 1.0.0) * @param {boolean} options.testOnly 是否仅测试连接 */ async function deployAndroidToServer(options) { const { privateKey, host, username, remotePath, apkPath, appVersion, testOnly = false } = options; const ssh = new NodeSSH(); try { console.log('🔐 正在连接到服务器...'); console.log(` 主机: ${username}@${host}`); // 连接到服务器 await ssh.connect({ host, username, privateKey, // 兼容多种 SSH 密钥算法 algorithms: { key: [ 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519' ], serverHostKey: [ 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519' ] } }); console.log('✅ SSH 连接成功!'); // 如果是测试模式,执行测试命令后返回 if (testOnly) { console.log('🧪 测试模式:执行测试命令...'); const result = await ssh.execCommand('echo "SSH key works!"'); console.log(` 输出: ${result.stdout}`); ssh.dispose(); console.log('✅ SSH 连接测试完成'); return; } // 检查本地 APK 文件是否存在 if (!fs.existsSync(apkPath)) { throw new Error(`本地 APK 文件不存在: ${apkPath}`); } // 路径转换:/var/www/html/releases -> /var/www/html/android-releases const androidRemotePath = remotePath.replace(/\/releases$/, '/android-releases'); console.log(`📱 Android APK 部署`); console.log(` H5 路径: ${remotePath}`); console.log(` Android 路径: ${androidRemotePath}`); // 生成时间戳 const timestamp = new Date().toISOString() .replace(/[-:]/g, '') .replace('T', '-') .split('.')[0]; // 生成版本号(格式: v1.1.5-20250129-143005) const version = `v${appVersion}-${timestamp}`; const remoteVersionDir = `${androidRemotePath}/${version}`; const apkFileName = path.basename(apkPath); const remoteApkPath = `${remoteVersionDir}/${apkFileName}`; console.log(`📦 部署版本: ${version}`); console.log(` 本地 APK: ${apkPath}`); console.log(` 远程路径: ${remoteApkPath}`); // 创建远程 android-releases 根目录 console.log('📁 创建远程目录结构...'); const mkdirResult = await ssh.execCommand(`mkdir -p ${remoteVersionDir}`); if (mkdirResult.code !== 0) { throw new Error(`创建远程目录失败: ${mkdirResult.stderr}`); } console.log('✅ 远程目录已创建'); // 上传 APK 文件 console.log('📤 开始上传 APK 文件...'); const fileStats = fs.statSync(apkPath); const fileSizeMB = (fileStats.size / 1024 / 1024).toFixed(2); console.log(` 文件大小: ${fileSizeMB} MB`); await ssh.putFile(apkPath, remoteApkPath); console.log('✅ APK 文件上传完成!'); // 验证上传的文件 console.log('🔍 验证上传的文件...'); const lsResult = await ssh.execCommand(`ls -lh ${remoteApkPath}`); if (lsResult.code !== 0) { throw new Error(`文件验证失败: ${lsResult.stderr}`); } console.log(` ${lsResult.stdout}`); // 更新 latest.apk 符号链接 console.log('🔗 更新 latest.apk 符号链接...'); const latestLinkPath = `${androidRemotePath}/latest.apk`; const symlinkCmd = `cd ${androidRemotePath} && ln -sfn ${version}/${apkFileName} latest.apk`; const symlinkResult = await ssh.execCommand(symlinkCmd); if (symlinkResult.code !== 0) { throw new Error(`更新符号链接失败: ${symlinkResult.stderr}`); } // 验证符号链接 console.log('🔍 验证符号链接...'); const readlinkResult = await ssh.execCommand(`readlink ${latestLinkPath}`); const currentTarget = readlinkResult.stdout.trim(); const expectedTarget = `${version}/${apkFileName}`; if (currentTarget === expectedTarget) { console.log(`✅ latest.apk 符号链接已正确更新 -> ${expectedTarget}`); } else { console.warn(`⚠️ 符号链接验证异常:`); console.warn(` 期望: ${expectedTarget}`); console.warn(` 实际: ${currentTarget}`); } // 生成/更新版本清单文件 console.log('📋 更新版本清单文件...'); await updateVersionsManifest(ssh, androidRemotePath, { version, appVersion, timestamp: new Date().toISOString(), filename: apkFileName, size: fileStats.size, downloadUrl: `/android-releases/${version}/${apkFileName}` }); // 列出所有版本 console.log('📊 版本列表:'); const versionsResult = await ssh.execCommand(`ls -1 ${androidRemotePath} | grep "^v"`); const versions = versionsResult.stdout.split('\n').filter(v => v.trim()); console.log(` 总计 ${versions.length} 个版本:`); versions.forEach((v, i) => { const marker = v === version ? '← 最新' : ''; console.log(` ${i + 1}. ${v} ${marker}`); }); console.log('\n🎉 Android APK 部署成功!'); console.log(` 版本: ${version}`); console.log(` 最新版本下载: ${androidRemotePath}/latest.apk`); console.log(` 版本清单: ${androidRemotePath}/versions.json`); ssh.dispose(); return version; } catch (error) { console.error('\n❌ 部署失败:', error.message); // 提供更详细的错误信息 if (error.level === 'client-authentication') { console.error(' → 认证失败,请检查 SSH 私钥是否正确'); } else if (error.level === 'client-timeout') { console.error(' → 连接超时,请检查网络和服务器地址'); } else if (error.code === 'ENOENT') { console.error(' → 文件或目录不存在'); } ssh.dispose(); throw error; } } /** * 更新版本清单文件 * @param {NodeSSH} ssh SSH 连接实例 * @param {string} remotePath 远程 android-releases 路径 * @param {Object} versionInfo 版本信息 */ async function updateVersionsManifest(ssh, remotePath, versionInfo) { const manifestPath = `${remotePath}/versions.json`; const tempLocalPath = path.join(__dirname, '.versions-temp.json'); let manifest = { latest: versionInfo.version, versions: [] }; try { // 尝试下载现有的版本清单文件 console.log(' 下载现有版本清单...'); await ssh.getFile(tempLocalPath, manifestPath); // 读取并解析 const content = fs.readFileSync(tempLocalPath, 'utf8'); manifest = JSON.parse(content); console.log(` 已加载现有清单,包含 ${manifest.versions.length} 个版本`); } catch (error) { // 文件不存在,使用默认清单 console.log(' 版本清单文件不存在,创建新清单'); } // 更新清单 manifest.latest = versionInfo.version; // 检查版本是否已存在 const existingIndex = manifest.versions.findIndex(v => v.version === versionInfo.version); if (existingIndex !== -1) { // 更新现有版本信息 manifest.versions[existingIndex] = versionInfo; console.log(' 更新现有版本信息'); } else { // 添加新版本到数组开头 manifest.versions.unshift(versionInfo); console.log(` 添加新版本,当前总计 ${manifest.versions.length} 个版本`); } // 写入本地临时文件 fs.writeFileSync(tempLocalPath, JSON.stringify(manifest, null, 2), 'utf8'); // 上传到服务器 console.log(' 上传版本清单到服务器...'); await ssh.putFile(tempLocalPath, manifestPath); // 清理临时文件 fs.unlinkSync(tempLocalPath); console.log('✅ 版本清单已更新'); } // CLI 入口点 if (require.main === module) { const args = process.argv.slice(2); const testOnly = args.includes('--test'); // 从环境变量读取配置 const privateKey = process.env.DEPLOY_KEY; const host = process.env.DEPLOY_HOST; const username = process.env.DEPLOY_USER; const remotePath = process.env.DEPLOY_PATH; // APK 文件路径(需要从命令行参数或环境变量获取) const apkPath = process.env.APK_PATH || args.find(arg => arg.endsWith('.apk')); const appVersion = process.env.APP_VERSION || '0.0.0'; // 验证必需的环境变量 const missingVars = []; if (!privateKey) missingVars.push('DEPLOY_KEY'); if (!host) missingVars.push('DEPLOY_HOST'); if (!username) missingVars.push('DEPLOY_USER'); if (!remotePath) missingVars.push('DEPLOY_PATH'); if (!testOnly && !apkPath) missingVars.push('APK_PATH 或命令行参数'); if (missingVars.length > 0) { console.error('❌ 缺少必需的环境变量/参数:'); missingVars.forEach(varName => { console.error(` - ${varName}`); }); console.error('\n请设置这些环境变量后重试。'); console.error('\n使用示例:'); console.error(' node deploy-android-to-server.js /path/to/app.apk'); console.error(' 或'); console.error(' APK_PATH=/path/to/app.apk APP_VERSION=1.0.0 node deploy-android-to-server.js'); process.exit(1); } // 执行部署 console.log('🚀 Android APK 部署系统启动\n'); if (testOnly) { console.log('🧪 模式: 连接测试'); } else { console.log('📦 模式: 完整部署'); } console.log('='.repeat(50) + '\n'); deployAndroidToServer({ privateKey, host, username, remotePath, apkPath, appVersion, testOnly }) .then(() => { console.log('\n' + '='.repeat(50)); process.exit(0); }) .catch(error => { console.error('\n' + '='.repeat(50)); process.exit(1); }); } module.exports = { deployAndroidToServer };