diff --git a/pc-fe/src/assets/imageFileIcon.png b/pc-fe/src/assets/imageFileIcon.png new file mode 100644 index 0000000..7570f04 Binary files /dev/null and b/pc-fe/src/assets/imageFileIcon.png differ diff --git a/pc-fe/src/main/grpc/BTGrpcClient.ts b/pc-fe/src/main/grpc/BTGrpcClient.ts deleted file mode 100644 index 0f3020b..0000000 --- a/pc-fe/src/main/grpc/BTGrpcClient.ts +++ /dev/null @@ -1,194 +0,0 @@ -// // src/grpc/BTGrpcClient.ts 调用后端服务 -// import * as grpc from '@grpc/grpc-js'; -// import * as protoLoader from '@grpc/proto-loader'; -// import path from 'path'; -// import { app } from 'electron'; - -// export interface DownloadRequest { -// torrent_url: string; -// item_name: string; -// item_id?: string; -// } - -// export interface ProgressCallback { -// (progress: ProgressUpdate): void; -// } - -// export interface ProgressUpdate { -// download_id: string; -// progress: number; -// download_speed: number; -// upload_speed: number; -// eta: number; -// total_size: number; -// downloaded_size: number; -// state: string; -// } - -// export class BTGrpcClient { -// private client: any; -// private progressCallbacks: Map = new Map(); -// private progressStream: any = null; - -// constructor() { -// this.initializeClient(); -// } - -// private initializeClient() { -// try { -// const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto'); - -// const packageDefinition = protoLoader.loadSync(PROTO_PATH, { -// keepCase: true, -// longs: String, -// enums: String, -// defaults: true, -// oneofs: true, -// }); - -// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); -// const bittorrent = protoDescriptor.bittorrent as any; - -// // 连接到后端 Agent,假设运行在 localhost:50051 -// this.client = new bittorrent.BTDownloadService( -// 'localhost:50051', -// grpc.credentials.createInsecure() -// ); - -// console.log('gRPC客户端初始化成功'); -// this.setupProgressStream(); -// } catch (error) { -// console.error('gRPC客户端初始化失败:', error); -// } -// } - -// // 设置进度流监听 -// private setupProgressStream() { -// try { -// this.progressStream = this.client.SubscribeProgress({}); -// // 注册数据事件监听器,接收进度更新 -// this.progressStream.on('data', (progress: ProgressUpdate) => { -// console.log('收到进度数据:', progress); // 添加调试日志 -// // BTGrpcClient 通过IPC将进度发送给主进程 -// this.progressCallbacks.forEach((callbacks, downloadId) => { -// if (downloadId === 'all' || downloadId === progress.download_id) { -// callbacks.forEach(callback => callback(progress)); -// } -// }); - -// // 如果有针对特定下载ID的回调,也要通知 -// const specificCallbacks = this.progressCallbacks.get(progress.download_id); -// if (specificCallbacks) { -// specificCallbacks.forEach(callback => callback(progress)); -// } -// }); - -// this.progressStream.on('error', (error: Error) => { -// console.error('进度流错误:', error); -// }); - -// this.progressStream.on('end', () => { -// console.log('进度流结束'); -// }); -// } catch (error) { -// console.error('设置进度流失败:', error); -// } -// } - -// // 开始下载 -// startDownload(request: DownloadRequest): Promise { -// return new Promise((resolve, reject) => { -// // MockBTService 接收请求,创建下载任务 -// this.client.StartDownload(request, (error: grpc.ServiceError, response: any) => { -// if (error) { -// reject(error); -// } else { -// resolve(response); -// } -// }); -// }); -// } - -// // 停止下载 -// stopDownload(downloadId: string): Promise { -// return new Promise((resolve, reject) => { -// this.client.StopDownload({ download_id: downloadId }, (error: grpc.ServiceError, response: any) => { -// if (error) { -// reject(error); -// } else { -// resolve(response); -// } -// }); -// }); -// } - -// // 注册进度回调 -// registerProgressCallback(downloadId: string, callback: ProgressCallback) { -// if (!this.progressCallbacks.has(downloadId)) { -// this.progressCallbacks.set(downloadId, []); -// } -// this.progressCallbacks.get(downloadId)!.push(callback); -// } - -// // 移除进度回调 -// removeProgressCallback(downloadId: string, callback: ProgressCallback) { -// const callbacks = this.progressCallbacks.get(downloadId); -// if (callbacks) { -// const index = callbacks.indexOf(callback); -// if (index > -1) { -// callbacks.splice(index, 1); -// } -// } -// } - -// // 检查连接状态 -// isConnected(): boolean { -// return this.client && this.client.getChannel().getConnectivityState(true) === grpc.connectivityState.READY; -// } - -// // 重新连接 -// reconnect() { -// try { -// this.client.close(); -// this.initializeClient(); -// } catch (error) { -// console.error('重新连接失败:', error); -// } -// } - -// getConnectionState(): string { -// if (!this.client) return 'NOT_CREATED'; - -// const state = this.client.getChannel().getConnectivityState(false); -// const stateNames = { -// [grpc.connectivityState.IDLE]: 'IDLE', -// [grpc.connectivityState.CONNECTING]: 'CONNECTING', -// [grpc.connectivityState.READY]: 'READY', -// [grpc.connectivityState.TRANSIENT_FAILURE]: 'TRANSIENT_FAILURE', -// [grpc.connectivityState.SHUTDOWN]: 'SHUTDOWN' -// } as const; - -// // 使用类型断言确保 state 是合法的 key -// return stateNames[state as keyof typeof stateNames] || 'UNKNOWN'; -// } - -// // 添加测试方法 -// async testConnection(): Promise { -// try { -// // 尝试调用一个简单的方法来测试连接 -// await new Promise((resolve, reject) => { -// this.client.ListDownloads({}, (error: any, response: any) => { -// if (error && error.code !== grpc.status.UNIMPLEMENTED) { -// reject(error); -// } else { -// resolve(response); -// } -// }); -// }); -// return true; -// } catch (error) { -// console.error('连接测试失败:', error); -// return false; -// } -// } -// } \ No newline at end of file diff --git a/pc-fe/src/main/grpc/MockBTService.ts b/pc-fe/src/main/grpc/MockBTService.ts deleted file mode 100644 index 92f11d3..0000000 --- a/pc-fe/src/main/grpc/MockBTService.ts +++ /dev/null @@ -1,323 +0,0 @@ -// // 本地测试用的 gRPC 服务器,用于模拟 BT 下载 -// // src/grpc/MockBTService.ts -// import * as grpc from '@grpc/grpc-js'; -// import * as protoLoader from '@grpc/proto-loader'; -// import path from 'path'; - -// // 进度更新接口 -// interface ProgressUpdate { -// download_id: string; -// progress: number; -// download_speed: number; -// item_name?: string; // 可选的文件名字段 -// upload_speed: number; -// eta: number; -// total_size: number; -// downloaded_size: number; -// state: string; -// } - -// // 模拟的下载任务 -// interface MockDownloadTask { -// id: string; -// itemName: string; -// torrentUrl: string; -// progress: number; -// totalSize: number; -// downloadedSize: number; -// downloadSpeed: number; -// state: 'downloading' | 'completed' | 'error' | 'paused'; -// startTime: number; -// } - -// export class MockBTService { -// private server: grpc.Server; -// private activeDownloads: Map = new Map(); -// private progressIntervals: Map = new Map(); -// // 存储所有的进度回调函数,发送信息 -// private progressCallbacks: ((progress: ProgressUpdate) => void)[] = []; - -// constructor() { -// this.server = new grpc.Server(); -// this.setupService(); -// } - -// private setupService() { -// // 设置gRPC服务,加载 bittorrent.proto 定义,实现所有gRPC服务方法 -// const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto'); - -// const packageDefinition = protoLoader.loadSync(PROTO_PATH, { -// keepCase: true, -// longs: String, -// enums: String, -// defaults: true, -// oneofs: true, -// }); - -// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); -// const bittorrent = protoDescriptor.bittorrent as any; - -// // 实现 gRPC 服务方法 -// this.server.addService(bittorrent.BTDownloadService.service, { -// StartDownload: this.startDownload.bind(this), -// StopDownload: this.stopDownload.bind(this), -// GetDownloadStatus: this.getDownloadStatus.bind(this), -// ListDownloads: this.listDownloads.bind(this), -// SubscribeProgress: this.subscribeProgress.bind(this), -// }); -// } - -// // 开始下载 -// // 在 startDownload 方法中,可以添加根据文件名设置不同大小的逻辑 -// private startDownload(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { -// const { torrent_url, item_name, item_id } = call.request; -// const downloadId = item_id || `download-${Date.now()}`; - -// console.log(`模拟开始下载: ${item_name}, ID: ${downloadId}`); - -// // 根据文件名或ID设置不同大小的文件 -// let fileSize = 1024 * 1024 * 100; // 默认100MB - -// if (item_name.includes('large') || item_name.includes('大文件')) { -// fileSize = 4 * 1024 * 1024 * 1024; // 4GB -// } else if (item_name.includes('medium') || item_name.includes('中等')) { -// fileSize = 1024 * 1024 * 1024; // 1GB -// } else if (item_name.includes('small') || item_name.includes('小文件')) { -// fileSize = 100 * 1024 * 1024; // 100MB -// } - -// // 创建模拟下载任务 -// const task: MockDownloadTask = { -// id: downloadId, -// itemName: item_name, -// torrentUrl: torrent_url, -// progress: 0, -// totalSize: fileSize, -// downloadedSize: 0, -// downloadSpeed: 1024 * 1024 * 2, // 2MB/s -// state: 'downloading', -// startTime: Date.now(), -// }; - -// this.activeDownloads.set(downloadId, task); -// console.log(`已创建下载任务: ${downloadId}, 大小: ${fileSize} bytes`); - -// // 启动进度模拟 -// this.startProgressSimulation(downloadId); - -// callback(null, { -// success: true, -// message: '下载已开始', -// download_id: downloadId, -// }); -// } - -// // 停止下载 -// private stopDownload(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { -// const { download_id } = call.request; - -// if (this.activeDownloads.has(download_id)) { -// const task = this.activeDownloads.get(download_id)!; -// task.state = 'paused'; - -// // 清除进度定时器 -// if (this.progressIntervals.has(download_id)) { -// clearInterval(this.progressIntervals.get(download_id)); -// this.progressIntervals.delete(download_id); -// } - -// console.log(`模拟停止下载: ${download_id}`); -// callback(null, { success: true, message: '下载已停止' }); -// } else { -// callback({ -// code: grpc.status.NOT_FOUND, -// message: `下载任务不存在: ${download_id}` -// }); -// } -// } - -// // 获取下载状态 -// private getDownloadStatus(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { -// const { download_id } = call.request; - -// if (this.activeDownloads.has(download_id)) { -// const task = this.activeDownloads.get(download_id)!; -// callback(null, { -// download_id: task.id, -// progress: task.progress, -// download_speed: task.downloadSpeed, -// state: task.state, -// total_size: task.totalSize, -// downloaded_size: task.downloadedSize, -// }); -// } else { -// callback({ -// code: grpc.status.NOT_FOUND, -// message: `下载任务不存在: ${download_id}` -// }); -// } -// } - -// // 列出所有下载 -// private listDownloads(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { -// const downloads = Array.from(this.activeDownloads.values()).map(task => ({ -// download_id: task.id, -// item_name: task.itemName, -// progress: task.progress, -// download_speed: task.downloadSpeed, -// state: task.state, -// total_size: task.totalSize, -// downloaded_size: task.downloadedSize, -// })); - -// callback(null, { downloads }); -// } - -// // 订阅进度更新(流式响应) -// private subscribeProgress(call: grpc.ServerWritableStream) { -// console.log('客户端订阅了进度更新'); - -// // 存储回调以便发送进度更新 -// const callback = (progress: ProgressUpdate) => { -// try { -// call.write(progress); -// } catch (error) { -// console.error('发送进度更新失败:', error); -// } -// }; - -// this.progressCallbacks.push(callback); - -// // 当客户端断开连接时清理 -// call.on('cancelled', () => { -// console.log('客户端取消了进度订阅'); -// const index = this.progressCallbacks.indexOf(callback); -// if (index > -1) { -// this.progressCallbacks.splice(index, 1); -// } -// }); - -// call.on('error', (error) => { -// console.error('进度流错误:', error); -// const index = this.progressCallbacks.indexOf(callback); -// if (index > -1) { -// this.progressCallbacks.splice(index, 1); -// } -// }); - -// // 处理客户端断开连接 -// call.on('end', () => { -// console.log('客户端断开连接'); -// const index = this.progressCallbacks.indexOf(callback); -// if (index > -1) { -// this.progressCallbacks.splice(index, 1); -// } -// }); -// } - -// // 启动进度模拟 -// // 在 MockBTService.ts 中修改 startProgressSimulation 方法 -// private startProgressSimulation(downloadId: string) { -// const interval = setInterval(() => { -// if (this.activeDownloads.has(downloadId)) { -// const task = this.activeDownloads.get(downloadId)!; - -// if (task.state === 'downloading' && task.progress < 100) { -// // // 更新进度,但确保不超过100% -// // task.progress += Math.random() * 2; -// // 使用固定的进度增加而不是随机值,使下载更加稳定 -// task.progress += 1.5; // 每秒增加1.5%的进度 - -// // 确保进度不超过100% -// if (task.progress >= 100) { -// task.progress = 100; -// task.state = 'completed'; -// task.downloadedSize = task.totalSize; // 确保已完成时已下载大小等于总大小 -// console.log(`下载完成: ${downloadId}`); - -// // 清除定时器 -// clearInterval(interval); -// this.progressIntervals.delete(downloadId); -// } else { -// // 根据进度计算已下载大小 -// task.downloadedSize = (task.totalSize * task.progress) / 100; -// } - -// // 计算剩余时间(秒) -// let eta = 0; -// if (task.progress < 100) { -// // 基于当前速度计算剩余时间 -// const progressPerSecond = 1.5; // 每秒进度百分比 -// eta = Math.round(((100 - task.progress) / progressPerSecond)); -// } - -// // 发送进度更新 -// this.sendProgressUpdate({ -// download_id: task.id, -// item_name: task.itemName, -// progress: task.progress, -// download_speed: task.downloadSpeed, -// upload_speed: 1024 * 512, // 512KB/s 上传速度 -// eta: task.progress >= 100 ? 0 : Math.round(((100 - task.progress) / 2) * 1000), -// total_size: task.totalSize, -// downloaded_size: task.downloadedSize, -// state: task.state, -// }); -// } -// } else { -// clearInterval(interval); -// this.progressIntervals.delete(downloadId); -// } -// }, 1000); // 每秒更新一次进度 - -// this.progressIntervals.set(downloadId, interval); -// } - -// // 发送进度更新给所有订阅者 -// private sendProgressUpdate(progress: ProgressUpdate) { -// console.log('发送进度更新:', progress); -// // 通过已注册的回调函数传递给 BTGrpcClient -// this.progressCallbacks.forEach((callback, index) => { -// try { -// callback(progress); -// } catch (error) { -// console.error(`发送进度更新给回调 ${index} 失败:`, error); -// } -// }); -// } - -// // 绑定端口并启动服务器 -// start(port: number = 50051): Promise { -// return new Promise((resolve, reject) => { -// this.server.bindAsync( -// `0.0.0.0:${port}`, -// grpc.ServerCredentials.createInsecure(), -// (error, port) => { -// if (error) { -// reject(error); -// } else { -// this.server.start(); -// console.log(`Mock gRPC 服务器运行在端口 ${port}`); -// resolve(); -// } -// } -// ); -// }); -// } - -// // 停止服务器 -// stop(): Promise { -// return new Promise((resolve) => { -// // 清理所有定时器 -// this.progressIntervals.forEach(interval => clearInterval(interval)); -// this.progressIntervals.clear(); -// this.activeDownloads.clear(); -// this.progressCallbacks = []; - -// this.server.tryShutdown(() => { -// console.log('Mock gRPC 服务器已停止'); -// resolve(); -// }); -// }); -// } -// } \ No newline at end of file diff --git a/pc-fe/src/main/grpc/README.md b/pc-fe/src/main/grpc/README.md deleted file mode 100644 index 36259e0..0000000 --- a/pc-fe/src/main/grpc/README.md +++ /dev/null @@ -1,219 +0,0 @@ -梳理整个gRPC下载逻辑和模拟下载的执行流程。 - -## 1. 整体架构概览 - -整个gRPC下载系统分为以下几个主要部分: - -1. **前端页面** (`grpc.tsx`) - 用户交互界面 -2. **预加载脚本** (`preload.ts`) - 桥接前端和主进程 -3. **主进程** (`platform.ts`) - 管理gRPC客户端和服务端 -4. **gRPC客户端** (`BTGrpcClient.ts`) - 调用后端服务 -5. **gRPC服务端** (`MockBTService.ts`) - 模拟下载服务 -6. **协议定义** (`bittorrent.proto`) - gRPC接口定义 - -## 2. 启动流程 - -### 2.1 应用启动时初始化gRPC - -```mermaid -graph TD - A[应用启动] --> B[app.whenReady()] - B --> C[initializeGrpc()] - C --> D{IS_TEST_MODE} - D -->|true| E[创建MockBTService实例] - D -->|false| F[连接真实后端] - E --> G[启动Mock gRPC服务器] - G --> H[创建BTGrpcClient实例] - H --> I[注册进度回调] - I --> J[启动健康检查] -``` - -在 `platform.ts` 中: -- 应用启动时调用 `initializeGrpc()` 函数 -- 根据 `IS_TEST_MODE` 决定是启动模拟服务还是连接真实后端 -- 创建 `MockBTService` 实例并启动服务器 -- 创建 `BTGrpcClient` 实例连接到gRPC服务 -- 注册进度回调函数,用于接收下载进度更新 - -### 2.2 gRPC服务端启动 (MockBTService) - -```mermaid -sequenceDiagram - participant M as MockBTService - participant S as gRPC Server - participant C as BTGrpcClient - - M->>S: 创建gRPC服务器实例 - M->>S: 加载proto定义 - M->>S: 实现服务方法 - M->>S: 绑定端口并启动 - S-->>M: 服务器启动成功 - C->>S: 连接服务器 - S-->>C: 连接建立 -``` - -在 `MockBTService.ts` 中: -1. 构造函数调用 [setupService()](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L43-L65) 设置gRPC服务 -2. 加载 `bittorrent.proto` 定义 -3. 实现所有gRPC服务方法: - - `StartDownload` - 开始下载 - - `StopDownload` - 停止下载 - - `GetDownloadStatus` - 获取下载状态 - - `ListDownloads` - 列出所有下载 - - `SubscribeProgress` - 订阅进度更新 -4. 通过 `server.bindAsync()` 绑定端口并启动服务 - -## 3. 下载流程详解 - -### 3.1 用户触发下载 - -```mermaid -graph TD - A[用户点击下载按钮] --> B[handleStartDownload()] - B --> C[调用window.electronAPI.grpcStartDownload()] - C --> D[IPC调用grpc-start-download] - D --> E[主进程处理] - E --> F[调用btGrpcClient.startDownload()] - F --> G[gRPC调用StartDownload()] - G --> H[MockBTService处理] - H --> I[创建下载任务] - I --> J[启动进度模拟] - J --> K[返回下载ID] - K --> L[前端接收响应] -``` - -详细步骤: -1. 用户点击"开始gRPC下载测试"按钮 -2. 调用 `handleStartDownload()` 函数 -3. 通过 `window.electronAPI.grpcStartDownload()` 发起IPC调用 -4. 主进程 `platform.ts` 接收 `grpc-start-download` 请求 -5. 调用 `btGrpcClient.startDownload()` 方法 -6. gRPC客户端向服务端发起 `StartDownload` 调用 -7. [MockBTService](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L32-L305) 接收请求,创建下载任务 -8. 启动进度模拟定时器 -9. 返回下载ID给前端 - -### 3.2 进度更新机制 - -```mermaid -sequenceDiagram - participant M as MockBTService - participant I as Interval - participant B as BTGrpcClient - participant P as Platform主进程 - participant F as 前端 - - I->>M: 定时触发 - M->>M: 更新任务进度 - M->>M: 构造进度数据 - M->>B: 发送进度更新 - B->>P: 调用回调函数 - P->>F: IPC发送grpc-progress-update - F->>F: 更新UI显示 -``` - -详细步骤: -1. `startProgressSimulation()` 创建定时器 -2. 定时器每秒触发一次,更新下载进度 -3. 构造 `ProgressUpdate` 对象 -4. 调用 `sendProgressUpdate()` 发送进度更新 -5. 通过已注册的回调函数传递给 `BTGrpcClient` -6. `BTGrpcClient` 通过IPC将进度发送给主进程 -7. 主进程通过 `webContents.send()` 将进度发送给前端 -8. 前端接收并更新UI - -## 4. 关键代码解析 - -### 4.1 前端监听进度更新 - -```typescript -// grpc.tsx -useEffect(() => { - // 设置进度监听 - const handleProgress = (progress: DownloadProgress) => { - console.log('收到进度更新:', progress); - setDownloads(prev => { - const newMap = new Map(prev); - newMap.set(progress.download_id, progress); - return newMap; - }); - }; - - window.electronAPI.onGrpcProgress(handleProgress); - - return () => { - window.electronAPI.removeAllGrpcProgressListeners(); - }; -}, []); -``` - -### 4.2 主进程转发进度更新 - -```typescript -// platform.ts -btGrpcClient.registerProgressCallback('all', (progress) => { - const mainWindow = BrowserWindow.getFocusedWindow(); - if (mainWindow) { - mainWindow.webContents.send('grpc-progress-update', progress); - } -}); -``` - -### 4.3 Mock服务模拟进度 - -```typescript -// MockBTService.ts -private startProgressSimulation(downloadId: string) { - const interval = setInterval(() => { - if (this.activeDownloads.has(downloadId)) { - const task = this.activeDownloads.get(downloadId)!; - - if (task.state === 'downloading' && task.progress < 100) { - // 更新进度 - task.progress += Math.random() * 2; - task.downloadedSize = (task.totalSize * task.progress) / 100; - - // 发送进度更新 - this.sendProgressUpdate({ - download_id: task.id, - item_name: task.itemName, - progress: task.progress, - download_speed: task.downloadSpeed, - upload_speed: 1024 * 512, - eta: Math.round(((100 - task.progress) / 2) * 1000), - total_size: task.totalSize, - downloaded_size: task.downloadedSize, - state: task.state, - }); - } - } - }, 1000); - - this.progressIntervals.set(downloadId, interval); -} -``` - -## 5. 数据流总结 - -```mermaid -graph LR - A[前端UI] --> B[preload.ts] - B --> C[主进程platform.ts] - C --> D[BTGrpcClient.ts] - D --> E[MockBTService.ts] - E --> F[定时模拟进度] - F --> G[发送进度更新] - G --> H[主进程转发] - H --> I[前端接收更新] - I --> A -``` - -整个流程形成了一个闭环: -1. 用户操作触发前端请求 -2. 请求通过IPC传递到主进程 -3. 主进程通过gRPC调用后端服务 -4. Mock服务模拟下载过程 -5. 进度更新通过相同路径返回前端 -6. 前端更新UI显示最新进度 - -这就是整个gRPC下载逻辑的完整执行流程。 \ No newline at end of file diff --git a/pc-fe/src/main/grpc/protos/bittorrent.proto b/pc-fe/src/main/grpc/protos/bittorrent.proto deleted file mode 100644 index f208939..0000000 --- a/pc-fe/src/main/grpc/protos/bittorrent.proto +++ /dev/null @@ -1,77 +0,0 @@ -// bittorrent.proto gRPC接口定义 -syntax = "proto3"; - -package bittorrent; - -service BTDownloadService { - rpc StartDownload (DownloadRequest) returns (DownloadResponse) {} - rpc StopDownload (StopRequest) returns (StopResponse) {} - rpc GetDownloadStatus (StatusRequest) returns (StatusResponse) {} - rpc ListDownloads (ListRequest) returns (ListResponse) {} - rpc SubscribeProgress (SubscribeRequest) returns (stream ProgressUpdate) {} -} - -message DownloadRequest { - string torrent_url = 1; - string item_name = 2; - string item_id = 3; -} - -message DownloadResponse { - bool success = 1; - string message = 2; - string download_id = 3; -} - -message StopRequest { - string download_id = 1; -} - -message StopResponse { - bool success = 1; - string message = 2; -} - -message ProgressUpdate { - string download_id = 1; - double progress = 2; - double download_speed = 3; - double upload_speed = 4; - int64 eta = 5; - int64 total_size = 6; - int64 downloaded_size = 7; - string state = 8; -} - -message StatusRequest { - string download_id = 1; -} - -message StatusResponse { - string download_id = 1; - double progress = 2; - double download_speed = 3; - string state = 4; - int64 total_size = 5; - int64 downloaded_size = 6; -} - -message ListRequest { -} - -message ListResponse { - repeated DownloadInfo downloads = 1; -} - -message DownloadInfo { - string download_id = 1; - string item_name = 2; - double progress = 3; - double download_speed = 4; - string state = 5; - int64 total_size = 6; - int64 downloaded_size = 7; -} - -message SubscribeRequest { -} \ No newline at end of file diff --git a/pc-fe/src/main/ipc/btDownload.ts b/pc-fe/src/main/ipc/btDownload.ts new file mode 100644 index 0000000..b413be5 --- /dev/null +++ b/pc-fe/src/main/ipc/btDownload.ts @@ -0,0 +1,105 @@ +// bt下载 +import { ipcMain } from 'electron'; +import request from '../utils/request'; +import { currentServerIp } from './sharedState'; // 修改导入 +// 添加下载相关 IPC 处理 +ipcMain.handle('start-download', async (event, downloadConfig) => { + // return {success:true,message:"开始下载"} + try { + console.log('开始下载:', downloadConfig); + + // 检查服务器 IP 是否已设置 + if (!currentServerIp) { + return { + success: false, + error: '服务器未连接,请先配置服务器' + }; + } + + // 使用保存的服务器 IP 构建 API 地址 + const apiUrl = `http://${currentServerIp}:3001/api/downloads`; + + // 发送 HTTP 请求到后端 API + const response = await request(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + data: JSON.stringify(downloadConfig) + }); + + let responseData; + try { + responseData = JSON.parse((response as any).data); + } catch (parseError) { + console.error('Failed to parse authentication response:', parseError); + return { success: false, message: '服务器响应格式错误' }; + } + // const result = await response.json(); + // 检查结果 - 只有code等于200时才算成功 + if (responseData.code === '200' || responseData.code === 200) { + console.log('开始下载成功:', responseData); + return { success: true, message: `开始下载成功`, data: responseData }; + } else { + // 认证失败,返回服务器提供的错误信息 + const errorMessage = responseData.message || responseData.msg || responseData.error || '开始下载成功'; + console.log('Authentication failed:', errorMessage); + return { success: false, message: errorMessage }; + } + } catch (error) { + console.error('下载请求失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '未知错误' + }; + } +}); + +ipcMain.handle('stop-download', async (event, downloadId) => { + // return {success:true,message:"停止下载成功"} + try { + console.log('停止下载:', downloadId); + + // 检查服务器 IP 是否已设置 + if (!currentServerIp) { + return { + success: false, + error: '服务器未连接,请先配置服务器' + }; + } + + // 使用保存的服务器 IP 构建 API 地址 + const apiUrl = `http://${currentServerIp}:3001/api/downloads/${downloadId}`; + + // 发送 HTTP 请求到后端 API + const response = await request(apiUrl, { + method: 'DELETE' + }); + + // const result = await response.json(); + let responseData; + try { + responseData = JSON.parse((response as any).data); + } catch (parseError) { + console.error('Failed to parse authentication response:', parseError); + return { success: false, message: '服务器响应格式错误' }; + } + // const result = await response.json(); + // 检查结果 - 只有code等于200时才算成功 + if (responseData.code === '200' || responseData.code === 200) { + console.log('停止下载成功:', responseData); + return { success: true, message: `停止下载成功`, data: responseData }; + } else { + // 认证失败,返回服务器提供的错误信息 + const errorMessage = responseData.message || responseData.msg || responseData.error || '停止下载失败'; + console.log('Authentication failed:', errorMessage); + return { success: false, message: errorMessage }; + } + } catch (error) { + console.error('停止下载失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '未知错误' + }; + } +}); \ No newline at end of file diff --git a/pc-fe/src/main/ipc/common.ts b/pc-fe/src/main/ipc/common.ts new file mode 100644 index 0000000..ebd6835 --- /dev/null +++ b/pc-fe/src/main/ipc/common.ts @@ -0,0 +1,111 @@ +// 通用 +import { ipcMain,app } from 'electron'; +import { BrowserWindow } from 'electron'; +import { getDeviceIdAndMac } from '../utils/utils'; + +// 服务器IP获取 +ipcMain.handle('get-current-server-ip', () => { + return (global as any).currentServerIp; +}); + +// 终端信息获取 +ipcMain.handle('get-device-info', async () => { + // return { success: true, deviceId: "fsfs" }; + try { + const deviceInfo = await getDeviceIdAndMac(); + console.log(`Using device Info: ${deviceInfo}`); + return { success: true, deviceInfo }; + } catch (error) { + console.error('获取设备信息失败:', error); + return { success: false, error: error instanceof Error ? error.message : '未知错误' }; + } +}); + +// 添加处理窗口调整 +ipcMain.handle('adjust-window-for-normal', async (event) => { + try { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + // 调整窗口大小和配置 + window.setKiosk(false); // 退出全屏模式 + window.setMinimumSize(1200, 800); + window.setSize(1200, 800); + window.setResizable(true); + window.setMaximizable(true); + window.setMinimizable(true); + // 保持无边框和隐藏标题栏的设置 + window.setFullScreen(false); + } + return { success: true }; + } catch (error) { + console.error('调整窗口失败:', error); + return { success: false, }; + } +}); + +/**拖动窗口 */ +ipcMain.handle('drag-window', (event) => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + // 通知渲染进程开始拖拽 + focusedWindow.webContents.send('start-drag'); + } +}); + +// 监听渲染进程发送的消息 +ipcMain.handle('getPlatform', () => { + return `hi, i'm from ${process.platform}`; +}); + +// 窗口控制:最小化,退出全屏,关闭, +// 获取窗口最大化状态 +ipcMain.handle('get-window-maximized', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + return window?.isMaximized() || false; +}); + +ipcMain.on('close-app', () => { + app.quit(); +}); + +// 最小化 +ipcMain.on('minimize-app', () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + focusedWindow.minimize(); + } + // window?.minimize(); +}); + +// 退出全屏 +ipcMain.on('restore-window', () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + focusedWindow.unmaximize(); + } + // if (window) { + // window.setFullScreen(false); + // } +}); + +// 设置全屏 +ipcMain.on('maximize-window', () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + focusedWindow.maximize(); + } +}) + +// 监听窗口状态变化并通知渲染进程 +ipcMain.on('register-window-state-listeners', (event) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + window.on('maximize', () => { + event.sender.send('window-maximized'); + }); + + window.on('unmaximize', () => { + event.sender.send('window-unmaximized'); + }); + } +}); \ No newline at end of file diff --git a/pc-fe/src/main/ipc/platform.ts b/pc-fe/src/main/ipc/config.ts similarity index 69% rename from pc-fe/src/main/ipc/platform.ts rename to pc-fe/src/main/ipc/config.ts index e16ff7c..f98d7ba 100644 --- a/pc-fe/src/main/ipc/platform.ts +++ b/pc-fe/src/main/ipc/config.ts @@ -1,5 +1,5 @@ import { ipcMain,app } from 'electron'; -import { getDeviceId, getWiredConnectionName, netmaskToCidr,simulateUpdate,performRealUpdate } from '../utils/utils'; +import { getDeviceIdAndMac, getWiredConnectionName, netmaskToCidr,simulateUpdate,performRealUpdate } from '../utils/utils'; import { BrowserWindow } from 'electron'; import { autoUpdater } from 'electron-updater'; import request from '../utils/request'; @@ -8,151 +8,51 @@ const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); -const window = getBrowserWindowRuntime(); - -let currentServerIp: string | undefined; - -// 添加处理窗口调整 -ipcMain.handle('adjust-window-for-normal', async (event) => { - try { - const window = BrowserWindow.fromWebContents(event.sender); - if (window) { - // 调整窗口大小和配置 - window.setKiosk(false); // 退出全屏模式 - window.setMinimumSize(1200, 800); - window.setSize(1200, 800); - window.setResizable(true); - window.setMaximizable(true); - window.setMinimizable(true); - // 保持无边框和隐藏标题栏的设置 - window.setFullScreen(false); - } - return { success: true }; - } catch (error) { - console.error('调整窗口失败:', error); - return { success: false, }; - } -}); - -/**拖动窗口 */ -ipcMain.handle('drag-window', (event) => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - // 通知渲染进程开始拖拽 - focusedWindow.webContents.send('start-drag'); - } -}); - -// 监听渲染进程发送的消息 -ipcMain.handle('getPlatform', () => { - return `hi, i'm from ${process.platform}`; -}); - -// 窗口控制:最小化,退出全屏,关闭, -// 获取窗口最大化状态 -ipcMain.handle('get-window-maximized', (event) => { - const window = BrowserWindow.fromWebContents(event.sender); - return window?.isMaximized() || false; -}); - -ipcMain.on('close-app', () => { - app.quit(); -}); - -// 最小化 -ipcMain.on('minimize-app', () => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - focusedWindow.minimize(); - } - // window?.minimize(); -}); - -// 退出全屏 -ipcMain.on('restore-window', () => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - focusedWindow.unmaximize(); - } - // if (window) { - // window.setFullScreen(false); - // } -}); - -// 设置全屏 -ipcMain.on('maximize-window', () => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - focusedWindow.maximize(); - } -}) - -// 监听窗口状态变化并通知渲染进程 -ipcMain.on('register-window-state-listeners', (event) => { - const window = BrowserWindow.fromWebContents(event.sender); - if (window) { - window.on('maximize', () => { - event.sender.send('window-maximized'); - }); - - window.on('unmaximize', () => { - event.sender.send('window-unmaximized'); - }); - } -}); - - -ipcMain.handle('get-device-id',async()=>{ - const deviceId = await getDeviceId(); - console.log(`Using device ID: ${deviceId}`); - // TODO:传给后端 -}) - -/* 1. 平台网络配置:IPC 处理应用有线网络配置 */ -ipcMain.handle('apply-wired-config',async(event,config)=>{ + /* 1. 平台网络配置:IPC 处理应用有线网络配置 */ +ipcMain.handle('apply-wired-config', async (event, config) => { // return { // success: true, // message: '网络配置已成功应用' // }; - try{ + try { console.log('应用网络配置:', config); // 获取有线连接名称 const connectionName = await getWiredConnectionName(); console.log('有线连接名称:', connectionName); - - if(config.method==='static'){ + + if (config.method === 'static') { // 使用nmcli配置静态IP,需要使用sudo权限,一次性设置所有参数 let modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method manual ipv4.addresses "${config.ipv4}/${netmaskToCidr(config.subnetMask)}" ipv4.gateway "${config.ipv4Gateway}"`; const dnsServers = [config.primaryDns, config.secondaryDns].filter(Boolean).join(','); modifyCmd += ` ipv4.dns "${dnsServers}"`; - + // 添加 IPv6 配置(如果存在 ipv6Gateway)????ipv6和长度需要吗?ui只写了ipv6网关 // ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。???? if (config.ipv6 && config.ipv6Gateway) { - const ipv6PrefixLength = config.ipv6PrefixLength && - config.ipv6PrefixLength >= 0 && - config.ipv6PrefixLength <= 128 ? - config.ipv6PrefixLength : 64; // 默认使用64 + const ipv6PrefixLength = config.ipv6PrefixLength && + config.ipv6PrefixLength >= 0 && + config.ipv6PrefixLength <= 128 ? + config.ipv6PrefixLength : 64; // 默认使用64 modifyCmd += ` ipv6.method manual ipv6.addresses "${config.ipv6}/${ipv6PrefixLength}" ipv6.gateway "${config.ipv6Gateway}"`; - }else if (config.ipv6 || config.ipv6Gateway) { + } else if (config.ipv6 || config.ipv6Gateway) { console.warn('IPv6配置不完整:需要同时提供IPv6地址和网关'); } // 执行配置命令 console.log('执行命令:', modifyCmd.replace('unis@123', '***')); await execAsync(modifyCmd); - + // 重新激活连接 await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`); - - }else{ + + } else { // DHCP配置,一次性设置所有参数 const modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""`; - + // 执行配置命令 console.log('执行命令:', modifyCmd.replace('unis@123', '***')); await execAsync(modifyCmd); - + // 重新激活连接 await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`); } @@ -160,7 +60,7 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{ success: true, message: '网络配置已成功应用' }; - }catch(error:unknown){ + } catch (error: unknown) { console.error('应用网络配置失败:', error); return { success: false, @@ -169,14 +69,16 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{ } }) -/**2. 服务器配置 */ +/**2. 服务器配置认证 */ ipcMain.handle('connect-server', async (event, { serverIp }) => { console.log(`Connecting to server: ${serverIp}`); - + // (global as any).currentServerIp = serverIp; + // console.log('Authentication successful, server IP stored:', serverIp); + // return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp }; try { // 获取设备ID - const deviceId = await getDeviceId(); - console.log(`Using device ID: ${deviceId}`); + const deviceInfo = await getDeviceIdAndMac(); + console.log(`Using device Info: ${JSON.stringify(deviceInfo)}`); // 构建新的API地址,使用POST请求 const apiUrl = `http://${serverIp}:8113/api/nex/v1/client/authentication`; @@ -189,7 +91,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => { headers: { 'Content-Type': 'application/json' }, - data: JSON.stringify({ device_id: deviceId }) + data: JSON.stringify({ device_id: deviceInfo.deviceId,mac_addr:deviceInfo.macAddr }) }); console.log('API response received:', response); @@ -208,7 +110,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => { // 检查认证结果 - 只有code等于200时才算成功 if (responseData.code === '200' || responseData.code === 200) { // 认证成功,存储服务器IP - currentServerIp = serverIp; + (global as any).currentServerIp = serverIp; console.log('Authentication successful, server IP stored:', serverIp); return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp }; } else { @@ -251,10 +153,6 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => { }; } }); -// 服务器IP获取 -ipcMain.handle('get-current-server-ip', () => { - return currentServerIp; -}); /**3. 侦测管理平台 */ // 下载并更新客户端 @@ -325,4 +223,5 @@ ipcMain.on('install-update-and-restart', () => { } } } -}); \ No newline at end of file +}); + diff --git a/pc-fe/src/main/ipc/grpc.ts b/pc-fe/src/main/ipc/grpc.ts deleted file mode 100644 index e463d5e..0000000 --- a/pc-fe/src/main/ipc/grpc.ts +++ /dev/null @@ -1,181 +0,0 @@ -// import { ipcMain,app,BrowserWindow } from 'electron'; - -// // 模拟grpc服务端 -// import { BTGrpcClient } from '../grpc/BTGrpcClient'; -// import { MockBTService } from '../grpc/MockBTService'; - -// // 客户端和服务端 通信 -// const IS_TEST_MODE = true; // 设置为 false 时连接真实后端 -// const GRPC_SERVER_PORT = 50051; -// // 先声明变量,但不立即初始化 -// let btGrpcClient: BTGrpcClient | null = null; -// let mockServer: MockBTService | null = null; -// let healthCheckInterval: NodeJS.Timeout | null = null; // 声明一个变量来存储定时器 - - -// // 初始化 gRPC 客户端 -// async function initializeGrpc() { -// try { -// if (IS_TEST_MODE) { -// console.log('启动模拟 gRPC 服务器...'); -// mockServer = new MockBTService(); -// await mockServer.start(GRPC_SERVER_PORT); - -// // 给一点时间让服务器启动 -// await new Promise(resolve => setTimeout(resolve, 1000)); -// } - -// // 创建 BTGrpcClient 实例连接到gRPC服务 -// btGrpcClient = new BTGrpcClient(); - -// // 注册进度回调函数,用于接收下载进度更新 -// btGrpcClient.registerProgressCallback('all', (progress) => { -// const mainWindow = BrowserWindow.getFocusedWindow(); -// if (mainWindow) { -// mainWindow.webContents.send('grpc-progress-update', progress); -// } -// }); - -// // 测试连接 -// setTimeout(async () => { -// try { -// const connected = await btGrpcClient!.testConnection(); -// console.log('gRPC 连接状态:', connected ? '已连接' : '未连接'); - -// const mainWindow = BrowserWindow.getFocusedWindow(); -// if (mainWindow) { -// mainWindow.webContents.send('grpc-connection-status', { -// connected, -// isMock: IS_TEST_MODE -// }); -// } -// } catch (error) { -// console.error('连接测试失败:', error); -// } -// }, 2000); - -// // 启动健康检查 -// startHealthCheck(); - -// } catch (error) { -// console.error('gRPC 初始化失败:', error); -// } -// } - -// // 启动健康检查 -// function startHealthCheck() { -// if (healthCheckInterval) { -// clearInterval(healthCheckInterval); -// } - -// healthCheckInterval = setInterval(async () => { -// try { -// if (!btGrpcClient) return; - -// const isConnected = await btGrpcClient.testConnection(); -// const mainWindow = BrowserWindow.getFocusedWindow(); -// if (mainWindow) { -// mainWindow.webContents.send('grpc-connection-status', { -// connected: isConnected, -// state: btGrpcClient.getConnectionState(), -// isMock: IS_TEST_MODE -// }); -// } -// } catch (error) { -// console.error('健康检查失败:', error); -// } -// }, 5000); // 每5秒检查一次 -// } - - -// // 应用启动时初始化 -// app.whenReady().then(() => { -// initializeGrpc(); -// }); - -// // 应用退出时清理 -// app.on('before-quit', async () => { -// if (healthCheckInterval) { -// clearInterval(healthCheckInterval); -// } - -// if (mockServer) { -// await mockServer.stop(); -// } -// }); - -// // 添加 gRPC 相关的 IPC 处理程序 -// ipcMain.handle('grpc-start-download', async (event, config) => { -// try { -// if (!btGrpcClient) { -// return { -// success: false, -// error: 'gRPC客户端未初始化' -// }; -// } - -// console.log('开始gRPC下载:', config); -// // gRPC客户端向服务端发起 StartDownload 调用 -// const result = await btGrpcClient.startDownload({ -// torrent_url: config.torrentUrl, -// item_name: config.itemName, -// item_id: config.itemId -// }); -// return { success: true, data: result }; -// } catch (error: any) { -// console.error('gRPC下载失败:', error); -// return { -// success: false, -// error: error.message || '未知错误', -// details: error.details -// }; -// } -// }); - -// ipcMain.handle('grpc-stop-download', async (event, downloadId) => { -// try { -// if (!btGrpcClient) { -// return { -// success: false, -// error: 'gRPC客户端未初始化' -// }; -// } - -// const result = await btGrpcClient.stopDownload(downloadId); -// return { success: true, data: result }; -// } catch (error: any) { -// return { -// success: false, -// error: error.message || '未知错误' -// }; -// } -// }); - -// ipcMain.handle('grpc-check-connection', async () => { -// if (!btGrpcClient) { -// return { connected: false, error: 'gRPC客户端未初始化' }; -// } - -// return { -// connected: btGrpcClient.isConnected(), -// state: btGrpcClient.getConnectionState() -// }; -// }); - -// // 重新连接 gRPC -// ipcMain.handle('grpc-reconnect', async () => { -// try { -// if (btGrpcClient) { -// btGrpcClient.reconnect(); -// return { success: true, message: '正在重新连接' }; -// } else { -// await initializeGrpc(); -// return { success: true, message: '正在初始化连接' }; -// } -// } catch (error: any) { -// return { -// success: false, -// error: error.message || '重新连接失败' -// }; -// } -// }); \ No newline at end of file diff --git a/pc-fe/src/main/ipc/requestApi.ts b/pc-fe/src/main/ipc/requestApi.ts new file mode 100644 index 0000000..9f6960e --- /dev/null +++ b/pc-fe/src/main/ipc/requestApi.ts @@ -0,0 +1,126 @@ +import { ipcMain } from 'electron'; +import request from '../utils/request'; +import { getDeviceIdAndMac } from '../utils/utils'; +// import { setCurrentServerIp, getCurrentServerIp } from './sharedState'; // 修改导 + +// gRPC 连接请求 +ipcMain.handle('connect-to-grpc', async (event, { serverIp, clientUser }: { serverIp?: string; clientUser?: string }) => { + console.log('gRPC 连接请求'); + // return { + // success: true, + // message: 'gRPC 连接成功', + // data: [] + // }; + try { + // 如果没有传 serverIp,则使用已存储的 serverIp + const ip = serverIp || (global as any).currentServerIp; + + if (!ip) { + return { + success: false, + error: '未找到服务器IP地址' + }; + } + + // 获取设备序列号 + const deviceInfo = await getDeviceIdAndMac(); + + if (!deviceInfo.deviceId) { + return { + success: false, + error: '无法获取设备序列号' + }; + } + + // 构建请求参数 + const requestData = { + serverIp: ip, + serverPort: 50051, + clientSn:deviceInfo.deviceId, // 使用设备序列号作为 clientSn, + clientUser: clientUser || '' // 如果没有用户名,则传空字符串 + }; + + console.log('连接 gRPC 服务器:', requestData); + + // 发送请求到后端接口 + const apiUrl = `http://${ip}:3001/api/grpc/connect`; + const response = await request(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + data: JSON.stringify(requestData) + }); + + let responseData; + try { + responseData = JSON.parse((response as any).data); + } catch (parseError) { + console.error('解析 gRPC 连接响应失败:', parseError); + return { success: false, message: '服务器响应格式错误' }; + } + + console.log('gRPC 连接响应:', responseData); + + if (responseData.code === '200' || responseData.code === 200) { + console.log('gRPC 连接成功'); + return { + success: true, + message: 'gRPC 连接成功', + data: responseData + }; + } else { + const errorMessage = responseData.message || responseData.msg || responseData.error || 'gRPC 连接失败'; + console.log('gRPC 连接失败:', errorMessage); + return { success: false, message: errorMessage }; + } + } catch (error) { + console.error('gRPC 连接请求失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '未知错误' + }; + } +}); + +// 镜像列表请求 +ipcMain.handle('get-images-list', async (event, deviceInfo: { deviceId: string; macAddr: string }) => { + // return { success: true, message: '获取镜像列表成功', data: []} + try { + if (!(global as any).currentServerIp) { + return { + success: false, + error: '服务器未连接' + }; + } + + const apiUrl = `http://${(global as any).currentServerIp}:8113/api/nex/v1/client/getImageList`; + + const response = await request(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + data: JSON.stringify({ + device_id: deviceInfo.deviceId, + macAddr: deviceInfo.macAddr + }) + }); + + let responseData; + try { + responseData = JSON.parse((response as any).data); + } catch (parseError) { + console.error('解析镜像列表响应失败:', parseError); + return { success: false, message: '服务器响应格式错误' }; + } + + return responseData; + } catch (error) { + console.error('获取镜像列表失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '未知错误' + }; + } +}); \ No newline at end of file diff --git a/pc-fe/src/main/ipc/sharedState.ts b/pc-fe/src/main/ipc/sharedState.ts new file mode 100644 index 0000000..36ab4e4 --- /dev/null +++ b/pc-fe/src/main/ipc/sharedState.ts @@ -0,0 +1,2 @@ +// 设置全局变量 +(global as any).currentServerIp; \ No newline at end of file diff --git a/pc-fe/src/main/preload.ts b/pc-fe/src/main/preload.ts index 7b7ca5c..1d9c30e 100644 --- a/pc-fe/src/main/preload.ts +++ b/pc-fe/src/main/preload.ts @@ -5,16 +5,29 @@ contextBridge.exposeInMainWorld('electronAPI', { getPlatform: async () => { return await ipcRenderer.invoke('getPlatform'); }, + // 【窗口】相关API closeApp: () => ipcRenderer.send('close-app'), minimizeApp: () => ipcRenderer.send('minimize-app'), restoreWindow: () => ipcRenderer.send('restore-window'), maximizeWindow: () => ipcRenderer.send('maximize-window'), getWindowMaximized: () => ipcRenderer.invoke('get-window-maximized'), adjustWindowForNormal:() => ipcRenderer.invoke('adjust-window-for-normal'), + // 【bt下载】相关 API + startDownload: (config: any) => ipcRenderer.invoke('start-download', config), + stopDownload: (downloadId: string) => ipcRenderer.invoke('stop-download', downloadId), + // 【配置步骤】相关API + applyWiredConfig: (config: any) => ipcRenderer.invoke('apply-wired-config', config), + connectServer: (config: any) => ipcRenderer.invoke('connect-server', config), // 版本更新相关API downloadAndUpdate: (url: string) => ipcRenderer.invoke('download-and-update', url), - // 服务器IP获取 + // 【镜像列表】获取 + getImagesList: (config: any) => ipcRenderer.invoke('get-images-list', config), + // 【服务器IP】获取 getCurrentServerIp: () => ipcRenderer.invoke('get-current-server-ip'), + // 【设备信息】获取 + getDeviceInfo: () => ipcRenderer.invoke('get-device-info'), + // 【通知后端grpc连接】 + connectToGrpc: (config:any) => ipcRenderer.invoke('connect-to-grpc', config), // 事件监听 onMainProcessMessage: (callback: (data: string) => void) => { ipcRenderer.on('main-process-message', (_, data) => callback(data)); diff --git a/pc-fe/src/main/utils/utils.ts b/pc-fe/src/main/utils/utils.ts index 2075112..3fbbbb6 100644 --- a/pc-fe/src/main/utils/utils.ts +++ b/pc-fe/src/main/utils/utils.ts @@ -6,11 +6,21 @@ const os = require('os'); const { promisify } = require('util'); const execAsync = promisify(exec); -/** 获取设备ID(芯片序列号) - * TODO: 增加获取mac地址的逻辑,需要传给后端 +const fs = require('fs'); +const path = require('path'); +const configPath = path.join(os.homedir(), '.xspace_configData.json'); // 用户数据文件路径 + +/** 获取设备ID(芯片序列号)和MAC地址 + * 返回设备唯一标识符和原始MAC地址 */ -export async function getDeviceId() { +export async function getDeviceIdAndMac() { + // return { + // deviceId:'125dfsdfsd', + // macAddr: "00:00:00:00:00" + // }; try { + let macAddr = ''; + // 尝试多种方法获取唯一的设备标识 const methods = [ // 方法1: CPU序列号 @@ -28,10 +38,27 @@ export async function getDeviceId() { const { stdout } = await execAsync(command); const deviceId = stdout.trim(); + // 同时获取MAC地址供后续使用 + if (!macAddr) { + const networkInterfaces = os.networkInterfaces(); + outerLoop: for (const interfaceName of Object.keys(networkInterfaces)) { + const interfaces = networkInterfaces[interfaceName]; + for (const iface of interfaces) { + if (iface.mac && iface.mac !== '00:00:00:00:00:00') { + macAddr = iface.mac.toUpperCase(); + break outerLoop; + } + } + } + } + if (deviceId && deviceId !== '' && deviceId !== 'unknown' && deviceId !== '0000000000000000') { console.log(`Device ID obtained using command: ${command}`); console.log(`Device ID: ${deviceId}`); - return deviceId; + return { + deviceId, + macAddr: macAddr || 'NOT_FOUND' + }; } } catch (error) { console.log(`Method failed: ${command}, error: ${(error as Error).message}`); @@ -46,8 +73,12 @@ export async function getDeviceId() { for (const iface of interfaces) { if (iface.mac && iface.mac !== '00:00:00:00:00:00') { const fallbackId = iface.mac.replace(/:/g, '').toUpperCase(); + macAddr = iface.mac.toUpperCase(); console.log(`Using MAC address as fallback device ID: ${fallbackId}`); - return fallbackId; + return { + deviceId: fallbackId, + macAddr + }; } } } @@ -55,12 +86,18 @@ export async function getDeviceId() { // 最后的fallback - 使用hostname const hostname = os.hostname(); console.log(`Using hostname as final fallback device ID: ${hostname}`); - return hostname; + return { + deviceId: hostname, + macAddr: macAddr || 'NOT_FOUND' + }; } catch (error) { - console.error('Error getting device ID:', error); + console.error('Error getting device Info:', error); // 返回一个默认的设备ID - return 'UNKNOWN_DEVICE'; + return { + deviceId: 'UNKNOWN_DEVICE', + macAddr: 'ERROR' + }; } } @@ -103,6 +140,39 @@ export function netmaskToCidr(netmask:string) { return netmaskMap[netmask] || '24'; } +// 读取保存的用户数据 +export function getConfig() { + try { + if (fs.existsSync(configPath)) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } + } catch (error) { + console.error('读取配置失败:', error); + } +} +export function setConfig(config: any) { + try { + const oldConfig = getConfig(); + const newConfig = { ...oldConfig, ...config }; + fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2)); + return true; + } catch (error) { + console.error('保存配置失败:', error); + return false; + } +} + +// export function getServerIp() { +// return getConfig().currentServerIp; +// } + +// export function setServerIp(ip: string) { +// const config = getConfig(); +// config.currentServerIp = ip; +// return setConfig(config); +// } + + /**模拟更新客户端 */ export async function simulateUpdate(event: any, url: any) { const focusedWindow = BrowserWindow.getFocusedWindow(); diff --git a/pc-fe/src/pages/components/Layout/index.tsx b/pc-fe/src/pages/components/Layout/index.tsx index a75bc5b..e12518f 100644 --- a/pc-fe/src/pages/components/Layout/index.tsx +++ b/pc-fe/src/pages/components/Layout/index.tsx @@ -1,10 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { Layout, message } from 'antd'; +import { message } from 'antd'; import { history, useLocation, Outlet } from 'umi'; import './index.less'; -const { Header, Sider, Content } = Layout; - const MainLayout: React.FC = () => { const [username, setUsername] = useState(''); const location = useLocation(); @@ -27,19 +25,26 @@ const MainLayout: React.FC = () => { // TODO: 第一次来:判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定:到配置ip/DHCP页面 setTimeout(() => { // history.push('/login'); - history.push('/configSteps?tab=terminalGetImage'); + history.push('/configSteps?tab=serverConfig'); },1000) - // const fetchDeviceId = async () => { - // try { - // const res = await window.electronAPI.invoke('get-device-id'); - // console.log('获取设备ID:', res); - // } catch (error) { - // console.error('获取设备ID失败:', error); - // } - // } - // fetchDeviceId() - }, []); + + useEffect(() => { + const fetchDeviceId = async () => { + try { + const result = await window.electronAPI.getDeviceInfo(); + if (result.success) { + console.log('获取设备信息:', JSON.stringify(result.deviceInfo)); + } else { + console.error('获取设备信息失败:', result.error); + } + } catch (error) { + console.error('获取设备信息异常:', error); + } + }; + + fetchDeviceId(); + }, []); const handleMenuClick = (key: string) => { // 使用路由导航 diff --git a/pc-fe/src/pages/components/TitleBar/index.less b/pc-fe/src/pages/components/TitleBar/index.less index 55105cd..df249ec 100644 --- a/pc-fe/src/pages/components/TitleBar/index.less +++ b/pc-fe/src/pages/components/TitleBar/index.less @@ -1,9 +1,10 @@ // src/pages/components/TitleBar/index.less .title-bar { - margin: 20px 0; - padding: 0 20px; + // margin: 20px 0; // 使用margin拖动不会起作用,需要使用padding + // height: 34px; + padding: 20px; width: 100%; - height: 34px; + height: 74px; display: flex; justify-content: space-between; align-items: center; diff --git a/pc-fe/src/pages/configSteps/components/networkConfig/index.tsx b/pc-fe/src/pages/configSteps/components/networkConfig/index.tsx index 9448696..0ab63f6 100644 --- a/pc-fe/src/pages/configSteps/components/networkConfig/index.tsx +++ b/pc-fe/src/pages/configSteps/components/networkConfig/index.tsx @@ -140,7 +140,7 @@ const NetworkConfig: React.FC = () => { if (activeTab === 'dhcp') { // 如果是 DHCP 模式,直接IPC 处理应用有线网络配置 try { - const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'dhcp' }); + const res = await window.electronAPI.applyWiredConfig({ method: 'dhcp' }); console.log('网络配置返回信息成功:', res); if(res.success){ message.success('网络配置成功'); @@ -171,7 +171,7 @@ const NetworkConfig: React.FC = () => { values.ipv6PrefixLength = parseInt(values.ipv6PrefixLength, 10); } - const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'static', ...values }); + const res = await window.electronAPI.applyWiredConfig({ method: 'static', ...values }); console.log('网络配置返回信息成功:', res); if(res.success){ message.success('网络配置成功'); diff --git a/pc-fe/src/pages/configSteps/components/serverConfig/index.tsx b/pc-fe/src/pages/configSteps/components/serverConfig/index.tsx index 1d8c132..ecaf710 100644 --- a/pc-fe/src/pages/configSteps/components/serverConfig/index.tsx +++ b/pc-fe/src/pages/configSteps/components/serverConfig/index.tsx @@ -54,7 +54,7 @@ const Index = () => { const values = await form.validateFields(); console.log('表单提交数据:', values); const { serverIp } = values; - const result = await window.electronAPI.invoke('connect-server', { serverIp }); + const result = await window.electronAPI.connectServer({ serverIp }); console.log('连接结果:', result); if (result.success) { // 连接成功,保存服务器IP到本地存储 @@ -62,7 +62,14 @@ const Index = () => { console.log('Connected server IP saved to localStorage:', serverIp); // TODO:跳转到版本更新 // setActiveTab("watchManagement") - setActiveTab("terminalGetImage") + // 通知后端连接 gRPC 服务 + const grpcResult = await window.electronAPI.connectToGrpc({ serverIp }); + if (grpcResult.success) { + // console.log('gRPC 连接成功'); + setActiveTab("terminalGetImage") + } else { + message.error(grpcResult.message || 'gRPC 连接失败'); + } // await ipcRenderer.invoke('show-login-window'); } else { message.error(result.message || '连接服务器失败'); diff --git a/pc-fe/src/pages/configSteps/components/terminalGetImage/index.tsx b/pc-fe/src/pages/configSteps/components/terminalGetImage/index.tsx index fa9966e..f89b013 100644 --- a/pc-fe/src/pages/configSteps/components/terminalGetImage/index.tsx +++ b/pc-fe/src/pages/configSteps/components/terminalGetImage/index.tsx @@ -1,43 +1,79 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import ButtonCom from '../../../components/ButtonCom'; import { useConfigStep } from '@/contexts/ConfigStepContext'; import styles from './index.less'; import { message } from 'antd'; import { history } from 'umi'; import TrueIcon from '@assets/true.png' +import WebSocketService from '../../../services/webSocketService'; +import { getDeviceInfo, getImagesList } from '../../../services/imageService'; const Index = () => { const { goToNextTab,setActiveTab } = useConfigStep(); const [imageStatus, setImageStatus] = useState<'loading' | 'success' | 'error'>('loading'); + const [isWsConnected, setIsWsConnected] = useState(false); + + const checkImageList = useCallback(async () => { + try { + // 获取设备信息 + const deviceInfo = await getDeviceInfo(); + + // 获取镜像列表 + const response = await getImagesList({ + deviceId: deviceInfo.deviceId, + macAddr: deviceInfo.macAddr + }); - // 模拟获取镜像数据的API调用 - useEffect(() => { - const fetchImageData = async () => { - try { - // 模拟API请求 - await new Promise(resolve => setTimeout(resolve, 3000)); - // 模拟随机成功或失败 - const isSuccess = Math.random() > 0.3; - setImageStatus(isSuccess ? 'success' : 'loading'); - - // 如果失败,继续轮询检查状态 - if (!isSuccess) { - const checkStatus = async () => { - // 模拟WebSocket或轮询检查 - await new Promise(resolve => setTimeout(resolve, 5000)); - setImageStatus('success'); // 模拟收到通知 - }; - checkStatus(); - } - } catch (error) { - message.error('获取镜像数据失败'); + if (response.code === 200 && response.data?.data?.length > 0) { + // 有镜像数据,直接进入下一步 + setImageStatus('success'); + } else { + // 没有镜像数据,需要监听WebSocket + await connectWebSocket(); } - }; - - fetchImageData(); + } catch (error) { + console.error('检查镜像数据失败:', error); + message.error('获取镜像数据失败'); + setImageStatus('error'); + } }, []); + const connectWebSocket = useCallback(async () => { + try { + // 连接WebSocket + const connected = await WebSocketService.connect(); + setIsWsConnected(connected); + + if (connected) { + // 监听镜像数据推送 + const removeListener = WebSocketService.addListener('2', (data:any) => { + if (data.type === '2') { + // 有镜像数据了,更新状态 + setImageStatus('success'); + // 移除监听器 + removeListener(); + } + }); + } else { + message.error('WebSocket连接失败'); + } + } catch (error) { + console.error('WebSocket连接失败:', error); + message.error('WebSocket连接失败'); + } + }, []); + + useEffect(() => { + // 组件挂载时检查镜像数据 + checkImageList(); + + // 组件卸载时清理 + return () => { + // 可以选择是否断开WebSocket连接 + }; + }, [checkImageList]); + const ImageLoadingComponent = () => (
diff --git a/pc-fe/src/pages/images/index.less b/pc-fe/src/pages/images/index.less deleted file mode 100644 index 5ef9d15..0000000 --- a/pc-fe/src/pages/images/index.less +++ /dev/null @@ -1,93 +0,0 @@ -// 页面头部样式 -.page-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; - - h2 { - margin: 0; - font-size: 24px; - font-weight: 600; - color: #333; - } - } - - // 镜像列表样式 - .image-list { - .image-detail { - .detail-item { - margin-bottom: 16px; - - label { - font-weight: 600; - color: #333; - display: inline-block; - width: 100px; - } - - span { - color: #666; - } - - p { - margin: 8px 0 0 100px; - color: #666; - line-height: 1.6; - } - } - } - } - - // 个人资料样式 - .profile-page { - .profile-content { - .profile-header { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 16px; - - .profile-info { - h3 { - margin: 0 0 4px 0; - font-size: 20px; - font-weight: 600; - color: #333; - } - - p { - margin: 0; - color: #666; - font-size: 14px; - } - } - } - - .quick-actions { - display: flex; - gap: 12px; - flex-wrap: wrap; - } - } - } - - // 响应式设计 - @media (max-width: 768px) { - .page-header { - flex-direction: column; - align-items: flex-start; - gap: 12px; - } - - .profile-content { - .profile-header { - flex-direction: column; - text-align: center; - } - - .quick-actions { - justify-content: center; - } - } - } \ No newline at end of file diff --git a/pc-fe/src/pages/images/index.tsx b/pc-fe/src/pages/images/index.tsx deleted file mode 100644 index 2aa53cf..0000000 --- a/pc-fe/src/pages/images/index.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Card, Table, Tag, Button, Space, Modal, message } from 'antd'; -import { - PlusOutlined, - EditOutlined, - DeleteOutlined, - EyeOutlined, - DownloadOutlined -} from '@ant-design/icons'; -import './index.less'; - -interface ImageItem { - id: string; - name: string; - version: string; - size: string; - status: 'active' | 'inactive' | 'building'; - createTime: string; - description: string; -} - -const ImageList: React.FC = () => { - const [images, setImages] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedImage, setSelectedImage] = useState(null); - const [detailVisible, setDetailVisible] = useState(false); - - useEffect(() => { - loadImages(); - }, []); - - const loadImages = () => { - setLoading(true); - // 模拟数据加载 - setTimeout(() => { - const mockData: ImageItem[] = [ - { - id: '1', - name: 'Windows 10 专业版', - version: 'v1.0.0', - size: '15.2 GB', - status: 'active', - createTime: '2024-01-15 10:30:00', - description: 'Windows 10 专业版镜像,包含常用办公软件' - }, - { - id: '2', - name: 'Ubuntu 22.04 LTS', - version: 'v2.1.0', - size: '8.5 GB', - status: 'active', - createTime: '2024-01-10 14:20:00', - description: 'Ubuntu 22.04 LTS 服务器版本,适用于开发环境' - }, - { - id: '3', - name: 'CentOS 8', - version: 'v1.5.0', - size: '12.1 GB', - status: 'building', - createTime: '2024-01-20 09:15:00', - description: 'CentOS 8 企业级服务器操作系统' - }, - { - id: '4', - name: 'macOS Monterey', - version: 'v1.2.0', - size: '18.7 GB', - status: 'inactive', - createTime: '2024-01-05 16:45:00', - description: 'macOS Monterey 开发环境镜像' - } - ]; - setImages(mockData); - setLoading(false); - }, 1000); - }; - - const getStatusTag = (status: string) => { - const statusMap = { - active: { color: 'green', text: '可用' }, - inactive: { color: 'red', text: '不可用' }, - building: { color: 'orange', text: '构建中' } - }; - const config = statusMap[status as keyof typeof statusMap]; - return {config.text}; - }; - - const handleViewDetail = (record: ImageItem) => { - setSelectedImage(record); - setDetailVisible(true); - }; - - const handleDownload = (record: ImageItem) => { - message.success(`开始下载镜像:${record.name}`); - }; - - const handleEdit = (record: ImageItem) => { - message.info(`编辑镜像:${record.name}`); - }; - - const handleDelete = (record: ImageItem) => { - Modal.confirm({ - title: '确认删除', - content: `确定要删除镜像 "${record.name}" 吗?`, - onOk: () => { - setImages(images.filter(img => img.id !== record.id)); - message.success('删除成功'); - } - }); - }; - - const columns = [ - { - title: '镜像名称', - dataIndex: 'name', - key: 'name', - width: 200, - }, - { - title: '版本', - dataIndex: 'version', - key: 'version', - width: 100, - }, - { - title: '大小', - dataIndex: 'size', - key: 'size', - width: 100, - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 100, - render: (status: string) => getStatusTag(status), - }, - { - title: '创建时间', - dataIndex: 'createTime', - key: 'createTime', - width: 180, - }, - { - title: '操作', - key: 'action', - width: 200, - render: (_: any, record: ImageItem) => ( - -