diff --git a/electron/main/common/ipc.ts b/electron/main/common/ipc.ts index bb36f9c2f4d6e89b65a1432c7d2d5f4befdea049..90fb15d2dc8574d0c36c47176e29da08243a8c3c 100644 --- a/electron/main/common/ipc.ts +++ b/electron/main/common/ipc.ts @@ -11,9 +11,13 @@ import { ipcMain, BrowserWindow } from 'electron'; import { toggleTheme, setSystemTheme } from './theme'; import { getConfigManager, DesktopConfig } from './config'; import { completeWelcomeFlow, showWelcomeWindow } from '../window/welcome'; +import { DeploymentIPCHandler } from '../deploy/main/DeploymentIPCHandler'; import https from 'https'; import http from 'http'; +// 全局部署服务IPC处理程序实例 +let deploymentIPCHandler: DeploymentIPCHandler | null = null; + /** * 注册所有IPC监听器 */ @@ -22,6 +26,7 @@ export function registerIpcListeners(): void { registerWindowControlListeners(); registerConfigListeners(); registerWelcomeListeners(); + registerDeploymentListeners(); } /** @@ -241,3 +246,32 @@ async function validateServerConnection(url: string): Promise<{ }; } } + +/** + * 注册部署服务相关的IPC监听器 + */ +function registerDeploymentListeners(): void { + // 初始化部署服务IPC处理程序 + if (!deploymentIPCHandler) { + deploymentIPCHandler = new DeploymentIPCHandler(); + } +} + +/** + * 设置部署服务的主窗口引用 + */ +export function setDeploymentMainWindow(window: BrowserWindow): void { + if (deploymentIPCHandler) { + deploymentIPCHandler.setMainWindow(window); + } +} + +/** + * 清理部署服务IPC处理程序 + */ +export function cleanupDeploymentHandlers(): void { + if (deploymentIPCHandler) { + deploymentIPCHandler.cleanup(); + deploymentIPCHandler = null; + } +} diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts index c9dc18ebce2cf9c86c1b571079218b50842214d1..fde1c3a1a5b53695e0f0e020a39932a94ec18ff5 100644 --- a/electron/main/deploy/core/DeploymentService.ts +++ b/electron/main/deploy/core/DeploymentService.ts @@ -9,7 +9,6 @@ import { getCachePath } from '../../common/cache-conf'; import type { DeploymentParams, DeploymentStatus, - DeploymentConfig, } from '../types/deployment.types'; import { EnvironmentChecker } from './EnvironmentChecker'; import { ValuesYamlManager } from './ValuesYamlManager'; @@ -72,10 +71,12 @@ export class DeploymentService { */ async startDeployment(params: DeploymentParams): Promise { try { + // 第一阶段:准备安装环境 this.updateStatus({ status: 'preparing', - message: '准备部署环境...', + message: '准备安装环境...', progress: 0, + currentStep: 'preparing-environment', }); // 1. 检查环境 @@ -87,19 +88,31 @@ export class DeploymentService { // 3. 配置 values.yaml await this.configureValues(params); - // 4. 执行部署脚本 + // 4. 执行部署脚本中的工具安装部分 + await this.installTools(); + + // 更新准备环境完成状态 + this.updateStatus({ + message: '准备安装环境完成', + progress: 25, + currentStep: 'environment-ready', + }); + + // 第二到第四阶段:按顺序安装各个服务 await this.executeDeploymentScripts(); this.updateStatus({ status: 'success', message: '部署完成!', progress: 100, + currentStep: 'completed', }); } catch (error) { this.updateStatus({ status: 'error', message: `部署失败: ${error instanceof Error ? error.message : String(error)}`, progress: 0, + currentStep: 'failed', }); throw error; } @@ -189,26 +202,72 @@ export class DeploymentService { }); } + /** + * 安装工具(准备环境的一部分) + */ + private async installTools(): Promise { + this.updateStatus({ + status: 'preparing', + message: '安装必要工具...', + progress: 15, + currentStep: 'installing-tools', + }); + + const scriptsPath = path.join(this.deploymentPath, 'deploy/scripts'); + const toolsScriptPath = path.join( + scriptsPath, + '2-install-tools/install_tools.sh', + ); + + // 检查脚本文件是否存在 + if (fs.existsSync(toolsScriptPath)) { + // 给脚本添加执行权限并执行 + await execAsync( + `chmod +x "${toolsScriptPath}" && bash "${toolsScriptPath}"`, + { + cwd: scriptsPath, + timeout: 300000, // 5分钟超时 + }, + ); + } + + this.updateStatus({ + message: '工具安装完成', + progress: 20, + }); + } + /** * 执行部署脚本 */ private async executeDeploymentScripts(): Promise { const scriptsPath = path.join(this.deploymentPath, 'deploy/scripts'); - // 按照需求只执行指定的脚本 + // 按照 timeLine.vue 中的步骤定义,执行指定的脚本(排除工具安装,因为已在准备环境阶段执行) const scripts = [ - { name: '2-install-tools', path: '2-install-tools/install_tools.sh' }, { name: '6-install-databases', path: '6-install-databases/install_databases.sh', + displayName: '数据库服务', + step: 'install-databases', + progressStart: 30, + progressEnd: 50, }, { name: '7-install-authhub', path: '7-install-authhub/install_authhub.sh', + displayName: 'AuthHub 服务', + step: 'install-authhub', + progressStart: 50, + progressEnd: 75, }, { name: '8-install-EulerCopilot', path: '8-install-EulerCopilot/install_eulercopilot.sh', + displayName: 'Intelligence 服务', + step: 'install-intelligence', + progressStart: 75, + progressEnd: 95, }, ]; @@ -217,8 +276,9 @@ export class DeploymentService { this.updateStatus({ status: 'deploying', - message: `执行 ${script.name}...`, - progress: 70 + i * 7, + message: `正在安装 ${script.displayName}...`, + progress: script.progressStart, + currentStep: script.step, }); const scriptPath = path.join(scriptsPath, script.path); @@ -233,6 +293,12 @@ export class DeploymentService { cwd: scriptsPath, timeout: 300000, // 5分钟超时 }); + + // 更新完成状态 + this.updateStatus({ + message: `${script.displayName} 安装完成`, + progress: script.progressEnd, + }); } } diff --git a/electron/main/deploy/types/deployment.types.ts b/electron/main/deploy/types/deployment.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c918fcc00caa992aed9a8fe9f89657a95648b7a --- /dev/null +++ b/electron/main/deploy/types/deployment.types.ts @@ -0,0 +1,49 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +export interface ModelConfig { + endpoint: string; + key: string; + name: string; + ctxLength?: number; + maxTokens?: number; +} + +export interface EmbeddingConfig { + type: 'openai' | 'mindie'; + endpoint: string; + key: string; + name: string; +} + +// 与 localDeploy.vue 中的 RuleForm 保持一致 +export interface ModelFormData { + url: string; + modelName: string; + apiKey: string; +} + +export interface DeploymentFormData { + ruleForm: ModelFormData; + embeddingRuleForm: ModelFormData; +} + +export interface DeploymentParams { + mainModel: ModelConfig; + embeddingModel: EmbeddingConfig; +} + +export interface DeploymentStatus { + status: + | 'idle' + | 'preparing' + | 'cloning' + | 'configuring' + | 'deploying' + | 'success' + | 'error'; + message: string; + progress: number; + currentStep?: string; + estimatedTime?: number; +} diff --git a/electron/main/index.ts b/electron/main/index.ts index 8da22e98cf96c42b331514417bd6262e9cf3b3f0..81e00dbdeb4c03297a13b040ba7d8f4128b09580 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -25,7 +25,7 @@ import { unregisterAllShortcuts, } from './common/shortcuts'; import { buildAppMenu } from './common/menu'; -import { registerIpcListeners } from './common/ipc'; +import { registerIpcListeners, cleanupDeploymentHandlers } from './common/ipc'; // 允许本地部署时使用无效证书,仅在 Electron 主进程下生效 if (process.versions.electron) { @@ -73,6 +73,7 @@ app.on('will-quit', () => { app.on('window-all-closed', () => { ipcMain.removeAllListeners(); + cleanupDeploymentHandlers(); if (process.platform !== 'darwin') { app.quit(); } diff --git a/electron/main/window/welcome.ts b/electron/main/window/welcome.ts index fb31606db4ee7ffee4997ea1ef6f37a0e628dda9..6e133a11f98472cbf8e8caaf312add9f5a257fd0 100644 --- a/electron/main/window/welcome.ts +++ b/electron/main/window/welcome.ts @@ -11,6 +11,7 @@ import { BrowserWindow } from 'electron'; import * as path from 'node:path'; import { getConfigManager } from '../common/config'; +import { setDeploymentMainWindow } from '../common/ipc'; /** * 欢迎窗口引用 @@ -60,6 +61,8 @@ export function createWelcomeWindow(): BrowserWindow { welcomeWindow.once('ready-to-show', () => { if (welcomeWindow && !welcomeWindow.isDestroyed()) { welcomeWindow.show(); + // 设置部署服务的主窗口引用 + setDeploymentMainWindow(welcomeWindow); } }); diff --git a/electron/preload/shared.ts b/electron/preload/shared.ts index c48faf3c63c70feef00f13b04c61dcdce0724b4e..728691081340e951e3d12d3b8fbaa63a18d645d2 100644 --- a/electron/preload/shared.ts +++ b/electron/preload/shared.ts @@ -20,7 +20,10 @@ import type { ServerValidationResult } from './types'; * IPC 通道验证 */ export function validateIPC(channel: string): true | never { - if (!channel || !channel.startsWith('copilot:')) { + if ( + !channel || + (!channel.startsWith('copilot:') && !channel.startsWith('deployment:')) + ) { // 允许窗口状态变化事件通过验证 if ( channel === 'window-maximized-change' || diff --git a/electron/preload/types.d.ts b/electron/preload/types.d.ts index eb849c0fc423bf8defcf958f23b316c9a349a1e3..c7f212c478f18aa0c34eb6ecf9b8a3f6077b05f2 100644 --- a/electron/preload/types.d.ts +++ b/electron/preload/types.d.ts @@ -89,6 +89,27 @@ export interface DesktopAppWelcomeAPI { cancel(): Promise; }; + // 部署服务 + deployment: { + startDeploymentFromForm(formData: { + ruleForm: { + url: string; + modelName: string; + apiKey: string; + }; + embeddingRuleForm: { + url: string; + modelName: string; + apiKey: string; + }; + }): Promise; + stopDeployment(): Promise; + getStatus(): Promise; + onStatusChange(callback: (status: any) => void): void; + removeStatusListener(): void; + cleanup(): Promise; + }; + // 系统信息 system: { platform: string; diff --git a/electron/preload/welcome.ts b/electron/preload/welcome.ts index 4029f26a5815be2ea8fd950c50f0095cdf009ded..196944f168fa82c6395c0124fbde7fc2babb1241 100644 --- a/electron/preload/welcome.ts +++ b/electron/preload/welcome.ts @@ -11,6 +11,64 @@ import { contextBridge } from 'electron'; import { sharedConfigAPI, systemAPI, utilsAPI, safeIPC } from './shared'; +// 导入部署服务API +const deploymentAPI = { + /** + * 从前端表单开始部署 + */ + startDeploymentFromForm: (formData: { + ruleForm: { + url: string; + modelName: string; + apiKey: string; + }; + embeddingRuleForm: { + url: string; + modelName: string; + apiKey: string; + }; + }): Promise => { + return safeIPC.invoke('deployment:startFromForm', formData); + }, + + /** + * 停止部署 + */ + stopDeployment: (): Promise => { + return safeIPC.invoke('deployment:stop'); + }, + + /** + * 获取部署状态 + */ + getStatus: (): Promise => { + return safeIPC.invoke('deployment:getStatus'); + }, + + /** + * 监听部署状态变化 + */ + onStatusChange: (callback: (status: any) => void): void => { + safeIPC.on('deployment:statusChanged', (_event, status) => { + callback(status); + }); + }, + + /** + * 移除状态变化监听器 + */ + removeStatusListener: (): void => { + safeIPC.removeAllListeners('deployment:statusChanged'); + }, + + /** + * 清理部署文件 + */ + cleanup: (): Promise => { + return safeIPC.invoke('deployment:cleanup'); + }, +}; + /** * 欢迎界面专用的 preload 脚本 * 提供配置管理和欢迎流程相关的 API,使用统一的后端验证接口 @@ -67,6 +125,9 @@ const welcomeAPI = { // 欢迎流程管理(专用实现) welcome: welcomeFlowAPI, + // 部署服务(新增) + deployment: deploymentAPI, + // 系统信息(使用共享模块) system: systemAPI, diff --git a/electron/welcome/deployServiceConnector.js b/electron/welcome/deployServiceConnector.js new file mode 100644 index 0000000000000000000000000000000000000000..17a385a8598094bc61723265fea39038f12dadbf --- /dev/null +++ b/electron/welcome/deployServiceConnector.js @@ -0,0 +1,41 @@ +/** + * 本地部署服务连接器 + * 由于不能修改 localDeploy.vue,这个文件作为桥梁连接前端表单和部署服务 + * 注意:此文件已被 localDeploy.vue 中的直接调用取代,保留用于兼容性 + */ + +// 等待 DOM 加载完成 +document.addEventListener('DOMContentLoaded', function () { + console.log('部署服务连接器已加载(兼容性版本)'); + + // 检查是否在 Electron 环境中 + if ( + typeof window.eulercopilotWelcome === 'undefined' || + typeof window.eulercopilotWelcome.deployment === 'undefined' + ) { + console.warn('部署服务不可用,请在 Electron 环境中运行'); + return; + } + + console.log('部署服务 API 可用'); +}); + +// 保留一些实用函数用于调试 +window.deploymentUtils = { + getFormData: function () { + console.log('获取表单数据的实用函数(调试用)'); + return null; + }, + + getDeploymentStatus: async function () { + if (window.eulercopilotWelcome && window.eulercopilotWelcome.deployment) { + try { + return await window.eulercopilotWelcome.deployment.getStatus(); + } catch (error) { + console.error('获取部署状态失败:', error); + return null; + } + } + return null; + }, +}; diff --git a/electron/welcome/localDeploy.vue b/electron/welcome/localDeploy.vue index 7f16531f7f6ebd923848ca1030ec34678d163d09..cf765878079891f01dfb269516414b20a5b77b67 100644 --- a/electron/welcome/localDeploy.vue +++ b/electron/welcome/localDeploy.vue @@ -22,14 +22,35 @@ {{ $t('localDeploy.model') }} success - - + + - - + + - - + + - + +
- + + + + - + + \ No newline at end of file diff --git a/package.json b/package.json index 23294e3bd499b34df8132e2b06edfb3496864fe2..cf7aa586cc88b096c5cec2e63dcdfabb0c9fe1ae 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@dagrejs/dagre": "1.1.2", "@element-plus/icons-vue": "^2.3.1", "@microsoft/fetch-event-source": "^2.0.1", + "@types/js-yaml": "^4.0.9", "axios": "1.7.9", "codemirror": "^6.0.1", "dayjs": "1.11.9", diff --git a/scripts/build-preload.ts b/scripts/build-preload.ts index 7eb339bd321b0e6c5fde056d2600d813289656ed..d44031f812296087f2c69d3e353b121e8e882344 100644 --- a/scripts/build-preload.ts +++ b/scripts/build-preload.ts @@ -55,6 +55,8 @@ function createRollupConfig(entry: PreloadEntry): RollupOptions { sourceMap: false, noEmitOnError: true, include: compilationInclude, + target: 'ES2022', + lib: ['ES2022', 'DOM'], }), alias({ entries: { diff --git a/scripts/build-welcome.ts b/scripts/build-welcome.ts index c1fca4b549d83678424d49adff32061cf0ed438b..50e0b27f6092aaf800648819f51c3eb4b2cd13fe 100644 --- a/scripts/build-welcome.ts +++ b/scripts/build-welcome.ts @@ -4,7 +4,13 @@ import vue from '@vitejs/plugin-vue'; import chalk from 'chalk'; import ora from 'ora'; import minimist from 'minimist'; -import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs'; +import { + mkdirSync, + existsSync, + readFileSync, + writeFileSync, + copyFileSync, +} from 'fs'; const argv = minimist(process.argv.slice(2)); const TAG = '[build-welcome.ts]'; @@ -40,11 +46,17 @@ const buildWelcome = async (watch = false) => { resolve: { alias: { '@': resolve(__dirname, '../src'), + // 添加别名让 deploy 能被正确解析 + '@deploy': resolve(__dirname, '../electron/main/deploy'), }, }, define: { 'process.env.NODE_ENV': watch ? '"development"' : '"production"', }, + // 确保 TypeScript 文件被正确处理 + esbuild: { + target: 'es2020', + }, }); // 复制并更新 HTML 文件 @@ -53,6 +65,20 @@ const buildWelcome = async (watch = false) => { mkdirSync(distDir, { recursive: true }); } + // 复制 deployServiceConnector.js 文件 + const connectorSrc = resolve( + __dirname, + '../electron/welcome/deployServiceConnector.js', + ); + const connectorDest = resolve(distDir, 'deployServiceConnector.js'); + if (existsSync(connectorSrc)) { + copyFileSync(connectorSrc, connectorDest); + } else { + console.warn( + `Warning: deployServiceConnector.js not found at ${connectorSrc}`, + ); + } + // 读取原始 HTML 文件 let htmlContent = readFileSync( resolve(__dirname, '../electron/welcome/welcome.html'), diff --git a/scripts/rollup.config.ts b/scripts/rollup.config.ts index acca2c6d980acaa28ffb16c7adc0c5ea537618bb..2f390da0ce516835faaa36e902a2c94fce06ab1a 100644 --- a/scripts/rollup.config.ts +++ b/scripts/rollup.config.ts @@ -40,6 +40,8 @@ export default function (opts: ConfigOptions) { sourceMap: sourcemap, noEmitOnError: true, include: compilationInclude, + target: 'ES2022', // 支持私有字段语法 + lib: ['ES2022', 'DOM'], // 总是包含 DOM 库,因为 preload 文件需要访问 window }), alias({ entries: { diff --git a/tsconfig.json b/tsconfig.json index 5ad27fa3964dcfed9b7de013c7bdfe4680891420..e8245a1b85853127738238acd6d23ebb79d18941 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,9 @@ "paths": { "src*": ["src/*"], "@/*": ["src/*"], - "src/*": ["./src/*"] + "src/*": ["./src/*"], + "@deploy": ["electron/main/deploy"], + "@deploy/*": ["electron/main/deploy/*"] }, "lib": ["esnext", "dom", "dom.iterable", "scripthost"] },