diff --git a/electron/main/common/config.ts b/electron/main/common/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..506cd7a19dbe265460c1010ce51e158fd82be9aa --- /dev/null +++ b/electron/main/common/config.ts @@ -0,0 +1,266 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { userDataPath } from './cache-conf'; + +/** + * 桌面应用配置接口 + */ +export interface DesktopConfig { + base_url: string; + // 可扩展其他配置项 + [key: string]: unknown; +} + +/** + * 默认配置 + */ +export const DEFAULT_CONFIG: DesktopConfig = { + base_url: 'https://www.eulercopilot.local', +}; + +/** + * 配置文件路径 + */ +export const CONFIG_DIR = path.join(userDataPath, 'Config'); +export const CONFIG_FILE_PATH = path.join(CONFIG_DIR, 'desktop-config.json'); +export const CONFIG_BACKUP_PATH = path.join( + CONFIG_DIR, + 'desktop-config.backup.json', +); + +/** + * 配置管理类 + */ +export class ConfigManager { + private static instance: ConfigManager; + private config: DesktopConfig | null = null; + + private constructor() {} + + /** + * 获取单例实例 + */ + public static getInstance(): ConfigManager { + if (!ConfigManager.instance) { + ConfigManager.instance = new ConfigManager(); + } + return ConfigManager.instance; + } + + /** + * 检查配置文件是否存在 + */ + public isConfigExists(): boolean { + return fs.existsSync(CONFIG_FILE_PATH); + } + + /** + * 确保配置目录存在 + */ + private ensureConfigDir(): void { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + } + + /** + * 配置验证函数 + */ + private validateConfig(config: any): config is DesktopConfig { + if (!config || typeof config !== 'object') { + return false; + } + + // 检查必需的 base_url 字段 + if (typeof config.base_url !== 'string' || !config.base_url.trim()) { + return false; + } + + // 检查 URL 格式 + try { + new URL(config.base_url); + } catch { + return false; + } + + return true; + } + + /** + * 创建备份配置文件 + */ + private createBackup(config: DesktopConfig): void { + try { + fs.writeFileSync( + CONFIG_BACKUP_PATH, + JSON.stringify(config, null, 2), + 'utf8', + ); + } catch (error) { + console.warn('Failed to create config backup:', error); + } + } + + /** + * 从备份恢复配置 + */ + private restoreFromBackup(): DesktopConfig | null { + try { + if (fs.existsSync(CONFIG_BACKUP_PATH)) { + const backupData = fs.readFileSync(CONFIG_BACKUP_PATH, 'utf8'); + const backupConfig = JSON.parse(backupData); + + if (this.validateConfig(backupConfig)) { + console.log('Restored config from backup'); + return backupConfig; + } + } + } catch (error) { + console.warn('Failed to restore from backup:', error); + } + + return null; + } + + /** + * 初始化配置文件(使用默认配置) + */ + public initializeConfig(): void { + try { + this.ensureConfigDir(); + if (!this.isConfigExists()) { + this.writeConfig(DEFAULT_CONFIG); + } + } catch (error) { + console.error('Failed to initialize config:', error); + throw error; + } + } + + /** + * 读取配置文件 + */ + public readConfig(): DesktopConfig { + if (this.config) { + return { ...this.config }; + } + + try { + if (!this.isConfigExists()) { + this.initializeConfig(); + return { ...DEFAULT_CONFIG }; + } + + const configContent = fs.readFileSync(CONFIG_FILE_PATH, 'utf-8'); + const parsedConfig = JSON.parse(configContent); + + // 验证配置文件的有效性 + if (!this.validateConfig(parsedConfig)) { + console.warn('Invalid config file detected, attempting recovery...'); + + // 尝试从备份恢复 + const backupConfig = this.restoreFromBackup(); + if (backupConfig) { + this.config = backupConfig; + // 重写配置文件 + this.writeConfig(backupConfig); + return { ...backupConfig }; + } + + // 备份恢复失败,使用默认配置 + console.warn('Backup recovery failed, using default config'); + this.config = { ...DEFAULT_CONFIG }; + this.writeConfig(this.config); + return { ...this.config }; + } + + // 合并默认配置,确保必需字段存在 + this.config = { ...DEFAULT_CONFIG, ...parsedConfig }; + return { ...this.config }; + } catch (error) { + console.error('Failed to read config:', error); + // 返回默认配置 + this.config = { ...DEFAULT_CONFIG }; + return { ...this.config }; + } + } + + /** + * 写入配置文件 + */ + public writeConfig(config: DesktopConfig): void { + try { + // 验证配置 + if (!this.validateConfig(config)) { + throw new Error('Invalid config provided'); + } + + this.ensureConfigDir(); + + // 在写入新配置前,如果存在旧配置,先创建备份 + if (this.isConfigExists() && this.config) { + this.createBackup(this.config); + } + + // 写入新配置 + fs.writeFileSync( + CONFIG_FILE_PATH, + JSON.stringify(config, null, 2), + 'utf-8', + ); + this.config = { ...config }; + } catch (error) { + console.error('Failed to write config:', error); + throw error; + } + } + + /** + * 更新配置(部分更新) + */ + public updateConfig(updates: Partial): DesktopConfig { + const currentConfig = this.readConfig(); + const newConfig = { ...currentConfig, ...updates }; + this.writeConfig(newConfig); + return newConfig; + } + + /** + * 获取配置项 + */ + public getConfigValue(key: keyof DesktopConfig): T | undefined { + const config = this.readConfig(); + return config[key] as T; + } + + /** + * 设置配置项 + */ + public setConfigValue(key: keyof DesktopConfig, value: unknown): void { + this.updateConfig({ [key]: value }); + } + + /** + * 重置为默认配置 + */ + public resetConfig(): void { + this.writeConfig(DEFAULT_CONFIG); + } +} + +/** + * 获取配置管理器实例的便捷函数 + */ +export function getConfigManager(): ConfigManager { + return ConfigManager.getInstance(); +} diff --git a/electron/main/window/welcome.ts b/electron/main/window/welcome.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2ff1ec627cd453d309d557fb72c763a35413790 --- /dev/null +++ b/electron/main/window/welcome.ts @@ -0,0 +1,162 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { BrowserWindow } from 'electron'; +import * as path from 'node:path'; +import { getConfigManager } from '../common/config'; + +/** + * 欢迎窗口引用 + */ +let welcomeWindow: BrowserWindow | null = null; + +/** + * 创建欢迎窗口 + */ +export function createWelcomeWindow(): BrowserWindow { + // 如果欢迎窗口已存在且未被销毁,则返回现有窗口 + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.focus(); + return welcomeWindow; + } + + // 创建欢迎窗口 + welcomeWindow = new BrowserWindow({ + width: 720, + height: 560, + minWidth: 720, + minHeight: 560, + center: true, + resizable: false, + maximizable: false, + minimizable: false, + modal: true, + alwaysOnTop: true, + title: '欢迎使用', + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, '../../preload/welcome.js'), // 欢迎界面专用预加载脚本 + }, + show: false, + }); + + // 加载欢迎界面的 HTML 文件 + welcomeWindow.loadFile(path.join(__dirname, '../../welcome/index.html')); + + // 开发模式下可以打开开发者工具 + if (process.env.NODE_ENV === 'development') { + welcomeWindow.webContents.openDevTools({ mode: 'detach' }); + } + + // 窗口准备显示时显示窗口 + welcomeWindow.once('ready-to-show', () => { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.show(); + } + }); + + // 窗口关闭时清理引用 + welcomeWindow.on('closed', () => { + welcomeWindow = null; + }); + + // 阻止窗口导航到外部链接 + welcomeWindow.webContents.on('will-navigate', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + if (parsedUrl.origin !== 'data:') { + event.preventDefault(); + } + }); + + // 阻止新窗口创建 + welcomeWindow.webContents.setWindowOpenHandler(() => { + return { action: 'deny' }; + }); + + return welcomeWindow; +} + +/** + * 显示欢迎窗口 + */ +export function showWelcomeWindow(): BrowserWindow { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.show(); + welcomeWindow.focus(); + return welcomeWindow; + } + return createWelcomeWindow(); +} + +/** + * 隐藏欢迎窗口 + */ +export function hideWelcomeWindow(): void { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.hide(); + } +} + +/** + * 关闭欢迎窗口 + */ +export function closeWelcomeWindow(): void { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.close(); + } +} + +/** + * 检查是否需要显示欢迎界面 + * 当配置文件不存在时,显示欢迎界面 + */ +export function checkAndShowWelcomeIfNeeded(): boolean { + const configManager = getConfigManager(); + + if (!configManager.isConfigExists()) { + console.log('Configuration file not found, showing welcome window'); + showWelcomeWindow(); + return true; + } + + return false; +} + +/** + * 完成欢迎流程 + * 初始化配置并关闭欢迎窗口,然后继续应用启动 + */ +export async function completeWelcomeFlow(): Promise { + const configManager = getConfigManager(); + + try { + // 确保配置文件已初始化 + configManager.initializeConfig(); + + // 关闭欢迎窗口 + closeWelcomeWindow(); + + console.log('Welcome flow completed, configuration initialized'); + + // 动态导入主模块以避免循环依赖 + const { continueAppStartup } = await import('../index'); + await continueAppStartup(); + } catch (error) { + console.error('Failed to complete welcome flow:', error); + } +} + +/** + * 获取当前欢迎窗口实例 + */ +export function getWelcomeWindow(): BrowserWindow | null { + return welcomeWindow; +} diff --git a/electron/preload/types.d.ts b/electron/preload/types.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..a74211953d847b8e607dc23a9162d46156f3a121 --- /dev/null +++ b/electron/preload/types.d.ts @@ -0,0 +1,104 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. + +/** + * Electron Preload API 类型定义 + */ + +export interface DesktopConfig { + base_url: string; + [key: string]: any; +} + +export interface DesktopAppAPI { + // IPC 渲染器 + ipcRenderer: { + invoke(channel: string, ...args: any[]): Promise; + on(channel: string, listener: (...args: any[]) => void): void; + once(channel: string, listener: (...args: any[]) => void): void; + removeListener(channel: string, listener: (...args: any[]) => void): void; + removeAllListeners(channel: string): void; + }; + + // 配置管理 + config: { + get(): Promise; + update(updates: Partial): Promise; + reset(): Promise; + setProxyUrl(url: string): Promise; + getProxyUrl(): Promise; + }; + + // 欢迎界面 + welcome: { + show(): Promise; + complete(): Promise; + }; + + // 窗口控制 + window: { + control(command: 'minimize' | 'maximize' | 'close'): Promise; + isMaximized(): Promise; + onMaximizedChange(callback: (isMaximized: boolean) => void): void; + offMaximizedChange(): void; + }; + + // 主题 + theme: { + toggle(): Promise; + setSystem(): Promise; + }; + + // 进程信息 + process: { + platform: string; + arch: string; + versions: NodeJS.ProcessVersions; + env: Record; + }; +} + +export interface DesktopAppWelcomeAPI { + // 配置管理 + config: { + get(): Promise; + update(updates: Partial): Promise; + reset(): Promise; + validateServer(url: string): Promise; + }; + + // 欢迎流程 + welcome: { + complete(): Promise; + cancel(): Promise; + }; + + // 窗口控制 + window: { + close(): Promise; + minimize(): Promise; + }; + + // 系统信息 + system: { + platform: string; + arch: string; + versions: NodeJS.ProcessVersions; + }; + + // 实用工具 + utils: { + isValidUrl(url: string): boolean; + formatUrl(url: string): string; + delay(ms: number): Promise; + }; +} + +declare global { + interface Window { + eulercopilot: DesktopAppAPI; + eulercopilotWelcome: DesktopAppWelcomeAPI; + } +} + +export {}; diff --git a/electron/preload/welcome.ts b/electron/preload/welcome.ts new file mode 100644 index 0000000000000000000000000000000000000000..b21ae88688f3b467ec6533fe947d20dc108425df --- /dev/null +++ b/electron/preload/welcome.ts @@ -0,0 +1,216 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { ipcRenderer, contextBridge } from 'electron'; +import type { DesktopConfig } from './types'; + +/** + * 欢迎界面专用的 preload 脚本 + * 提供配置管理和欢迎流程相关的 API + */ + +/** + * 欢迎界面 API + */ +const welcomeAPI = { + // 配置管理 + config: { + /** + * 获取当前配置 + */ + get: async (): Promise => { + try { + return await ipcRenderer.invoke('copilot:get-config'); + } catch (error) { + console.error('Failed to get config:', error); + return null; + } + }, + + /** + * 更新配置 + */ + update: async ( + updates: Partial, + ): Promise => { + try { + return await ipcRenderer.invoke('copilot:update-config', updates); + } catch (error) { + console.error('Failed to update config:', error); + return null; + } + }, + + /** + * 重置配置为默认值 + */ + reset: async (): Promise => { + try { + return await ipcRenderer.invoke('copilot:reset-config'); + } catch (error) { + console.error('Failed to reset config:', error); + return null; + } + }, + + /** + * 验证服务器连接 + */ + validateServer: async (url: string): Promise => { + try { + // 这里可以添加服务器连接验证逻辑 + // 暂时返回 true,实际实现时可以通过 IPC 调用主进程进行网络检查 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(url, { + method: 'HEAD', + mode: 'no-cors', + signal: controller.signal, + }); + + clearTimeout(timeoutId); + return response.ok || response.type === 'opaque'; + } catch (error) { + console.warn('Server validation failed:', error); + return false; + } + }, + }, + + // 欢迎流程管理 + welcome: { + /** + * 完成欢迎设置流程 + */ + complete: async (): Promise => { + try { + return await ipcRenderer.invoke('copilot:complete-welcome'); + } catch (error) { + console.error('Failed to complete welcome flow:', error); + return false; + } + }, + + /** + * 取消欢迎流程(关闭窗口) + */ + cancel: async (): Promise => { + try { + await ipcRenderer.invoke('copilot:window-control', 'close'); + } catch (error) { + console.error('Failed to cancel welcome flow:', error); + } + }, + }, + + // 窗口控制 + window: { + /** + * 关闭窗口 + */ + close: async (): Promise => { + try { + await ipcRenderer.invoke('copilot:window-control', 'close'); + } catch (error) { + console.error('Failed to close window:', error); + } + }, + + /** + * 最小化窗口 + */ + minimize: async (): Promise => { + try { + await ipcRenderer.invoke('copilot:window-control', 'minimize'); + } catch (error) { + console.error('Failed to minimize window:', error); + } + }, + }, + + // 系统信息 + system: { + /** + * 获取平台信息 + */ + get platform(): string { + return process.platform; + }, + + /** + * 获取架构信息 + */ + get arch(): string { + return process.arch; + }, + + /** + * 获取版本信息 + */ + get versions(): NodeJS.ProcessVersions { + return process.versions; + }, + }, + + // 实用工具 + utils: { + /** + * 检查 URL 格式是否有效 + */ + isValidUrl: (url: string): boolean => { + try { + new URL(url); + return true; + } catch { + return false; + } + }, + + /** + * 格式化 URL(确保有协议) + */ + formatUrl: (url: string): string => { + if (!url) return ''; + + // 移除尾部斜杠 + url = url.replace(/\/+$/, ''); + + // 如果没有协议,默认添加 https:// + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + return url; + }, + + /** + * 延迟执行 + */ + delay: (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); + }, + }, +}; + +// 类型定义已在 types.d.ts 中声明 + +// 暴露 API 到渲染进程 +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('eulercopilotWelcome', welcomeAPI); + } catch (error) { + console.error('Failed to expose welcome API:', error); + } +} else { + (window as any).eulercopilotWelcome = welcomeAPI; +} + +console.log('Welcome preload script loaded'); diff --git a/electron/welcome/index.html b/electron/welcome/index.html new file mode 100644 index 0000000000000000000000000000000000000000..32ac67f06525a6b07c9586297f731880586e5e82 --- /dev/null +++ b/electron/welcome/index.html @@ -0,0 +1,12 @@ + + + + + 欢迎使用 openEuler Intelligence + + +
+

欢迎使用 openEuler Intelligence

+
+ +