diff --git a/MODULE_DEV_GUIDE.md b/MODULE_DEV_GUIDE.md index 6d2b39d..3865d3b 100644 --- a/MODULE_DEV_GUIDE.md +++ b/MODULE_DEV_GUIDE.md @@ -357,3 +357,136 @@ appStore.toggleModule(moduleId, enabled) - **瀑布流布局避免卡片等高撑开**:使用 `columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid` 替代 `grid`,让卡片按内容高度自然排列。 - **模块禁用清理系统状态**:模块 `onDisable` 钩子应清理系统级副作用(如系统代理注册表项),避免模块停用后遗留导致系统异常。 - **应用退出清理**:在 `lib.rs` 的 `quit_app` 命令和托盘退出事件中都要调用 `cleanup_on_exit` + `stop_all`,确保任何退出路径都清理干净。 + +## 近期重要变更(影响其他模块开发) + +以下是最近几次改动中确立的约定和模式,开发新模块时需遵循。 + +### 后端:子进程启动必须隐藏控制台窗口 + +`process_manager.rs` 暴露了公共函数 `setup_creation_flags(cmd: &mut Command)`,Windows 上会设置 `CREATE_NO_WINDOW` 标志。 + +**所有用 `std::process::Command` 启动外部程序的地方都必须调用它**,否则会弹出黑色控制台窗口(即使程序是后台运行)。包括: + +- `ProcessManager::start()` 启动 mihomo 等内核 +- `ProcessManager::check_and_cleanup()` 崩溃重启 +- 任何调用 `mihomo.exe -v`、`aria2c --version` 等查询版本的场景 +- 未来下载器模块启动 aria2 时同样适用 + +跨平台:非 Windows 平台该函数为空实现,无需条件编译。 + +```rust +use crate::process_manager::setup_creation_flags; + +let mut cmd = std::process::Command::new(&path); +cmd.arg("-v"); +setup_creation_flags(&mut cmd); // 必须调用 +cmd.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null()); +``` + +### 后端:长时间 wait() 不要阻塞主线程 + +`ProcessManager::stop()` 中 `child.kill()` 是快速的,但 `child.wait()` 可能阻塞几十到几百毫秒。**wait() 已移到后台线程**: + +```rust +thread::spawn(move || { + let _ = entry.child.wait(); +}); +``` + +新模块若有类似的"终止外部进程"逻辑,也应遵循此模式,避免 Tauri 命令阻塞导致前端卡顿。 + +### 后端:内核/二进制下载用流式 + 事件推送 + +`mihomo_manager.rs` 的 `install_kernel` 确立了"下载二进制资源"的标准模式,未来下载器模块下载 aria2 内核时应复用: + +- **流式下载**:`reqwest::Response::bytes_stream()` + `futures_util::StreamExt`,避免大文件一次性读入内存 +- **进度事件**:通过 `app.emit("xxx-install-progress", progress)` 推送,事件载荷结构参考 `InstallProgress` +- **事件节流**:仅在百分比变化 ≥1% 时 emit,避免事件轰炸 +- **zip 解压**:用 `zip` crate(纯 Rust),不要用 PowerShell `Expand-Archive`(有执行策略问题) +- **文件名匹配**:解压后用 `find_exe_in_dir` 查找 `.exe`(zip 内文件名可能含版本号,不是固定名字),找到后重命名为标准名 + +### 后端:GitHub API rate limit 规避 + +`check_kernel_update` 采用 **API 优先 + 重定向回退** 策略: + +1. 先调 `api.github.com/.../releases/latest`(能拿完整资产列表,命名变化时更健壮) +2. 失败(403 rate limit / 网络错误)时回退到访问 `github.com/.../releases/latest`,从 302 重定向的最终 URL 提取版本号,按稳定命名规则构造下载 URL + +**新模块若需要查 GitHub 最新版本,应复用此模式**,不要直接调 API(未认证 60次/小时/IP 极易超限)。 + +### 后端:资产命名规则适配 + +mihomo v1.19+ 改了 Windows 资产命名,按 CPU 微架构分级: + +- 旧:`mihomo-windows-amd64-vX.X.X.zip`(已废弃) +- 新:`mihomo-windows-amd64-v3-vX.X.X.zip`(v1/v2/v3 对应 CPU level) + +`fetch_latest_via_api` 中的匹配优先级:v3 标准 > v3-go124 > v3-go123 > v3 其他 > v2 > v1 > 旧命名。若未来其他内核也有类似分级,参考此优先级策略。 + +### 前端:Transition 内的 v-if/v-else 必须加 key + +**这是 dev 模式的坑**。`` 内的 `v-if`/`v-else` 分支如果缺 `:key`,Vue 3.5.x dev 模式下会触发 `__vnode` 写入竞态,导致: + +- 控制台报错 `Cannot set properties of null (setting '__vnode')` +- vnode 树损坏,所有事件派发失效(按钮点击没反应) +- **build 模式不报错**(生产构建剥除了 `__vnode` instrumentation),容易漏掉 + +**约定**:`` 内所有分支(v-if/v-else-if/v-else)都必须加 `:key`,即使是原生 div 也要加。 + +```vue + + +
占位
+
+``` + +`ModuleContainer.vue` 和 `ProxyModule.vue` 的 Progress 区块已修复,新模块开发时注意。 + +### 前端:模块内 Tabs 顶部固定模式 + +模块根容器用 `h-full overflow-hidden flex flex-col`,Tabs 用 `flex-1 min-h-0 flex flex-col`,TabsList 加 `shrink-0`,TabsContent 加 `flex-1 min-h-0 overflow-y-auto`: + +```vue +
+ + ... + ... + +
+``` + +这样 TabsList 固定在顶部,只有 TabsContent 滚动。`min-h-0` 是 flex 子元素 overflow 生效的关键,不能省。 + +### 前端:Tauri 事件监听需在 store 中管理生命周期 + +`proxyStore.ts` 的 `installKernel` 确立了模式: + +- 监听在方法调用时注册,`finally` 块中取消 +- 用模块级变量保存 `UnlistenFn`,避免重复注册 +- 错误事件由后端保证 emit(前端不重复弹 toast,统一由 watch 处理) + +```typescript +let progressUnlisten: UnlistenFn | null = null +const installKernel = async () => { + if (!progressUnlisten) { + progressUnlisten = await listen('xxx-progress', (e) => { + progress.value = e.payload + }) + } + try { + await invoke('xxx_command') + } finally { + if (progressUnlisten) { + progressUnlisten() + progressUnlisten = null + } + } +} +``` + +### 前端:窗口隐藏前 blur 焦点 + +TitleBar 的关闭按钮实际是 `hide()` 到托盘。webview 快速隐藏时浏览器 `mouseleave` 可能不触发,导致从托盘恢复后按钮仍显示 hover 高亮。 + +**修复**:`hide()` 前调用 `document.activeElement.blur()`,并监听 `onFocusChanged` 在窗口重新获得焦点时再 blur 一次。新模块若有类似的"隐藏窗口"操作(如全局快捷键隐藏),同样需要 blur。 diff --git a/bun.lock b/bun.lock index 3b8b443..7815a7c 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", "vue": "^3.5.13", + "vue-draggable-plus": "^0.6.1", "vue-sonner": "^2.0.9", }, "devDependencies": { @@ -236,6 +237,8 @@ "@types/node": ["@types/node@26.1.1", "https://registry.npmmirror.com/@types/node/-/node-26.1.1.tgz", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + "@types/sortablejs": ["@types/sortablejs@1.15.9", "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.9.tgz", {}, "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="], + "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="], @@ -410,6 +413,8 @@ "vue-demi": ["vue-demi@0.14.10", "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], + "vue-draggable-plus": ["vue-draggable-plus@0.6.1", "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.6.1.tgz", { "dependencies": { "@types/sortablejs": "^1.15.8" }, "peerDependencies": { "@vue/composition-api": "*" }, "optionalPeers": ["@vue/composition-api"] }, "sha512-FbtQ/fuoixiOfTZzG3yoPl4JAo9HJXRHmBQZFB9x2NYCh6pq0TomHf7g5MUmpaDYv+LU2n6BPq2YN9sBO+FbIg=="], + "vue-sonner": ["vue-sonner@2.0.9", "https://registry.npmmirror.com/vue-sonner/-/vue-sonner-2.0.9.tgz", { "peerDependencies": { "@nuxt/kit": "^4.0.3", "@nuxt/schema": "^4.0.3", "nuxt": "^4.0.3" }, "optionalPeers": ["@nuxt/kit", "@nuxt/schema", "nuxt"] }, "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw=="], "vue-tsc": ["vue-tsc@2.2.12", "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="], diff --git a/lighting_s.png b/lighting_s.png deleted file mode 100644 index 746bf90..0000000 Binary files a/lighting_s.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 668bdb0..9c1884d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "thing", "version": "0.1.0", "dependencies": { - "@lucide/vue": "^1.24.0", + "@lucide/vue": "^1.25.0", "@tailwindcss/vite": "^4.3.2", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "~2", @@ -602,9 +602,9 @@ } }, "node_modules/@lucide/vue": { - "version": "1.24.0", - "resolved": "https://registry.npmmirror.com/@lucide/vue/-/vue-1.24.0.tgz", - "integrity": "sha512-5bNPX0G2YEWdUlBYk7pE8SgDg/f1mkIFpJ9vtE44pW/cwRz7Ioc0tOTESoVJAPvxIELSmYekX+XXIJMjsswNIg==", + "version": "1.25.0", + "resolved": "https://registry.npmmirror.com/@lucide/vue/-/vue-1.25.0.tgz", + "integrity": "sha512-hkEetV+v48ScIn3uwqwWQ66sI8foeP2q6OMI09GzLFH4SfvBlfe3JHYlMBdBCqFC7WRlhFsndyDn/awRKRc2OQ==", "license": "ISC", "peerDependencies": { "vue": ">=3.0.1" diff --git a/package.json b/package.json index 7f0e3eb..cf0b907 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "tauri": "tauri" }, "dependencies": { - "@lucide/vue": "^1.24.0", + "@lucide/vue": "^1.25.0", "@tailwindcss/vite": "^4.3.2", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "~2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 57684f4..5d73644 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -47,6 +58,15 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -354,6 +374,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -428,6 +467,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -478,6 +519,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "combine" version = "4.6.7" @@ -497,6 +548,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cookie" version = "0.18.1" @@ -566,6 +623,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -684,6 +756,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + [[package]] name = "deranged" version = "0.5.8" @@ -693,6 +771,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -722,6 +811,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1296,9 +1386,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1515,6 +1607,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.38.0" @@ -1816,6 +1917,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1914,6 +2024,16 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.103" @@ -2043,6 +2163,27 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "markup5ever" version = "0.38.0" @@ -2526,6 +2667,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2870,6 +3021,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -2891,12 +3043,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams 0.4.2", "web-sys", ] @@ -2930,7 +3084,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -3331,6 +3485,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3917,6 +4082,7 @@ name = "thing" version = "0.1.0" dependencies = [ "chrono", + "futures-util", "reqwest 0.12.28", "serde", "serde_json", @@ -3925,8 +4091,10 @@ dependencies = [ "tauri-build", "tauri-plugin-autostart", "tauri-plugin-opener", + "tokio", "windows-sys 0.52.0", "winreg 0.52.0", + "zip", ] [[package]] @@ -4553,6 +4721,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -5235,6 +5416,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.8.3" @@ -5345,6 +5535,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] name = "zerotrie" @@ -5379,12 +5583,82 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.14.0", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.18", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.13.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fe7879e..709088f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,7 +24,10 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" chrono = "0.4" -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.12", features = ["json", "stream"] } +futures-util = "0.3" +tokio = { version = "1", features = ["io-util"] } +zip = "2" [target.'cfg(windows)'.dependencies] winreg = "0.52" diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 2fc180d..ce69eab 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index d084ec1..d58728d 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index 7535f5b..f6b9f66 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png index 6b09990..29c13bd 100644 Binary files a/src-tauri/icons/64x64.png and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 4ce876e..3440ead 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index 00d33e6..489915c 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index e11220b..e55f130 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index 4c4477e..243c6ce 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 083d98f..79671b0 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index ee7b0a8..caccd7a 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index 0687c91..3122c8d 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index fc09c5c..4dd39e4 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index b03606e..443f9fa 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 3eed9bb..2399786 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 2ffbf24..0000000 --- a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 3ea7f43..0000000 Binary files a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 30954bd..0000000 Binary files a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index d9480f7..0000000 Binary files a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index d0f0ed0..0000000 Binary files a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 9e35572..0000000 Binary files a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 4d3f65f..0000000 Binary files a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 932f770..0000000 Binary files a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 78e59bd..0000000 Binary files a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index ef55e83..0000000 Binary files a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5a1cf3..0000000 Binary files a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 827e095..0000000 Binary files a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 25ec170..0000000 Binary files a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 17f657b..0000000 Binary files a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 4191bc0..0000000 Binary files a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 7c47675..0000000 Binary files a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml deleted file mode 100644 index ea9c223..0000000 --- a/src-tauri/icons/android/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #fff - \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 7b08abc..1246434 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index ac8512f..46e0f4f 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e9c27b8..c7081ed 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png deleted file mode 100644 index ae713fb..0000000 Binary files a/src-tauri/icons/ios/AppIcon-20x20@1x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png deleted file mode 100644 index 77bad93..0000000 Binary files a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png deleted file mode 100644 index 77bad93..0000000 Binary files a/src-tauri/icons/ios/AppIcon-20x20@2x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png deleted file mode 100644 index 4b58b86..0000000 Binary files a/src-tauri/icons/ios/AppIcon-20x20@3x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png deleted file mode 100644 index c00f3c7..0000000 Binary files a/src-tauri/icons/ios/AppIcon-29x29@1x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png deleted file mode 100644 index 27f28eb..0000000 Binary files a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png deleted file mode 100644 index 27f28eb..0000000 Binary files a/src-tauri/icons/ios/AppIcon-29x29@2x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png deleted file mode 100644 index 1aaf248..0000000 Binary files a/src-tauri/icons/ios/AppIcon-29x29@3x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png deleted file mode 100644 index 77bad93..0000000 Binary files a/src-tauri/icons/ios/AppIcon-40x40@1x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png deleted file mode 100644 index 79610e9..0000000 Binary files a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png deleted file mode 100644 index 79610e9..0000000 Binary files a/src-tauri/icons/ios/AppIcon-40x40@2x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png deleted file mode 100644 index d2ecc34..0000000 Binary files a/src-tauri/icons/ios/AppIcon-40x40@3x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png deleted file mode 100644 index 9b1045d..0000000 Binary files a/src-tauri/icons/ios/AppIcon-512@2x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png deleted file mode 100644 index d2ecc34..0000000 Binary files a/src-tauri/icons/ios/AppIcon-60x60@2x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png deleted file mode 100644 index df848c5..0000000 Binary files a/src-tauri/icons/ios/AppIcon-60x60@3x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png deleted file mode 100644 index 5f02d75..0000000 Binary files a/src-tauri/icons/ios/AppIcon-76x76@1x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png deleted file mode 100644 index 15cd753..0000000 Binary files a/src-tauri/icons/ios/AppIcon-76x76@2x.png and /dev/null differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png deleted file mode 100644 index c1efd2b..0000000 Binary files a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and /dev/null differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d7e7ff0..e9888d5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,7 +10,7 @@ use logger::{ use mihomo_manager::{ proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection, proxy_delete_profile, proxy_get_connections, proxy_get_proxies, proxy_get_settings, proxy_get_system_proxy, - proxy_import_profile, proxy_kernel_info, proxy_patch_configs, proxy_restart, proxy_save_settings, + proxy_import_profile, proxy_install_kernel, proxy_kernel_info, proxy_patch_configs, proxy_restart, proxy_save_settings, proxy_select_proxy, proxy_set_system_proxy, proxy_start, proxy_status, proxy_stop, proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager, }; @@ -59,6 +59,7 @@ pub fn run() { proxy_kernel_info, proxy_check_kernel_update, proxy_update_kernel, + proxy_install_kernel, proxy_status, proxy_start, proxy_stop, diff --git a/src-tauri/src/mihomo_manager.rs b/src-tauri/src/mihomo_manager.rs index 2c4b228..5e48b4a 100644 --- a/src-tauri/src/mihomo_manager.rs +++ b/src-tauri/src/mihomo_manager.rs @@ -1,13 +1,15 @@ use chrono::Local; +use futures_util::StreamExt; use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_yaml::Value as YamlValue; use std::fs; +use std::io::{Read, Write}; use std::path::PathBuf; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Emitter, Manager}; use tauri::path::BaseDirectory; -use crate::process_manager::{ProcessInfo, ProcessManager, StartProcessParams}; +use crate::process_manager::{ProcessInfo, ProcessManager, StartProcessParams, setup_creation_flags}; // ===================== 数据结构 ===================== @@ -44,6 +46,10 @@ pub struct ProxySettings { pub auto_switch_group: String, #[serde(default)] pub auto_switch_region: String, + /// 内核下载镜像源列表(前缀拼接到 GitHub URL 前)。 + /// 空字符串 = 直连 GitHub,其余为镜像站前缀(含尾斜杠)。 + #[serde(default = "default_kernel_mirrors")] + pub kernel_mirrors: Vec, } fn default_mixed_port() -> u16 { 7890 } @@ -51,6 +57,15 @@ fn default_external_controller() -> String { "127.0.0.1:9090".into() } fn default_mode() -> String { "rule".into() } fn default_log_level() -> String { "info".into() } fn default_auto_switch_interval() -> u32 { 5 } +/// 默认镜像源:空串=直连 GitHub 优先,后续为公益镜像(按稳定性排序) +fn default_kernel_mirrors() -> Vec { + vec![ + String::new(), + "https://ghproxy.net/".into(), + "https://gh-proxy.com/".into(), + "https://ghfast.top/".into(), + ] +} impl Default for ProxySettings { fn default() -> Self { @@ -70,6 +85,7 @@ impl Default for ProxySettings { auto_switch_interval: 5, auto_switch_group: String::new(), auto_switch_region: String::new(), + kernel_mirrors: default_kernel_mirrors(), } } } @@ -116,6 +132,19 @@ pub struct ProxyStatus { pub restart_count: u32, } +/// 内核安装进度事件载荷 +/// - stage: downloading | extracting | replacing | done | error +/// - percent: 0-100(无 total_bytes 时为 0,前端按 downloadedBytes 显示) +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InstallProgress { + pub stage: String, + pub percent: u8, + pub downloaded_bytes: u64, + pub total_bytes: Option, + pub message: String, +} + // ===================== MihomoManager ===================== pub struct MihomoManager { @@ -236,9 +265,15 @@ impl MihomoManager { let path = self.kernel_path(); let exists = path.exists(); let version = if exists { - std::process::Command::new(&path) - .arg("-v") - .output() + let mut cmd = std::process::Command::new(&path); + cmd.arg("-v"); + // 隐藏控制台窗口(mihomo.exe -v 也会弹窗) + setup_creation_flags(&mut cmd); + // 重定向 stdio,避免继承主进程控制台 + cmd.stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .stdin(std::process::Stdio::null()); + cmd.output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .and_then(|s| { @@ -270,14 +305,38 @@ impl MihomoManager { } /// 检查 GitHub 上的最新 mihomo 版本 + /// 策略:优先用 API(能拿到完整资产列表,命名变化时更健壮), + /// 失败时回退到重定向解析(不受 API rate limit 限制) pub async fn check_kernel_update(&self) -> Result { - let resp: serde_json::Value = self + match self.fetch_latest_via_api().await { + Ok(info) => Ok(info), + Err(api_err) => { + eprintln!("[kernel] API 查询失败,回退到重定向解析: {}", api_err); + self.fetch_latest_via_redirect().await + } + } + } + + /// 通过 GitHub API 查询最新版本(受 rate limit 限制:未认证 60次/小时/IP) + async fn fetch_latest_via_api(&self) -> Result { + let resp = self .client .get("https://api.github.com/repos/MetaCubeX/mihomo/releases/latest") .header("User-Agent", "thing-app") + .timeout(std::time::Duration::from_secs(15)) .send() .await - .map_err(|e| format!("请求 GitHub API 失败: {}", e))? + .map_err(|e| format!("请求 GitHub API 失败: {}", e))?; + let status = resp.status(); + if !status.is_success() { + let body = resp.text().await.unwrap_or_default(); + return Err(format!( + "GitHub API 返回 HTTP {}:{}", + status.as_u16(), + if body.len() > 300 { format!("{}...", &body[..300]) } else { body } + )); + } + let resp: serde_json::Value = resp .json() .await .map_err(|e| format!("解析 GitHub 响应失败: {}", e))?; @@ -288,33 +347,92 @@ impl MihomoManager { .unwrap_or("unknown") .to_string(); - // 查找 windows amd64 zip 资产(非 compatible 版本) - let download_url = resp - .get("assets") - .and_then(|a| a.as_array()) - .and_then(|assets| { - assets.iter().find_map(|asset| { + let assets_arr = resp.get("assets").and_then(|a| a.as_array()); + + // 收集所有 windows amd64 zip 候选资产(排除 compatible/arm64/386) + let candidates: Vec<(String, String)> = assets_arr + .map(|assets| { + assets.iter().filter_map(|asset| { let name = asset.get("name")?.as_str()?; let url = asset.get("browser_download_url")?.as_str()?; - // 匹配 mihomo-windows-amd64-v*.zip,排除 compatible/arm64 if name.starts_with("mihomo-windows-amd64-") && name.ends_with(".zip") && !name.contains("compatible") && !name.contains("arm64") + && !name.contains("386") { - Some(url.to_string()) + Some((name.to_string(), url.to_string())) } else { None } - }) + }).collect() }) - .ok_or_else(|| "未找到适用的 Windows amd64 内核资产".to_string())?; + .unwrap_or_default(); - // 当前版本 + // 按优先级匹配:v3 标准 > v3-go124 > v3-go123 > v3 其他 > v2 > v1 > 旧命名 + let download_url = candidates + .iter().find(|(n, _)| n.contains("-v3-v") && !n.contains("-go")) + .or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go124-"))) + .or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go123-"))) + .or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go"))) + .or_else(|| candidates.iter().find(|(n, _)| n.contains("-v2-v"))) + .or_else(|| candidates.iter().find(|(n, _)| n.contains("-v1-v"))) + .or_else(|| candidates.iter().find(|(n, _)| { + !n.contains("-v1-") && !n.contains("-v2-") && !n.contains("-v3-") + })) + .map(|(_, u)| u.clone()) + .ok_or_else(|| { + let candidates_str = candidates.iter() + .map(|(n, _)| n.as_str()) + .collect::>() + .join(", "); + format!("API 未找到适用的 Windows amd64 内核资产。候选:[{}]", candidates_str) + })?; + + Ok(self.build_update_info(latest_version, download_url)) + } + + /// 通过 releases/latest 重定向解析版本号(不受 API rate limit 限制) + /// 访问 https://github.com/MetaCubeX/mihomo/releases/latest 会 302 到 + /// https://github.com/MetaCubeX/mihomo/releases/tag/v1.19.13 + /// 从最终 URL 提取版本号后,按 v1.19+ 稳定命名规则构造下载 URL + async fn fetch_latest_via_redirect(&self) -> Result { + let resp = self + .client + .get("https://github.com/MetaCubeX/mihomo/releases/latest") + .header("User-Agent", "thing-app") + .timeout(std::time::Duration::from_secs(15)) + .send() + .await + .map_err(|e| format!("请求 GitHub releases 页面失败: {}", e))?; + + // 从重定向后的最终 URL 提取版本号 + let final_url = resp.url().to_string(); + let latest_version = final_url + .rsplit('/') + .next() + .filter(|s| s.starts_with('v') && s.chars().any(|c| c == '.')) + .ok_or_else(|| format!("无法从重定向 URL 提取版本号: {}", final_url))? + .to_string(); + + // 构造下载 URL:mihomo v1.19+ 稳定使用 -v3-vX.X.X.zip 命名(CPU level v3) + let download_url = format!( + "https://github.com/MetaCubeX/mihomo/releases/download/{}/mihomo-windows-amd64-v3-{}.zip", + latest_version, latest_version + ); + + eprintln!( + "[kernel] 重定向解析成功: version={}, url={}", + latest_version, download_url + ); + Ok(self.build_update_info(latest_version, download_url)) + } + + /// 根据最新版本和下载 URL 构造更新信息(含当前版本比较) + fn build_update_info(&self, latest_version: String, download_url: String) -> KernelUpdateInfo { let current = self.kernel_info().version; let has_update = match ¤t { Some(c) => { - // 简单比较:从当前版本字符串提取版本号 let cur_ver = c .split_whitespace() .find(|s| s.starts_with('v') && s.chars().filter(|c| *c == '.').count() >= 2) @@ -323,85 +441,266 @@ impl MihomoManager { } None => true, }; - - Ok(KernelUpdateInfo { + KernelUpdateInfo { current_version: current, latest_version, download_url, has_update, - }) + } } - /// 下载并安装内核更新 - pub async fn update_kernel(&self) -> Result { + /// 下载并安装内核(首次安装与更新共用此方法) + /// - mirror_prefix: 用户选择的镜像源前缀(空串=直连 GitHub) + /// - 流式下载:实时推送下载进度到前端 + /// - zip crate 解压:替代 PowerShell,避免执行策略问题 + /// - 备份旧内核:替换前备份为 .bak + /// 任何阶段失败都会 emit error 事件,避免前端进度卡在初始状态 + pub async fn install_kernel(&self, app: &AppHandle, mirror_prefix: String) -> Result { + let result = self.install_kernel_inner(app, mirror_prefix).await; + if let Err(ref e) = result { + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "error".into(), + percent: 0, + downloaded_bytes: 0, + total_bytes: None, + message: e.clone(), + }, + ); + } + result + } + + async fn install_kernel_inner(&self, app: &AppHandle, mirror_prefix: String) -> Result { let info = self.check_kernel_update().await?; let zip_path = self.cores_dir().join("mihomo-update.zip"); let extract_dir = self.cores_dir().join("mihomo-update-tmp"); - // 下载 zip - let resp = self - .client - .get(&info.download_url) - .header("User-Agent", "thing-app") - .send() - .await - .map_err(|e| format!("下载内核失败: {}", e))?; - if !resp.status().is_success() { - return Err(format!("下载失败: HTTP {}", resp.status())); - } - let bytes = resp - .bytes() - .await - .map_err(|e| format!("读取下载内容失败: {}", e))?; - fs::write(&zip_path, &bytes).map_err(|e| format!("保存 zip 失败: {}", e))?; + // 拼接用户选择的镜像源 URL + let url = if mirror_prefix.is_empty() { + info.download_url.clone() + } else { + format!("{}{}", mirror_prefix, info.download_url) + }; + let label = if mirror_prefix.is_empty() { "GitHub 直连".to_string() } else { mirror_prefix.clone() }; + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "downloading".into(), + percent: 0, + downloaded_bytes: 0, + total_bytes: None, + message: format!("正在下载:{}", label), + }, + ); - // 清理旧解压目录 + // 单源下载(用户已选择) + match self.download_with_progress(app, &url, &zip_path).await { + Ok(()) => {} + Err(e) => { + let msg = format!("下载失败({}):{}", label, e); + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "error".into(), + percent: 0, + downloaded_bytes: 0, + total_bytes: None, + message: msg.clone(), + }, + ); + let _ = fs::remove_file(&zip_path); + return Err(msg); + } + } + + // 解压阶段 + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "extracting".into(), + percent: 92, + downloaded_bytes: 0, + total_bytes: None, + message: "正在解压...".into(), + }, + ); if extract_dir.exists() { fs::remove_dir_all(&extract_dir).ok(); } fs::create_dir_all(&extract_dir).map_err(|e| e.to_string())?; - - // 用 PowerShell 解压 - let output = std::process::Command::new("powershell") - .args([ - "-NoProfile", - "-Command", - &format!( - "Expand-Archive -Path '{}' -DestinationPath '{}' -Force", - zip_path.to_string_lossy(), - extract_dir.to_string_lossy() - ), - ]) - .output() - .map_err(|e| format!("解压失败: {}", e))?; - if !output.status.success() { - let err = String::from_utf8_lossy(&output.stderr); - return Err(format!("解压失败: {}", err)); + if let Err(e) = self.extract_zip(&zip_path, &extract_dir) { + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "error".into(), + percent: 0, + downloaded_bytes: 0, + total_bytes: None, + message: format!("解压失败:{}", e), + }, + ); + return Err(e); } - // 查找解压出的 mihomo.exe - let new_exe = extract_dir.join("mihomo.exe"); - if !new_exe.exists() { - // 可能在不同子目录 - return Err("解压后未找到 mihomo.exe".into()); - } + // 在解压目录中递归查找 exe 文件 + // mihomo zip 内的 exe 名字通常与 zip 同名(如 mihomo-windows-amd64-v3-v1.19.13.exe), + // 不是固定的 mihomo.exe,所以查找唯一的 .exe 文件即可 + let new_exe = self + .find_exe_in_dir(&extract_dir) + .ok_or_else(|| "解压后未找到任何 .exe 文件".to_string())?; - // 备份旧内核 + // 替换阶段 + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "replacing".into(), + percent: 96, + downloaded_bytes: 0, + total_bytes: None, + message: "正在安装...".into(), + }, + ); let kernel = self.kernel_path(); if kernel.exists() { let bak = self.cores_dir().join("mihomo.exe.bak"); fs::remove_file(&bak).ok(); fs::rename(&kernel, &bak).map_err(|e| format!("备份旧内核失败: {}", e))?; } - - // 移动新内核 fs::rename(&new_exe, &kernel).map_err(|e| format!("替换内核失败: {}", e))?; // 清理临时文件 fs::remove_file(&zip_path).ok(); fs::remove_dir_all(&extract_dir).ok(); - Ok(self.kernel_info()) + let final_info = self.kernel_info(); + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "done".into(), + percent: 100, + downloaded_bytes: 0, + total_bytes: None, + message: format!( + "安装完成 ({})", + final_info.version.as_deref().unwrap_or("unknown") + ), + }, + ); + Ok(final_info) + } + + /// 流式下载并实时推送进度事件 + async fn download_with_progress( + &self, + app: &AppHandle, + url: &str, + dest: &PathBuf, + ) -> Result<(), String> { + let resp = self + .client + .get(url) + .header("User-Agent", "thing-app") + .timeout(std::time::Duration::from_secs(30)) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + if !resp.status().is_success() { + return Err(format!("HTTP {}", resp.status())); + } + let total = resp.content_length(); + let mut stream = resp.bytes_stream(); + let mut file = fs::File::create(dest).map_err(|e| format!("创建文件失败: {}", e))?; + let mut downloaded: u64 = 0; + let mut last_percent: u8 = 0; + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| format!("读取流失败: {}", e))?; + file.write_all(&chunk).map_err(|e| format!("写入文件失败: {}", e))?; + downloaded += chunk.len() as u64; + // 下载占总进度的 0-90% + let percent = match total { + Some(t) if t > 0 => ((downloaded as f64 / t as f64) * 90.0) as u8, + _ => 0, + }; + // 仅在变化超过 1% 时 emit,避免事件轰炸 + if percent >= last_percent + 1 { + last_percent = percent; + let _ = app.emit( + "kernel-install-progress", + InstallProgress { + stage: "downloading".into(), + percent, + downloaded_bytes: downloaded, + total_bytes: total, + message: format!("已下载 {:.2} MB", downloaded as f64 / 1024.0 / 1024.0), + }, + ); + } + } + file.flush().map_err(|e| format!("flush 失败: {}", e))?; + Ok(()) + } + + /// 用 zip crate 解压(纯 Rust,避免 PowerShell 执行策略问题) + fn extract_zip(&self, zip_path: &PathBuf, dest: &PathBuf) -> Result<(), String> { + let file = fs::File::open(zip_path).map_err(|e| format!("打开 zip 失败: {}", e))?; + let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("读取 zip 失败: {}", e))?; + for i in 0..archive.len() { + let mut entry = archive + .by_index(i) + .map_err(|e| format!("读取条目失败: {}", e))?; + let outpath = match entry.enclosed_name() { + Some(p) => dest.join(p), + None => continue, + }; + if entry.is_dir() { + fs::create_dir_all(&outpath).map_err(|e| e.to_string())?; + } else { + if let Some(parent) = outpath.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let mut outfile = fs::File::create(&outpath).map_err(|e| e.to_string())?; + let mut buf = [0u8; 8192]; + loop { + let n = entry.read(&mut buf).map_err(|e| e.to_string())?; + if n == 0 { + break; + } + outfile.write_all(&buf[..n]).map_err(|e| e.to_string())?; + } + } + } + Ok(()) + } + + /// 递归查找目录中的 .exe 文件 + /// mihomo zip 内的 exe 名字不固定(可能含版本号、CPU level 等), + /// 策略:收集所有 .exe,优先返回名字含 "mihomo" 的,否则返回第一个 + fn find_exe_in_dir(&self, dir: &PathBuf) -> Option { + let mut exes: Vec = Vec::new(); + self.collect_exes(dir, &mut exes); + if exes.is_empty() { + return None; + } + // 优先选名字含 mihomo 的 + exes.iter() + .find(|p| p.file_name().and_then(|n| n.to_str()).map(|s| s.to_lowercase().contains("mihomo")).unwrap_or(false)) + .or_else(|| exes.first()) + .cloned() + } + + fn collect_exes(&self, dir: &PathBuf, out: &mut Vec) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + self.collect_exes(&path, out); + } else if path.extension().and_then(|e| e.to_str()).map(|s| s.eq_ignore_ascii_case("exe")).unwrap_or(false) { + out.push(path); + } + } + } } // ---------- 配置生成 ---------- @@ -848,8 +1147,22 @@ pub async fn proxy_check_kernel_update( } #[tauri::command] -pub async fn proxy_update_kernel(state: tauri::State<'_, MihomoManager>) -> Result { - state.update_kernel().await +pub async fn proxy_update_kernel( + state: tauri::State<'_, MihomoManager>, + app: AppHandle, + mirror_prefix: Option, +) -> Result { + state.install_kernel(&app, mirror_prefix.unwrap_or_default()).await +} + +/// 首次安装内核(与 update_kernel 共用 install_kernel 实现,语义独立便于前端区分场景) +#[tauri::command] +pub async fn proxy_install_kernel( + state: tauri::State<'_, MihomoManager>, + app: AppHandle, + mirror_prefix: Option, +) -> Result { + state.install_kernel(&app, mirror_prefix.unwrap_or_default()).await } #[tauri::command] diff --git a/src-tauri/src/process_manager.rs b/src-tauri/src/process_manager.rs index 79a8346..aad980c 100644 --- a/src-tauri/src/process_manager.rs +++ b/src-tauri/src/process_manager.rs @@ -6,6 +6,24 @@ use std::thread; use std::time::Duration; use tauri::{AppHandle, Emitter, Manager}; +// Windows 平台用于隐藏控制台窗口的标志位 +// CREATE_NO_WINDOW = 0x08000000,阻止子进程创建新的控制台窗口 +#[cfg(windows)] +pub const CREATE_NO_WINDOW: u32 = 0x08000000; + +/// 为 Command 设置平台特定的创建标志(Windows 上隐藏控制台窗口) +/// 公开以便其他模块(如 mihomo_manager 调用 mihomo -v 查询版本)复用 +#[cfg(windows)] +pub fn setup_creation_flags(cmd: &mut Command) { + use std::os::windows::process::CommandExt; + cmd.creation_flags(CREATE_NO_WINDOW); +} + +#[cfg(not(windows))] +pub fn setup_creation_flags(_cmd: &mut Command) { + // 非 Windows 平台无需处理 +} + /// 进程状态枚举 #[derive(Serialize, Clone, Debug)] #[serde(rename_all = "lowercase")] @@ -94,6 +112,8 @@ impl ProcessManager { cmd.stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()); + // Windows 上隐藏控制台窗口(mihomo.exe 是控制台程序,否则会弹黑框) + setup_creation_flags(&mut cmd); let child = cmd .spawn() @@ -123,6 +143,8 @@ impl ProcessManager { } /// 停止指定进程 + /// kill 在主线程执行(快速),wait 移到后台线程执行避免阻塞前端 + /// Windows 上 kill 后 wait 可能需要等待子进程清理资源,有几十毫秒到几百毫秒延迟 pub fn stop(&self, id: &str) -> Result<(), String> { let mut processes = self.processes.lock().map_err(|e| e.to_string())?; @@ -131,7 +153,11 @@ impl ProcessManager { .child .kill() .map_err(|e| format!("终止进程 '{}' 失败: {}", id, e))?; - let _ = entry.child.wait(); + // 后台等待子进程退出,避免阻塞当前调用线程(前端会感知卡顿) + // child 已 move 进闭包,wait 在后台完成 + thread::spawn(move || { + let _ = entry.child.wait(); + }); Ok(()) } else { Err(format!("进程 '{}' 不存在", id)) @@ -240,6 +266,7 @@ impl ProcessManager { cmd.stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()); + setup_creation_flags(&mut cmd); match cmd.spawn() { Ok(new_child) => { diff --git a/src/components/layout/ModuleContainer.vue b/src/components/layout/ModuleContainer.vue index 37602c5..04a9da2 100644 --- a/src/components/layout/ModuleContainer.vue +++ b/src/components/layout/ModuleContainer.vue @@ -24,6 +24,7 @@ defineProps<{

未找到模块

diff --git a/src/components/layout/TitleBar.vue b/src/components/layout/TitleBar.vue index 7905129..d102f97 100644 --- a/src/components/layout/TitleBar.vue +++ b/src/components/layout/TitleBar.vue @@ -65,10 +65,43 @@ const maximize = async () => { await tauriWindow?.toggleMaximize() } +// hover 抑制标志:窗口隐藏时设为 true,重新显示后短暂保持 true 再恢复 +// 期间用 CSS 覆盖 :hover 样式,避免按钮残留高亮(webview 隐藏时 mouseleave 不触发) +const hoverSuppressed = ref(true) + const close = async () => { + // 隐藏前立即抑制 hover,避免冻结的 :hover 状态残留到下次显示 + hoverSuppressed.value = true + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } await tauriWindow?.hide() } +// 窗口重新获得焦点时,先保持抑制(防止冻结的 hover 显示), +// 短暂延迟后恢复 hover(让真实鼠标位置重新接管) +if (tauriWindow) { + tauriWindow.onFocusChanged(({ payload: focused }) => { + if (focused) { + hoverSuppressed.value = true + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + // 100ms 后恢复 hover,足够让浏览器重置伪类状态 + setTimeout(() => { + hoverSuppressed.value = false + }, 100) + } else { + hoverSuppressed.value = true + } + }) +} + +// 初始时窗口已显示,恢复 hover +setTimeout(() => { + hoverSuppressed.value = false +}, 200) + const handleBlur = () => { setTimeout(() => { isSearchFocused.value = false @@ -147,22 +180,22 @@ const handleBlur = () => {
-
- - -
- \ No newline at end of file + + + \ No newline at end of file diff --git a/src/components/ui/progress/Progress.vue b/src/components/ui/progress/Progress.vue new file mode 100644 index 0000000..5532af7 --- /dev/null +++ b/src/components/ui/progress/Progress.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/ui/progress/index.ts b/src/components/ui/progress/index.ts new file mode 100644 index 0000000..8598b96 --- /dev/null +++ b/src/components/ui/progress/index.ts @@ -0,0 +1 @@ +export { default as Progress } from "./Progress.vue" diff --git a/src/modules/proxy/ProxyModule.vue b/src/modules/proxy/ProxyModule.vue index 5c2be5b..1ba28d4 100644 --- a/src/modules/proxy/ProxyModule.vue +++ b/src/modules/proxy/ProxyModule.vue @@ -2,7 +2,7 @@ import { Globe, Play, Square, RotateCw, Power, Zap, Plus, RefreshCw, Trash2, Check, AlertCircle, Server, Settings as SettingsIcon, ListChecks, - Upload, Link2, Loader2, Download, Timer, Target, FolderOpen, Copy + Upload, Link2, Loader2, Download, Timer, Target, FolderOpen, Copy, DownloadCloud } from '@lucide/vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { toast } from 'vue-sonner' @@ -21,9 +21,8 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { ScrollArea } from '@/components/ui/scroll-area' import { Separator } from '@/components/ui/separator' import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion' -import { - Select, SelectContent, SelectItem, SelectTrigger, SelectValue -} from '@/components/ui/select' +import { Progress } from '@/components/ui/progress' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, @@ -573,6 +572,97 @@ const handleUpdateKernel = async () => { } } +// ===== 首次安装内核 ===== +const installStageText = computed(() => { + const stage = store.installProgress?.stage + switch (stage) { + case 'downloading': return '正在下载' + case 'extracting': return '正在解压' + case 'replacing': return '正在安装' + case 'done': return '安装完成' + case 'error': return '安装失败' + default: return '' + } +}) + +const installStageColor = computed(() => { + const stage = store.installProgress?.stage + if (stage === 'done') return 'text-emerald-500' + if (stage === 'error') return 'text-destructive' + return 'text-primary' +}) + +/** 进度条显示百分比:有 totalBytes 时用 percent,否则显示已下载 MB 而不显示百分比 */ +const installPercentDisplay = computed(() => { + const p = store.installProgress + if (!p) return 0 + // downloading 阶段用 percent(后端按 0-90 计算) + // extracting=92 / replacing=96 / done=100 / error=0 + if (p.stage === 'downloading' && !p.totalBytes) { + // 无总长度时,前端不可知百分比,进度条用 indeterminate 动画 + return 0 + } + return p.percent +}) + +const installHasTotal = computed(() => store.installProgress?.totalBytes != null) + +const formatMB = (bytes: number) => `${(bytes / 1024 / 1024).toFixed(2)} MB` + +// ===== 首次安装内核 ===== +// 镜像源选择:'__direct' = GitHub 直连(value 不能用空串,reka-ui SelectItem 禁止空 value) +// | 'ghproxy.net' 等预设 key | '__custom' = 自定义 +const MIRROR_PRESETS = [ + { label: 'GitHub 直连', value: '__direct', hint: '需能访问 GitHub,速度最快' }, + { label: 'ghproxy.net', value: 'https://ghproxy.net/', hint: '老牌公益镜像' }, + { label: 'gh-proxy.com', value: 'https://gh-proxy.com/', hint: '公益镜像' }, + { label: 'ghfast.top', value: 'https://ghfast.top/', hint: '较新镜像' }, + { label: '自定义', value: '__custom', hint: '手动输入镜像站前缀' }, +] as const + +const mirrorChoice = ref('__direct') // 默认直连 +const customMirror = ref('') + +const selectedMirrorPrefix = computed(() => { + if (mirrorChoice.value === '__custom') { + // 自定义:保证以 / 结尾,避免拼接错误 + const v = customMirror.value.trim() + if (!v) return '' + return v.endsWith('/') ? v : v + '/' + } + // __direct 映射回空串(后端用空串表示直连) + if (mirrorChoice.value === '__direct') return '' + return mirrorChoice.value +}) + +const handleInstallKernel = async () => { + if (store.installing) return + try { + await store.installKernel(selectedMirrorPrefix.value) + } catch { + // 忽略:watch 已处理 UI 反馈 + } +} + +// 监听安装进度终态,弹 toast 并延时清空进度 +watch( + () => store.installProgress?.stage, + (stage) => { + if (stage === 'done') { + toast.success('内核安装完成', { + description: store.installProgress?.message + }) + // 2 秒后清空进度,让用户看到 100% 终态 + setTimeout(() => store.clearInstallProgress(), 2000) + } else if (stage === 'error') { + toast.error('内核安装失败', { + description: store.installProgress?.message + }) + setTimeout(() => store.clearInstallProgress(), 5000) + } + } +) + // ===== 节点 ===== const refreshProxies = async () => { await loadProxiesWithError() @@ -761,7 +851,8 @@ const saveSettingsForm = async () => { - + +
@@ -775,7 +866,7 @@ const saveSettingsForm = async () => { @click="handleCheckUpdate" > - 检查更新 + 检查更新 @@ -789,7 +880,7 @@ const saveSettingsForm = async () => { 未安装
-
+
当前版本 {{ versionShort }}
@@ -799,14 +890,15 @@ const saveSettingsForm = async () => { {{ kernelUpdateInfo.latestVersion }} - +
@@ -828,9 +920,96 @@ const saveSettingsForm = async () => {
-

- 请将 mihomo.exe 放到 src-tauri/binaries/ 后重启应用,或直接放到上述 cores 目录。 -

+ + +
+
+ + +

+ {{ MIRROR_PRESETS.find(m => m.value === mirrorChoice)?.hint }} +

+
+
+ + +

+ 前缀会拼接到 GitHub 下载链接前,需以 / 结尾(自动补全) +

+
+ +
+ + +
+
+ + + + + {{ installStageText }} + + + {{ store.installProgress.percent }}% + +
+ +
+
+
+

+ + +

+
@@ -865,18 +1044,18 @@ const saveSettingsForm = async () => {
- -