From 909af7764987023f1b05da1725151a23ad5eea11 Mon Sep 17 00:00:00 2001 From: AoXuan Date: Tue, 2 Sep 2025 16:09:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor(services):=20=E8=A1=A5=E4=B8=80?= =?UTF-8?q?=E4=B8=8Bjs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist-electron/services/downloadService.js | 86 +++ .../services/environmentService.js | 65 +++ frontend/dist-electron/services/gitService.js | 321 ++++++++++++ .../dist-electron/services/pythonService.js | 489 ++++++++++++++++++ 4 files changed, 961 insertions(+) create mode 100644 frontend/dist-electron/services/downloadService.js create mode 100644 frontend/dist-electron/services/environmentService.js create mode 100644 frontend/dist-electron/services/gitService.js create mode 100644 frontend/dist-electron/services/pythonService.js diff --git a/frontend/dist-electron/services/downloadService.js b/frontend/dist-electron/services/downloadService.js new file mode 100644 index 0000000..3f5628b --- /dev/null +++ b/frontend/dist-electron/services/downloadService.js @@ -0,0 +1,86 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setMainWindow = setMainWindow; +exports.downloadFile = downloadFile; +const https = __importStar(require("https")); +const fs = __importStar(require("fs")); +const http = __importStar(require("http")); +let mainWindow = null; +function setMainWindow(window) { + mainWindow = window; +} +function downloadFile(url, outputPath) { + return new Promise((resolve, reject) => { + console.log(`开始下载文件: ${url}`); + console.log(`保存路径: ${outputPath}`); + const file = fs.createWriteStream(outputPath); + // 创建HTTP客户端,兼容https和http + const client = url.startsWith('https') ? https : http; + client + .get(url, response => { + const totalSize = parseInt(response.headers['content-length'] || '0', 10); + let downloadedSize = 0; + console.log(`文件大小: ${totalSize} bytes`); + response.pipe(file); + response.on('data', chunk => { + downloadedSize += chunk.length; + const progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0; + console.log(`下载进度: ${progress}% (${downloadedSize}/${totalSize})`); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + progress, + status: 'downloading', + message: `下载中... ${progress}%`, + }); + } + }); + file.on('finish', () => { + file.close(); + console.log(`文件下载完成: ${outputPath}`); + resolve(); + }); + file.on('error', err => { + console.error(`文件写入错误: ${err.message}`); + fs.unlink(outputPath, () => { }); // 删除不完整的文件 + reject(err); + }); + }) + .on('error', err => { + console.error(`下载错误: ${err.message}`); + reject(err); + }); + }); +} diff --git a/frontend/dist-electron/services/environmentService.js b/frontend/dist-electron/services/environmentService.js new file mode 100644 index 0000000..fb26df5 --- /dev/null +++ b/frontend/dist-electron/services/environmentService.js @@ -0,0 +1,65 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAppRoot = getAppRoot; +exports.checkEnvironment = checkEnvironment; +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const electron_1 = require("electron"); +// 获取应用根目录 +function getAppRoot() { + return process.env.NODE_ENV === 'development' ? process.cwd() : path.dirname(electron_1.app.getPath('exe')); +} +// 检查环境 +function checkEnvironment(appRoot) { + const environmentPath = path.join(appRoot, 'environment'); + const pythonPath = path.join(environmentPath, 'python'); + const gitPath = path.join(environmentPath, 'git'); + const backendPath = path.join(appRoot, 'backend'); + const requirementsPath = path.join(backendPath, 'requirements.txt'); + const pythonExists = fs.existsSync(pythonPath); + const gitExists = fs.existsSync(gitPath); + const backendExists = fs.existsSync(backendPath); + // 检查依赖是否已安装(简单检查是否存在site-packages目录) + const sitePackagesPath = path.join(pythonPath, 'Lib', 'site-packages'); + const dependenciesInstalled = fs.existsSync(sitePackagesPath) && fs.readdirSync(sitePackagesPath).length > 10; + return { + pythonExists, + gitExists, + backendExists, + dependenciesInstalled, + isInitialized: pythonExists && gitExists && backendExists && dependenciesInstalled, + }; +} diff --git a/frontend/dist-electron/services/gitService.js b/frontend/dist-electron/services/gitService.js new file mode 100644 index 0000000..5a60096 --- /dev/null +++ b/frontend/dist-electron/services/gitService.js @@ -0,0 +1,321 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setMainWindow = setMainWindow; +exports.downloadGit = downloadGit; +exports.cloneBackend = cloneBackend; +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const child_process_1 = require("child_process"); +const adm_zip_1 = __importDefault(require("adm-zip")); +const downloadService_1 = require("./downloadService"); +let mainWindow = null; +function setMainWindow(window) { + mainWindow = window; +} +const gitDownloadUrl = 'http://221.236.27.82:10197/d/AUTO_MAA/git.zip'; +// 递归复制目录,包括文件和隐藏文件 +function copyDirSync(src, dest) { + 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()) { + copyDirSync(srcPath, destPath); + } + else { + // 直接覆盖写,不需要先删除 + fs.copyFileSync(srcPath, destPath); + } + } +} +// 获取Git环境变量配置 +function getGitEnvironment(appRoot) { + const gitDir = path.join(appRoot, 'environment', 'git'); + const binPath = path.join(gitDir, 'bin'); + const mingw64BinPath = path.join(gitDir, 'mingw64', 'bin'); + const gitCorePath = path.join(gitDir, 'mingw64', 'libexec', 'git-core'); + return { + ...process.env, + // 修复remote-https问题的关键:确保所有Git相关路径都在PATH中 + PATH: `${binPath};${mingw64BinPath};${gitCorePath};${process.env.PATH}`, + GIT_EXEC_PATH: gitCorePath, + GIT_TEMPLATE_DIR: path.join(gitDir, 'mingw64', 'share', 'git-core', 'templates'), + HOME: process.env.USERPROFILE || process.env.HOME, + // // SSL证书路径 + // GIT_SSL_CAINFO: path.join(gitDir, 'mingw64', 'ssl', 'certs', 'ca-bundle.crt'), + // 禁用系统Git配置 + GIT_CONFIG_NOSYSTEM: '1', + // 禁用交互式认证 + GIT_TERMINAL_PROMPT: '0', + GIT_ASKPASS: '', + // // 修复remote-https问题的关键环境变量 + // CURL_CA_BUNDLE: path.join(gitDir, 'mingw64', 'ssl', 'certs', 'ca-bundle.crt'), + // 确保Git能找到所有必要的程序 + GIT_HTTP_LOW_SPEED_LIMIT: '0', + GIT_HTTP_LOW_SPEED_TIME: '0', + }; +} +// 检查是否为Git仓库 +function isGitRepository(dirPath) { + const gitDir = path.join(dirPath, '.git'); + return fs.existsSync(gitDir); +} +// 下载Git +async function downloadGit(appRoot) { + try { + const environmentPath = path.join(appRoot, 'environment'); + const gitPath = path.join(environmentPath, 'git'); + if (!fs.existsSync(environmentPath)) { + fs.mkdirSync(environmentPath, { recursive: true }); + } + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'git', + progress: 0, + status: 'downloading', + message: '开始下载Git...', + }); + } + // 使用自定义Git压缩包 + const zipPath = path.join(environmentPath, 'git.zip'); + await (0, downloadService_1.downloadFile)(gitDownloadUrl, zipPath); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'git', + progress: 100, + status: 'extracting', + message: '正在解压Git...', + }); + } + // 解压Git到临时目录,然后移动到正确位置 + console.log(`开始解压Git到: ${gitPath}`); + // 创建临时解压目录 + const tempExtractPath = path.join(environmentPath, 'git_temp'); + if (!fs.existsSync(tempExtractPath)) { + fs.mkdirSync(tempExtractPath, { recursive: true }); + console.log(`创建临时解压目录: ${tempExtractPath}`); + } + // 解压到临时目录 + const zip = new adm_zip_1.default(zipPath); + zip.extractAllTo(tempExtractPath, true); + console.log(`Git解压到临时目录: ${tempExtractPath}`); + // 检查解压后的目录结构 + const tempContents = fs.readdirSync(tempExtractPath); + console.log(`临时目录内容:`, tempContents); + // 如果解压后有git子目录,则从git子目录移动内容 + let sourceDir = tempExtractPath; + if (tempContents.length === 1 && tempContents[0] === 'git') { + sourceDir = path.join(tempExtractPath, 'git'); + console.log(`检测到git子目录,使用源目录: ${sourceDir}`); + } + // 确保目标Git目录存在 + if (!fs.existsSync(gitPath)) { + fs.mkdirSync(gitPath, { recursive: true }); + console.log(`创建Git目录: ${gitPath}`); + } + // 移动文件到最终目录 + const sourceContents = fs.readdirSync(sourceDir); + for (const item of sourceContents) { + const sourcePath = path.join(sourceDir, item); + const targetPath = path.join(gitPath, item); + // 如果目标已存在,先删除 + if (fs.existsSync(targetPath)) { + if (fs.statSync(targetPath).isDirectory()) { + fs.rmSync(targetPath, { recursive: true, force: true }); + } + else { + fs.unlinkSync(targetPath); + } + } + // 移动文件或目录 + fs.renameSync(sourcePath, targetPath); + console.log(`移动: ${sourcePath} -> ${targetPath}`); + } + // 清理临时目录 + fs.rmSync(tempExtractPath, { recursive: true, force: true }); + console.log(`清理临时目录: ${tempExtractPath}`); + console.log(`Git解压完成到: ${gitPath}`); + // 删除zip文件 + fs.unlinkSync(zipPath); + console.log(`删除临时文件: ${zipPath}`); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'git', + progress: 100, + status: 'completed', + message: 'Git安装完成', + }); + } + return { success: true }; + } + catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'git', + progress: 0, + status: 'error', + message: `Git下载失败: ${errorMessage}`, + }); + } + return { success: false, error: errorMessage }; + } +} +// 克隆后端代码(替换原有核心逻辑) +async function cloneBackend(appRoot, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') { + try { + const backendPath = appRoot; + const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe'); + if (!fs.existsSync(gitPath)) + throw new Error(`Git可执行文件不存在: ${gitPath}`); + const gitEnv = getGitEnvironment(appRoot); + // 检查 git 是否可用 + await new Promise((resolve, reject) => { + const proc = (0, child_process_1.spawn)(gitPath, ['--version'], { env: gitEnv }); + proc.on('close', code => (code === 0 ? resolve() : reject(new Error('git 无法正常运行')))); + proc.on('error', reject); + }); + // ==== 下面是关键逻辑 ==== + if (isGitRepository(backendPath)) { + // 已是 git 仓库,直接 pull + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'backend', + progress: 0, + status: 'downloading', + message: '正在更新后端代码...', + }); + } + await new Promise((resolve, reject) => { + const proc = (0, child_process_1.spawn)(gitPath, ['pull'], { stdio: 'pipe', env: gitEnv, cwd: backendPath }); + proc.stdout?.on('data', d => console.log('git pull:', d.toString())); + proc.stderr?.on('data', d => console.log('git pull err:', d.toString())); + proc.on('close', code => code === 0 ? resolve() : reject(new Error(`git pull失败,退出码: ${code}`))); + proc.on('error', reject); + }); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'backend', + progress: 100, + status: 'completed', + message: '后端代码更新完成', + }); + } + } + else { + // 不是 git 仓库,clone 到 tmp,再拷贝出来 + const tmpDir = path.join(appRoot, 'git_tmp'); + if (fs.existsSync(tmpDir)) + fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.mkdirSync(tmpDir, { recursive: true }); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'backend', + progress: 0, + status: 'downloading', + message: '正在克隆后端代码...', + }); + } + await new Promise((resolve, reject) => { + const proc = (0, child_process_1.spawn)(gitPath, [ + 'clone', + '--progress', + '--verbose', + '--single-branch', + '--depth', + '1', + '--branch', + 'feature/refactor', + repoUrl, + tmpDir, + ], { + stdio: 'pipe', + env: gitEnv, + cwd: appRoot, + }); + proc.stdout?.on('data', d => console.log('git clone:', d.toString())); + proc.stderr?.on('data', d => console.log('git clone err:', d.toString())); + proc.on('close', code => code === 0 ? resolve() : reject(new Error(`git clone失败,退出码: ${code}`))); + proc.on('error', reject); + }); + // 复制所有文件到 backendPath(appRoot),包含 .git + const tmpFiles = fs.readdirSync(tmpDir); + for (const file of tmpFiles) { + const src = path.join(tmpDir, file); + const dst = path.join(backendPath, file); + if (fs.existsSync(dst)) { + if (fs.statSync(dst).isDirectory()) + fs.rmSync(dst, { recursive: true, force: true }); + else + fs.unlinkSync(dst); + } + if (fs.statSync(src).isDirectory()) + copyDirSync(src, dst); + else + fs.copyFileSync(src, dst); + } + fs.rmSync(tmpDir, { recursive: true, force: true }); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'backend', + progress: 100, + status: 'completed', + message: '后端代码克隆完成', + }); + } + } + return { success: true }; + } + catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('获取后端代码失败:', errorMessage); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'backend', + progress: 0, + status: 'error', + message: `后端代码获取失败: ${errorMessage}`, + }); + } + return { success: false, error: errorMessage }; + } +} diff --git a/frontend/dist-electron/services/pythonService.js b/frontend/dist-electron/services/pythonService.js new file mode 100644 index 0000000..05a64c1 --- /dev/null +++ b/frontend/dist-electron/services/pythonService.js @@ -0,0 +1,489 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setMainWindow = setMainWindow; +exports.downloadPython = downloadPython; +exports.installDependencies = installDependencies; +exports.installPipPackage = installPipPackage; +exports.startBackend = startBackend; +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const child_process_1 = require("child_process"); +const adm_zip_1 = __importDefault(require("adm-zip")); +const downloadService_1 = require("./downloadService"); +let mainWindow = null; +function setMainWindow(window) { + mainWindow = window; +} +// Python镜像源URL映射 +const pythonMirrorUrls = { + official: 'https://www.python.org/ftp/python/3.12.0/python-3.12.0-embed-amd64.zip', + tsinghua: 'https://mirrors.tuna.tsinghua.edu.cn/python/3.12.0/python-3.12.0-embed-amd64.zip', + ustc: 'https://mirrors.ustc.edu.cn/python/3.12.0/python-3.12.0-embed-amd64.zip', + huawei: 'https://mirrors.huaweicloud.com/repository/toolkit/python/3.12.0/python-3.12.0-embed-amd64.zip', + aliyun: 'https://mirrors.aliyun.com/python-release/windows/python-3.12.0-embed-amd64.zip', +}; +// 检查pip是否已安装 +function isPipInstalled(pythonPath) { + const scriptsPath = path.join(pythonPath, 'Scripts'); + const pipExePath = path.join(scriptsPath, 'pip.exe'); + const pip3ExePath = path.join(scriptsPath, 'pip3.exe'); + console.log(`检查pip安装状态:`); + console.log(`Scripts目录: ${scriptsPath}`); + console.log(`pip.exe路径: ${pipExePath}`); + console.log(`pip3.exe路径: ${pip3ExePath}`); + const scriptsExists = fs.existsSync(scriptsPath); + const pipExists = fs.existsSync(pipExePath); + const pip3Exists = fs.existsSync(pip3ExePath); + console.log(`Scripts目录存在: ${scriptsExists}`); + console.log(`pip.exe存在: ${pipExists}`); + console.log(`pip3.exe存在: ${pip3Exists}`); + return scriptsExists && (pipExists || pip3Exists); +} +// 安装pip +async function installPip(pythonPath, appRoot) { + console.log('开始检查pip安装状态...'); + const pythonExe = path.join(pythonPath, 'python.exe'); + // 检查Python可执行文件是否存在 + if (!fs.existsSync(pythonExe)) { + throw new Error(`Python可执行文件不存在: ${pythonExe}`); + } + // 检查pip是否已安装 + if (isPipInstalled(pythonPath)) { + console.log('pip已经安装,跳过安装步骤'); + console.log('检测到pip.exe文件存在,认为pip安装成功'); + console.log('pip检查完成'); + return; + } + console.log('pip未安装,开始安装...'); + const getPipPath = path.join(pythonPath, 'get-pip.py'); + const getPipUrl = 'http://221.236.27.82:10197/d/AUTO_MAA/get-pip.py'; + console.log(`Python可执行文件路径: ${pythonExe}`); + console.log(`get-pip.py下载URL: ${getPipUrl}`); + console.log(`get-pip.py保存路径: ${getPipPath}`); + // 下载get-pip.py + console.log('开始下载get-pip.py...'); + try { + await (0, downloadService_1.downloadFile)(getPipUrl, getPipPath); + console.log('get-pip.py下载完成'); + // 检查下载的文件大小 + const stats = fs.statSync(getPipPath); + console.log(`get-pip.py文件大小: ${stats.size} bytes`); + if (stats.size < 10000) { + // 如果文件小于10KB,可能是无效文件 + throw new Error(`get-pip.py文件大小异常: ${stats.size} bytes,可能下载失败`); + } + } + catch (error) { + console.error('下载get-pip.py失败:', error); + throw new Error(`下载get-pip.py失败: ${error}`); + } + // 执行pip安装 + await new Promise((resolve, reject) => { + console.log('执行pip安装命令...'); + const process = (0, child_process_1.spawn)(pythonExe, [getPipPath], { + cwd: pythonPath, + stdio: 'pipe', + }); + process.stdout?.on('data', data => { + const output = data.toString(); + console.log('pip安装输出:', output); + }); + process.stderr?.on('data', data => { + const errorOutput = data.toString(); + console.log('pip安装错误输出:', errorOutput); + }); + process.on('close', code => { + console.log(`pip安装完成,退出码: ${code}`); + if (code === 0) { + console.log('pip安装成功'); + resolve(); + } + else { + reject(new Error(`pip安装失败,退出码: ${code}`)); + } + }); + process.on('error', error => { + console.error('pip安装进程错误:', error); + reject(error); + }); + }); + // 验证pip是否安装成功 + console.log('验证pip安装...'); + await new Promise((resolve, reject) => { + const verifyProcess = (0, child_process_1.spawn)(pythonExe, ['-m', 'pip', '--version'], { + cwd: pythonPath, + stdio: 'pipe', + }); + verifyProcess.stdout?.on('data', data => { + const output = data.toString(); + console.log('pip版本信息:', output); + }); + verifyProcess.stderr?.on('data', data => { + const errorOutput = data.toString(); + console.log('pip版本检查错误:', errorOutput); + }); + verifyProcess.on('close', code => { + if (code === 0) { + console.log('pip验证成功'); + resolve(); + } + else { + reject(new Error(`pip验证失败,退出码: ${code}`)); + } + }); + verifyProcess.on('error', error => { + console.error('pip验证进程错误:', error); + reject(error); + }); + }); + // 清理临时文件 + console.log('清理临时文件...'); + try { + if (fs.existsSync(getPipPath)) { + fs.unlinkSync(getPipPath); + console.log('get-pip.py临时文件已删除'); + } + } + catch (error) { + console.warn('清理get-pip.py文件时出错:', error); + } + console.log('pip安装和验证完成'); +} +// 下载Python +async function downloadPython(appRoot, mirror = 'ustc') { + try { + const environmentPath = path.join(appRoot, 'environment'); + const pythonPath = path.join(environmentPath, 'python'); + // 确保environment目录存在 + if (!fs.existsSync(environmentPath)) { + fs.mkdirSync(environmentPath, { recursive: true }); + } + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'python', + progress: 0, + status: 'downloading', + message: '开始下载Python...', + }); + } + // 根据选择的镜像源获取下载链接 + const pythonUrl = pythonMirrorUrls[mirror] || pythonMirrorUrls.ustc; + const zipPath = path.join(environmentPath, 'python.zip'); + await (0, downloadService_1.downloadFile)(pythonUrl, zipPath); + // 检查下载的Python文件大小 + const stats = fs.statSync(zipPath); + console.log(`Python压缩包大小: ${stats.size} bytes (${(stats.size / 1024 / 1024).toFixed(2)} MB)`); + // Python 3.12.0嵌入式版本应该大约30MB,如果小于5MB可能是无效文件 + if (stats.size < 5 * 1024 * 1024) { + // 5MB + fs.unlinkSync(zipPath); // 删除无效文件 + throw new Error(`Python下载文件大小异常: ${stats.size} bytes (${(stats.size / 1024).toFixed(2)} KB)。可能是对应镜像站不可用。请选择任意一个其他镜像源进行下载!`); + } + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'python', + progress: 100, + status: 'extracting', + message: '正在解压Python...', + }); + } + // 解压Python到指定目录 + console.log(`开始解压Python到: ${pythonPath}`); + // 确保Python目录存在 + if (!fs.existsSync(pythonPath)) { + fs.mkdirSync(pythonPath, { recursive: true }); + console.log(`创建Python目录: ${pythonPath}`); + } + const zip = new adm_zip_1.default(zipPath); + zip.extractAllTo(pythonPath, true); + console.log(`Python解压完成到: ${pythonPath}`); + // 删除zip文件 + fs.unlinkSync(zipPath); + console.log(`删除临时文件: ${zipPath}`); + // 启用 site-packages 支持 + const pthFile = path.join(pythonPath, 'python312._pth'); + if (fs.existsSync(pthFile)) { + let content = fs.readFileSync(pthFile, 'utf-8'); + content = content.replace(/^#import site/m, 'import site'); + fs.writeFileSync(pthFile, content, 'utf-8'); + console.log('已启用 site-packages 支持'); + } + // 安装pip + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'python', + progress: 80, + status: 'installing', + message: '正在安装pip...', + }); + } + await installPip(pythonPath, appRoot); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'python', + progress: 100, + status: 'completed', + message: 'Python和pip安装完成', + }); + } + return { success: true }; + } + catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'python', + progress: 0, + status: 'error', + message: `Python下载失败: ${errorMessage}`, + }); + } + return { success: false, error: errorMessage }; + } +} +// pip镜像源URL映射 +const pipMirrorUrls = { + official: 'https://pypi.org/simple/', + tsinghua: 'https://pypi.tuna.tsinghua.edu.cn/simple/', + ustc: 'https://pypi.mirrors.ustc.edu.cn/simple/', + aliyun: 'https://mirrors.aliyun.com/pypi/simple/', + douban: 'https://pypi.douban.com/simple/', +}; +// 安装Python依赖 +async function installDependencies(appRoot, mirror = 'tsinghua') { + try { + const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe'); + const backendPath = path.join(appRoot); + const requirementsPath = path.join(appRoot, 'requirements.txt'); + // 检查文件是否存在 + if (!fs.existsSync(pythonPath)) { + throw new Error('Python可执行文件不存在'); + } + if (!fs.existsSync(requirementsPath)) { + throw new Error('requirements.txt文件不存在'); + } + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'dependencies', + progress: 0, + status: 'downloading', + message: '正在安装Python依赖包...', + }); + } + // 获取pip镜像源URL + const pipMirrorUrl = pipMirrorUrls[mirror] || pipMirrorUrls.tsinghua; + // 使用Scripts文件夹中的pip.exe + const pythonDir = path.join(appRoot, 'environment', 'python'); + const pipExePath = path.join(pythonDir, 'Scripts', 'pip.exe'); + console.log(`开始安装Python依赖`); + console.log(`Python目录: ${pythonDir}`); + console.log(`pip.exe路径: ${pipExePath}`); + console.log(`requirements.txt路径: ${requirementsPath}`); + console.log(`pip镜像源: ${pipMirrorUrl}`); + // 检查pip.exe是否存在 + if (!fs.existsSync(pipExePath)) { + throw new Error(`pip.exe不存在: ${pipExePath}`); + } + // 安装依赖 - 直接使用pip.exe而不是python -m pip + await new Promise((resolve, reject) => { + const process = (0, child_process_1.spawn)(pipExePath, [ + 'install', + '-r', + requirementsPath, + '-i', + pipMirrorUrl, + '--trusted-host', + new URL(pipMirrorUrl).hostname, + ], { + cwd: backendPath, + stdio: 'pipe', + }); + process.stdout?.on('data', data => { + const output = data.toString(); + console.log('Pip output:', output); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'dependencies', + progress: 50, + status: 'downloading', + message: '正在安装依赖包...', + }); + } + }); + process.stderr?.on('data', data => { + const errorOutput = data.toString(); + console.error('Pip error:', errorOutput); + }); + process.on('close', code => { + console.log(`pip安装完成,退出码: ${code}`); + if (code === 0) { + resolve(); + } + else { + reject(new Error(`依赖安装失败,退出码: ${code}`)); + } + }); + process.on('error', error => { + console.error('pip进程错误:', error); + reject(error); + }); + }); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'dependencies', + progress: 100, + status: 'completed', + message: 'Python依赖安装完成', + }); + } + return { success: true }; + } + catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'dependencies', + progress: 0, + status: 'error', + message: `依赖安装失败: ${errorMessage}`, + }); + } + return { success: false, error: errorMessage }; + } +} +// 导出pip安装函数 +async function installPipPackage(appRoot) { + try { + const pythonPath = path.join(appRoot, 'environment', 'python'); + if (!fs.existsSync(pythonPath)) { + throw new Error('Python环境不存在,请先安装Python'); + } + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'pip', + progress: 0, + status: 'installing', + message: '正在安装pip...', + }); + } + await installPip(pythonPath, appRoot); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'pip', + progress: 100, + status: 'completed', + message: 'pip安装完成', + }); + } + return { success: true }; + } + catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (mainWindow) { + mainWindow.webContents.send('download-progress', { + type: 'pip', + progress: 0, + status: 'error', + message: `pip安装失败: ${errorMessage}`, + }); + } + return { success: false, error: errorMessage }; + } +} +// 启动后端 +async function startBackend(appRoot) { + try { + const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe'); + const backendPath = path.join(appRoot); + const mainPyPath = path.join(backendPath, 'main.py'); + // 检查文件是否存在 + if (!fs.existsSync(pythonPath)) { + throw new Error('Python可执行文件不存在'); + } + if (!fs.existsSync(mainPyPath)) { + throw new Error('后端主文件不存在'); + } + console.log(`启动后端指令: "${pythonPath}" "${mainPyPath}"(cwd: ${appRoot})`); + // 启动后端进程 + const backendProcess = (0, child_process_1.spawn)(pythonPath, [mainPyPath], { + cwd: appRoot, + stdio: 'pipe', + env: { + ...process.env, + PYTHONIOENCODING: 'utf-8', // 设置Python输出编码为UTF-8 + }, + }); + // 等待后端启动 + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('后端启动超时')); + }, 30000); // 30秒超时 + backendProcess.stdout?.on('data', data => { + const output = data.toString(); + console.log('Backend output:', output); + // 检查是否包含启动成功的标志 + if (output.includes('Uvicorn running') || output.includes('36163')) { + clearTimeout(timeout); + resolve(); + } + }); + // ✅ 重要:也要监听 stderr + backendProcess.stderr?.on('data', data => { + const output = data.toString(); + console.error('Backend error:', output); // 保留原有日志 + // ✅ 在 stderr 中也检查启动标志 + if (output.includes('Uvicorn running') || output.includes('36163')) { + clearTimeout(timeout); + resolve(); + } + }); + backendProcess.stderr?.on('data', data => { + console.error('Backend error:', data.toString()); + }); + backendProcess.on('error', error => { + clearTimeout(timeout); + reject(error); + }); + }); + return { success: true }; + } + catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } +}