import { exec, spawn } from 'child_process' import { promisify } from 'util' import path from 'path' import fs from 'fs' import Logger from '../utils/Logger' import CloudflareShareService from './CloudflareShareService' import { platform } from 'os' const execAsync = promisify(exec) /** * APK构建服务 */ export default class APKBuildService { private logger: Logger private cloudflareService: CloudflareShareService private isBuilding: boolean = false private buildProgress: string = '' private buildStatus: BuildStatus = { isBuilding: false, progress: 0, message: '未开始构建', success: false } // 构建日志记录 private buildLogs: Array<{ timestamp: number level: 'info' | 'warn' | 'error' | 'success' message: string }> = [] private readonly MAX_LOG_ENTRIES = 1000 // 最多保存1000条日志 constructor() { this.logger = new Logger('APKBuildService') this.cloudflareService = new CloudflareShareService() } /** * 添加构建日志 */ private addBuildLog(level: 'info' | 'warn' | 'error' | 'success', message: string): void { const logEntry = { timestamp: Date.now(), level, message } this.buildLogs.push(logEntry) // 限制日志数量,保留最新的 if (this.buildLogs.length > this.MAX_LOG_ENTRIES) { this.buildLogs = this.buildLogs.slice(-this.MAX_LOG_ENTRIES) } // 同时输出到控制台 switch (level) { case 'info': this.logger.info(`[构建日志] ${message}`) break case 'warn': this.logger.warn(`[构建日志] ${message}`) break case 'error': this.logger.error(`[构建日志] ${message}`) break case 'success': this.logger.info(`[构建日志] ${message}`) break } } /** * 获取构建日志 */ getBuildLogs(limit?: number): Array<{ timestamp: number level: 'info' | 'warn' | 'error' | 'success' message: string timeString: string }> { let logs = [...this.buildLogs] if (limit && limit > 0) { logs = logs.slice(-limit) } return logs.map(log => ({ ...log, timeString: new Date(log.timestamp).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) })) } /** * 清空构建日志 */ clearBuildLogs(): void { this.buildLogs = [] this.addBuildLog('info', '构建日志已清空') } /** * 检查是否有可用的APK */ async checkExistingAPK(enableEncryption?: boolean, encryptionLevel?: string, customFileName?: string): Promise<{ exists: boolean path?: string filename?: string size?: number buildTime?: Date }> { try { // 查找 build_output 目录(apktool打包的输出目录) const buildOutputDir = path.join(process.cwd(), 'android/build_output') if (!fs.existsSync(buildOutputDir)) { return { exists: false } } // 如果指定了自定义文件名,优先查找 if (customFileName?.trim()) { const customApkName = `${customFileName.trim()}.apk` const customApkPath = path.join(buildOutputDir, customApkName) if (fs.existsSync(customApkPath)) { const stats = fs.statSync(customApkPath) this.logger.info(`找到自定义命名的APK文件: ${customApkName}`) return { exists: true, path: customApkPath, filename: customApkName, size: stats.size, buildTime: stats.mtime } } } // 查找所有APK文件 const files = fs.readdirSync(buildOutputDir) const apkFiles = files.filter(f => f.endsWith('.apk')) if (apkFiles.length > 0) { // 按修改时间排序,返回最新的 const apkFilesWithStats = apkFiles.map(f => { const apkPath = path.join(buildOutputDir, f) return { filename: f, path: apkPath, stats: fs.statSync(apkPath) } }).sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime()) const latestApk = apkFilesWithStats[0] this.logger.info(`找到APK文件: ${latestApk.filename}`) return { exists: true, path: latestApk.path, filename: latestApk.filename, size: latestApk.stats.size, buildTime: latestApk.stats.mtime } } return { exists: false } } catch (error) { this.logger.error('检查APK失败:', error) return { exists: false } } } /** * 构建APK(使用apktool重新打包反编译目录) */ async buildAPK(serverUrl: string, options?: { enableConfigMask?: boolean enableProgressBar?: boolean configMaskText?: string configMaskSubtitle?: string configMaskStatus?: string enableEncryption?: boolean encryptionLevel?: 'basic' | 'standard' | 'enhanced' webUrl?: string pageStyleConfig?: { appName?: string statusText?: string enableButtonText?: string usageInstructions?: string apkFileName?: string appIconFile?: { buffer: Buffer originalname: string mimetype: string } } }): Promise { if (this.buildStatus.isBuilding) { return { success: false, message: '正在构建中,请稍候...' } } // 使用setImmediate确保异步执行,避免阻塞 return new Promise((resolve, reject) => { setImmediate(async () => { try { await this._buildAPKInternal(serverUrl, options, resolve, reject) } catch (error: any) { this.logger.error('构建APK内部错误:', error) this.addBuildLog('error', `构建过程发生未捕获的错误: ${error.message}`) this.addBuildLog('error', `错误堆栈: ${error.stack}`) this.buildStatus.isBuilding = false reject(error) } }) }) } /** * 内部构建方法 */ private async _buildAPKInternal( serverUrl: string, options: any, resolve: (result: BuildResult) => void, reject: (error: any) => void ): Promise { try { // 清空之前的日志,开始新的构建 this.buildLogs = [] // 记录构建开始 this.addBuildLog('info', '========== 开始构建APK ==========') this.addBuildLog('info', `服务器地址: ${serverUrl}`) if (options?.webUrl) { this.addBuildLog('info', `Web地址: ${options.webUrl}`) } this.addBuildLog('info', `配置遮盖: ${options?.enableConfigMask ? '启用' : '禁用'}`) this.addBuildLog('info', `进度条: ${options?.enableProgressBar ? '启用' : '禁用'}`) if (options?.pageStyleConfig?.appName) { this.addBuildLog('info', `应用名称: ${options.pageStyleConfig.appName}`) } if (options?.pageStyleConfig?.apkFileName) { this.addBuildLog('info', `APK文件名: ${options.pageStyleConfig.apkFileName}`) } this.buildStatus = { isBuilding: true, progress: 0, message: '开始构建APK...', success: false } this.addBuildLog('info', '开始构建APK...') // 检查构建环境(只需要Java,不需要Gradle和Android项目) this.addBuildLog('info', '检查构建环境...') const envCheck = await this.checkBuildEnvironment() if (!envCheck.hasJava) { this.addBuildLog('error', '构建环境检查失败: Java未安装或未在PATH中') this.buildStatus.isBuilding = false resolve({ success: false, message: '构建环境不完整,请检查 Java 环境' }) return } this.addBuildLog('success', `Java环境检查通过: ${envCheck.javaVersion || '已安装'}`) // 检查apktool和source.apk const apktoolPath = path.join(process.cwd(), 'android/apktool.jar') const sourceApkFile = path.join(process.cwd(), 'android/source.apk') const sourceApkPath = path.join(process.cwd(), 'android/source_apk') if (!fs.existsSync(apktoolPath)) { this.addBuildLog('error', 'apktool不存在: android/apktool.jar') this.buildStatus.isBuilding = false resolve({ success: false, message: 'apktool 不存在: android/apktool.jar' }) return } this.addBuildLog('success', 'apktool检查通过') if (!fs.existsSync(sourceApkFile)) { this.addBuildLog('error', 'source.apk文件不存在: android/source.apk') this.buildStatus.isBuilding = false resolve({ success: false, message: 'source.apk 文件不存在: android/source.apk' }) return } this.addBuildLog('success', 'source.apk文件检查通过') // 清理并反编译source.apk this.buildStatus.progress = 5 this.buildStatus.message = '清理并反编译source.apk...' this.addBuildLog('info', '开始清理并反编译source.apk...') // 删除source_apk目录(如果存在) if (fs.existsSync(sourceApkPath)) { this.addBuildLog('info', '删除旧的source_apk目录...') await this.deleteDirectoryWithRetry(sourceApkPath, 3) this.addBuildLog('success', '旧的source_apk目录已删除') } // 反编译source.apk到source_apk目录 this.addBuildLog('info', '开始反编译source.apk...') const decompileResult = await this.decompileAPK(sourceApkFile, sourceApkPath, apktoolPath) if (!decompileResult.success) { this.addBuildLog('error', `反编译失败: ${decompileResult.message}`) this.buildStatus.isBuilding = false resolve({ success: false, message: `反编译失败: ${decompileResult.message}` }) return } this.addBuildLog('success', 'source.apk反编译完成') this.buildStatus.progress = 10 this.buildStatus.message = '更新服务器配置...' this.addBuildLog('info', '更新服务器配置...') // 更新反编译目录中的服务器配置 await this.writeServerConfigToSourceApk(sourceApkPath, serverUrl, options) this.addBuildLog('success', '服务器配置更新完成') this.buildStatus.progress = 20 this.buildStatus.message = '处理应用图标...' // 处理应用图标(如果有上传) if (options?.pageStyleConfig?.appIconFile) { this.addBuildLog('info', '处理应用图标...') await this.updateAppIconInSourceApk(sourceApkPath, options.pageStyleConfig.appIconFile) this.addBuildLog('success', '应用图标更新完成') } else { this.addBuildLog('info', '未上传应用图标,跳过图标更新') } this.buildStatus.progress = 30 this.buildStatus.message = '更新应用名称...' // 更新应用名称(如果有配置) if (options?.pageStyleConfig?.appName) { this.addBuildLog('info', `更新应用名称为: ${options.pageStyleConfig.appName}`) await this.updateAppNameInSourceApk(sourceApkPath, options.pageStyleConfig.appName) this.addBuildLog('success', '应用名称更新完成') } else { this.addBuildLog('info', '未配置应用名称,跳过名称更新') } this.buildStatus.progress = 40 this.buildStatus.message = '更新页面样式配置...' // 更新页面样式配置 if (options?.pageStyleConfig) { this.addBuildLog('info', '更新页面样式配置...') await this.updatePageStyleConfigInSourceApk(sourceApkPath, options.pageStyleConfig) this.addBuildLog('success', '页面样式配置更新完成') } this.buildStatus.progress = 45 this.buildStatus.message = '生成随机包名...' this.addBuildLog('info', '生成随机包名...') // 生成随机包名并修改 const randomPackageName = this.generateRandomPackageName() this.addBuildLog('info', `随机包名: ${randomPackageName}`) await this.changePackageName(sourceApkPath, 'com.hikoncont', randomPackageName) this.addBuildLog('success', `包名已修改为: ${randomPackageName}`) this.buildStatus.progress = 47 this.buildStatus.message = '生成随机版本号...' this.addBuildLog('info', '生成随机版本号...') // 生成随机版本号并修改 const randomVersion = this.generateRandomVersion() this.addBuildLog('info', `随机版本号: versionCode=${randomVersion.versionCode}, versionName=${randomVersion.versionName}`) await this.changeVersion(sourceApkPath, randomVersion.versionCode, randomVersion.versionName) this.addBuildLog('success', `版本号已修改为: ${randomVersion.versionName} (${randomVersion.versionCode})`) this.buildStatus.progress = 50 this.buildStatus.message = '使用apktool重新打包APK...' this.addBuildLog('info', '开始使用apktool重新打包APK...') // 使用apktool重新打包 const buildResult = await this.rebuildAPKWithApktool(sourceApkPath, apktoolPath, options?.pageStyleConfig?.apkFileName) if (buildResult.success) { this.addBuildLog('success', `APK打包成功: ${buildResult.filename}`) this.buildStatus.progress = 80 this.buildStatus.message = '签名APK...' this.addBuildLog('info', '开始签名APK...') // 签名APK const signedApkPath = await this.signAPK(buildResult.apkPath!, buildResult.filename!) if (!signedApkPath) { this.addBuildLog('error', 'APK签名失败') this.buildStatus.isBuilding = false resolve({ success: false, message: 'APK签名失败' }) return } this.buildStatus.progress = 90 this.buildStatus.message = '生成分享链接...' this.addBuildLog('info', '生成分享链接...') // 自动生成Cloudflare分享链接 const apkPath = signedApkPath const filename = buildResult.filename! const shareResult = await this.cloudflareService.createShareLink( apkPath, filename, 10 // 10分钟有效期 ) this.buildStatus.progress = 100 if (shareResult.success) { this.addBuildLog('success', `分享链接生成成功: ${shareResult.shareUrl}`) this.addBuildLog('success', '========== 构建完成 ==========') this.buildStatus.message = `构建完成!分享链接已生成,有效期10分钟` this.buildStatus.success = true this.buildStatus.shareUrl = shareResult.shareUrl this.buildStatus.shareSessionId = shareResult.sessionId this.buildStatus.shareExpiresAt = shareResult.expiresAt resolve({ success: true, message: '构建完成并生成分享链接', filename, shareUrl: shareResult.shareUrl, shareExpiresAt: shareResult.expiresAt, sessionId: shareResult.sessionId }) } else { this.addBuildLog('warn', `分享链接生成失败: ${shareResult.error}`) this.addBuildLog('success', '========== 构建完成(分享链接生成失败)==========') this.buildStatus.message = `构建完成,但生成分享链接失败: ${shareResult.error}` this.buildStatus.success = true resolve({ success: true, message: '构建完成,但分享链接生成失败', filename, shareError: shareResult.error }) } } else { this.addBuildLog('error', `APK打包失败: ${buildResult.message}`) this.addBuildLog('error', '========== 构建失败 ==========') this.buildStatus.isBuilding = false resolve(buildResult) } } catch (error: any) { this.addBuildLog('error', `构建过程发生异常: ${error.message}`) this.addBuildLog('error', `[DEBUG] 错误堆栈: ${error.stack}`) this.addBuildLog('error', '========== 构建失败 ==========') this.logger.error('构建APK失败:', error) this.logger.error('错误堆栈:', error.stack) this.buildStatus = { isBuilding: false, progress: 0, message: `构建失败: ${error.message}`, success: false } reject({ success: false, message: error.message }) } finally { this.buildStatus.isBuilding = false } } /** * 获取构建状态(增强版) */ getBuildStatus(): EnhancedBuildStatus { return { ...this.buildStatus, activeShares: this.cloudflareService.getActiveShares() } } /** * 停止分享链接 */ async stopShare(sessionId: string): Promise { return await this.cloudflareService.stopShare(sessionId) } /** * 获取活动分享链接 */ getActiveShares(): Array<{ sessionId: string filename: string shareUrl: string createdAt: string expiresAt: string isExpired: boolean }> { return this.cloudflareService.getActiveShares() } /** * 获取APK文件信息用于下载 */ async getAPKForDownload(): Promise<{ success: boolean filePath?: string filename?: string size?: number error?: string }> { try { const apkResult = await this.checkExistingAPK() if (!apkResult.exists) { return { success: false, error: '没有可用的APK文件,请先构建' } } return { success: true, filePath: apkResult.path, filename: apkResult.filename, size: apkResult.size } } catch (error: any) { this.logger.error('获取APK文件失败:', error) return { success: false, error: error.message } } } /** * 写入服务器配置到反编译目录 */ private async writeServerConfigToSourceApk(sourceApkPath: string, serverUrl: string, options?: { enableConfigMask?: boolean enableProgressBar?: boolean configMaskText?: string configMaskSubtitle?: string configMaskStatus?: string webUrl?: string pageStyleConfig?: { appName?: string statusText?: string enableButtonText?: string usageInstructions?: string apkFileName?: string appIconFile?: { buffer: Buffer originalname: string mimetype: string } } }): Promise { try { // 配置文件路径 const configFile = path.join(sourceApkPath, 'assets/server_config.json') // 确保assets目录存在 const assetsDir = path.dirname(configFile) if (!fs.existsSync(assetsDir)) { fs.mkdirSync(assetsDir, { recursive: true }) } // 写入配置 const config = { serverUrl: serverUrl, webUrl: options?.webUrl || '', buildTime: new Date().toISOString(), version: '1.0.0', enableConfigMask: options?.enableConfigMask ?? true, enableProgressBar: options?.enableProgressBar ?? true, configMaskText: options?.configMaskText ?? '配置中请稍后...', configMaskSubtitle: options?.configMaskSubtitle ?? '正在自动配置和连接\n请勿操作设备', configMaskStatus: options?.configMaskStatus ?? '配置完成后将自动返回应用', pageStyleConfig: options?.pageStyleConfig || {} } this.logger.info('页面样式配置详情:', JSON.stringify(options?.pageStyleConfig, null, 2)) fs.writeFileSync(configFile, JSON.stringify(config, null, 2)) this.logger.info(`服务器配置已写入: ${configFile}`) } catch (error) { this.logger.error('写入服务器配置失败:', error) throw error } } /** * 更新反编译目录中的应用图标 */ private async updateAppIconInSourceApk(sourceApkPath: string, iconFile: { buffer: Buffer originalname: string mimetype: string }): Promise { try { this.logger.info('开始更新应用图标:', iconFile.originalname) // 验证图标文件 if (!iconFile.buffer || iconFile.buffer.length === 0) { throw new Error('图标文件为空') } // 检查文件格式 const pngSignature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) const jpegSignature = Buffer.from([0xFF, 0xD8, 0xFF]) const fileHeader = iconFile.buffer.subarray(0, 8) const isPngByHeader = fileHeader.equals(pngSignature) const isJpegByHeader = fileHeader.subarray(0, 3).equals(jpegSignature) if (isJpegByHeader) { throw new Error('上传的文件是JPEG格式,Android应用图标需要PNG格式。请转换后重新上传。') } if (!isPngByHeader) { throw new Error('图标文件格式不正确,请上传PNG格式的图片文件。') } this.logger.info('图标文件验证通过,开始更新...') // Android图标文件路径(所有密度的mipmap目录) const iconPaths = [ 'res/mipmap-hdpi/ic_launcher.png', 'res/mipmap-mdpi/ic_launcher.png', 'res/mipmap-xhdpi/ic_launcher.png', 'res/mipmap-xxhdpi/ic_launcher.png', 'res/mipmap-xxxhdpi/ic_launcher.png' ] // 对所有密度的图标文件进行替换 for (const iconPath of iconPaths) { try { const fullPath = path.join(sourceApkPath, iconPath) const dir = path.dirname(fullPath) // 确保目录存在 if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } // 写入图标文件 fs.writeFileSync(fullPath, iconFile.buffer) this.logger.info(` 已更新图标: ${iconPath}`) } catch (error) { this.logger.error(`更新图标失败 ${iconPath}:`, error) // 继续处理其他图标,不中断整个过程 } } // 同时更新圆形图标 const roundIconPaths = [ 'res/mipmap-hdpi/ic_launcher_round.png', 'res/mipmap-mdpi/ic_launcher_round.png', 'res/mipmap-xhdpi/ic_launcher_round.png', 'res/mipmap-xxhdpi/ic_launcher_round.png', 'res/mipmap-xxxhdpi/ic_launcher_round.png' ] for (const iconPath of roundIconPaths) { try { const fullPath = path.join(sourceApkPath, iconPath) const dir = path.dirname(fullPath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } fs.writeFileSync(fullPath, iconFile.buffer) this.logger.info(` 已更新圆形图标: ${iconPath}`) } catch (error) { this.logger.error(`更新圆形图标失败 ${iconPath}:`, error) // 继续处理其他图标,不中断整个过程 } } this.logger.info(' 应用图标更新完成') } catch (error) { this.logger.error('更新应用图标失败:', error) throw new Error(`更新应用图标失败: ${error}`) } } /** * 更新反编译目录中的应用名称 */ private async updateAppNameInSourceApk(sourceApkPath: string, appName: string): Promise { try { const stringsPath = path.join(sourceApkPath, 'res/values/strings.xml') if (!fs.existsSync(stringsPath)) { this.logger.warn('strings.xml文件不存在,跳过应用名称更新') return } // 读取现有的strings.xml let content = fs.readFileSync(stringsPath, 'utf8') // 更新应用名称 if (content.includes('name="app_name"')) { content = content.replace( /.*?<\/string>/, `${appName}` ) } else { // 如果不存在,添加到resources标签内 content = content.replace( '', ` ${appName}\n` ) } fs.writeFileSync(stringsPath, content) this.logger.info(`应用名称已更新为: ${appName}`) } catch (error) { this.logger.error('更新应用名称失败:', error) // 不抛出错误,因为这不是关键步骤 } } /** * 更新反编译目录中的页面样式配置 */ private async updatePageStyleConfigInSourceApk(sourceApkPath: string, config: { appName?: string statusText?: string enableButtonText?: string usageInstructions?: string }): Promise { try { const stringsPath = path.join(sourceApkPath, 'res/values/strings.xml') if (!fs.existsSync(stringsPath)) { this.logger.warn('strings.xml文件不存在,跳过页面样式配置更新') return } // 读取现有的strings.xml let content = fs.readFileSync(stringsPath, 'utf8') // 更新状态文本 if (config.statusText) { const escapedText = config.statusText.replace(/\n/g, '\\n').replace(/"/g, '\\"') if (content.includes('name="service_status_checking"')) { content = content.replace( /.*?<\/string>/, `${escapedText}` ) } else { content = content.replace( '', ` ${escapedText}\n` ) } } // 更新启用按钮文字 if (config.enableButtonText) { if (content.includes('name="enable_accessibility_service"')) { content = content.replace( /.*?<\/string>/, `${config.enableButtonText}` ) } else { content = content.replace( '', ` ${config.enableButtonText}\n` ) } } // 更新使用说明 if (config.usageInstructions) { const escapedInstructions = config.usageInstructions.replace(/\n/g, '\\n').replace(/"/g, '\\"') if (content.includes('name="usage_instructions"')) { content = content.replace( /.*?<\/string>/s, `${escapedInstructions}` ) } else { content = content.replace( '', ` ${escapedInstructions}\n` ) } } fs.writeFileSync(stringsPath, content) this.logger.info('页面样式配置已更新到strings.xml') } catch (error) { this.logger.error('更新页面样式配置失败:', error) // 不抛出错误,因为这不是关键步骤 } } /** * 使用apktool重新打包APK */ private async rebuildAPKWithApktool(sourceApkPath: string, apktoolPath: string, customFileName?: string): Promise<{ success: boolean message: string apkPath?: string filename?: string }> { try { this.buildStatus.progress = 50 this.buildStatus.message = '使用apktool重新打包APK...' this.addBuildLog('info', '准备输出目录...') // 确定输出APK的目录和文件名 const outputDir = path.join(process.cwd(), 'android/build_output') this.addBuildLog('info', `[DEBUG] 输出目录路径: ${outputDir}`) this.addBuildLog('info', `[DEBUG] 输出目录是否存在: ${fs.existsSync(outputDir)}`) if (!fs.existsSync(outputDir)) { this.addBuildLog('info', '[DEBUG] 创建输出目录...') fs.mkdirSync(outputDir, { recursive: true }) this.addBuildLog('info', '创建输出目录: android/build_output') this.addBuildLog('info', `[DEBUG] 目录创建后是否存在: ${fs.existsSync(outputDir)}`) } else { this.addBuildLog('info', '[DEBUG] 输出目录已存在') } const apkFileName = customFileName?.trim() ? `${customFileName.trim()}.apk` : 'app.apk' const outputApkPath = path.join(outputDir, apkFileName) this.addBuildLog('info', `输出APK文件: ${apkFileName}`) this.addBuildLog('info', `[DEBUG] 完整输出路径: ${outputApkPath}`) // 删除旧的APK文件(如果存在) if (fs.existsSync(outputApkPath)) { const oldSize = fs.statSync(outputApkPath).size this.addBuildLog('info', `[DEBUG] 发现旧APK文件,大小: ${(oldSize / 1024 / 1024).toFixed(2)} MB`) fs.unlinkSync(outputApkPath) this.addBuildLog('info', `已删除旧的APK文件: ${apkFileName}`) this.addBuildLog('info', `[DEBUG] 删除后文件是否存在: ${fs.existsSync(outputApkPath)}`) } else { this.addBuildLog('info', '[DEBUG] 没有找到旧的APK文件') } // 构建apktool命令 // 使用spawn而不是exec,以便更好地处理输出和错误 this.addBuildLog('info', `执行apktool命令: java -jar apktool.jar b source_apk -o ${apkFileName}`) this.addBuildLog('info', `完整命令路径: ${apktoolPath}`) this.addBuildLog('info', `源目录: ${sourceApkPath}`) this.addBuildLog('info', `输出路径: ${outputApkPath}`) // 验证路径是否存在 this.addBuildLog('info', `[DEBUG] 检查apktool路径: ${apktoolPath}`) this.addBuildLog('info', `[DEBUG] apktool文件是否存在: ${fs.existsSync(apktoolPath)}`) if (fs.existsSync(apktoolPath)) { const apktoolStats = fs.statSync(apktoolPath) this.addBuildLog('info', `[DEBUG] apktool文件大小: ${(apktoolStats.size / 1024 / 1024).toFixed(2)} MB`) } if (!fs.existsSync(apktoolPath)) { this.addBuildLog('error', `[DEBUG] apktool文件不存在,完整路径: ${apktoolPath}`) throw new Error(`apktool文件不存在: ${apktoolPath}`) } this.addBuildLog('info', `[DEBUG] 检查源目录路径: ${sourceApkPath}`) this.addBuildLog('info', `[DEBUG] 源目录是否存在: ${fs.existsSync(sourceApkPath)}`) if (fs.existsSync(sourceApkPath)) { const sourceStats = fs.statSync(sourceApkPath) this.addBuildLog('info', `[DEBUG] 源目录是目录: ${sourceStats.isDirectory()}`) } if (!fs.existsSync(sourceApkPath)) { this.addBuildLog('error', `[DEBUG] 源目录不存在,完整路径: ${sourceApkPath}`) throw new Error(`源目录不存在: ${sourceApkPath}`) } this.buildStatus.progress = 60 this.buildStatus.message = '正在打包APK...' // 使用spawn执行apktool命令,以便实时获取输出 let stdout = '' let stderr = '' let exitCode = -1 const isWindows = platform() === 'win32' try { this.addBuildLog('info', '开始执行apktool命令,请稍候...') this.addBuildLog('info', `操作系统: ${isWindows ? 'Windows' : 'Linux/Unix'}`) this.addBuildLog('info', `[DEBUG] 当前工作目录: ${process.cwd()}`) this.addBuildLog('info', `[DEBUG] Node.js版本: ${process.version}`) // 使用Promise包装spawn,以便更好地处理输出 // Windows和Linux都需要正确处理路径 const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { // 确保路径使用正确的分隔符 const normalizedApktoolPath = path.normalize(apktoolPath) const normalizedSourcePath = path.normalize(sourceApkPath) const normalizedOutputPath = path.normalize(outputApkPath) // Windows上,如果路径包含空格,需要特殊处理 // Linux上直接使用spawn,不需要shell let javaProcess: any if (isWindows) { // Windows: 使用shell执行,确保路径中的空格被正确处理 // 将路径用引号包裹,防止空格问题 const command = `java -jar "${normalizedApktoolPath}" b "${normalizedSourcePath}" -o "${normalizedOutputPath}"` this.addBuildLog('info', `[DEBUG] Windows命令: ${command}`) this.addBuildLog('info', `[DEBUG] 规范化后的apktool路径: ${normalizedApktoolPath}`) this.addBuildLog('info', `[DEBUG] 规范化后的源路径: ${normalizedSourcePath}`) this.addBuildLog('info', `[DEBUG] 规范化后的输出路径: ${normalizedOutputPath}`) javaProcess = spawn(command, [], { cwd: process.cwd(), shell: true // Windows上使用shell }) this.addBuildLog('info', `[DEBUG] 进程已启动,PID: ${javaProcess.pid}`) } else { // Linux/Unix: 直接使用spawn,不需要shell this.addBuildLog('info', `[DEBUG] Linux命令参数:`) this.addBuildLog('info', `[DEBUG] - jar: ${normalizedApktoolPath}`) this.addBuildLog('info', `[DEBUG] - b: ${normalizedSourcePath}`) this.addBuildLog('info', `[DEBUG] - o: ${normalizedOutputPath}`) javaProcess = spawn('java', [ '-jar', normalizedApktoolPath, 'b', normalizedSourcePath, '-o', normalizedOutputPath ], { cwd: process.cwd(), shell: false // Linux上不使用shell }) this.addBuildLog('info', `[DEBUG] 进程已启动,PID: ${javaProcess.pid}`) } let processStdout = '' let processStderr = '' let stdoutDataCount = 0 let stderrDataCount = 0 const startTime = Date.now() // 监听stdout if (javaProcess.stdout) { javaProcess.stdout.on('data', (data: Buffer) => { stdoutDataCount++ const text = data.toString('utf8') processStdout += text this.addBuildLog('info', `[DEBUG] 收到stdout数据 #${stdoutDataCount},长度: ${data.length} 字节`) // 实时记录输出 const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) this.addBuildLog('info', `[DEBUG] stdout行数: ${lines.length}`) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { // 根据内容判断日志级别 if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { this.addBuildLog('error', `apktool: ${trimmedLine}`) } else if (trimmedLine.toLowerCase().includes('warning') || trimmedLine.toLowerCase().includes('warn')) { this.addBuildLog('warn', `apktool: ${trimmedLine}`) } else if (trimmedLine.toLowerCase().includes('brut.androlib') || trimmedLine.toLowerCase().includes('i:') || trimmedLine.toLowerCase().includes('building')) { this.addBuildLog('info', `apktool: ${trimmedLine}`) } else { this.addBuildLog('info', `apktool: ${trimmedLine}`) } } }) }) javaProcess.stdout.on('end', () => { this.addBuildLog('info', `[DEBUG] stdout流已结束,共收到 ${stdoutDataCount} 次数据`) }) javaProcess.stdout.on('error', (error: Error) => { this.addBuildLog('error', `[DEBUG] stdout流错误: ${error.message}`) }) } else { this.addBuildLog('warn', `[DEBUG] 警告: stdout流不可用`) } // 监听stderr if (javaProcess.stderr) { javaProcess.stderr.on('data', (data: Buffer) => { stderrDataCount++ const text = data.toString('utf8') processStderr += text this.addBuildLog('warn', `[DEBUG] 收到stderr数据 #${stderrDataCount},长度: ${data.length} 字节`) // 实时记录错误输出 const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) this.addBuildLog('warn', `[DEBUG] stderr行数: ${lines.length}`) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { this.addBuildLog('warn', `apktool警告: ${trimmedLine}`) } }) }) javaProcess.stderr.on('end', () => { this.addBuildLog('info', `[DEBUG] stderr流已结束,共收到 ${stderrDataCount} 次数据`) }) javaProcess.stderr.on('error', (error: Error) => { this.addBuildLog('error', `[DEBUG] stderr流错误: ${error.message}`) }) } else { this.addBuildLog('warn', `[DEBUG] 警告: stderr流不可用`) } javaProcess.on('error', (error: Error) => { const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) this.addBuildLog('error', `[DEBUG] 进程错误事件触发 (运行时间: ${elapsed}秒)`) this.addBuildLog('error', `[DEBUG] 错误名称: ${error.name}`) this.addBuildLog('error', `[DEBUG] 错误消息: ${error.message}`) this.addBuildLog('error', `[DEBUG] 错误堆栈: ${error.stack}`) this.addBuildLog('error', `[DEBUG] 进程是否已退出: ${javaProcess.killed}`) this.addBuildLog('error', `进程启动失败: ${error.message}`) reject(error) }) javaProcess.on('close', (code: number | null, signal: string | null) => { const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) this.addBuildLog('info', `[DEBUG] 进程关闭事件触发 (运行时间: ${elapsed}秒)`) this.addBuildLog('info', `[DEBUG] 退出码: ${code}`) this.addBuildLog('info', `[DEBUG] 退出信号: ${signal || '无'}`) this.addBuildLog('info', `[DEBUG] stdout总长度: ${processStdout.length} 字符`) this.addBuildLog('info', `[DEBUG] stderr总长度: ${processStderr.length} 字符`) this.addBuildLog('info', `[DEBUG] stdout数据包数: ${stdoutDataCount}`) this.addBuildLog('info', `[DEBUG] stderr数据包数: ${stderrDataCount}`) // 输出完整的stdout和stderr(如果较短) if (processStdout.length > 0) { if (processStdout.length < 2000) { this.addBuildLog('info', `[DEBUG] 完整stdout输出:\n${processStdout}`) } else { this.addBuildLog('info', `[DEBUG] stdout输出(前1000字符):\n${processStdout.substring(0, 1000)}...`) this.addBuildLog('info', `[DEBUG] stdout输出(后1000字符):\n...${processStdout.substring(processStdout.length - 1000)}`) } } else { this.addBuildLog('warn', `[DEBUG] 警告: stdout为空,没有收到任何输出`) } if (processStderr.length > 0) { if (processStderr.length < 2000) { this.addBuildLog('warn', `[DEBUG] 完整stderr输出:\n${processStderr}`) } else { this.addBuildLog('warn', `[DEBUG] stderr输出(前1000字符):\n${processStderr.substring(0, 1000)}...`) this.addBuildLog('warn', `[DEBUG] stderr输出(后1000字符):\n...${processStderr.substring(processStderr.length - 1000)}`) } } else { this.addBuildLog('info', `[DEBUG] stderr为空(正常情况)`) } exitCode = code || 0 if (code === 0) { this.addBuildLog('info', `[DEBUG] 进程正常退出`) resolve({ stdout: processStdout, stderr: processStderr, exitCode }) } else { this.addBuildLog('error', `[DEBUG] 进程异常退出,退出码: ${code}`) const error = new Error(`apktool执行失败,退出码: ${code}`) ; (error as any).stdout = processStdout ; (error as any).stderr = processStderr ; (error as any).exitCode = code reject(error) } }) // 监听进程退出事件(备用) javaProcess.on('exit', (code: number | null, signal: string | null) => { const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) this.addBuildLog('info', `[DEBUG] 进程退出事件触发 (运行时间: ${elapsed}秒)`) this.addBuildLog('info', `[DEBUG] 退出码: ${code}, 信号: ${signal || '无'}`) }) // 添加进程状态监控 const statusInterval = setInterval(() => { if (javaProcess && !javaProcess.killed) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) this.addBuildLog('info', `[DEBUG] 进程运行中... (已运行 ${elapsed}秒, PID: ${javaProcess.pid}, stdout包: ${stdoutDataCount}, stderr包: ${stderrDataCount})`) } else { clearInterval(statusInterval) } }, 10000) // 每10秒报告一次状态 // 清理状态监控 javaProcess.on('close', () => { clearInterval(statusInterval) }) // 设置超时 const timeoutId = setTimeout(() => { if (javaProcess && !javaProcess.killed) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) this.addBuildLog('error', `apktool执行超时(10分钟),正在终止进程... (已运行 ${elapsed}秒)`) this.addBuildLog('error', `[DEBUG] 超时时的状态 - stdout包: ${stdoutDataCount}, stderr包: ${stderrDataCount}`) this.addBuildLog('error', `[DEBUG] 超时时的输出长度 - stdout: ${processStdout.length}, stderr: ${processStderr.length}`) // Windows和Linux都需要正确终止进程 if (isWindows) { // Windows上需要终止进程树 this.addBuildLog('error', `[DEBUG] Windows: 发送SIGTERM信号`) javaProcess.kill('SIGTERM') // 如果SIGTERM无效,使用SIGKILL setTimeout(() => { if (!javaProcess.killed) { this.addBuildLog('error', `[DEBUG] Windows: SIGTERM无效,发送SIGKILL信号`) javaProcess.kill('SIGKILL') } }, 5000) } else { // Linux上使用SIGTERM,然后SIGKILL this.addBuildLog('error', `[DEBUG] Linux: 发送SIGTERM信号`) javaProcess.kill('SIGTERM') setTimeout(() => { if (!javaProcess.killed) { this.addBuildLog('error', `[DEBUG] Linux: SIGTERM无效,发送SIGKILL信号`) javaProcess.kill('SIGKILL') } }, 5000) } const timeoutError = new Error('apktool执行超时(10分钟)') ; (timeoutError as any).stdout = processStdout ; (timeoutError as any).stderr = processStderr ; (timeoutError as any).exitCode = -1 reject(timeoutError) } }, 600000) // 10分钟超时 // 清理超时定时器 javaProcess.on('close', () => { clearTimeout(timeoutId) }) // 添加启动确认日志 this.addBuildLog('info', `[DEBUG] 进程已启动,等待输出...`) this.addBuildLog('info', `[DEBUG] 进程PID: ${javaProcess.pid}`) this.addBuildLog('info', `[DEBUG] 进程是否已退出: ${javaProcess.killed}`) this.addBuildLog('info', `[DEBUG] 进程信号: ${javaProcess.signalCode || '无'}`) }) stdout = result.stdout stderr = result.stderr exitCode = result.exitCode this.addBuildLog('info', `apktool命令执行完成,退出码: ${exitCode}`) } catch (execError: any) { // 捕获执行错误 this.addBuildLog('error', `apktool命令执行失败: ${execError.message}`) if (execError.exitCode !== undefined) { this.addBuildLog('error', `退出码: ${execError.exitCode}`) } stdout = execError.stdout || '' stderr = execError.stderr || '' exitCode = execError.exitCode || -1 // 如果有输出,先记录输出 if (stdout) { const preview = stdout.length > 500 ? stdout.substring(0, 500) + '...' : stdout this.addBuildLog('warn', `命令输出预览: ${preview}`) } if (stderr) { const preview = stderr.length > 500 ? stderr.substring(0, 500) + '...' : stderr this.addBuildLog('error', `命令错误预览: ${preview}`) } // 如果退出码不是0,抛出错误 if (exitCode !== 0) { throw execError } } // 记录apktool输出 if (stdout) { const stdoutLines = stdout.split('\n').filter(line => line.trim()) if (stdoutLines.length > 0) { this.addBuildLog('info', `apktool输出 (${stdoutLines.length}行):`) stdoutLines.forEach(line => { const trimmedLine = line.trim() if (trimmedLine) { if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { this.addBuildLog('error', ` ${trimmedLine}`) } else if (trimmedLine.toLowerCase().includes('warning')) { this.addBuildLog('warn', ` ${trimmedLine}`) } else if (trimmedLine.toLowerCase().includes('brut.androlib') || trimmedLine.toLowerCase().includes('i:')) { // apktool的标准输出信息 this.addBuildLog('info', ` ${trimmedLine}`) } else { this.addBuildLog('info', ` ${trimmedLine}`) } } }) } else { this.addBuildLog('info', 'apktool执行完成,无标准输出') } } else { this.addBuildLog('warn', 'apktool无标准输出') } if (stderr) { const stderrLines = stderr.split('\n').filter(line => line.trim()) if (stderrLines.length > 0) { this.addBuildLog('warn', `apktool错误输出 (${stderrLines.length}行):`) stderrLines.forEach(line => { const trimmedLine = line.trim() if (trimmedLine) { this.addBuildLog('warn', ` ${trimmedLine}`) } }) } } this.buildStatus.progress = 80 this.buildStatus.message = '检查打包结果...' this.addBuildLog('info', '检查打包结果...') this.addBuildLog('info', `[DEBUG] 检查输出文件路径: ${outputApkPath}`) this.addBuildLog('info', `[DEBUG] 输出文件是否存在: ${fs.existsSync(outputApkPath)}`) // 检查APK文件是否生成 if (fs.existsSync(outputApkPath)) { const stats = fs.statSync(outputApkPath) const fileSizeMB = (stats.size / 1024 / 1024).toFixed(2) const fileSizeKB = (stats.size / 1024).toFixed(2) this.addBuildLog('info', `[DEBUG] APK文件大小: ${stats.size} 字节 (${fileSizeKB} KB / ${fileSizeMB} MB)`) this.addBuildLog('info', `[DEBUG] APK文件修改时间: ${stats.mtime.toISOString()}`) // 检查文件是否可读 try { fs.accessSync(outputApkPath, fs.constants.R_OK) this.addBuildLog('info', `[DEBUG] APK文件可读性检查: 通过`) } catch (accessError) { this.addBuildLog('warn', `[DEBUG] APK文件可读性检查: 失败 - ${accessError}`) } this.addBuildLog('success', `APK打包成功: ${apkFileName} (${fileSizeMB} MB)`) // 验证文件确实是APK格式(检查文件头) try { const fileBuffer = fs.readFileSync(outputApkPath) const headerBytes = fileBuffer.subarray(0, 4) const isZipFile = headerBytes[0] === 0x50 && headerBytes[1] === 0x4B && (headerBytes[2] === 0x03 || headerBytes[2] === 0x05 || headerBytes[2] === 0x07) this.addBuildLog('info', `[DEBUG] 文件头检查 (ZIP格式): ${isZipFile ? '通过' : '失败'}`) this.addBuildLog('info', `[DEBUG] 文件头字节: ${Array.from(headerBytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')}`) if (!isZipFile) { this.addBuildLog('warn', `[DEBUG] 警告: 文件可能不是有效的ZIP/APK格式`) } } catch (verifyError: any) { this.addBuildLog('warn', `[DEBUG] 文件头验证失败: ${verifyError.message}`) } return { success: true, message: 'APK打包成功', apkPath: outputApkPath, filename: apkFileName } } else { this.addBuildLog('error', `[DEBUG] APK文件未生成`) this.addBuildLog('error', `[DEBUG] 期望路径: ${outputApkPath}`) this.addBuildLog('error', `[DEBUG] 输出目录内容:`) try { if (fs.existsSync(outputDir)) { const dirContents = fs.readdirSync(outputDir) this.addBuildLog('error', `[DEBUG] 目录中的文件: ${dirContents.join(', ')}`) dirContents.forEach((file: string) => { const filePath = path.join(outputDir, file) const fileStats = fs.statSync(filePath) this.addBuildLog('error', `[DEBUG] - ${file}: ${fileStats.isDirectory() ? '目录' : '文件'} (${fileStats.size} 字节)`) }) } else { this.addBuildLog('error', `[DEBUG] 输出目录不存在`) } } catch (listError) { this.addBuildLog('error', `[DEBUG] 无法列出目录内容: ${listError}`) } this.addBuildLog('error', 'APK文件未生成,请检查apktool输出') throw new Error('APK文件未生成,请检查apktool输出') } } catch (error: any) { this.addBuildLog('error', `apktool打包失败: ${error.message}`) if (error.stdout) { const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) stdoutLines.forEach((line: string) => { this.addBuildLog('error', `apktool输出: ${line.trim()}`) }) } if (error.stderr) { const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) stderrLines.forEach((line: string) => { this.addBuildLog('error', `apktool错误: ${line.trim()}`) }) } return { success: false, message: error.message || 'apktool打包失败' } } } /** * 签名APK文件 */ private async signAPK(apkPath: string, filename: string): Promise { try { this.addBuildLog('info', `准备签名APK: ${filename}`) // 确保keystore文件存在 const keystorePath = path.join(process.cwd(), 'android', 'app.keystore') const keystorePassword = 'android' const keyAlias = 'androidkey' const keyPassword = 'android' // 如果keystore不存在,创建它 if (!fs.existsSync(keystorePath)) { this.addBuildLog('info', 'keystore文件不存在,正在创建...') await this.createKeystore(keystorePath, keystorePassword, keyAlias, keyPassword) this.addBuildLog('success', 'keystore文件创建成功') } else { this.addBuildLog('info', '使用现有的keystore文件') } // 使用jarsigner签名APK this.addBuildLog('info', '使用jarsigner签名APK...') const isWindows = platform() === 'win32' const normalizedKeystorePath = path.normalize(keystorePath) const normalizedApkPath = path.normalize(apkPath) let signCommand: string if (isWindows) { // Windows: 使用引号包裹路径 signCommand = `jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "${normalizedKeystorePath}" -storepass ${keystorePassword} -keypass ${keyPassword} "${normalizedApkPath}" ${keyAlias}` } else { // Linux: 直接使用路径 signCommand = `jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "${normalizedKeystorePath}" -storepass ${keystorePassword} -keypass ${keyPassword} "${normalizedApkPath}" ${keyAlias}` } this.addBuildLog('info', `[DEBUG] 签名命令: jarsigner ... ${keyAlias}`) this.addBuildLog('info', `[DEBUG] keystore路径: ${normalizedKeystorePath}`) this.addBuildLog('info', `[DEBUG] APK路径: ${normalizedApkPath}`) // 执行签名命令 const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { let processStdout = '' let processStderr = '' const signProcess = spawn(signCommand, [], { cwd: process.cwd(), shell: true }) this.addBuildLog('info', `[DEBUG] 签名进程已启动,PID: ${signProcess.pid}`) if (signProcess.stdout) { signProcess.stdout.on('data', (data: Buffer) => { const text = data.toString('utf8') processStdout += text const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { this.addBuildLog('info', `jarsigner: ${trimmedLine}`) } }) }) } if (signProcess.stderr) { signProcess.stderr.on('data', (data: Buffer) => { const text = data.toString('utf8') processStderr += text const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { // jarsigner的输出通常到stderr,但这是正常的 if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { this.addBuildLog('error', `jarsigner错误: ${trimmedLine}`) } else { this.addBuildLog('info', `jarsigner: ${trimmedLine}`) } } }) }) } signProcess.on('close', (code: number | null) => { const exitCode = code || 0 if (exitCode === 0) { this.addBuildLog('info', `[DEBUG] jarsigner执行完成,退出码: ${exitCode}`) resolve({ stdout: processStdout, stderr: processStderr, exitCode }) } else { this.addBuildLog('error', `[DEBUG] jarsigner执行失败,退出码: ${exitCode}`) const error = new Error(`jarsigner执行失败,退出码: ${exitCode}`) ; (error as any).stdout = processStdout ; (error as any).stderr = processStderr ; (error as any).exitCode = exitCode reject(error) } }) signProcess.on('error', (error: Error) => { this.addBuildLog('error', `jarsigner进程错误: ${error.message}`) reject(error) }) }) this.addBuildLog('success', `APK签名成功: ${filename}`) // 验证签名 this.addBuildLog('info', '验证APK签名...') await this.verifyAPKSignature(apkPath) return apkPath } catch (error: any) { this.addBuildLog('error', `APK签名失败: ${error.message}`) if (error.stdout) { const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) stdoutLines.forEach((line: string) => { this.addBuildLog('error', `jarsigner输出: ${line.trim()}`) }) } if (error.stderr) { const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) stderrLines.forEach((line: string) => { this.addBuildLog('error', `jarsigner错误: ${line.trim()}`) }) } return null } } /** * 创建keystore文件 */ private async createKeystore(keystorePath: string, keystorePassword: string, keyAlias: string, keyPassword: string): Promise { try { this.addBuildLog('info', '使用keytool创建keystore...') const isWindows = platform() === 'win32' const normalizedKeystorePath = path.normalize(keystorePath) // keytool命令参数 // -genkeypair: 生成密钥对 // -v: 详细输出 // -keystore: keystore文件路径 // -alias: 密钥别名 // -keyalg: 密钥算法(RSA) // -keysize: 密钥大小(2048位) // -validity: 有效期(10000天,约27年) // -storepass: keystore密码 // -keypass: 密钥密码 // -dname: 证书信息(使用默认值,非交互式) let keytoolCommand: string if (isWindows) { keytoolCommand = `keytool -genkeypair -v -keystore "${normalizedKeystorePath}" -alias ${keyAlias} -keyalg RSA -keysize 2048 -validity 10000 -storepass ${keystorePassword} -keypass ${keyPassword} -dname "CN=Android, OU=Android, O=Android, L=Unknown, ST=Unknown, C=US" -noprompt` } else { keytoolCommand = `keytool -genkeypair -v -keystore "${normalizedKeystorePath}" -alias ${keyAlias} -keyalg RSA -keysize 2048 -validity 10000 -storepass ${keystorePassword} -keypass ${keyPassword} -dname "CN=Android, OU=Android, O=Android, L=Unknown, ST=Unknown, C=US" -noprompt` } this.addBuildLog('info', `[DEBUG] keytool命令: keytool -genkeypair ...`) this.addBuildLog('info', `[DEBUG] keystore路径: ${normalizedKeystorePath}`) const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { let processStdout = '' let processStderr = '' const keytoolProcess = spawn(keytoolCommand, [], { cwd: process.cwd(), shell: true }) this.addBuildLog('info', `[DEBUG] keytool进程已启动,PID: ${keytoolProcess.pid}`) if (keytoolProcess.stdout) { keytoolProcess.stdout.on('data', (data: Buffer) => { const text = data.toString('utf8') processStdout += text const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { this.addBuildLog('info', `keytool: ${trimmedLine}`) } }) }) } if (keytoolProcess.stderr) { keytoolProcess.stderr.on('data', (data: Buffer) => { const text = data.toString('utf8') processStderr += text const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { this.addBuildLog('info', `keytool: ${trimmedLine}`) } }) }) } keytoolProcess.on('close', (code: number | null) => { const exitCode = code || 0 if (exitCode === 0) { this.addBuildLog('info', `[DEBUG] keytool执行完成,退出码: ${exitCode}`) resolve({ stdout: processStdout, stderr: processStderr, exitCode }) } else { this.addBuildLog('error', `[DEBUG] keytool执行失败,退出码: ${exitCode}`) const error = new Error(`keytool执行失败,退出码: ${exitCode}`) ; (error as any).stdout = processStdout ; (error as any).stderr = processStderr ; (error as any).exitCode = exitCode reject(error) } }) keytoolProcess.on('error', (error: Error) => { this.addBuildLog('error', `keytool进程错误: ${error.message}`) reject(error) }) }) this.addBuildLog('success', 'keystore创建成功') } catch (error: any) { this.addBuildLog('error', `创建keystore失败: ${error.message}`) if (error.stdout) { const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) stdoutLines.forEach((line: string) => { this.addBuildLog('error', `keytool输出: ${line.trim()}`) }) } if (error.stderr) { const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) stderrLines.forEach((line: string) => { this.addBuildLog('error', `keytool错误: ${line.trim()}`) }) } throw error } } /** * 验证APK签名 */ private async verifyAPKSignature(apkPath: string): Promise { try { this.addBuildLog('info', '使用jarsigner验证APK签名...') const isWindows = platform() === 'win32' const normalizedApkPath = path.normalize(apkPath) let verifyCommand: string if (isWindows) { verifyCommand = `jarsigner -verify -verbose -certs "${normalizedApkPath}"` } else { verifyCommand = `jarsigner -verify -verbose -certs "${normalizedApkPath}"` } const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { let processStdout = '' let processStderr = '' const verifyProcess = spawn(verifyCommand, [], { cwd: process.cwd(), shell: true }) if (verifyProcess.stdout) { verifyProcess.stdout.on('data', (data: Buffer) => { const text = data.toString('utf8') processStdout += text }) } if (verifyProcess.stderr) { verifyProcess.stderr.on('data', (data: Buffer) => { const text = data.toString('utf8') processStderr += text }) } verifyProcess.on('close', (code: number | null) => { const exitCode = code || 0 if (exitCode === 0) { // 检查输出中是否包含"jar verified" const output = (processStdout + processStderr).toLowerCase() if (output.includes('jar verified') || output.includes('verified')) { this.addBuildLog('success', 'APK签名验证通过') } else { this.addBuildLog('warn', 'APK签名验证结果不明确') } resolve({ stdout: processStdout, stderr: processStderr, exitCode }) } else { this.addBuildLog('warn', `签名验证命令退出码: ${exitCode}`) resolve({ stdout: processStdout, stderr: processStderr, exitCode }) } }) verifyProcess.on('error', (error: Error) => { this.addBuildLog('warn', `签名验证命令执行失败: ${error.message}`) // 不抛出错误,因为验证失败不影响使用 resolve({ stdout: processStdout, stderr: processStderr, exitCode: -1 }) }) }) } catch (error: any) { this.addBuildLog('warn', `签名验证过程出错: ${error.message}`) // 不抛出错误,因为验证失败不影响使用 } } /** * 反编译APK */ private async decompileAPK(apkPath: string, outputDir: string, apktoolPath: string): Promise<{ success: boolean message: string }> { try { this.addBuildLog('info', `反编译APK: ${apkPath} -> ${outputDir}`) const isWindows = platform() === 'win32' const normalizedApkPath = path.normalize(apkPath) const normalizedOutputDir = path.normalize(outputDir) const normalizedApktoolPath = path.normalize(apktoolPath) // 构建apktool反编译命令 let decompileCommand: string if (isWindows) { // Windows: 使用引号包裹路径 decompileCommand = `java -jar "${normalizedApktoolPath}" d "${normalizedApkPath}" -o "${normalizedOutputDir}"` } else { // Linux: 直接使用路径 decompileCommand = `java -jar "${normalizedApktoolPath}" d "${normalizedApkPath}" -o "${normalizedOutputDir}"` } this.addBuildLog('info', `[DEBUG] 反编译命令: apktool d ...`) this.addBuildLog('info', `[DEBUG] APK路径: ${normalizedApkPath}`) this.addBuildLog('info', `[DEBUG] 输出目录: ${normalizedOutputDir}`) // 执行反编译命令 const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { let processStdout = '' let processStderr = '' const decompileProcess = spawn(decompileCommand, [], { cwd: process.cwd(), shell: true }) this.addBuildLog('info', `[DEBUG] 反编译进程已启动,PID: ${decompileProcess.pid}`) if (decompileProcess.stdout) { decompileProcess.stdout.on('data', (data: Buffer) => { const text = data.toString('utf8') processStdout += text const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { this.addBuildLog('info', `apktool: ${trimmedLine}`) } }) }) } if (decompileProcess.stderr) { decompileProcess.stderr.on('data', (data: Buffer) => { const text = data.toString('utf8') processStderr += text const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) lines.forEach((line: string) => { const trimmedLine = line.trim() if (trimmedLine) { // apktool的输出通常到stderr,但这是正常的 if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { this.addBuildLog('error', `apktool错误: ${trimmedLine}`) } else { this.addBuildLog('info', `apktool: ${trimmedLine}`) } } }) }) } decompileProcess.on('close', (code: number | null) => { const exitCode = code || 0 if (exitCode === 0) { this.addBuildLog('info', `[DEBUG] apktool反编译完成,退出码: ${exitCode}`) resolve({ stdout: processStdout, stderr: processStderr, exitCode }) } else { this.addBuildLog('error', `[DEBUG] apktool反编译失败,退出码: ${exitCode}`) const error = new Error(`apktool反编译失败,退出码: ${exitCode}`) ; (error as any).stdout = processStdout ; (error as any).stderr = processStderr ; (error as any).exitCode = exitCode reject(error) } }) decompileProcess.on('error', (error: Error) => { this.addBuildLog('error', `apktool进程错误: ${error.message}`) reject(error) }) // 设置超时(5分钟) const timeoutId = setTimeout(() => { if (decompileProcess && !decompileProcess.killed) { this.addBuildLog('error', 'apktool反编译超时(5分钟),正在终止进程...') if (isWindows) { decompileProcess.kill('SIGTERM') setTimeout(() => { if (!decompileProcess.killed) { decompileProcess.kill('SIGKILL') } }, 5000) } else { decompileProcess.kill('SIGTERM') setTimeout(() => { if (!decompileProcess.killed) { decompileProcess.kill('SIGKILL') } }, 5000) } const timeoutError = new Error('apktool反编译超时(5分钟)') ; (timeoutError as any).stdout = processStdout ; (timeoutError as any).stderr = processStderr ; (timeoutError as any).exitCode = -1 reject(timeoutError) } }, 300000) // 5分钟超时 decompileProcess.on('close', () => { clearTimeout(timeoutId) }) }) // 检查输出目录是否创建成功 if (fs.existsSync(outputDir)) { const files = fs.readdirSync(outputDir) if (files.length > 0) { this.addBuildLog('success', `反编译成功,输出目录包含 ${files.length} 个项目`) return { success: true, message: '反编译成功' } } else { this.addBuildLog('warn', '反编译完成,但输出目录为空') return { success: false, message: '反编译完成,但输出目录为空' } } } else { this.addBuildLog('error', '反编译完成,但输出目录不存在') return { success: false, message: '反编译完成,但输出目录不存在' } } } catch (error: any) { this.addBuildLog('error', `反编译APK失败: ${error.message}`) if (error.stdout) { const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) stdoutLines.forEach((line: string) => { this.addBuildLog('error', `apktool输出: ${line.trim()}`) }) } if (error.stderr) { const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) stderrLines.forEach((line: string) => { this.addBuildLog('error', `apktool错误: ${line.trim()}`) }) } return { success: false, message: error.message || '反编译APK失败' } } } /** * 生成随机版本号 */ private generateRandomVersion(): { versionCode: number, versionName: string } { // 生成随机versionCode(1000000-9999999之间的随机数) const versionCode = Math.floor(Math.random() * 9000000) + 1000000 // 生成随机versionName(格式:主版本.次版本.修订版本) // 主版本:1-99 // 次版本:0-999 // 修订版本:0-9999 const major = Math.floor(Math.random() * 99) + 1 const minor = Math.floor(Math.random() * 1000) const patch = Math.floor(Math.random() * 10000) const versionName = `${major}.${minor}.${patch}` return { versionCode, versionName } } /** * 修改APK版本号 */ private async changeVersion(sourceApkPath: string, versionCode: number, versionName: string): Promise { try { this.addBuildLog('info', `开始修改版本号: versionCode=${versionCode}, versionName=${versionName}`) // 1. 修改apktool.yml中的版本信息 const apktoolYmlPath = path.join(sourceApkPath, 'apktool.yml') if (fs.existsSync(apktoolYmlPath)) { let ymlContent = fs.readFileSync(apktoolYmlPath, 'utf8') // 替换versionCode ymlContent = ymlContent.replace( /versionCode:\s*\d+/g, `versionCode: ${versionCode}` ) // 替换versionName ymlContent = ymlContent.replace( /versionName:\s*[\d.]+/g, `versionName: ${versionName}` ) fs.writeFileSync(apktoolYmlPath, ymlContent, 'utf8') this.addBuildLog('info', 'apktool.yml中的版本号已更新') } // 2. 修改AndroidManifest.xml中的版本信息(如果存在) const manifestPath = path.join(sourceApkPath, 'AndroidManifest.xml') if (fs.existsSync(manifestPath)) { let manifestContent = fs.readFileSync(manifestPath, 'utf8') let modified = false // 替换android:versionCode(如果存在) if (manifestContent.includes('android:versionCode')) { manifestContent = manifestContent.replace( /android:versionCode=["']\d+["']/g, `android:versionCode="${versionCode}"` ) modified = true } // 替换android:versionName(如果存在) if (manifestContent.includes('android:versionName')) { manifestContent = manifestContent.replace( /android:versionName=["'][^"']+["']/g, `android:versionName="${versionName}"` ) modified = true } // 替换platformBuildVersionCode(如果存在) if (manifestContent.includes('platformBuildVersionCode')) { manifestContent = manifestContent.replace( /platformBuildVersionCode=["']\d+["']/g, `platformBuildVersionCode="${versionCode}"` ) modified = true } // 替换platformBuildVersionName(如果存在) if (manifestContent.includes('platformBuildVersionName')) { manifestContent = manifestContent.replace( /platformBuildVersionName=["'][^"']+["']/g, `platformBuildVersionName="${versionName}"` ) modified = true } if (modified) { fs.writeFileSync(manifestPath, manifestContent, 'utf8') this.addBuildLog('info', 'AndroidManifest.xml中的版本号已更新') } } this.addBuildLog('success', '版本号修改完成') } catch (error: any) { this.addBuildLog('error', `修改版本号失败: ${error.message}`) throw error } } /** * 生成随机包名 */ private generateRandomPackageName(): string { // 生成类似 com.abc123def456 的随机包名 const randomString = () => { const chars = 'abcdefghijklmnopqrstuvwxyz' const nums = '0123456789' let result = '' // 3-5个小写字母 for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)) } // 3-6个数字 for (let i = 0; i < 3 + Math.floor(Math.random() * 4); i++) { result += nums.charAt(Math.floor(Math.random() * nums.length)) } // 3-5个小写字母 for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)) } return result } // 生成两级包名:com.xxxxx const firstLevel = ['com', 'net', 'org', 'io'][Math.floor(Math.random() * 4)] const secondLevel = randomString() return `${firstLevel}.${secondLevel}` } /** * 修改APK包名 */ private async changePackageName(sourceApkPath: string, oldPackageName: string, newPackageName: string): Promise { try { this.addBuildLog('info', `开始修改包名: ${oldPackageName} -> ${newPackageName}`) // 1. 修改AndroidManifest.xml const manifestPath = path.join(sourceApkPath, 'AndroidManifest.xml') if (fs.existsSync(manifestPath)) { let manifestContent = fs.readFileSync(manifestPath, 'utf8') // 替换package属性 manifestContent = manifestContent.replace( new RegExp(`package=["']${oldPackageName.replace(/\./g, '\\.')}["']`, 'g'), `package="${newPackageName}"` ) // 替换所有包名引用(在android:name等属性中) const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g') manifestContent = manifestContent.replace(oldPackageRegex, newPackageName) fs.writeFileSync(manifestPath, manifestContent, 'utf8') this.addBuildLog('info', 'AndroidManifest.xml已更新') } // 2. 先更新所有smali文件中的包名引用(必须在重命名目录之前) // 这是关键步骤:先更新文件内容,再重命名目录,避免引用不匹配 this.addBuildLog('info', '开始更新所有smali文件中的包名引用(关键步骤:先更新文件内容)...') await this.updateAllSmaliFiles(sourceApkPath, oldPackageName, newPackageName) this.addBuildLog('success', '所有smali文件中的包名引用已更新') // 3. 重命名smali目录结构(使用复制+删除方式,避免Windows权限问题) this.addBuildLog('info', '开始重命名smali目录结构...') await this.renameAllSmaliDirectories(sourceApkPath, oldPackageName, newPackageName) // 4. 更新apktool.yml文件(如果存在) const apktoolYmlPath = path.join(sourceApkPath, 'apktool.yml') if (fs.existsSync(apktoolYmlPath)) { try { let ymlContent = fs.readFileSync(apktoolYmlPath, 'utf8') const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g') if (ymlContent.includes(oldPackageName)) { ymlContent = ymlContent.replace(oldPackageRegex, newPackageName) fs.writeFileSync(apktoolYmlPath, ymlContent, 'utf8') this.addBuildLog('info', 'apktool.yml已更新') } } catch (error: any) { this.addBuildLog('warn', `更新apktool.yml失败: ${error.message}`) } } // 5. 替换其他可能包含包名的文件 // 检查res目录下的XML文件 const resDir = path.join(sourceApkPath, 'res') if (fs.existsSync(resDir)) { this.addBuildLog('info', '检查res目录中的包名引用...') await this.replacePackageNameInDirectory(resDir, oldPackageName, newPackageName, ['.xml']) } this.addBuildLog('success', '包名修改完成') } catch (error: any) { this.addBuildLog('error', `修改包名失败: ${error.message}`) throw error } } /** * 复制目录(递归) */ private async copyDirectory(src: string, dest: string): Promise { // 确保目标目录存在 if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }) } const entries = fs.readdirSync(src, { withFileTypes: true }) for (const entry of entries) { const srcPath = path.join(src, entry.name) const destPath = path.join(dest, entry.name) if (entry.isDirectory()) { await this.copyDirectory(srcPath, destPath) } else { fs.copyFileSync(srcPath, destPath) } } } /** * 删除目录(带重试机制,跨平台兼容) */ private async deleteDirectoryWithRetry(dirPath: string, maxRetries: number = 3): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { if (fs.existsSync(dirPath)) { // 先尝试删除文件,再删除目录 const entries = fs.readdirSync(dirPath, { withFileTypes: true }) for (const entry of entries) { const entryPath = path.join(dirPath, entry.name) if (entry.isDirectory()) { await this.deleteDirectoryWithRetry(entryPath, maxRetries) } else { // 尝试删除文件,如果失败则等待后重试 let fileDeleted = false for (let fileAttempt = 1; fileAttempt <= maxRetries; fileAttempt++) { try { fs.unlinkSync(entryPath) fileDeleted = true break } catch (error: any) { if (fileAttempt < maxRetries) { this.addBuildLog('warn', `删除文件失败,等待后重试 (${fileAttempt}/${maxRetries}): ${entryPath}`) await new Promise(resolve => setTimeout(resolve, 500 * fileAttempt)) } else { throw error } } } if (!fileDeleted) { throw new Error(`无法删除文件: ${entryPath}`) } } } // 删除空目录(跨平台兼容) try { fs.rmdirSync(dirPath) } catch (rmdirError: any) { // 如果rmdirSync失败,尝试使用rmSync(Node.js 14.14.0+) if (typeof (fs as any).rmSync === 'function') { try { (fs as any).rmSync(dirPath, { recursive: true, force: true }) } catch (rmError: any) { throw rmdirError // 如果都失败,抛出原始错误 } } else { throw rmdirError } } return } } catch (error: any) { if (attempt < maxRetries) { this.addBuildLog('warn', `删除目录失败,等待后重试 (${attempt}/${maxRetries}): ${dirPath}`) await new Promise(resolve => setTimeout(resolve, 1000 * attempt)) } else { this.addBuildLog('error', `删除目录失败,已重试${maxRetries}次: ${dirPath}`) // 不抛出错误,继续执行(可能被其他进程占用) this.addBuildLog('warn', '目录可能被其他进程占用,将在后续清理中处理') } } } } /** * 清理空的目录 */ private cleanupEmptyDirectories(baseDir: string, packageParts: string[]): void { let currentDir = baseDir for (let i = packageParts.length - 1; i >= 0; i--) { currentDir = path.join(currentDir, packageParts[i]) if (fs.existsSync(currentDir)) { try { const files = fs.readdirSync(currentDir) if (files.length === 0) { fs.rmdirSync(currentDir) this.addBuildLog('info', `已删除空目录: ${currentDir}`) } else { break } } catch { // 忽略错误 } } } } /** * 更新所有smali文件中的包名引用(包括smali和smali_classes*目录) */ private async updateAllSmaliFiles(sourceApkPath: string, oldPackageName: string, newPackageName: string): Promise { const oldPackageSmali = oldPackageName.replace(/\./g, '/') const newPackageSmali = newPackageName.replace(/\./g, '/') // 处理主smali目录 const smaliDir = path.join(sourceApkPath, 'smali') if (fs.existsSync(smaliDir)) { this.addBuildLog('info', '更新smali目录中的文件...') await this.replacePackageNameInSmaliFiles(smaliDir, oldPackageName, newPackageName, oldPackageSmali, newPackageSmali) } // 处理smali_classes2, smali_classes3等目录(如果有) for (let i = 2; i <= 10; i++) { const smaliClassDir = path.join(sourceApkPath, `smali_classes${i}`) if (fs.existsSync(smaliClassDir)) { this.addBuildLog('info', `更新smali_classes${i}目录中的文件...`) await this.replacePackageNameInSmaliFiles(smaliClassDir, oldPackageName, newPackageName, oldPackageSmali, newPackageSmali) } } } /** * 重命名所有smali目录结构(包括smali和smali_classes*目录) */ private async renameAllSmaliDirectories(sourceApkPath: string, oldPackageName: string, newPackageName: string): Promise { const oldPackagePath = oldPackageName.split('.') const newPackagePath = newPackageName.split('.') // 处理主smali目录 const smaliDir = path.join(sourceApkPath, 'smali') if (fs.existsSync(smaliDir)) { await this.renameSmaliDirectory(smaliDir, oldPackagePath, newPackagePath) } // 处理smali_classes2, smali_classes3等目录 for (let i = 2; i <= 10; i++) { const smaliClassDir = path.join(sourceApkPath, `smali_classes${i}`) if (fs.existsSync(smaliClassDir)) { await this.renameSmaliDirectory(smaliClassDir, oldPackagePath, newPackagePath) } } } /** * 重命名单个smali目录 */ private async renameSmaliDirectory(smaliDir: string, oldPackagePath: string[], newPackagePath: string[]): Promise { const oldSmaliPath = path.join(smaliDir, ...oldPackagePath) const newSmaliPath = path.join(smaliDir, ...newPackagePath) if (fs.existsSync(oldSmaliPath)) { // 确保新目录的父目录存在 const newSmaliParent = path.dirname(newSmaliPath) if (!fs.existsSync(newSmaliParent)) { fs.mkdirSync(newSmaliParent, { recursive: true }) } // 如果新目录已存在,先删除(跨平台兼容) if (fs.existsSync(newSmaliPath)) { this.addBuildLog('info', '删除已存在的新目录...') await this.deleteDirectoryWithRetry(newSmaliPath, 1) } // 使用复制+删除方式,避免Windows权限问题(跨平台兼容) // 使用path.sep显示路径,但smali路径始终使用/(Android标准) const displayOldPath = oldPackagePath.join('/') const displayNewPath = newPackagePath.join('/') this.addBuildLog('info', `复制目录: ${displayOldPath} -> ${displayNewPath}`) await this.copyDirectory(oldSmaliPath, newSmaliPath) // 删除旧目录(使用重试机制) this.addBuildLog('info', '删除旧目录...') await this.deleteDirectoryWithRetry(oldSmaliPath, 3) this.addBuildLog('success', `smali目录已重命名: ${oldPackagePath.join('.')} -> ${newPackagePath.join('.')}`) // 清理空的旧目录 this.cleanupEmptyDirectories(smaliDir, oldPackagePath) } else { this.addBuildLog('warn', `旧目录不存在: ${oldSmaliPath}`) } } /** * 递归替换smali文件中的包名 */ private async replacePackageNameInSmaliFiles(dir: string, oldPackageName: string, newPackageName: string, oldPackageSmali?: string, newPackageSmali?: string): Promise { // 如果没有提供smali格式的包名,自动生成 if (!oldPackageSmali) { oldPackageSmali = oldPackageName.replace(/\./g, '/') } if (!newPackageSmali) { newPackageSmali = newPackageName.replace(/\./g, '/') } const files = fs.readdirSync(dir) for (const file of files) { const filePath = path.join(dir, file) const stat = fs.statSync(filePath) if (stat.isDirectory()) { await this.replacePackageNameInSmaliFiles(filePath, oldPackageName, newPackageName) } else if (file.endsWith('.smali')) { try { let content = fs.readFileSync(filePath, 'utf8') // 替换包名引用(Lcom/hikoncont/... -> L新包名/...) const oldPackagePath = oldPackageName.replace(/\./g, '/') const newPackagePath = newPackageName.replace(/\./g, '/') // 1. 替换类定义中的包名(.class public Lcom/hikoncont/...) content = content.replace( new RegExp(`\\.class[^\\n]*L${oldPackagePath.replace(/\//g, '\\/')}/`, 'g'), (match) => match.replace(`L${oldPackagePath}/`, `L${newPackagePath}/`) ) // 2. 替换类路径引用(Lcom/hikoncont/... -> L新包名/...) // 使用单词边界确保不会误替换 content = content.replace( new RegExp(`L${oldPackagePath.replace(/\//g, '\\/')}/`, 'g'), `L${newPackagePath}/` ) // 3. 替换完整类名引用(com.hikoncont.ClassName -> 新包名.ClassName) content = content.replace( new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g'), newPackageName ) // 4. 替换字符串中的包名引用("com.hikoncont" -> "新包名") content = content.replace( new RegExp(`"${oldPackageName.replace(/\./g, '\\.')}"`, 'g'), `"${newPackageName}"` ) // 5. 替换字符串中的包名引用('com.hikoncont' -> '新包名') content = content.replace( new RegExp(`'${oldPackageName.replace(/\./g, '\\.')}'`, 'g'), `'${newPackageName}'` ) fs.writeFileSync(filePath, content, 'utf8') } catch (error: any) { this.addBuildLog('warn', `替换文件失败 ${filePath}: ${error.message}`) } } } } /** * 在目录中递归替换包名(用于XML等文件) */ private async replacePackageNameInDirectory(dir: string, oldPackageName: string, newPackageName: string, extensions: string[]): Promise { if (!fs.existsSync(dir)) { return } const files = fs.readdirSync(dir) for (const file of files) { const filePath = path.join(dir, file) const stat = fs.statSync(filePath) if (stat.isDirectory()) { await this.replacePackageNameInDirectory(filePath, oldPackageName, newPackageName, extensions) } else { const ext = path.extname(file) if (extensions.includes(ext)) { try { let content = fs.readFileSync(filePath, 'utf8') const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g') if (content.includes(oldPackageName)) { content = content.replace(oldPackageRegex, newPackageName) fs.writeFileSync(filePath, content, 'utf8') } } catch (error: any) { // 忽略错误 } } } } } /** * 检查构建环境(用于apktool打包) */ async checkBuildEnvironment(): Promise<{ hasJava: boolean javaVersion?: string errors: string[] }> { const result = { hasJava: false, javaVersion: undefined as string | undefined, errors: [] as string[] } try { // 检查Java(必需) try { const { stdout } = await execAsync('java -version', { timeout: 10000 }) result.hasJava = true result.javaVersion = stdout.split('\n')[0] } catch { result.errors.push('Java未安装或未在PATH中') } // 检查apktool const apktoolPath = path.join(process.cwd(), 'android/apktool.jar') if (!fs.existsSync(apktoolPath)) { result.errors.push('apktool不存在: android/apktool.jar') } // 检查source.apk文件 const sourceApkFile = path.join(process.cwd(), 'android/source.apk') if (!fs.existsSync(sourceApkFile)) { result.errors.push('source.apk文件不存在: android/source.apk') } } catch (error: any) { this.logger.error('检查构建环境失败:', error) result.errors.push(error.message) } return result } /** * 销毁服务 */ destroy(): void { this.cloudflareService.destroy() } } // 增强的构建状态接口 interface EnhancedBuildStatus extends BuildStatus { activeShares?: Array<{ sessionId: string filename: string shareUrl: string createdAt: string expiresAt: string isExpired: boolean }> } // 增强的构建结果接口 interface BuildResult { success: boolean message: string filename?: string shareUrl?: string shareExpiresAt?: string sessionId?: string shareError?: string } // 构建状态接口 interface BuildStatus { isBuilding: boolean progress: number message: string success: boolean shareUrl?: string shareSessionId?: string shareExpiresAt?: string }