diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9dc18ebce2cf9c86c1b571079218b50842214d1 --- /dev/null +++ b/electron/main/deploy/core/DeploymentService.ts @@ -0,0 +1,263 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +import * as path from 'path'; +import * as fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { getCachePath } from '../../common/cache-conf'; +import type { + DeploymentParams, + DeploymentStatus, + DeploymentConfig, +} from '../types/deployment.types'; +import { EnvironmentChecker } from './EnvironmentChecker'; +import { ValuesYamlManager } from './ValuesYamlManager'; + +const execAsync = promisify(exec); + +/** + * 部署服务核心类 + */ +export class DeploymentService { + private cachePath: string; + private deploymentPath: string; + private environmentChecker: EnvironmentChecker; + private valuesYamlManager: ValuesYamlManager; + private currentStatus: DeploymentStatus = { + status: 'idle', + message: '', + progress: 0, + }; + private statusCallback?: (status: DeploymentStatus) => void; + + constructor() { + this.cachePath = getCachePath(); + // 创建专门的部署工作目录 + this.deploymentPath = path.join( + this.cachePath, + 'deployment', + 'euler-copilot-framework', + ); + this.environmentChecker = new EnvironmentChecker(); + this.valuesYamlManager = new ValuesYamlManager(); + } + + /** + * 设置状态回调函数 + */ + setStatusCallback(callback: (status: DeploymentStatus) => void) { + this.statusCallback = callback; + } + + /** + * 更新部署状态 + */ + private updateStatus(status: Partial) { + this.currentStatus = { ...this.currentStatus, ...status }; + if (this.statusCallback) { + this.statusCallback(this.currentStatus); + } + } + + /** + * 获取当前状态 + */ + getStatus(): DeploymentStatus { + return { ...this.currentStatus }; + } + + /** + * 开始部署流程 + */ + async startDeployment(params: DeploymentParams): Promise { + try { + this.updateStatus({ + status: 'preparing', + message: '准备部署环境...', + progress: 0, + }); + + // 1. 检查环境 + await this.checkEnvironment(); + + // 2. 克隆仓库 + await this.cloneRepository(); + + // 3. 配置 values.yaml + await this.configureValues(params); + + // 4. 执行部署脚本 + await this.executeDeploymentScripts(); + + this.updateStatus({ + status: 'success', + message: '部署完成!', + progress: 100, + }); + } catch (error) { + this.updateStatus({ + status: 'error', + message: `部署失败: ${error instanceof Error ? error.message : String(error)}`, + progress: 0, + }); + throw error; + } + } + + /** + * 检查环境 + */ + private async checkEnvironment(): Promise { + this.updateStatus({ + status: 'preparing', + message: '检查系统环境...', + progress: 10, + }); + + const checkResult = await this.environmentChecker.checkAll(); + if (!checkResult.success) { + throw new Error(`环境检查失败: ${checkResult.errors.join(', ')}`); + } + + this.updateStatus({ + message: '环境检查通过', + progress: 20, + }); + } + + /** + * 克隆远程仓库 + */ + private async cloneRepository(): Promise { + this.updateStatus({ + status: 'cloning', + message: '克隆部署仓库...', + progress: 30, + }); + + // 确保部署目录的父目录存在 + const deploymentParentDir = path.dirname(this.deploymentPath); + if (!fs.existsSync(deploymentParentDir)) { + fs.mkdirSync(deploymentParentDir, { recursive: true }); + } + + // 检查是否已经克隆过 + const gitDir = path.join(this.deploymentPath, '.git'); + if (fs.existsSync(gitDir)) { + // 已存在,执行 git pull 更新 + await execAsync('git pull origin master', { cwd: this.deploymentPath }); + this.updateStatus({ + message: '更新部署仓库完成', + progress: 40, + }); + } else { + // 不存在,克隆仓库 + const repoUrl = 'https://gitee.com/openeuler/euler-copilot-framework.git'; + await execAsync( + `git clone ${repoUrl} ${path.basename(this.deploymentPath)}`, + { + cwd: deploymentParentDir, + }, + ); + this.updateStatus({ + message: '克隆部署仓库完成', + progress: 40, + }); + } + } + + /** + * 配置 values.yaml 文件 + */ + private async configureValues(params: DeploymentParams): Promise { + this.updateStatus({ + status: 'configuring', + message: '配置部署参数...', + progress: 50, + }); + + const valuesPath = path.join( + this.deploymentPath, + 'deploy/chart/euler_copilot/values.yaml', + ); + await this.valuesYamlManager.updateModelsConfig(valuesPath, params); + + this.updateStatus({ + message: '配置部署参数完成', + progress: 60, + }); + } + + /** + * 执行部署脚本 + */ + private async executeDeploymentScripts(): Promise { + const scriptsPath = path.join(this.deploymentPath, 'deploy/scripts'); + + // 按照需求只执行指定的脚本 + const scripts = [ + { name: '2-install-tools', path: '2-install-tools/install_tools.sh' }, + { + name: '6-install-databases', + path: '6-install-databases/install_databases.sh', + }, + { + name: '7-install-authhub', + path: '7-install-authhub/install_authhub.sh', + }, + { + name: '8-install-EulerCopilot', + path: '8-install-EulerCopilot/install_eulercopilot.sh', + }, + ]; + + for (let i = 0; i < scripts.length; i++) { + const script = scripts[i]; + + this.updateStatus({ + status: 'deploying', + message: `执行 ${script.name}...`, + progress: 70 + i * 7, + }); + + const scriptPath = path.join(scriptsPath, script.path); + + // 检查脚本文件是否存在 + if (!fs.existsSync(scriptPath)) { + throw new Error(`脚本文件不存在: ${scriptPath}`); + } + + // 给脚本添加执行权限并执行 + await execAsync(`chmod +x "${scriptPath}" && bash "${scriptPath}"`, { + cwd: scriptsPath, + timeout: 300000, // 5分钟超时 + }); + } + } + + /** + * 停止部署 + */ + async stopDeployment(): Promise { + this.updateStatus({ + status: 'idle', + message: '部署已停止', + progress: 0, + }); + } + + /** + * 清理部署文件 + */ + async cleanup(): Promise { + if (fs.existsSync(this.deploymentPath)) { + fs.rmSync(this.deploymentPath, { recursive: true, force: true }); + } + this.updateStatus({ + status: 'idle', + message: '清理完成', + progress: 0, + }); + } +} diff --git a/electron/main/deploy/core/EnvironmentChecker.ts b/electron/main/deploy/core/EnvironmentChecker.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ee2aaa9e066101aeb2bfff0ff2383a12fa93b48 --- /dev/null +++ b/electron/main/deploy/core/EnvironmentChecker.ts @@ -0,0 +1,216 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +import * as os from 'os'; +import * as fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +export interface EnvironmentCheckResult { + success: boolean; + errors: string[]; + warnings: string[]; +} + +/** + * 环境检查器 - 迁移自 reference-scripts/scripts/1-check-env/check_env.sh + * 但根据需求调整了检查条件 + */ +export class EnvironmentChecker { + /** + * 执行所有环境检查 + * 迁移自 reference-scripts/scripts/1-check-env,但跳过了 OS 版本检查和离线模式 + */ + async checkAll(): Promise { + const result: EnvironmentCheckResult = { + success: true, + errors: [], + warnings: [], + }; + + // 检查主机名 + try { + await this.checkHostname(); + } catch (error) { + result.warnings.push(`主机名检查: ${error}`); + } + + // 检查DNS + try { + await this.checkDns(); + } catch (error) { + result.warnings.push(`DNS检查: ${error}`); + } + + // 检查内存 (调整为4GB) + try { + await this.checkRam(); + } catch (error) { + result.errors.push(`内存检查: ${error}`); + result.success = false; + } + + // 检查磁盘空间 (调整为4GB) + try { + await this.checkDiskSpace(); + } catch (error) { + result.errors.push(`磁盘空间检查: ${error}`); + result.success = false; + } + + // 检查网络连接 (只考虑联网部署) + try { + await this.checkNetwork(); + } catch (error) { + result.errors.push(`网络检查: ${error}`); + result.success = false; + } + + // 检查必需的系统工具 + try { + await this.checkRequiredTools(); + } catch (error) { + result.errors.push(`系统工具检查: ${error}`); + result.success = false; + } + + return result; + } + + /** + * 检查主机名 + */ + private async checkHostname(): Promise { + const hostname = os.hostname(); + if (!hostname || hostname.trim() === '') { + throw new Error('未设置主机名'); + } + } + + /** + * 检查DNS配置 + */ + private async checkDns(): Promise { + try { + const resolvConf = fs.readFileSync('/etc/resolv.conf', 'utf8'); + if (!resolvConf.includes('nameserver')) { + throw new Error('DNS未配置'); + } + } catch (error: any) { + if (error.code === 'ENOENT') { + throw new Error('resolv.conf文件不存在'); + } + throw error; + } + } + + /** + * 检查内存 (最低4GB) + */ + private async checkRam(): Promise { + const totalMem = os.totalmem(); + const totalMemMB = Math.floor(totalMem / (1024 * 1024)); + const requiredMB = 4 * 1024; // 4GB + + if (totalMemMB < requiredMB) { + throw new Error(`内存不足,当前: ${totalMemMB}MB,需要: ${requiredMB}MB`); + } + } + + /** + * 检查磁盘空间 (最低4GB可用空间) + */ + private async checkDiskSpace(): Promise { + try { + const { stdout } = await execAsync('df -h /'); + const lines = stdout.trim().split('\n'); + if (lines.length < 2) { + throw new Error('无法获取磁盘信息'); + } + + // 解析df输出,获取可用空间 + const diskInfo = lines[1].split(/\s+/); + const availableStr = diskInfo[3]; // 第4列是可用空间 + + // 转换为MB进行比较 + const availableMB = this.parseDiskSize(availableStr); + const requiredMB = 4 * 1024; // 4GB + + if (availableMB < requiredMB) { + throw new Error( + `磁盘空间不足,可用: ${Math.floor(availableMB)}MB,需要: ${requiredMB}MB`, + ); + } + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error('磁盘空间检查失败'); + } + } + + /** + * 解析磁盘大小字符串 (如 "1.5G", "500M") 并转换为MB + */ + private parseDiskSize(sizeStr: string): number { + const match = sizeStr.match(/^(\d+(?:\.\d+)?)(.)$/); + if (!match) { + return 0; + } + + const value = parseFloat(match[1]); + const unit = match[2].toUpperCase(); + + switch (unit) { + case 'K': + return value / 1024; + case 'M': + return value; + case 'G': + return value * 1024; + case 'T': + return value * 1024 * 1024; + default: + return value / (1024 * 1024); // 假设为字节 + } + } + + /** + * 检查网络连接 + */ + private async checkNetwork(): Promise { + try { + // 使用curl检查网络连接 + await execAsync( + 'curl -s --connect-timeout 5 https://www.baidu.com > /dev/null', + { + timeout: 10000, + }, + ); + } catch (error) { + throw new Error('无法访问外部网络,请检查网络连接'); + } + } + + /** + * 检查必需的系统工具 + */ + private async checkRequiredTools(): Promise { + const requiredTools = ['git', 'curl', 'docker', 'kubectl', 'helm']; + const missingTools: string[] = []; + + for (const tool of requiredTools) { + try { + await execAsync(`which ${tool}`); + } catch (error) { + missingTools.push(tool); + } + } + + if (missingTools.length > 0) { + throw new Error(`缺少必需工具: ${missingTools.join(', ')}`); + } + } +} diff --git a/electron/main/deploy/core/LocalDeployHandler.ts b/electron/main/deploy/core/LocalDeployHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..f920df845be2b2967c29000ffd0a097dbb42dc2d --- /dev/null +++ b/electron/main/deploy/core/LocalDeployHandler.ts @@ -0,0 +1,135 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +import { DeploymentService } from './DeploymentService'; +import type { + DeploymentParams, + DeploymentFormData, +} from '../types/deployment.types'; + +/** + * 本地部署处理器 - 连接前端表单和部署服务 + */ +export class LocalDeployHandler { + private deploymentService: DeploymentService; + + constructor() { + this.deploymentService = new DeploymentService(); + } + + /** + * 处理前端表单提交的部署请求 + * @param formData 来自 localDeploy.vue 的表单数据 + */ + async handleDeployment(formData: DeploymentFormData): Promise { + // 将前端表单数据转换为部署参数 + const deploymentParams: DeploymentParams = { + mainModel: { + endpoint: this.formatEndpoint(formData.ruleForm.url), + key: formData.ruleForm.apiKey, + name: formData.ruleForm.modelName, + ctxLength: 8192, + maxTokens: 2048, + }, + embeddingModel: { + type: 'openai', // 根据需求,只考虑 openai 的情况 + endpoint: this.formatEndpoint(formData.embeddingRuleForm.url), + key: formData.embeddingRuleForm.apiKey, + name: formData.embeddingRuleForm.modelName, + }, + }; + + // 验证参数 + this.validateDeploymentParams(deploymentParams); + + // 开始部署 + await this.deploymentService.startDeployment(deploymentParams); + } + + /** + * 格式化 API 端点 URL + * @param url 原始 URL + * @returns 格式化后的 URL + */ + private formatEndpoint(url: string): string { + if (!url) { + throw new Error('URL 不能为空'); + } + + // 移除尾部斜杠 + url = url.replace(/\/+$/, ''); + + // 如果没有协议,默认添加 https:// + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + return url; + } + + /** + * 验证部署参数 + * @param params 部署参数 + */ + private validateDeploymentParams(params: DeploymentParams): void { + const { mainModel, embeddingModel } = params; + + // 验证主模型参数 + if (!mainModel.endpoint) { + throw new Error('主模型 URL 不能为空'); + } + if (!mainModel.name) { + throw new Error('主模型名称不能为空'); + } + if (!mainModel.key) { + throw new Error('主模型 API Key 不能为空'); + } + + // 验证 embedding 模型参数 + if (!embeddingModel.endpoint) { + throw new Error('Embedding 模型 URL 不能为空'); + } + if (!embeddingModel.name) { + throw new Error('Embedding 模型名称不能为空'); + } + if (!embeddingModel.key) { + throw new Error('Embedding 模型 API Key 不能为空'); + } + + // 验证 URL 格式 + try { + new URL(mainModel.endpoint); + new URL(embeddingModel.endpoint); + } catch { + throw new Error('URL 格式不正确'); + } + } + + /** + * 设置状态回调 + */ + setStatusCallback(callback: (status: any) => void) { + this.deploymentService.setStatusCallback(callback); + } + + /** + * 获取当前状态 + */ + getStatus() { + return this.deploymentService.getStatus(); + } + + /** + * 停止部署 + */ + async stopDeployment(): Promise { + await this.deploymentService.stopDeployment(); + } + + /** + * 清理部署文件 + */ + async cleanup(): Promise { + await this.deploymentService.cleanup(); + } +} diff --git a/electron/main/deploy/core/ValuesYamlManager.ts b/electron/main/deploy/core/ValuesYamlManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..571805d8693d8d57e64b54994a1bcb840348a59f --- /dev/null +++ b/electron/main/deploy/core/ValuesYamlManager.ts @@ -0,0 +1,101 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import type { DeploymentParams } from '../types/deployment.types'; + +/** + * Values.yaml 文件管理器 + */ +export class ValuesYamlManager { + /** + * 更新 values.yaml 中的 models 配置 + */ + async updateModelsConfig( + valuesPath: string, + params: DeploymentParams, + ): Promise { + try { + // 读取现有的 values.yaml 文件 + const valuesContent = fs.readFileSync(valuesPath, 'utf8'); + const valuesData = yaml.load(valuesContent) as any; + + // 确保 models 节点存在 + if (!valuesData.models) { + valuesData.models = {}; + } + + // 配置 answer 模型(主模型) + valuesData.models.answer = { + endpoint: params.mainModel.endpoint, + key: params.mainModel.key, + name: params.mainModel.name, + ctxLength: params.mainModel.ctxLength || 8192, + maxTokens: params.mainModel.maxTokens || 2048, + }; + + // 配置 functionCall 模型(使用相同的主模型) + valuesData.models.functionCall = { + backend: 'function_call', // 根据需求文档,这里应该是 "function_call" 而不是 "openai" + endpoint: params.mainModel.endpoint, + key: params.mainModel.key, + name: params.mainModel.name, + ctxLength: params.mainModel.ctxLength || 8192, + maxTokens: params.mainModel.maxTokens || 2048, + }; + + // 配置 embedding 模型 (只考虑 openai 类型) + valuesData.models.embedding = { + type: 'openai', // 只考虑 openai 的情况 + endpoint: params.embeddingModel.endpoint, + key: params.embeddingModel.key, + name: params.embeddingModel.name, + }; + + // 写回文件 + const updatedYaml = yaml.dump(valuesData, { + indent: 2, + lineWidth: -1, // 禁用行宽限制 + noRefs: true, + sortKeys: false, + }); + + fs.writeFileSync(valuesPath, updatedYaml, 'utf8'); + } catch (error) { + throw new Error( + `更新 values.yaml 失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * 验证 values.yaml 文件格式 + */ + validateValuesFile(valuesPath: string): boolean { + try { + const content = fs.readFileSync(valuesPath, 'utf8'); + yaml.load(content); + return true; + } catch (error) { + console.error('Values.yaml 验证失败:', error); + return false; + } + } + + /** + * 备份 values.yaml 文件 + */ + backupValuesFile(valuesPath: string): string { + const backupPath = `${valuesPath}.backup.${Date.now()}`; + fs.copyFileSync(valuesPath, backupPath); + return backupPath; + } + + /** + * 恢复 values.yaml 文件 + */ + restoreValuesFile(valuesPath: string, backupPath: string): void { + fs.copyFileSync(backupPath, valuesPath); + } +} diff --git a/electron/main/deploy/index.ts b/electron/main/deploy/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c602ab300cdb4fd05ec4ddb94a3c6adc446ec5b7 --- /dev/null +++ b/electron/main/deploy/index.ts @@ -0,0 +1,23 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +import { DeploymentService } from './core/DeploymentService'; +import { LocalDeployHandler } from './core/LocalDeployHandler'; +import type { + DeploymentParams, + DeploymentStatus, + DeploymentFormData, + ModelFormData, +} from './types/deployment.types'; + +export { DeploymentService, LocalDeployHandler }; +export type { + DeploymentParams, + DeploymentStatus, + DeploymentFormData, + ModelFormData as RuleForm, +}; + +// 导出单例实例 +export const deploymentService = new DeploymentService(); +export const localDeployHandler = new LocalDeployHandler(); diff --git a/electron/main/deploy/main/DeploymentIPCHandler.ts b/electron/main/deploy/main/DeploymentIPCHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..17b93f56eac93713f72189edef81c609d48ea0b2 --- /dev/null +++ b/electron/main/deploy/main/DeploymentIPCHandler.ts @@ -0,0 +1,99 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +import { ipcMain, BrowserWindow } from 'electron'; +import { deploymentService } from '../index'; +import { LocalDeployHandler } from '../core/LocalDeployHandler'; +import type { DeploymentFormData } from '../types/deployment.types'; + +/** + * 部署服务 IPC 处理程序 + */ +export class DeploymentIPCHandler { + private localDeployHandler: LocalDeployHandler; + private mainWindow: BrowserWindow | undefined; + + constructor() { + this.localDeployHandler = new LocalDeployHandler(); + this.mainWindow = undefined; + this.setupHandlers(); + } + + /** + * 设置主窗口引用 + */ + setMainWindow(window: BrowserWindow) { + this.mainWindow = window; + + // 设置状态变化回调 + this.localDeployHandler.setStatusCallback((status) => { + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send('deployment:statusChanged', status); + } + }); + } + + /** + * 设置 IPC 处理程序 + */ + setupHandlers() { + // 处理前端表单提交的部署请求 + ipcMain.handle( + 'deployment:startFromForm', + async (_event, formData: DeploymentFormData) => { + try { + await this.localDeployHandler.handleDeployment(formData); + } catch (error) { + console.error('部署失败:', error); + throw error; + } + }, + ); + + // 开始部署(原有接口保留兼容性) + ipcMain.handle('deployment:start', async (_event, params) => { + try { + await deploymentService.startDeployment(params); + } catch (error) { + console.error('部署失败:', error); + throw error; + } + }); + + // 停止部署 + ipcMain.handle('deployment:stop', async () => { + try { + await this.localDeployHandler.stopDeployment(); + } catch (error) { + console.error('停止部署失败:', error); + throw error; + } + }); + + // 获取部署状态 + ipcMain.handle('deployment:getStatus', () => { + return this.localDeployHandler.getStatus(); + }); + + // 清理部署文件 + ipcMain.handle('deployment:cleanup', async () => { + try { + await this.localDeployHandler.cleanup(); + } catch (error) { + console.error('清理失败:', error); + throw error; + } + }); + } + + /** + * 清理处理程序 + */ + cleanup() { + ipcMain.removeHandler('deployment:startFromForm'); + ipcMain.removeHandler('deployment:start'); + ipcMain.removeHandler('deployment:stop'); + ipcMain.removeHandler('deployment:getStatus'); + ipcMain.removeHandler('deployment:cleanup'); + } +} diff --git a/electron/main/deploy/preload/deploymentPreload.ts b/electron/main/deploy/preload/deploymentPreload.ts new file mode 100644 index 0000000000000000000000000000000000000000..4cc335dd769ad4894f808908dabcd92579c39f5f --- /dev/null +++ b/electron/main/deploy/preload/deploymentPreload.ts @@ -0,0 +1,73 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +import { contextBridge, ipcRenderer } from 'electron'; +import type { + DeploymentParams, + DeploymentStatus, + DeploymentFormData, +} from '../types/deployment.types'; + +// 暴露给渲染进程的部署服务 API +const deploymentAPI = { + /** + * 从前端表单开始部署 + */ + startDeploymentFromForm: (formData: DeploymentFormData): Promise => { + return ipcRenderer.invoke('deployment:startFromForm', formData); + }, + + /** + * 开始部署(原有接口) + */ + startDeployment: (params: DeploymentParams): Promise => { + return ipcRenderer.invoke('deployment:start', params); + }, + + /** + * 停止部署 + */ + stopDeployment: (): Promise => { + return ipcRenderer.invoke('deployment:stop'); + }, + + /** + * 获取部署状态 + */ + getStatus: (): Promise => { + return ipcRenderer.invoke('deployment:getStatus'); + }, + + /** + * 监听部署状态变化 + */ + onStatusChange: (callback: (status: DeploymentStatus) => void): void => { + ipcRenderer.on('deployment:statusChanged', (_event, status) => { + callback(status); + }); + }, + + /** + * 移除状态变化监听器 + */ + removeStatusListener: (): void => { + ipcRenderer.removeAllListeners('deployment:statusChanged'); + }, + + /** + * 清理部署文件 + */ + cleanup: (): Promise => { + return ipcRenderer.invoke('deployment:cleanup'); + }, +}; + +// 注册到全局对象 +contextBridge.exposeInMainWorld('deploymentService', deploymentAPI); + +// 类型声明 +declare global { + interface Window { + deploymentService: typeof deploymentAPI; + } +} diff --git a/electron/welcome/timeLine.vue b/electron/welcome/timeLine.vue index b08c5bc7c0e769b74a43345827d225630ea77533..38eff90c7a9d51896fe6d853d5d81c418ef464b9 100644 --- a/electron/welcome/timeLine.vue +++ b/electron/welcome/timeLine.vue @@ -29,18 +29,18 @@ }" > {{ activity.content }} - {{$t('localDeploy.installation')}} + 安装中
- {{ $t('localDeploy.complete') }} + 完成 - {{ $t('localDeploy.retry') }} + 重试 - {{ $t('localDeploy.stopInstall') }} + 停止安装