diff --git a/.env b/.env index 3a4679a..0dce20f 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ +IS_DEBUG="true" GRPC_ENDPOINT="10.66.66.234:9090" COSMOS_ENDPOINT="http://10.66.66.234:26657" -NODE_SECRET="aHVnZSBjb21wYW55IHBob25lIHdlc3QgcGxhY2Ugc2VtaW5hciBtaXJhY2xlIGxlbmQgbWFuZGF0ZSB0aGVuIGFkanVzdCBxdWl0IG1lYXQgY2hlYXAgbm9vZGxlIGNvdXBsZSBkZWZpbmUgbXVzY2xlIHB1bHNlIHNpc3RlciBwaWVjZSBkZXZpY2UgcHJpdmF0ZSBob29k" -IS_DEBUG="true" -ACCOUNT_NAME="de1" +ACCOUNT_NAME="ccvdexre" +NODE_SECRET= "initial typical business width enforce buddy magic country piano head cable blossom gate caught disagree pepper moral pair vessel protect mixture deposit artwork liquid" VIET_EVENTS_URL="ws://10.66.66.234:8080/events" VITE_BASE_URL="http://10.66.66.234:6060" VITE_BLOCK_URL="http://10.66.66.234:1317" \ No newline at end of file diff --git a/package.json b/package.json index 043376e..6cfda15 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@tanstack/react-table": "^8.20.6", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-fs": "~2.2.1", "@tauri-apps/plugin-global-shortcut": "~2", "@tauri-apps/plugin-http": "~2.2.0", "@tauri-apps/plugin-os": "~2", @@ -61,6 +62,7 @@ "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "uuid": "^11.0.4", + "xlsx": "^0.18.5", "zod": "^3.24.1", "zustand": "^5.0.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb639db..8e127a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@tauri-apps/plugin-dialog': specifier: ~2 version: 2.2.0 + '@tauri-apps/plugin-fs': + specifier: ~2.2.1 + version: 2.2.1 '@tauri-apps/plugin-global-shortcut': specifier: ~2 version: 2.2.0 @@ -158,6 +161,9 @@ importers: uuid: specifier: ^11.0.4 version: 11.0.5 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 zod: specifier: ^3.24.1 version: 3.24.1 @@ -1523,6 +1529,9 @@ packages: '@tauri-apps/plugin-dialog@2.2.0': resolution: {integrity: sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==} + '@tauri-apps/plugin-fs@2.2.1': + resolution: {integrity: sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==} + '@tauri-apps/plugin-global-shortcut@2.2.0': resolution: {integrity: sha512-clI9Bg/BcxWXNDK+ij601o1qC2WxMEy8ovhGgEW5Ai17oPy0KK8uwzmc59KiVnOYKpBWHCUPqBxG+KBNUFXgzw==} @@ -1606,6 +1615,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + ahooks@3.8.4: resolution: {integrity: sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg==} engines: {node: '>=8.0.0'} @@ -1701,6 +1714,10 @@ packages: caniuse-lite@1.0.30001684: resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + chalk@4.1.1: resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} engines: {node: '>=10'} @@ -1735,6 +1752,10 @@ packages: code-inspector-plugin@0.19.1: resolution: {integrity: sha512-WtgGT3Ky5QUtv34eEXf57oIa15yLseVTtV1ngELxHgpg46ebjvaROpGOLtOEtyu0EB+RhLJ2Mj6hEnLe3UCcLg==} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1771,6 +1792,11 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1883,6 +1909,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -2675,6 +2705,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} @@ -2885,6 +2919,14 @@ packages: engines: {node: '>= 8'} hasBin: true + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2893,6 +2935,11 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4151,6 +4198,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.1.1 + '@tauri-apps/plugin-fs@2.2.1': + dependencies: + '@tauri-apps/api': 2.1.1 + '@tauri-apps/plugin-global-shortcut@2.2.0': dependencies: '@tauri-apps/api': 2.1.1 @@ -4257,6 +4308,8 @@ snapshots: acorn@8.14.0: {} + adler-32@1.3.1: {} + ahooks@3.8.4(react@18.3.1): dependencies: '@babel/runtime': 7.26.0 @@ -4398,6 +4451,11 @@ snapshots: caniuse-lite@1.0.30001684: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chalk@4.1.1: dependencies: ansi-styles: 4.3.0 @@ -4458,6 +4516,8 @@ snapshots: transitivePeerDependencies: - supports-color + codepage@1.15.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4487,6 +4547,8 @@ snapshots: optionalDependencies: typescript: 5.7.2 + crc-32@1.2.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4605,6 +4667,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + frac@1.1.2: {} + fraction.js@4.3.7: {} fsevents@2.3.3: @@ -5424,6 +5488,10 @@ snapshots: source-map-js@1.2.1: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + string-convert@0.2.1: {} string-width@4.2.3: @@ -5644,6 +5712,10 @@ snapshots: dependencies: isexe: 2.0.0 + wmf@1.0.2: {} + + word@0.3.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -5656,6 +5728,16 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + yallist@3.1.1: {} yaml@2.6.1: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 257ddc3..d40dcfe 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2960,6 +2960,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-global-shortcut", "tauri-plugin-http", "tauri-plugin-os", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 470427a..fc88599 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -69,6 +69,7 @@ tauri-plugin-process = "2" tauri-plugin-http = "2" tauri-plugin-store = "2" tauri-plugin-websocket = "2" +tauri-plugin-fs = "2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 9b68054..d787aa7 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,7 +2,9 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:default", "core:window:allow-close", @@ -55,6 +57,16 @@ }, "store:default", "websocket:default", - "http:default" + "http:default", + "fs:default", + { + "identifier": "fs:allow-exists", + "allow": [ + { + "path": "$APPDATA/*" + } + ] + }, + "fs:default" ] -} +} \ No newline at end of file diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 7d2d74f..dc2a36c 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -36,6 +36,14 @@ pub async fn get_nodes(app: AppHandle) -> CommandResult> cmd_result(core::api::get_nodes(config).await) } +/// 核心:获取节点列表,可测试核心是否可用 +#[tauri::command] +#[specta::specta] +pub async fn get_nodes_update(app: AppHandle) -> CommandResult> { + let config = cmd_result(get_config(&app))?; + cmd_result(core::api::get_nodes_update(config).await) +} + /// 核心:开启代理 #[tauri::command] #[specta::specta] diff --git a/src-tauri/src/core/api.rs b/src-tauri/src/core/api.rs index 4c5ffa0..0a928e1 100644 --- a/src-tauri/src/core/api.rs +++ b/src-tauri/src/core/api.rs @@ -12,7 +12,6 @@ use paw_common::preclude::{CoreConfig, CoreResponse}; #[derive(Debug, Clone, Deserialize, Serialize, specta::Type)] pub struct ProxyNodeInfo { pub name: String, - pub ip: String, pub country_code: String, pub country_name: String, pub country_name_zh: String, @@ -60,6 +59,28 @@ pub fn config_header(config: CoreConfig) -> reqwest::header::HeaderMap { headers } +/// 获取所有可用节点信息,可以用来当作测试健康度的函数 +/// +/// get /nodes +pub async fn get_nodes_update(config: CoreConfig) -> Result> { + let client = reqwest::Client::new(); + let response = client + .get(format!("{}/nodes?update=true", config.core_api_url())) + .headers(config_header(config)) + .timeout(Duration::from_millis(100)) // 由于正常情况下,节点获取速度很快,而且是本地API,为了加快检查核心是否正常,设置较短的超时 + .send() + .await?; + + if response.status().is_success() { + let result: GetNodesResponse = response.json().await?; + debug!("Successfully got nodes: {:?}", result.data); + Ok(result.data) + } else { + debug!("Failed to get nodes: {}", response.text().await?); + bail!("Failed to get nodes") + } +} + /// 获取所有可用节点信息,可以用来当作测试健康度的函数 /// /// get /nodes diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 611be46..95cd9b1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,7 @@ pub fn run() -> Result<()> { // 在这里注册所有需要用的 command let builder = Builder::::new().commands(collect_commands![ cmds::get_nodes, + cmds::get_nodes_update, cmds::enable_proxy, cmds::disable_proxy, cmds::select_node, @@ -54,6 +55,7 @@ pub fn run() -> Result<()> { // 构建 tauri 程序 tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_websocket::init()) .plugin(tauri_plugin_http::init()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8f2f3b8..be0383e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -5,7 +5,6 @@ "build": { "beforeDevCommand": "pnpm web:dev", "devUrl": "http://127.0.0.1:1420", - "beforeBuildCommand": "pnpm web:build", "frontendDist": "../dist" }, "app": { diff --git a/src/App.tsx b/src/App.tsx index df43b40..c57bb6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,11 @@ +import { useEffect, useRef } from "react"; import dayjs from "dayjs"; import { useStartupCheck } from "@/hooks/useStartupCheck"; import { usePreventDefault } from "@/hooks/usePreventDefaultEventListener"; import { useGlobalShortcut } from "@/hooks/useGlobalShortcut"; import { useRecreateTheCircuit } from "@/hooks/useRecreateTheCircuit"; import { useCoreConfig } from "@/hooks/useCoreConfig"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { setMaliciousNodeList, setNodeDownList, @@ -12,16 +13,13 @@ import { setWeb3List2, } from "@/store/web3Slice"; import type { AppDispatch } from "@/store"; - import eventBus, { eventTypes } from "@/utils/eventBus"; -import { WebSocketClient } from "@/utils/webSocketClient"; +import { WebSocketClient } from "@/utils/webSocketClientV2"; import { blockChainApi } from "@/api/block"; import { getRandomCountryKey } from "@/data"; - import Titlebar from "@/components/Titlebar"; import Layout from "@/layout"; import Tray from "@/components/Tray"; -import { api } from "./utils/api"; function App() { // 执行启动自检 @@ -35,178 +33,289 @@ function App() { // 读取配置,若文件不存在则持久化到本地 `%APPDATA%/com.paw.paw-gui` const { loadCoreConfig } = useCoreConfig(); loadCoreConfig(); + const dispatch = useDispatch(); - // const { } = useSelector( - // (state: RootState) => state.web3Reducer - // ); - let eventsWs: WebSocketClient | null = null; - const openWsTraffic = async () => { - if (eventsWs) return; - eventsWs = new WebSocketClient("ws://10.66.66.234:8080/events", {}); - console.log(eventsWs, "openWsTraffic Start"); - // 执行 WebSocket 操作 - await eventsWs?.connect(); - eventsWs?.addListener((msg: any) => { - try { - const msgData = msg ? JSON.parse(msg.data) : {}; - if (msgData.code === 0) { - // console.log(msgData, "msgDatamsgData"); - switch (msgData.event) { - case eventTypes.NODE_UP: - console.log("节点上线"); - break; - case eventTypes.NODE_DOWN: - // 添加下线节点到store 里面 - if (msgData.data.name) { - // 获取一个随机的国家code - const countryCode = getRandomCountryKey(); - dispatch( - setNodeDownList({ - name: msgData.data.name, - code: countryCode, - }) - ); - } - console.log("节点下线"); - break; - case eventTypes.MALICIOUS_NODE: - // 添加恶意节点到store 里面 - if (msgData.data.name) { - // 获取一个随机的国家code - const countryCode = getRandomCountryKey(); - dispatch( - setMaliciousNodeList({ - name: msgData.data.name, - code: countryCode, - }) - ); - } - console.log("检测到恶意节点"); - break; - case eventTypes.NODE_INIT_COMPLATE: - console.log("节点预配置完成"); - break; - case eventTypes.NODE_REMOVE: - console.log("节点清除"); - break; - case eventTypes.NODE_ADD: - console.log("添加节点"); - break; - case eventTypes.NODE_INIT: - console.log("节点预配置"); - break; - default: - break; - } + const eventsWsRef = useRef(null); + const blockchainTimerRef = useRef(null); + const wsInitializedRef = useRef(false); + + // 处理 WebSocket 消息 + const handleWebSocketMessage = (msg: any) => { + try { + const msgData = msg ? JSON.parse(msg.data) : {}; + console.log("Received WebSocket message:", msgData); + + if (msgData?.code === 0) { + switch (msgData.event) { + case eventTypes.NODE_UP: + console.log("节点上线"); + break; + + case eventTypes.NODE_DOWN: + // 添加下线节点到store 里面 + if (msgData.data.name) { + // 获取一个随机的国家code + const countryCode = getRandomCountryKey(); + dispatch( + setNodeDownList({ + name: msgData.data.name, + code: countryCode, + }) + ); + } + console.log("节点下线"); + break; + + case eventTypes.MALICIOUS_NODE: + // 添加恶意节点到store 里面 + if (msgData.data.name) { + // 获取一个随机的国家code + const countryCode = getRandomCountryKey(); + dispatch( + setMaliciousNodeList({ + name: msgData.data.name, + code: countryCode, + }) + ); + } + console.log("检测到恶意节点"); + break; + + case eventTypes.NODE_INIT_COMPLATE: + if (eventsWsRef.current?.isWebSocketConnected()) { + eventsWsRef.current.sendMessage( + JSON.stringify({ + ...msgData, + event: eventTypes.NODE_ADD, + }) + ); + console.log("节点预配置完成:", { + ...msgData, + event: eventTypes.NODE_ADD, + }); + } + break; + + case eventTypes.NODE_REMOVE: + console.log("节点清除"); + break; + + case eventTypes.NODE_ADD: + console.log("添加节点"); + break; + + default: + break; } - } catch (error) { - console.log(error, "error"); } - }); - console.log(eventsWs, "openWsTraffic End"); - }; - - const createdSoketEventBus = async () => { - eventBus.on(eventTypes.NODE_INIT_COMPLATE, (data: any) => { - console.log("节点预配置完成", data); - eventsWs?.sendMessage(data); - }); - eventBus.on(eventTypes.NODE_REMOVE, (data: any) => { - console.log("节点清除"); - const timestamp = dayjs().unix(); - const params = { - code: 0, - event: eventTypes.NODE_REMOVE, - data: { - name: data, - }, - timestamp, - }; - console.log(JSON.stringify(params), "节点清除 params"); - eventsWs?.sendMessage(JSON.stringify(params)); - }); - }; - - const closeWsTraffic = async () => { - if (eventsWs) { - await eventsWs.disconnect(); - eventsWs = null; + } catch (error) { + console.error("Error processing WebSocket message:", error); } }; - const removeSoketEventBus = () => { - eventBus.off(eventTypes.NODE_INIT_COMPLATE); + // 初始化 WebSocket 连接 + const initWebSocket = async () => { + if (eventsWsRef.current) return; + + console.log("Initializing WebSocket connection..."); + + eventsWsRef.current = new WebSocketClient( + "ws://10.66.66.234:8080/events", + {}, + { + autoReconnect: true, + maxReconnectAttempts: 10, + reconnectDelay: 2000, + } + ); + + const connected = await eventsWsRef.current.connect(); + + if (connected) { + console.log("WebSocket connected successfully"); + eventsWsRef.current.addListener(handleWebSocketMessage); + wsInitializedRef.current = true; + } else { + console.error("Failed to establish WebSocket connection"); + } + }; + + // 设置事件总线监听器 + const setupEventBusListeners = () => { + console.log("执行了没") + // 添加节点 + eventBus.on(eventTypes.NODE_ADD, (data: any) => { + console.log("添加节点", data); + + if (eventsWsRef.current?.isWebSocketConnected()) { + const timestamp = dayjs().unix(); + const params = { + code: 0, + event: eventTypes.NODE_ADD, + data: data, + timestamp, + }; + eventsWsRef.current.sendMessage(JSON.stringify(params)); + } else { + console.warn("WebSocket not connected, can't send NODE_ADD message"); + } + }); + + // 节点预配置事件 + eventBus.on(eventTypes.NODE_INIT, (data: any) => { + console.log("节点预配置", data); + + if (eventsWsRef.current?.isWebSocketConnected()) { + const timestamp = dayjs().unix(); + const params = { + code: 0, + event: "node_init", + data: {}, + timestamp: timestamp, + }; + eventsWsRef.current.sendMessage(JSON.stringify(params)); + } else { + console.warn("WebSocket not connected, can't send NODE_INIT message"); + } + }); + + // 节点清除事件 + eventBus.on(eventTypes.NODE_REMOVE, (data: any) => { + console.log("节点清除", data); + + if (eventsWsRef.current?.isWebSocketConnected()) { + const timestamp = dayjs().unix(); + const params = { + code: 0, + event: eventTypes.NODE_REMOVE, + data: { + name: data, + }, + timestamp, + }; + eventsWsRef.current.sendMessage(JSON.stringify(params)); + } else { + console.warn("WebSocket not connected, can't send NODE_REMOVE message"); + } + }); + }; + + // 清理事件总线监听器 + const cleanupEventBusListeners = () => { + eventBus.off(eventTypes.NODE_INIT); eventBus.off(eventTypes.NODE_REMOVE); }; - const timer = useRef(null); + // 启动区块链数据轮询 + const startBlockchainPolling = () => { + // 立即执行一次 + fetchBlockchainData(); - const initWebsocketsAndEventBus = async () => { - await openWsTraffic(); - await createdSoketEventBus(); - timer.current = setInterval(() => { - blockChainApi.getLatestBlock().then((res) => { - blockChainApi - .getTxsByBlock(res.data.block.last_commit.height) - .then((res) => { - const height = res.data.block.last_commit.height; - const timer = res.data.block.header.time; - const txs = res.data.txs; - const balance = res.data.txs.reduce((acc: any, item: any) => { - const blance = - item.auth_info.fee.gas_limit * - Number(blockChainApi.blockConfig.minimum_gas_price); - return acc + blance; - }, 0) - const item = { - height: height, - txs: txs, - timerstamp: dayjs(timer).format("HH:mm:ss"), - balance, - // 保留三位小数 - balanceToFixed: Number(balance).toFixed(3), - }; - // const timerstamp = dayjs().unix(); - dispatch(setWeb3List(item)); - console.log(item, "getTxsByBlock"); - }); - }); - }, 5000); + // 设置定时器定期获取数据 + blockchainTimerRef.current = setInterval(fetchBlockchainData, 5000); + }; + + // 获取区块链数据 + const fetchBlockchainData = async () => { + try { + const blockRes = await blockChainApi.getLatestBlock(); + const height = blockRes.data.block.last_commit.height; + + const txsRes = await blockChainApi.getTxsByBlock(height); + const timer = txsRes.data.block.header.time; + const txs = txsRes.data.txs; + + const balance = txs.reduce((acc: number, item: any) => { + const blance = + item.auth_info.fee.gas_limit * + Number(blockChainApi.blockConfig.minimum_gas_price); + return acc + blance; + }, 0); + + const item = { + height: height, + txs: txs, + timerstamp: dayjs(timer).format("HH:mm:ss"), + balance, + balanceToFixed: Number(balance).toFixed(3), + }; + + dispatch(setWeb3List(item)); + } catch (error) { + console.error("Error fetching blockchain data:", error); + } + }; + + // 初始化所有服务 + const initializeServices = async () => { + // 初始化示例数据 + dispatch( + setWeb3List2([ + { + id: "6", + name: "Cardano Wallet", + payType: "ADA", + status: "active", + createdAt: 1737436420, + upDatedAt: 5, + balance: "65", + address: + "addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f", + privateKey: + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + publicKey: + "addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f", + numberTransactions: "35", + transactions: 1, + txs: [], + height: 1204, + timerstamp: dayjs().format("HH:mm:ss"), + balanceToFixed: 0, + }, + ]) + ); + + // 初始化 WebSocket + await initWebSocket(); + + // 设置事件总线 + setupEventBusListeners(); + + // 启动区块链数据轮询 + startBlockchainPolling(); + }; + + // 清理所有资源 + const cleanupServices = () => { + // 清理区块链数据轮询 + if (blockchainTimerRef.current) { + clearInterval(blockchainTimerRef.current); + blockchainTimerRef.current = null; + } + + // 清理事件总线 + cleanupEventBusListeners(); + + // 关闭 WebSocket 连接 + if (eventsWsRef.current) { + eventsWsRef.current.disconnect(); + eventsWsRef.current = null; + } + + wsInitializedRef.current = false; }; useEffect(() => { - dispatch(setWeb3List2([ - { - id: "6", - name: "Cardano Wallet", - payType: "ADA", - status: "active", - createdAt: 1737436420, - upDatedAt: 5, - balance: "65", - address: - "addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f", - privateKey: - "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", - publicKey: - "addr1qxck6ztj8lrxd0j2jz8f7tznzfu9wqv9qrplrh3r9eq8g9n0n3anjy2a4x54kd2sort3qvnc7mct82krlnpnxvl7v3sxmrv3f", - numberTransactions: "35", - transactions: 1, - txs: [], - height: 0, - timerstamp: dayjs().format("HH:mm:ss"), - balanceToFixed: 0, - }, - ])) - setTimeout(() => { - initWebsocketsAndEventBus(); + // 延迟初始化以确保组件完全挂载 + const initTimer = setTimeout(() => { + initializeServices(); }, 1000); + + // 组件卸载时清理资源 return () => { - if (timer.current) { - clearInterval(timer.current); - } - closeWsTraffic(); - removeSoketEventBus(); + clearTimeout(initTimer); + cleanupServices(); + console.log("App component unmounted, all services cleaned up"); }; }, []); diff --git a/src/api/block.ts b/src/api/block.ts index bd88617..513b720 100644 --- a/src/api/block.ts +++ b/src/api/block.ts @@ -32,7 +32,7 @@ export class BlockChainApi { const res = await api.get(this.baseUrl + "/cosmos/base/node/v1beta1/config"); this.blockConfig = { halt_height: res.data.halt_height, - minimum_gas_price: res.data.minimum_gas_price || 0.0001, + minimum_gas_price: res.data.minimum_gas_price || 0.00005, pruning_interval: res.data.pruning_interval, pruning_keep_recent: res.data.pruning_keep_recent, } diff --git a/src/bindings.ts b/src/bindings.ts index c16884a..90d05ec 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -20,6 +20,17 @@ async getNodes() : Promise> { else return { status: "error", error: e as any }; } }, +/** + * 核心:获取节点列表,可测试核心是否可用 + */ +async getNodesUpdate() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_nodes_update") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, /** * 核心:开启代理 */ @@ -209,7 +220,7 @@ export type CoreConfig = { socks_port: number; socks_user: string; socks_pass: s /** * 节点信息 */ -export type ProxyNodeInfo = { name: string; ip: string; country_code: string; country_name: string; country_name_zh: string; city_name: string; city_name_zh: string; delay: number; download: number; upload: number; survive_score: number; exit: boolean; use: boolean } +export type ProxyNodeInfo = { name: string; country_code: string; country_name: string; country_name_zh: string; city_name: string; city_name_zh: string; delay: number; download: number; upload: number; survive_score: number; exit: boolean; use: boolean } /** tauri-specta globals **/ diff --git a/src/hooks/useStartupCheck.ts b/src/hooks/useStartupCheck.ts index 5c75b29..e8c611d 100644 --- a/src/hooks/useStartupCheck.ts +++ b/src/hooks/useStartupCheck.ts @@ -180,7 +180,7 @@ export function useStartupCheck() { try { const [versionResult, nodesResult] = await Promise.all([ commands.getServiceVersion(), - commands.getNodes(), + commands.getNodesUpdate(), ]); const suitableServiceVersion = await app.getVersion(); diff --git a/src/layout/index.tsx b/src/layout/index.tsx index e75bd8e..c6e0c93 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -37,11 +37,11 @@ export default function Layout() { title: "面向溯源对抗的数据转发", icon: , }, - // { - // id: 'proxies', - // title: '节点池', - // icon: , - // }, + { + id: 'proxies', + title: '节点池', + icon: , + }, ]; const handleClickMenu = (index: number) => { diff --git a/src/pages/anti-forensics-forwarding/components/ClearNodeDialog/index.scss b/src/pages/anti-forensics-forwarding/components/ClearNodeDialog/index.scss deleted file mode 100644 index 593b011..0000000 --- a/src/pages/anti-forensics-forwarding/components/ClearNodeDialog/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -.linkAdd_ComboxContent{ - width: 800px !important; - margin-top: 18px; -} diff --git a/src/pages/anti-forensics-forwarding/components/ClearNodeDialog/index.tsx b/src/pages/anti-forensics-forwarding/components/ClearNodeDialog/index.tsx deleted file mode 100644 index cce0d08..0000000 --- a/src/pages/anti-forensics-forwarding/components/ClearNodeDialog/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { FormInstance } from "antd"; - -import { FormDialog } from "@/components/FormDialog"; -import { useDispatch, useSelector } from "react-redux"; - -import "./index.scss"; -import { EllipsisTooltip } from "@/components/Encapsulation"; -import NotFailNodeIcon from "@/assets/svg/common/not-fail-node.svg?react"; -import NotWarningNodeIcon from "@/assets/svg/common/not-warning-node.svg?react"; -import { NODEDIALOGTYPE } from "../../index"; -import { cn, getUrl } from "@/lib/utils"; -import eventBus, { eventTypes } from "@/utils/eventBus"; - -import { AppDispatch, RootState } from "@/store"; -import { removeMaliciousNodeList, removeNodeDownList } from "@/store/web3Slice"; -import { countryCodeMap } from "@/data"; - -export interface DialogConfig { - title: string; - desc: string; - successText: string; -} - -export const ProxyItem: React.FC<{ proxyInfo: any; clasName?: string }> = ( - props -) => { - const { code, exit = false } = props.proxyInfo; - return ( -
-
-
-
- -
- -
-
-
- ); -}; - -export const ClearNodeDialog = ({ - open, - setOpen, - successHandle, - dialogLoading, - form, - type, - canSubmit, -}: { - open: boolean; - setOpen: (open: boolean) => void; - successHandle: () => void; - dialogLoading: boolean; - canSubmit: boolean; - form: FormInstance; - type: DialogConfig; -}) => { - const dispatch = useDispatch(); - const { maliciousNodeList, nodeDownList } = useSelector( - (state: RootState) => state.web3Reducer - ); - const [isClear, setIsClear] = useState(false); - const showDialog = (open: boolean) => { - setOpen(open); - }; - const onSuccessHandle = () => { - successHandle(); - setIsClear(true); - if (type.title === NODEDIALOGTYPE.ClearFailNode.title) { - nodeDownList.forEach((item) => { - dispatch(removeNodeDownList(item.name)); - eventBus.emit(eventTypes.NODE_REMOVE, item.name); - }); - } else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) { - maliciousNodeList.forEach((item) => { - dispatch(removeMaliciousNodeList(item.name)); - eventBus.emit(eventTypes.NODE_REMOVE, item.name); - }); - } - // showDialog(false); - }; - - const proxyList = useMemo(() => { - if (type.title === NODEDIALOGTYPE.ClearFailNode.title) { - return nodeDownList; - } else { - return maliciousNodeList; - } - }, [nodeDownList, maliciousNodeList, type.title]); - - useEffect(() => { - if (!open) { - setIsClear(false); - } - }, [open]); - - return ( - -
- {proxyList.length > 0 ? ( - proxyList - .filter((item: any) => item?.name) - .map((item: any) => { - return ; - }) - ) : ( -
- {type.title === NODEDIALOGTYPE.ClearFailNode.title ? ( - - ) : ( - - )} - -
- {type.title === NODEDIALOGTYPE.ClearFailNode.title - ? "暂无掉线节点" - : "暂无恶意节点"} -
-
- )} -
-
- ); -}; diff --git a/src/pages/anti-forensics-forwarding/components/FormAlertDialog/index.scss b/src/pages/anti-forensics-forwarding/components/FormAlertDialog/index.scss deleted file mode 100644 index 593b011..0000000 --- a/src/pages/anti-forensics-forwarding/components/FormAlertDialog/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -.linkAdd_ComboxContent{ - width: 800px !important; - margin-top: 18px; -} diff --git a/src/pages/anti-forensics-forwarding/components/FormAlertDialog/index.tsx b/src/pages/anti-forensics-forwarding/components/FormAlertDialog/index.tsx deleted file mode 100644 index 577a903..0000000 --- a/src/pages/anti-forensics-forwarding/components/FormAlertDialog/index.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { Form, FormInstance } from "antd"; - -import { Input } from "@/components/ui/input"; -import { FormDialog } from "@/components/FormDialog"; -import { countryCodeMap } from "@/data"; -import { DIALOGTYPE } from "../../index"; - -import "./index.scss"; -import { DefaultLink } from "./pathChoose"; -import { Button } from "@/components/ui/button"; - -export interface DialogConfig { - title: string; - desc: string; - successText: string; -} - -const PledgeAmount = ({ - value, - onChange, -}: { - value?: string; - onChange?: (data: string) => void; -}) => { - return ( -
- { - onChange?.(e.target.value); - }} - /> - SOL -
- ); -}; -const IpPortInput = ({ - value, - onChange, -}: { - value?: { port: string; ip: string }; - onChange?: (data: { port: string; ip: string }) => void; -}) => { - const [ipAndPort, setIpAndPort] = useState<{ - port: string; - ip: string; - }>(value ? value : { port: "", ip: "" }); - const handleChagne = ({ - key, - val, - }: { - key: "port" | "ip"; - val: string; - }) => { - const newValue = value ? { ...value } : { port: "", ip: "" }; - newValue[key] = val; - onChange?.(newValue); - }; - useEffect(() => { - if (value) { - setIpAndPort(value); - } - }, [value]); - return ( -
- { - handleChagne?.({ key: "ip", val: e.target.value }); - }} - /> - { - handleChagne?.({ key: "port", val: e.target.value }); - }} - /> -
- ); -}; - -const NodeForm = ({ form }: { form: FormInstance }) => { - return ( -
- {/* -
uid
-
*/} - - - - - - - - - - - - - - - -
- ); -}; - -const NetworkForm = ({ form }: { form: FormInstance }) => { - return ( -
- - key)} - /> - - - key)} - /> - -
- ); -}; - -export const FormAlertDialog = ({ - open, - setOpen, - successHandle, - dialogLoading, - form, - type, - canSubmit, - handleSelectFile, -}: { - open: boolean; - setOpen: (open: boolean) => void; - successHandle: () => void; - dialogLoading: boolean; - canSubmit: boolean; - form: FormInstance; - type: DialogConfig; - handleSelectFile: () => void; -}) => { - const showDialog = (open: boolean) => { - setOpen(open); - }; - - - return ( - - - {open && type.title === DIALOGTYPE.ADDNode.title ? ( - - ) : ( - - )} - - ); -}; diff --git a/src/pages/anti-forensics-forwarding/components/FormAlertDialog/pathChoose.tsx b/src/pages/anti-forensics-forwarding/components/FormAlertDialog/pathChoose.tsx deleted file mode 100644 index 9b899b3..0000000 --- a/src/pages/anti-forensics-forwarding/components/FormAlertDialog/pathChoose.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useMemo, useState, useEffect } from 'react' -// import LinkButtonCol from "@/assets/svg/LinkButtonCol.svg?react"; -import Gurad from '@/assets/svg/ProxiesGuard.svg?react' -import Exit from '@/assets/svg/Exit.svg?react' -import Down from '@/assets/svg/down.svg?react' -import HiddenNode from '@/assets/svg/HiddenNode.svg?react' -import InletNodeSvg from '@/assets/svg/InletNode.svg?react' -// import EntryNode from "@/assets/svg/EntryNode.svg?react"; - -import { cn, getUrl } from '@/lib/utils' -// import { useProxiesExit, useProxiesGuard } from "@/api/link"; -// import { Button } from "@/components/ui/button"; -import { Combobox, IComboboxValue } from '@/components/Combobox' -// import { ProxiesList } from "@/pages/home/types"; -import { SearchOptionText } from '@/pages/home/components/MultipleLinks' -import { countryCodeMap } from '@/pages/home/data' - -export const DefaultLink = ({ - value, - flag = '', - className, - des, - type = 'hidden', - disabled, - countries, - isChecked = true, - onChange, -}: { - value?: string - des: string - flag?: string - countries: any[] - className?: string - type?: string - disabled?: boolean - isChecked?: boolean - onChange?: (value: IComboboxValue) => void -}) => { - // const ingressName = countryCodeMap[flag.toUpperCase()] - - const [ingressName, setIngressName] = useState(des ?? '') - const [ingressOptions, setIngressOptions] = useState< - { - label: string - value: string - icon: JSX.Element - num: number - }[] - >([]) - - const getOptions = () => { - const options = countries.map((item) => { - const name = countryCodeMap[item.toUpperCase()] - return { - label: name, - value: item, - icon: ( -
- -
- ), - num: 0, - } - }) - setIngressOptions(options) - } - - useEffect(() => { - // type === 'hidden' && - isChecked && setIngressName(countryCodeMap[flag.toUpperCase()] ?? des) - getOptions() - }, [flag]) - - useEffect(() => { - if (value) { - setIngressName(value) - } - }, [value]) - - return ( -
- { - onChange?.(e) - }} - title="输入国家名称" - radioText={SearchOptionText} - downIcon={} - options={ingressOptions} - placeholder={ - type === 'entry' - ? '入口节点' - : type === 'hidden' - ? '隐匿节点' - : '出口节点' - } - muiltip={false} - contentClass="w-full p-2 pt-0 linkAdd_ComboxContent my-[4px]" - contentProps={{ - alignOffset: 0, - }} - disabled={disabled} - comboxTitle={} - /> -
- ) -} - -export const ComboxTitle = ({ type }: { type: string }) => { - const getNode = useMemo(() => { - switch (type) { - case 'guard': - return ( - <> - - - 守卫节点 - - - ) - case 'hidden': - return ( - <> - - - 隐匿节点 - - - ) - case 'entry': - return ( - <> - - - 入口节点 - - - ) - default: - return ( - <> - - - 出口节点 - - - ) - } - }, [type]) - - return
{getNode}
-} -// export const PathChoose = ({ value, onChange, pathText, type }: -// { -// value?: string -// type: string -// onChange?: (value: IComboboxValue) => void -// pathText: { -// guard: string -// exit: string -// } -// } -// ) => { -// const [flagText, setFlagText] = useState() -// const { data: proxiesList, refetch: refetchGuard } = useProxiesGuard() -// const { data: proxiesExit, refetch: refetchExit } = useProxiesExit() -// const [maxHeightClass, setMaxHeightClass] = useState("max-h-[300px]"); - -// const currentList = useMemo(() => { -// return type === 'guard' ? proxiesList?.data : proxiesExit?.data -// }, [proxiesList, proxiesExit, type]) - -// const getOptions = (value_: ProxiesList[]) => { -// const options = value_ -// .filter(item => item.name === value) -// .map(item => { -// return { -// label: item.name, -// value: item.name, -// icon:
-// {/* */} -//
, -// num: item.delay, -// } -// }) -// return options -// } - -// const onHandleChange = (e: IComboboxValue) => { -// const value_ = currentList?.find((index: any) => index.name === e) -// setFlagText(value_) -// onChange?.(e) -// } - -// const currentOption = useMemo(() => { -// if (!currentList) return [] -// const value_ = currentList.find((index: any) => index.name === value) -// setFlagText(value_) -// return getOptions(currentList) -// }, [currentList]) - -// useEffect(() => { -// const updateMaxHeight = () => { -// if (window.innerHeight < 1080) { -// setMaxHeightClass("max-h-[260px]"); -// } else { -// setMaxHeightClass("max-h-[300px]"); -// } -// }; - -// updateMaxHeight(); // 初始调用一次 -// window.addEventListener("resize", updateMaxHeight); // 监听窗口大小变化 -// return () => window.removeEventListener("resize", updateMaxHeight); // 清除监听器 -// }, []); - -// return ( -// <> -// -//
-//
-//
{pathText.guard}
-// -//
-//
-//
{pathText.exit}
-// } -// options={currentOption} -// onComboxOpen={type === 'guard' ? refetchGuard : refetchExit} -// placeholder='请选择节点' -// muiltip={false} -// contentClass="p-2 pt-0 linkAdd_ComboxContent my-[4px]" -// commandListClassName={maxHeightClass} -// ellipsisTooltipClassName="text-[16px]" -// comboxTitle={} -// contentProps={{ -// 'alignOffset': -380 -// }} -// /> -//
-//
-// -// ) -// } diff --git a/src/pages/decentralized-lastic-network/components/FormAlertDialog/index.tsx b/src/pages/decentralized-lastic-network/components/FormAlertDialog/index.tsx index faa75ae..494bd5f 100644 --- a/src/pages/decentralized-lastic-network/components/FormAlertDialog/index.tsx +++ b/src/pages/decentralized-lastic-network/components/FormAlertDialog/index.tsx @@ -16,27 +16,7 @@ export interface DialogConfig { successText: string; } -const PledgeAmount = ({ - value, - onChange, -}: { - value?: string; - onChange?: (data: string) => void; -}) => { - return ( -
- { - onChange?.(e.target.value); - }} - /> - SOL -
- ); -}; + const IpPortInput = ({ value, onChange, diff --git a/src/pages/decentralized-lastic-network/index.tsx b/src/pages/decentralized-lastic-network/index.tsx index 31e6f09..024e44b 100644 --- a/src/pages/decentralized-lastic-network/index.tsx +++ b/src/pages/decentralized-lastic-network/index.tsx @@ -1,6 +1,8 @@ import { Form } from "antd"; import { open as openFile } from "@tauri-apps/plugin-dialog"; - +import { readFile } from "@tauri-apps/plugin-fs"; +import * as XLSX from "xlsx"; +import { every, has } from "lodash-es"; import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { WorldGeo } from "./components/world-geo"; @@ -12,7 +14,7 @@ import AddSvg from "@/assets/svg/home/add.svg?react"; import InterSvg from "@/assets/svg/home/inter.svg?react"; import TrashSvg from "@/assets/svg/home/trash.svg?react"; import { cn } from "@/lib/utils"; - +import eventBus, { eventTypes } from "@/utils/eventBus"; import { setProxyInfoProxies, setProxiesList2, @@ -72,7 +74,8 @@ const DecentralizedElasticNetwork = () => { await form.validateFields(); const formValue: any = form.getFieldsValue(); if (type.title === DIALOGTYPE.ADDNode.title) { - setOpen(false); + eventBus.emit(eventTypes.NODE_INIT, {}); + // setOpen(false); } else { const { inbound, outbound } = formValue || {}; if (inbound && outbound) { @@ -100,15 +103,41 @@ const DecentralizedElasticNetwork = () => { ], }); if (selected && typeof selected === "string") { - if (selected.includes("验证节点包1")) { - console.log("验证节点包1"); - // dispatch(setProxiesList1()); + const data = await readFile(selected); + // 将二进制数据转换为 ArrayBuffer + const arrayBuffer = data.buffer; + // 使用 XLSX 库解析 Excel 文件 + const workbook = XLSX.read(arrayBuffer, { type: "array" }); + + // 获取第一个工作表 + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + + // 将工作表转换为 JSON + const jsonData = XLSX.utils.sheet_to_json(worksheet); + // 判断是否为空 + if (jsonData.length !== 0) { + console.log("Excel 数据:", jsonData); + // 取第一条数据检查是否包含exit,ip,name,port,private_key,publick_key字段 + const firstRow: any = jsonData[0]; + // 使用lodash 判断是否包含这些字段 + const requiredFields = [ + "exit", + "ip", + "name", + "port", + "private_key", + "publick_key", + ]; + if (every(requiredFields, (field) => has(firstRow, field))) { + jsonData.forEach((item: any) => { + eventBus.emit(eventTypes.NODE_ADD, item); + }); + } + setOpen(false); + return; } - if (selected.includes("验证节点包2")) { - console.log("验证节点包2"); - dispatch(setProxiesList2()); - } - setOpen(false); + console.error("Excel 文件为空或格式不正确"); } } catch (err) { console.error("Error selecting file:", err); @@ -180,7 +209,7 @@ const DecentralizedElasticNetwork = () => { return (
{ {item.transactions}
*/}
{item?.balanceToFixed} SOL
-
+
{item.txs.length}笔交易
-
+
{item.timerstamp}
+
{item.height} H
); @@ -246,13 +276,14 @@ const DecentralizedElasticNetwork = () => { className="w-full h-full" />
-
{item.balance} SOL
-
+
{item.balance} SOL
+
{item.numberTransactions}笔交易
-
+
{item.timerstamp}
+
{item.height} H
); diff --git a/src/pages/new-home/components/ClearNodeDialog/index.scss b/src/pages/new-home/components/ClearNodeDialog/index.scss deleted file mode 100644 index 593b011..0000000 --- a/src/pages/new-home/components/ClearNodeDialog/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -.linkAdd_ComboxContent{ - width: 800px !important; - margin-top: 18px; -} diff --git a/src/pages/new-home/components/ClearNodeDialog/index.tsx b/src/pages/new-home/components/ClearNodeDialog/index.tsx deleted file mode 100644 index cce0d08..0000000 --- a/src/pages/new-home/components/ClearNodeDialog/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { FormInstance } from "antd"; - -import { FormDialog } from "@/components/FormDialog"; -import { useDispatch, useSelector } from "react-redux"; - -import "./index.scss"; -import { EllipsisTooltip } from "@/components/Encapsulation"; -import NotFailNodeIcon from "@/assets/svg/common/not-fail-node.svg?react"; -import NotWarningNodeIcon from "@/assets/svg/common/not-warning-node.svg?react"; -import { NODEDIALOGTYPE } from "../../index"; -import { cn, getUrl } from "@/lib/utils"; -import eventBus, { eventTypes } from "@/utils/eventBus"; - -import { AppDispatch, RootState } from "@/store"; -import { removeMaliciousNodeList, removeNodeDownList } from "@/store/web3Slice"; -import { countryCodeMap } from "@/data"; - -export interface DialogConfig { - title: string; - desc: string; - successText: string; -} - -export const ProxyItem: React.FC<{ proxyInfo: any; clasName?: string }> = ( - props -) => { - const { code, exit = false } = props.proxyInfo; - return ( -
-
-
-
- -
- -
-
-
- ); -}; - -export const ClearNodeDialog = ({ - open, - setOpen, - successHandle, - dialogLoading, - form, - type, - canSubmit, -}: { - open: boolean; - setOpen: (open: boolean) => void; - successHandle: () => void; - dialogLoading: boolean; - canSubmit: boolean; - form: FormInstance; - type: DialogConfig; -}) => { - const dispatch = useDispatch(); - const { maliciousNodeList, nodeDownList } = useSelector( - (state: RootState) => state.web3Reducer - ); - const [isClear, setIsClear] = useState(false); - const showDialog = (open: boolean) => { - setOpen(open); - }; - const onSuccessHandle = () => { - successHandle(); - setIsClear(true); - if (type.title === NODEDIALOGTYPE.ClearFailNode.title) { - nodeDownList.forEach((item) => { - dispatch(removeNodeDownList(item.name)); - eventBus.emit(eventTypes.NODE_REMOVE, item.name); - }); - } else if (type.title === NODEDIALOGTYPE.ClearWargingNode.title) { - maliciousNodeList.forEach((item) => { - dispatch(removeMaliciousNodeList(item.name)); - eventBus.emit(eventTypes.NODE_REMOVE, item.name); - }); - } - // showDialog(false); - }; - - const proxyList = useMemo(() => { - if (type.title === NODEDIALOGTYPE.ClearFailNode.title) { - return nodeDownList; - } else { - return maliciousNodeList; - } - }, [nodeDownList, maliciousNodeList, type.title]); - - useEffect(() => { - if (!open) { - setIsClear(false); - } - }, [open]); - - return ( - -
- {proxyList.length > 0 ? ( - proxyList - .filter((item: any) => item?.name) - .map((item: any) => { - return ; - }) - ) : ( -
- {type.title === NODEDIALOGTYPE.ClearFailNode.title ? ( - - ) : ( - - )} - -
- {type.title === NODEDIALOGTYPE.ClearFailNode.title - ? "暂无掉线节点" - : "暂无恶意节点"} -
-
- )} -
-
- ); -}; diff --git a/src/pages/new-home/components/FormAlertDialog/index.scss b/src/pages/new-home/components/FormAlertDialog/index.scss deleted file mode 100644 index 593b011..0000000 --- a/src/pages/new-home/components/FormAlertDialog/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -.linkAdd_ComboxContent{ - width: 800px !important; - margin-top: 18px; -} diff --git a/src/pages/new-home/components/FormAlertDialog/index.tsx b/src/pages/new-home/components/FormAlertDialog/index.tsx deleted file mode 100644 index 577a903..0000000 --- a/src/pages/new-home/components/FormAlertDialog/index.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { Form, FormInstance } from "antd"; - -import { Input } from "@/components/ui/input"; -import { FormDialog } from "@/components/FormDialog"; -import { countryCodeMap } from "@/data"; -import { DIALOGTYPE } from "../../index"; - -import "./index.scss"; -import { DefaultLink } from "./pathChoose"; -import { Button } from "@/components/ui/button"; - -export interface DialogConfig { - title: string; - desc: string; - successText: string; -} - -const PledgeAmount = ({ - value, - onChange, -}: { - value?: string; - onChange?: (data: string) => void; -}) => { - return ( -
- { - onChange?.(e.target.value); - }} - /> - SOL -
- ); -}; -const IpPortInput = ({ - value, - onChange, -}: { - value?: { port: string; ip: string }; - onChange?: (data: { port: string; ip: string }) => void; -}) => { - const [ipAndPort, setIpAndPort] = useState<{ - port: string; - ip: string; - }>(value ? value : { port: "", ip: "" }); - const handleChagne = ({ - key, - val, - }: { - key: "port" | "ip"; - val: string; - }) => { - const newValue = value ? { ...value } : { port: "", ip: "" }; - newValue[key] = val; - onChange?.(newValue); - }; - useEffect(() => { - if (value) { - setIpAndPort(value); - } - }, [value]); - return ( -
- { - handleChagne?.({ key: "ip", val: e.target.value }); - }} - /> - { - handleChagne?.({ key: "port", val: e.target.value }); - }} - /> -
- ); -}; - -const NodeForm = ({ form }: { form: FormInstance }) => { - return ( -
- {/* -
uid
-
*/} - - - - - - - - - - - - - - - -
- ); -}; - -const NetworkForm = ({ form }: { form: FormInstance }) => { - return ( -
- - key)} - /> - - - key)} - /> - -
- ); -}; - -export const FormAlertDialog = ({ - open, - setOpen, - successHandle, - dialogLoading, - form, - type, - canSubmit, - handleSelectFile, -}: { - open: boolean; - setOpen: (open: boolean) => void; - successHandle: () => void; - dialogLoading: boolean; - canSubmit: boolean; - form: FormInstance; - type: DialogConfig; - handleSelectFile: () => void; -}) => { - const showDialog = (open: boolean) => { - setOpen(open); - }; - - - return ( - - - {open && type.title === DIALOGTYPE.ADDNode.title ? ( - - ) : ( - - )} - - ); -}; diff --git a/src/pages/new-home/components/FormAlertDialog/pathChoose.tsx b/src/pages/new-home/components/FormAlertDialog/pathChoose.tsx deleted file mode 100644 index 9b899b3..0000000 --- a/src/pages/new-home/components/FormAlertDialog/pathChoose.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useMemo, useState, useEffect } from 'react' -// import LinkButtonCol from "@/assets/svg/LinkButtonCol.svg?react"; -import Gurad from '@/assets/svg/ProxiesGuard.svg?react' -import Exit from '@/assets/svg/Exit.svg?react' -import Down from '@/assets/svg/down.svg?react' -import HiddenNode from '@/assets/svg/HiddenNode.svg?react' -import InletNodeSvg from '@/assets/svg/InletNode.svg?react' -// import EntryNode from "@/assets/svg/EntryNode.svg?react"; - -import { cn, getUrl } from '@/lib/utils' -// import { useProxiesExit, useProxiesGuard } from "@/api/link"; -// import { Button } from "@/components/ui/button"; -import { Combobox, IComboboxValue } from '@/components/Combobox' -// import { ProxiesList } from "@/pages/home/types"; -import { SearchOptionText } from '@/pages/home/components/MultipleLinks' -import { countryCodeMap } from '@/pages/home/data' - -export const DefaultLink = ({ - value, - flag = '', - className, - des, - type = 'hidden', - disabled, - countries, - isChecked = true, - onChange, -}: { - value?: string - des: string - flag?: string - countries: any[] - className?: string - type?: string - disabled?: boolean - isChecked?: boolean - onChange?: (value: IComboboxValue) => void -}) => { - // const ingressName = countryCodeMap[flag.toUpperCase()] - - const [ingressName, setIngressName] = useState(des ?? '') - const [ingressOptions, setIngressOptions] = useState< - { - label: string - value: string - icon: JSX.Element - num: number - }[] - >([]) - - const getOptions = () => { - const options = countries.map((item) => { - const name = countryCodeMap[item.toUpperCase()] - return { - label: name, - value: item, - icon: ( -
- -
- ), - num: 0, - } - }) - setIngressOptions(options) - } - - useEffect(() => { - // type === 'hidden' && - isChecked && setIngressName(countryCodeMap[flag.toUpperCase()] ?? des) - getOptions() - }, [flag]) - - useEffect(() => { - if (value) { - setIngressName(value) - } - }, [value]) - - return ( -
- { - onChange?.(e) - }} - title="输入国家名称" - radioText={SearchOptionText} - downIcon={} - options={ingressOptions} - placeholder={ - type === 'entry' - ? '入口节点' - : type === 'hidden' - ? '隐匿节点' - : '出口节点' - } - muiltip={false} - contentClass="w-full p-2 pt-0 linkAdd_ComboxContent my-[4px]" - contentProps={{ - alignOffset: 0, - }} - disabled={disabled} - comboxTitle={} - /> -
- ) -} - -export const ComboxTitle = ({ type }: { type: string }) => { - const getNode = useMemo(() => { - switch (type) { - case 'guard': - return ( - <> - - - 守卫节点 - - - ) - case 'hidden': - return ( - <> - - - 隐匿节点 - - - ) - case 'entry': - return ( - <> - - - 入口节点 - - - ) - default: - return ( - <> - - - 出口节点 - - - ) - } - }, [type]) - - return
{getNode}
-} -// export const PathChoose = ({ value, onChange, pathText, type }: -// { -// value?: string -// type: string -// onChange?: (value: IComboboxValue) => void -// pathText: { -// guard: string -// exit: string -// } -// } -// ) => { -// const [flagText, setFlagText] = useState() -// const { data: proxiesList, refetch: refetchGuard } = useProxiesGuard() -// const { data: proxiesExit, refetch: refetchExit } = useProxiesExit() -// const [maxHeightClass, setMaxHeightClass] = useState("max-h-[300px]"); - -// const currentList = useMemo(() => { -// return type === 'guard' ? proxiesList?.data : proxiesExit?.data -// }, [proxiesList, proxiesExit, type]) - -// const getOptions = (value_: ProxiesList[]) => { -// const options = value_ -// .filter(item => item.name === value) -// .map(item => { -// return { -// label: item.name, -// value: item.name, -// icon:
-// {/* */} -//
, -// num: item.delay, -// } -// }) -// return options -// } - -// const onHandleChange = (e: IComboboxValue) => { -// const value_ = currentList?.find((index: any) => index.name === e) -// setFlagText(value_) -// onChange?.(e) -// } - -// const currentOption = useMemo(() => { -// if (!currentList) return [] -// const value_ = currentList.find((index: any) => index.name === value) -// setFlagText(value_) -// return getOptions(currentList) -// }, [currentList]) - -// useEffect(() => { -// const updateMaxHeight = () => { -// if (window.innerHeight < 1080) { -// setMaxHeightClass("max-h-[260px]"); -// } else { -// setMaxHeightClass("max-h-[300px]"); -// } -// }; - -// updateMaxHeight(); // 初始调用一次 -// window.addEventListener("resize", updateMaxHeight); // 监听窗口大小变化 -// return () => window.removeEventListener("resize", updateMaxHeight); // 清除监听器 -// }, []); - -// return ( -// <> -// -//
-//
-//
{pathText.guard}
-// -//
-//
-//
{pathText.exit}
-// } -// options={currentOption} -// onComboxOpen={type === 'guard' ? refetchGuard : refetchExit} -// placeholder='请选择节点' -// muiltip={false} -// contentClass="p-2 pt-0 linkAdd_ComboxContent my-[4px]" -// commandListClassName={maxHeightClass} -// ellipsisTooltipClassName="text-[16px]" -// comboxTitle={} -// contentProps={{ -// 'alignOffset': -380 -// }} -// /> -//
-//
-// -// ) -// } diff --git a/src/pages/new-home/index.tsx b/src/pages/new-home/index.tsx index e412193..c64ff02 100644 --- a/src/pages/new-home/index.tsx +++ b/src/pages/new-home/index.tsx @@ -22,8 +22,6 @@ import { } from "@/store/web3Slice"; import type { AppDispatch, RootState } from "@/store"; import "./index.scss"; -import { DialogConfig, FormAlertDialog } from "./components/FormAlertDialog"; -import { ClearNodeDialog } from "./components/ClearNodeDialog"; import { getPassAuthentication, getTrafficObfuscation, @@ -74,10 +72,7 @@ const NewHome = () => { const [openNode, setOpenNode] = useState(false); const [dialogLoading] = useState(false); const [selectedApp, setSelectedApp] = useState(null); - const [type, setType] = useState(DIALOGTYPE.ADDNode); - const [nodeType, setNodeType] = useState( - NODEDIALOGTYPE.ClearFailNode - ); + const [tooltipClosed, setTooltipClosed] = useState(true); @@ -113,60 +108,9 @@ const NewHome = () => { setSelectedApp(item); }; - async function handleSelectFile() { - try { - const selected = await openFile({ - multiple: false, - directory: false, - filters: [ - { - name: "Excel Files", - extensions: ["xlsx", "xls"], // 移除了扩展名前的点号 - }, - ], - }); - if (selected && typeof selected === "string") { - if (selected.includes("验证节点包1")) { - console.log("验证节点包1"); - dispatch(setProxiesList1()); - } - if (selected.includes("验证节点包2")) { - console.log("验证节点包2"); - dispatch(setProxiesList2()); - } - setOpen(false); - } - } catch (err) { - console.error("Error selecting file:", err); - } - } - const ICircuitRequest = useMemo(() => { - return { - open, - setOpen, - successHandle, - dialogLoading, - form, - type, - canSubmit: true, - handleSelectFile, - }; - }, [open, dialogLoading, type, handleSelectFile]); - const nodeSuccessHandle = () => {}; - const ClearNodeDialogProps = useMemo(() => { - return { - open: openNode, - setOpen: setOpenNode, - successHandle: nodeSuccessHandle, - dialogLoading, - form, - type: nodeType, - canSubmit: true, - }; - }, [openNode, dialogLoading, nodeType]); const [dataInfo, setDataInfo] = useState({ passAuthentication: { @@ -327,12 +271,15 @@ const NewHome = () => { {item.transactions}
*/}
{item?.balanceToFixed} SOL
-
+
{item.txs.length}笔交易
-
+
{item.timerstamp}
+
+ {item.height} H +
); @@ -370,12 +317,15 @@ const NewHome = () => { />
{item.balance} SOL
-
+
{item.numberTransactions}笔交易
-
+
{item.timerstamp}
+
+ {item.height} H +
); @@ -440,8 +390,7 @@ const NewHome = () => { ); })}
- - + ); }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 07871a5..3838f52 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,5 @@ import { createBrowserRouter, Navigate } from 'react-router-dom' -// import HomePage from '@/pages/home' +import HomePage from '@/pages/home' import NewHomePage from '@/pages/new-home' import DecentralizedElasticNetworkPage from '@/pages/decentralized-lastic-network' import AntiForensicsForwardingPage from '@/pages/anti-forensics-forwarding' @@ -35,7 +35,7 @@ export const router = createBrowserRouter([ // }, { path: '/proxies', - element: , + element: , }, ], }, diff --git a/src/utils/eventBus.ts b/src/utils/eventBus.ts index b13c51f..b1feb99 100644 --- a/src/utils/eventBus.ts +++ b/src/utils/eventBus.ts @@ -11,7 +11,7 @@ export const eventTypes = { // 检测到恶意节点 MALICIOUS_NODE: "malicious_node", // 节点预配置完成 - NODE_INIT_COMPLATE: "node_init_complate", + NODE_INIT_COMPLATE: "node_init_complete", // 节点清除 NODE_REMOVE: "node_remove", // 添加节点 diff --git a/src/utils/webSocketClientV2.ts b/src/utils/webSocketClientV2.ts new file mode 100644 index 0000000..7f2070b --- /dev/null +++ b/src/utils/webSocketClientV2.ts @@ -0,0 +1,245 @@ +import WebSocket, { + Message, + ConnectionConfig, +} from '@tauri-apps/plugin-websocket' + +export class WebSocketClient { + private ws: WebSocket | null = null + private isConnected = false + private reconnectAttempts = 0 + private maxReconnectAttempts = 5 + private reconnectDelay = 1000 // 初始重连延迟(毫秒) + private listeners: ((msg: Message) => void)[] = [] + private autoReconnect = true + + // 构造函数初始化 WebSocket 连接 + constructor( + private url: string, + private config: ConnectionConfig = {}, + options?: { + autoReconnect?: boolean, + maxReconnectAttempts?: number, + reconnectDelay?: number + } + ) { + if (options) { + this.autoReconnect = options.autoReconnect ?? true + this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5 + this.reconnectDelay = options.reconnectDelay ?? 1000 + } + } + + // 连接到 WebSocket + async connect(): Promise { + try { + if (this.isConnected && this.ws) { + console.log('Already connected') + return true + } + + this.ws = await WebSocket.connect(this.url, this.config) + this.isConnected = true + this.reconnectAttempts = 0 // 重置重连计数 + console.log(`Connected to ${this.url}`) + + // 重新添加之前的所有监听器 + for (const listener of this.listeners) { + this.ws.addListener(listener) + } + + // 添加内部监听器来处理连接关闭 + this.ws.addListener(this.handleWebSocketMessage.bind(this)) + + return true + } catch (error) { + console.error('Failed to connect:', error) + this.isConnected = false + this.ws = null + + // 如果启用了自动重连,尝试重连 + if (this.autoReconnect) { + return this.attemptReconnect() + } + + return false + } + } + + // 处理 WebSocket 消息,特别是关闭事件 + private async handleWebSocketMessage(msg: Message): Promise { + if (msg.type === 'Close') { + this.isConnected = false + this.ws = null + console.log('Connection closed by server') + + // 如果启用了自动重连,尝试重连 + if (this.autoReconnect) { + await this.attemptReconnect() + } + } + } + + // 尝试重新连接 + private async attemptReconnect(): Promise { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error(`Failed to reconnect after ${this.maxReconnectAttempts} attempts`) + return false + } + + this.reconnectAttempts++ + const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1) // 指数退避策略 + + console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${delay}ms...`) + + await new Promise(resolve => setTimeout(resolve, delay)) + + try { + return await this.connect() + } catch (error) { + console.error('Reconnection attempt failed:', error) + return false + } + } + + // 手动重连 + async reconnect(): Promise { + this.reconnectAttempts = 0 // 重置重连计数 + if (this.isConnected && this.ws) { + try { + await this.disconnect() + } catch (error) { + console.error('Error during disconnect before reconnect:', error) + } + } + return this.connect() + } + + // 发送消息 + async sendMessage(message: any): Promise { + if (!this.ws || !this.isConnected) { + console.error('WebSocket is not connected') + + // 如果启用了自动重连,尝试重连后再发送 + if (this.autoReconnect) { + const reconnected = await this.attemptReconnect() + if (!reconnected) return false + } else { + return false + } + } + + try { + await this.ws?.send(message) + console.log('Message sent:', message) + return true + } catch (error) { + console.error('Failed to send message:', error) + + // 如果发送失败,可能连接已关闭 + this.isConnected = false + + // 如果启用了自动重连,尝试重连后再发送 + if (this.autoReconnect) { + const reconnected = await this.attemptReconnect() + if (reconnected) { + // 重连成功,重试发送消息 + return this.sendMessage(message) + } + } + + return false + } + } + + // 添加消息监听器 + addListener(callback: (msg: Message) => void): void { + // 保存监听器以便重连时重新添加 + this.listeners.push(callback) + + if (!this.ws) { + console.warn('WebSocket is not connected, listener will be added after connection') + return + } + + this.ws.addListener(callback) + } + + + // 断开 WebSocket 连接 + async disconnect(): Promise { + if (!this.ws) { + console.warn('WebSocket is not connected') + this.isConnected = false + return true + } + + try { + await this.ws.disconnect() + this.isConnected = false + this.ws = null + console.log('Disconnected') + return true + } catch (error) { + console.error('Failed to disconnect:', error) + // 即使断开连接失败,也将状态设置为未连接 + this.isConnected = false + this.ws = null + return false + } + } + + // 检查连接状态 + isWebSocketConnected(): boolean { + return this.isConnected && this.ws !== null + } + + // 设置自动重连选项 + setAutoReconnect(enable: boolean): void { + this.autoReconnect = enable + } + + // 设置最大重连尝试次数 + setMaxReconnectAttempts(attempts: number): void { + this.maxReconnectAttempts = attempts + } + + // 设置重连延迟 + setReconnectDelay(delay: number): void { + this.reconnectDelay = delay + } +} + +// 使用示例 +/* +const webSocketClient = new WebSocketClient( + 'ws://127.0.0.1:8080', + {}, // 连接配置 + { + autoReconnect: true, + maxReconnectAttempts: 5, + reconnectDelay: 1000 + } +); + +async function init() { + // 连接 + const connected = await webSocketClient.connect(); + + if (connected) { + // 添加消息监听器 + webSocketClient.addListener((msg) => { + console.log('Received Message:', msg); + }); + + // 发送消息 + await webSocketClient.sendMessage('Hello World!'); + } +} + +// 在应用关闭时断开连接 +async function cleanup() { + await webSocketClient.disconnect(); +} + +init(); +*/ \ No newline at end of file