diff --git a/pc-fe/.umirc.ts b/pc-fe/.umirc.ts index 3ae51a7..2e6dc81 100644 --- a/pc-fe/.umirc.ts +++ b/pc-fe/.umirc.ts @@ -36,6 +36,10 @@ export default defineConfig({ path: '/', component: '@/pages/components/Layout/index', routes: [ + { + path: '/grpc', + component: '@/pages/grpc/grpc', + }, { path: '/welcome', component: '@/pages/welcome', diff --git a/pc-fe/package-lock.json b/pc-fe/package-lock.json index 012669c..5880101 100644 --- a/pc-fe/package-lock.json +++ b/pc-fe/package-lock.json @@ -10,13 +10,17 @@ "hasInstallScript": true, "dependencies": { "@ant-design/icons": "^6.0.0", + "@grpc/grpc-js": "^1.13.4", + "@grpc/proto-loader": "^0.8.0", "antd": "^5.26.6", "axios": "^1.11.0", "classnames": "^2.5.1", + "google-protobuf": "^4.0.0", "umi": "^4.0.42" }, "devDependencies": { "@tsconfig/node14": "^1.0.3", + "@types/google-protobuf": "^3.15.12", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@umijs/plugin-electron": "^0.2.0", @@ -3207,6 +3211,55 @@ "use-isomorphic-layout-effect": "^1.1.1" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3600,6 +3653,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@loadable/component": { "version": "5.15.2", "resolved": "https://registry.npmmirror.com/@loadable/component/-/component-5.15.2.tgz", @@ -4153,6 +4216,70 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@rc-component/async-validator": { "version": "5.0.4", "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz", @@ -4791,6 +4918,13 @@ "@types/node": "*" } }, + "node_modules/@types/google-protobuf": { + "version": "3.15.12", + "resolved": "https://registry.npmmirror.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", + "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -12033,6 +12167,12 @@ "license": "MIT", "peer": true }, + "node_modules/google-protobuf": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/google-protobuf/-/google-protobuf-4.0.0.tgz", + "integrity": "sha512-b8wmenhUMf2WNL+xIJ/slvD/hEE6V3nRnG86O2bzkBrMweM9gnqZE1dfXlDjibY3aXJXDNbAHepevYyQ7qWKsQ==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", @@ -14102,6 +14242,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14140,6 +14286,12 @@ "license": "MIT", "peer": true }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", @@ -16664,6 +16816,30 @@ "license": "ISC", "optional": true }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/pc-fe/package.json b/pc-fe/package.json index 194955b..d4559e9 100644 --- a/pc-fe/package.json +++ b/pc-fe/package.json @@ -17,13 +17,17 @@ "homepage": "http://10.209.8.11/users/sign_in", "dependencies": { "@ant-design/icons": "^6.0.0", + "@grpc/grpc-js": "^1.13.4", + "@grpc/proto-loader": "^0.8.0", "antd": "^5.26.6", "axios": "^1.11.0", "classnames": "^2.5.1", + "google-protobuf": "^4.0.0", "umi": "^4.0.42" }, "devDependencies": { "@tsconfig/node14": "^1.0.3", + "@types/google-protobuf": "^3.15.12", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@umijs/plugin-electron": "^0.2.0", diff --git a/pc-fe/src/main/grpc/BTGrpcClient.ts b/pc-fe/src/main/grpc/BTGrpcClient.ts new file mode 100644 index 0000000..e746c2a --- /dev/null +++ b/pc-fe/src/main/grpc/BTGrpcClient.ts @@ -0,0 +1,194 @@ +// 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 new file mode 100644 index 0000000..d06afd9 --- /dev/null +++ b/pc-fe/src/main/grpc/MockBTService.ts @@ -0,0 +1,323 @@ +// 本地测试用的 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 new file mode 100644 index 0000000..36259e0 --- /dev/null +++ b/pc-fe/src/main/grpc/README.md @@ -0,0 +1,219 @@ +梳理整个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 new file mode 100644 index 0000000..f208939 --- /dev/null +++ b/pc-fe/src/main/grpc/protos/bittorrent.proto @@ -0,0 +1,77 @@ +// 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/platform.ts b/pc-fe/src/main/ipc/platform.ts index b132aff..d1654cf 100644 --- a/pc-fe/src/main/ipc/platform.ts +++ b/pc-fe/src/main/ipc/platform.ts @@ -1,11 +1,191 @@ -import { ipcMain,app } from 'electron'; +import { ipcMain,app,BrowserWindow } from 'electron'; import { getDeviceId, getWiredConnectionName,netmaskToCidr } from '../utils/utils'; const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); +// 模拟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; // 声明一个变量来存储定时器 + const window = getBrowserWindowRuntime(); +// 初始化 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 || '重新连接失败' + }; + } +}); + + // 监听渲染进程发送的消息 ipcMain.handle('getPlatform', () => { return `hi, i'm from ${process.platform}`; @@ -27,7 +207,6 @@ ipcMain.on('exit-kiosk', () => { }); - ipcMain.handle('get-deviceid',async()=>{ const deviceId = await getDeviceId(); console.log(`Using device ID: ${deviceId}`); diff --git a/pc-fe/src/main/preload.ts b/pc-fe/src/main/preload.ts index 1042f48..d13be4b 100644 --- a/pc-fe/src/main/preload.ts +++ b/pc-fe/src/main/preload.ts @@ -1,6 +1,6 @@ import { contextBridge, ipcRenderer } from 'electron'; -// 页面调用的方法 +// 页面调用的方法:桥接前端和主进程 contextBridge.exposeInMainWorld('electronAPI', { getPlatform: async () => { return await ipcRenderer.invoke('getPlatform'); @@ -8,10 +8,25 @@ contextBridge.exposeInMainWorld('electronAPI', { closeApp: () => ipcRenderer.send('close-app'), minimizeApp: () => ipcRenderer.send('minimize-app'), exitKiosk: () => ipcRenderer.send('exit-kiosk'), + + // 新增的 gRPC API + grpcStartDownload: (config: any) => ipcRenderer.invoke('grpc-start-download', config), + grpcStopDownload: (downloadId: string) => ipcRenderer.invoke('grpc-stop-download', downloadId), + grpcCheckConnection: () => ipcRenderer.invoke('grpc-check-connection'), + // gRPC 进度监听 + onGrpcProgress: (callback: (progress: any) => void) => { + ipcRenderer.on('grpc-progress-update', (_, progress) => callback(progress)); + }, + // 移除监听器 + removeAllGrpcProgressListeners: () => { + ipcRenderer.removeAllListeners('grpc-progress-update'); + }, + // 事件监听 onMainProcessMessage: (callback: (data: string) => void) => { ipcRenderer.on('main-process-message', (_, data) => callback(data)); }, + on(...args: Parameters) { const [channel, listener] = args return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args)) diff --git a/pc-fe/src/pages/components/Layout/index.tsx b/pc-fe/src/pages/components/Layout/index.tsx index 3549248..a065a38 100644 --- a/pc-fe/src/pages/components/Layout/index.tsx +++ b/pc-fe/src/pages/components/Layout/index.tsx @@ -25,9 +25,9 @@ const MainLayout: React.FC = () => { useEffect(() => { // TODO: 第一次来:判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定:到配置ip/DHCP页面 - // setTimeout(() => { - // history.push('/configSteps'); - // },1000) + setTimeout(() => { + history.push('/grpc'); + },1000) // const fetchDeviceId = async () => { // try { // const res = await window.electronAPI.invoke('get-deviceid'); diff --git a/pc-fe/src/pages/grpc/grpc.less b/pc-fe/src/pages/grpc/grpc.less new file mode 100644 index 0000000..19b51d4 --- /dev/null +++ b/pc-fe/src/pages/grpc/grpc.less @@ -0,0 +1,15 @@ +.welcomeCon{ + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color:#fff; + .showTextCon{ + display: flex; + flex-direction: column; + justify-self: center; + align-items: center; + } +} \ No newline at end of file diff --git a/pc-fe/src/pages/grpc/grpc.tsx b/pc-fe/src/pages/grpc/grpc.tsx new file mode 100644 index 0000000..9ab2aa8 --- /dev/null +++ b/pc-fe/src/pages/grpc/grpc.tsx @@ -0,0 +1,254 @@ +// index.tsx 修改 +import React, { useState, useEffect, useCallback } from 'react'; +import styles from './grpc.less'; +// import WelcomeIcon from '../../assets/welcome-icon.jpg'; + +// 类型定义 +interface DownloadProgress { + download_id: string; + item_name?: string; + progress: number; + download_speed: number; + upload_speed: number; + eta: number; + total_size: number; + downloaded_size: number; + state: string; +} + +// declare global { +// interface Window { +// electronAPI: { +// grpcStartDownload: (config: any) => Promise; +// grpcStopDownload: (downloadId: string) => Promise; +// grpcCheckConnection: () => Promise<{ connected: boolean }>; +// onGrpcProgress: (callback: (progress: DownloadProgress) => void) => void; +// removeAllGrpcProgressListeners: () => void; +// }; +// } +// } + +const Welcome: React.FC = () => { + const [downloads, setDownloads] = useState>(new Map()); + const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting'); + + useEffect(() => { + // 检查 gRPC 连接状态 + const checkConnection = async () => { + try { + const result = await window.electronAPI.grpcCheckConnection(); + setConnectionStatus(result.connected ? 'connected' : 'disconnected'); + } catch (error) { + setConnectionStatus('disconnected'); + } + }; + + checkConnection(); + + // 设置进度监听 + 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(); + }; + }, []); + + // 在 React 组件中添加连接状态监听 + useEffect(() => { + const handleConnectionStatus = (event: any, status: any) => { + console.log('收到gRPC连接状态更新:', status); + setConnectionStatus(status.connected ? 'connected' : 'disconnected'); + console.log('gRPC连接状态:', status); + }; + + window.electronAPI.on('grpc-connection-status', handleConnectionStatus); + + return () => { + window.electronAPI.off('grpc-connection-status', handleConnectionStatus); + }; + }, []); + + const handleStartDownload = useCallback(async () => { + try { + // 根据时间生成不同的文件名,便于测试不同大小的文件 + const fileName = `测试文件-${Date.now()}`; + const result = await window.electronAPI.grpcStartDownload({ + torrentUrl: 'magnet:?xt=urn:btih:EXAMPLEHASH123456789', + itemName: fileName, + itemId: `test-${Date.now()}` + }); + + if (result.success) { + console.log('下载开始成功:', result.data); + } else { + console.error('下载开始失败:', result.error); + } + } catch (error) { + console.error('调用下载失败:', error); + } + }, []); + + const handleRefreshDownloads = useCallback(async () => { + try { + // 这里可以调用一个获取当前下载列表的API(如果有的话) + console.log('当前下载任务:', downloads); + } catch (error) { + console.error('刷新下载列表失败:', error); + } + }, [downloads]); + + // 添加一个测试大文件下载的函数 + const handleStartLargeDownload = useCallback(async () => { + try { + // 发起IPC调用 + const result = await window.electronAPI.grpcStartDownload({ + torrentUrl: 'magnet:?xt=urn:btih:EXAMPLEHASH123456789', + itemName: 'large-大文件测试', + itemId: `large-test-${Date.now()}` + }); + + if (result.success) { + console.log('大文件下载开始成功:', result.data); + } else { + console.error('大文件下载开始失败:', result.error); + } + } catch (error) { + console.error('调用大文件下载失败:', error); + } + }, []); + + const handleReconnect = useCallback(async () => { + try { + console.log('尝试重新连接 gRPC...'); + const result = await window.electronAPI.invoke('grpc-reconnect'); + + if (result.success) { + console.log('重新连接成功:', result.message); + } else { + console.error('重新连接失败:', result.error); + } + } catch (error) { + console.error('重新连接异常:', error); + } + }, []); + + const renderConnectionStatus = () => { + switch (connectionStatus) { + case 'connecting': + return ● 连接中...; + case 'connected': + return ● 已连接; + case 'disconnected': + return ● 未连接; + default: + return ● 未知状态; + } + }; + + return ( +
+
+ {/* 连接状态显示 */} +
+ 后端连接状态: {renderConnectionStatus()} +
+ + {/* 下载测试按钮 */} +
+ + + + + +
+ + {/* 显示当前下载数量 */} +
+ 当前下载任务数: {downloads.size} +
+ + {/* 下载列表显示 */} + {Array.from(downloads.entries()).map(([id, progress]) => ( +
+
{progress.download_id}
+
文件名: {progress.item_name || '未知'}
+
进度: {progress.progress.toFixed(1)}%
+
状态: {progress.state}
+
速度: {(progress.download_speed / 1024 / 1024).toFixed(2)} MB/s
+
已下载: {(progress.downloaded_size / 1024 / 1024).toFixed(2)} MB
+
总大小: {(progress.total_size / 1024 / 1024).toFixed(2)} MB
+
剩余时间: {Math.ceil(progress.eta / 60)} 分钟
+
+ ))} +
+
+ ); +}; + +export default Welcome; \ No newline at end of file