feat(initialization): 添加初始化页面和相关功能
- 新增初始化页面组件和路由 - 实现环境检查、Git下载、后端代码克隆等功能 - 添加下载服务和环境服务模块 - 更新类型定义,增加 Electron API 接口
This commit is contained in:
58
frontend/electron/services/downloadService.ts
Normal file
58
frontend/electron/services/downloadService.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as https from 'https'
|
||||
import * as fs from 'fs'
|
||||
import { BrowserWindow } from 'electron'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function setMainWindow(window: BrowserWindow) {
|
||||
mainWindow = window
|
||||
}
|
||||
|
||||
// 下载文件的通用函数
|
||||
export function downloadFile(url: string, outputPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`开始下载文件: ${url}`)
|
||||
console.log(`保存路径: ${outputPath}`)
|
||||
|
||||
const file = fs.createWriteStream(outputPath)
|
||||
|
||||
https.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 = Math.round((downloadedSize / totalSize) * 100)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
35
frontend/electron/services/environmentService.ts
Normal file
35
frontend/electron/services/environmentService.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { app } from 'electron'
|
||||
|
||||
// 获取应用根目录
|
||||
export function getAppRoot(): string {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
? process.cwd()
|
||||
: path.dirname(app.getPath('exe'))
|
||||
}
|
||||
|
||||
// 检查环境
|
||||
export function checkEnvironment(appRoot: string) {
|
||||
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
|
||||
}
|
||||
}
|
||||
425
frontend/electron/services/gitService.ts
Normal file
425
frontend/electron/services/gitService.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { downloadFile } from './downloadService'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function setMainWindow(window: BrowserWindow) {
|
||||
mainWindow = window
|
||||
}
|
||||
|
||||
const gitDownloadUrl = 'https://alist-automaa.fearr.xyz/d/AUTO_MAA/git.zip'
|
||||
|
||||
// 获取Git环境变量配置
|
||||
function getGitEnvironment(appRoot: string) {
|
||||
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: string): boolean {
|
||||
const gitDir = path.join(dirPath, '.git')
|
||||
return fs.existsSync(gitDir)
|
||||
}
|
||||
|
||||
// 下载Git
|
||||
export async function downloadGit(appRoot: string): Promise<{ success: boolean; error?: string }> {
|
||||
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 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 AdmZip(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 }
|
||||
}
|
||||
}
|
||||
|
||||
// 克隆后端代码
|
||||
export async function cloneBackend(appRoot: string, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git'): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const backendPath = path.join(appRoot)
|
||||
const backendCheckPath = path.join(appRoot,'app')
|
||||
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
|
||||
|
||||
console.log(`开始获取后端代码`)
|
||||
console.log(`Git路径: ${gitPath}`)
|
||||
console.log(`仓库URL: ${repoUrl}`)
|
||||
console.log(`目标路径: ${backendPath}`)
|
||||
|
||||
// 检查Git可执行文件是否存在
|
||||
if (!fs.existsSync(gitPath)) {
|
||||
throw new Error(`Git可执行文件不存在: ${gitPath}`)
|
||||
}
|
||||
|
||||
// 获取Git环境变量
|
||||
const gitEnv = getGitEnvironment(appRoot)
|
||||
|
||||
// 先测试Git是否能正常运行
|
||||
console.log('测试Git版本...')
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const testProcess = spawn(gitPath, ['--version'], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv
|
||||
})
|
||||
|
||||
testProcess.stdout?.on('data', (data) => {
|
||||
console.log('Git版本信息:', data.toString())
|
||||
})
|
||||
|
||||
testProcess.stderr?.on('data', (data) => {
|
||||
console.log('Git版本错误:', data.toString())
|
||||
})
|
||||
|
||||
testProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('Git版本检查成功')
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`Git版本检查失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
|
||||
testProcess.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Git版本检查失败:', error)
|
||||
throw new Error(`Git无法正常运行: ${error}`)
|
||||
}
|
||||
|
||||
console.log('Git环境变量:', gitEnv)
|
||||
|
||||
// 检查backend目录是否存在且是否为Git仓库
|
||||
if (fs.existsSync(backendCheckPath)) {
|
||||
if (isGitRepository(backendPath)) {
|
||||
// 如果是Git仓库,执行pull更新
|
||||
console.log('检测到Git仓库,执行pull更新...')
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '正在更新后端代码...'
|
||||
})
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const process = spawn(gitPath, [
|
||||
'clone',
|
||||
'--progress',
|
||||
'--verbose',
|
||||
'-b', 'feature/refactor-backend',
|
||||
repoUrl,
|
||||
backendPath
|
||||
], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: appRoot
|
||||
})
|
||||
|
||||
process.stdout?.on('data', (data) => {
|
||||
const output = data.toString()
|
||||
console.log('Git pull output:', output)
|
||||
})
|
||||
|
||||
process.stderr?.on('data', (data) => {
|
||||
const errorOutput = data.toString()
|
||||
console.log('Git pull stderr:', errorOutput)
|
||||
})
|
||||
|
||||
process.on('close', (code) => {
|
||||
console.log(`git pull完成,退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`代码更新失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', (error) => {
|
||||
console.error('git pull进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: '后端代码更新完成'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 如果目录存在但不是Git仓库,删除后重新克隆
|
||||
console.log('目录存在但不是Git仓库,删除后重新克隆...')
|
||||
fs.rmSync(backendPath, { recursive: true, force: true })
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '正在克隆后端代码...'
|
||||
})
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const process = spawn(gitPath, [
|
||||
'clone',
|
||||
'--progress',
|
||||
'--verbose',
|
||||
repoUrl,
|
||||
backendPath
|
||||
], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: appRoot
|
||||
})
|
||||
|
||||
process.stdout?.on('data', (data) => {
|
||||
const output = data.toString()
|
||||
console.log('Git clone output:', output)
|
||||
})
|
||||
|
||||
process.stderr?.on('data', (data) => {
|
||||
const errorOutput = data.toString()
|
||||
console.log('Git clone stderr:', errorOutput)
|
||||
})
|
||||
|
||||
process.on('close', (code) => {
|
||||
console.log(`git clone完成,退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`代码克隆失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', (error) => {
|
||||
console.error('git clone进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: '后端代码克隆完成'
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果目录不存在,直接克隆
|
||||
console.log('目录不存在,开始克隆...')
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: '正在克隆后端代码...'
|
||||
})
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const process = spawn(gitPath, [
|
||||
'clone',
|
||||
'--progress',
|
||||
'--verbose',
|
||||
'-b', 'feature/refactor-backend',
|
||||
repoUrl,
|
||||
backendPath
|
||||
], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: appRoot
|
||||
})
|
||||
|
||||
process.stdout?.on('data', (data) => {
|
||||
const output = data.toString()
|
||||
console.log('Git clone output:', output)
|
||||
})
|
||||
|
||||
process.stderr?.on('data', (data) => {
|
||||
const errorOutput = data.toString()
|
||||
console.log('Git clone stderr:', errorOutput)
|
||||
})
|
||||
|
||||
process.on('close', (code) => {
|
||||
console.log(`git clone完成,退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`代码克隆失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', (error) => {
|
||||
console.error('git clone进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
456
frontend/electron/services/pythonService.ts
Normal file
456
frontend/electron/services/pythonService.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { downloadFile } from './downloadService'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function setMainWindow(window: BrowserWindow) {
|
||||
mainWindow = window
|
||||
}
|
||||
|
||||
// Python镜像源URL映射
|
||||
const pythonMirrorUrls = {
|
||||
official: 'https://www.python.org/ftp/python/3.13.0/python-3.13.0-embed-amd64.zip',
|
||||
tsinghua: 'https://mirrors.tuna.tsinghua.edu.cn/python/3.13.0/python-3.13.0-embed-amd64.zip',
|
||||
ustc: 'https://mirrors.ustc.edu.cn/python/3.13.0/python-3.13.0-embed-amd64.zip',
|
||||
huawei: 'https://mirrors.huaweicloud.com/repository/toolkit/python/3.13.0/python-3.13.0-embed-amd64.zip',
|
||||
aliyun: 'https://mirrors.aliyun.com/python-release/windows/python-3.13.0-embed-amd64.zip'
|
||||
}
|
||||
|
||||
// 检查pip是否已安装
|
||||
function isPipInstalled(pythonPath: string): boolean {
|
||||
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: string, appRoot: string): Promise<void> {
|
||||
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 = 'https://alist-automaa.fearr.xyz/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 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<void>((resolve, reject) => {
|
||||
console.log('执行pip安装命令...')
|
||||
|
||||
const process = 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<void>((resolve, reject) => {
|
||||
const verifyProcess = 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
|
||||
export async function downloadPython(appRoot: string, mirror = 'ustc'): Promise<{ success: boolean; error?: string }> {
|
||||
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 as keyof typeof pythonMirrorUrls] || pythonMirrorUrls.ustc
|
||||
const zipPath = path.join(environmentPath, 'python.zip')
|
||||
|
||||
await downloadFile(pythonUrl, zipPath)
|
||||
|
||||
// 检查下载的Python文件大小
|
||||
const stats = fs.statSync(zipPath)
|
||||
console.log(`Python压缩包大小: ${stats.size} bytes (${(stats.size / 1024 / 1024).toFixed(2)} MB)`)
|
||||
|
||||
// Python 3.13.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 AdmZip(zipPath)
|
||||
zip.extractAllTo(pythonPath, true)
|
||||
console.log(`Python解压完成到: ${pythonPath}`)
|
||||
|
||||
// 删除zip文件
|
||||
fs.unlinkSync(zipPath)
|
||||
console.log(`删除临时文件: ${zipPath}`)
|
||||
|
||||
// 启用 site-packages 支持
|
||||
const pthFile = path.join(pythonPath, 'python313._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依赖
|
||||
export async function installDependencies(appRoot: string, mirror = 'tsinghua'): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||
const backendPath = path.join(appRoot, 'backend')
|
||||
const requirementsPath = path.join(backendPath, '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 as keyof typeof pipMirrorUrls] || 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<void>((resolve, reject) => {
|
||||
const process = 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 }
|
||||
}
|
||||
}
|
||||
|
||||
// 启动后端
|
||||
export async function startBackend(appRoot: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||
const backendPath = path.join(appRoot, 'backend')
|
||||
const mainPyPath = path.join(backendPath, 'app','main.py')
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
throw new Error('Python可执行文件不存在')
|
||||
}
|
||||
if (!fs.existsSync(mainPyPath)) {
|
||||
throw new Error('后端主文件不存在')
|
||||
}
|
||||
|
||||
// 启动后端进程
|
||||
const backendProcess = spawn(pythonPath, [mainPyPath], {
|
||||
cwd: backendPath,
|
||||
stdio: 'pipe'
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 等待后端启动
|
||||
await new Promise<void>((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') || output.includes('8000')) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user