From dbd46f2b76c377ffd2dbc66908ad672eb7c92c71 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Thu, 13 Nov 2025 21:45:04 +0800 Subject: [PATCH 01/26] fix: ruleEditor i18n keys --- src/components/profile/rules-editor-viewer.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/profile/rules-editor-viewer.tsx b/src/components/profile/rules-editor-viewer.tsx index 9ece10982..0eeab9629 100644 --- a/src/components/profile/rules-editor-viewer.tsx +++ b/src/components/profile/rules-editor-viewer.tsx @@ -47,6 +47,7 @@ import { RuleItem } from "@/components/profile/rule-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; +import type { TranslationKey } from "@/types/generated/i18n-keys"; import getSystem from "@/utils/get-system"; import { BaseSearchBox } from "../base/base-search-box"; @@ -249,13 +250,14 @@ const RULE_TYPE_LABEL_KEYS: Record = Object.fromEntries( const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; -const PROXY_POLICY_LABEL_KEYS: Record = +const PROXY_POLICY_LABEL_KEYS: Record = builtinProxyPolicies.reduce( (acc, policy) => { - acc[policy] = `proxy.policies.${policy}`; + acc[policy] = + `proxies.components.enums.policies.${policy}` as TranslationKey; return acc; }, - {} as Record, + {} as Record, ); export const RulesEditorViewer = (props: Props) => { From f1bd56ea13a009b43ec32e5d3fbbf5e70c7761a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:17:55 +0800 Subject: [PATCH 02/26] chore(deps): update cargo dependencies (#5453) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src-tauri/Cargo.lock | 26 +++++++++++++------------- src-tauri/Cargo.toml | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bebb20d2a..861530e0c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1257,7 +1257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -3415,7 +3415,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -5030,7 +5030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -6416,7 +6416,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7402,9 +7402,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" +checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8" dependencies = [ "anyhow", "bytes", @@ -7456,9 +7456,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38" +checksum = "87d6f8cafe6a75514ce5333f115b7b1866e8e68d9672bf4ca89fc0f35697ea9d" dependencies = [ "anyhow", "cargo_toml", @@ -7478,9 +7478,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190" +checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f" dependencies = [ "base64 0.22.1", "brotli", @@ -7505,9 +7505,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f" +checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -9321,7 +9321,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6efd1d19a..14ae5b2f0 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -14,7 +14,7 @@ rust-version = "1.91" identifier = "io.github.clash-verge-rev.clash-verge-rev" [build-dependencies] -tauri-build = { version = "2.5.1", features = [] } +tauri-build = { version = "2.5.2", features = [] } [dependencies] warp = { version = "0.4.2", features = ["server"] } @@ -43,7 +43,7 @@ serde = { version = "1.0.228", features = ["derive"] } reqwest = { version = "0.12.24", features = ["json", "cookies"] } regex = "1.12.2" sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } -tauri = { version = "2.9.2", features = [ +tauri = { version = "2.9.3", features = [ "protocol-asset", "devtools", "tray-icon", From 4e800d165b2aabb64ae4f808a36254991303cee3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:59:10 +0800 Subject: [PATCH 03/26] chore(deps): update npm dependencies (#5455) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- pnpm-lock.yaml | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index db4e538b0..d7b3da756 100644 --- a/package.json +++ b/package.json @@ -71,9 +71,9 @@ "react-dom": "19.2.0", "react-error-boundary": "6.0.0", "react-hook-form": "^7.66.0", - "react-i18next": "16.3.1", + "react-i18next": "16.3.3", "react-markdown": "10.1.0", - "react-router": "^7.9.5", + "react-router": "^7.9.6", "react-virtuoso": "^4.14.1", "swr": "^2.3.6", "tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 314a64ef5..3a4d1b722 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,14 +114,14 @@ importers: specifier: ^7.66.0 version: 7.66.0(react@19.2.0) react-i18next: - specifier: 16.3.1 - version: 16.3.1(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + specifier: 16.3.3 + version: 16.3.3(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.4)(react@19.2.0) react-router: - specifier: ^7.9.5 - version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^7.9.6 + version: 7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-virtuoso: specifier: ^4.14.1 version: 4.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -3634,8 +3634,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@16.3.1: - resolution: {integrity: sha512-HbYaBeA58Hg38OzdEvJp4kLIvk10rp9F9Jq+wNkqtqxDXObtdYMSsQnegWgdUVcpZjZuK9ZxehM+Z9BW2Vqgqw==} + react-i18next@16.3.3: + resolution: {integrity: sha512-IaY2W+ueVd/fe7H6Wj2S4bTuLNChnajFUlZFfCTrTHWzGcOrUHlVzW55oXRSl+J51U8Onn6EvIhQ+Bar9FUcjw==} peerDependencies: i18next: '>= 25.6.2' react: '>= 16.8.0' @@ -3662,8 +3662,8 @@ packages: '@types/react': '>=18' react: '>=18' - react-router@7.9.5: - resolution: {integrity: sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==} + react-router@7.9.6: + resolution: {integrity: sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -8200,7 +8200,7 @@ snapshots: dependencies: react: 19.2.0 - react-i18next@16.3.1(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.3.3(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 @@ -8233,7 +8233,7 @@ snapshots: transitivePeerDependencies: - supports-color - react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router@7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: cookie: 1.0.2 react: 19.2.0 From e41f91006346471f9564e6fc6e578b8a9a07d27b Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:50:18 +0800 Subject: [PATCH 04/26] feat: add infoPlist configuration for macOS build --- Changelog.md | 1 + src-tauri/packages/macos/info_merge.plist | 10 ++++++++++ src-tauri/tauri.macos.conf.json | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src-tauri/packages/macos/info_merge.plist diff --git a/Changelog.md b/Changelog.md index 082960a0b..264ac6303 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,7 @@ ### 🐞 修复问题 - Linux 无法切换 TUN 堆栈 +- macOS service 启动项显示名称(试验性修改)
✨ 新增功能 diff --git a/src-tauri/packages/macos/info_merge.plist b/src-tauri/packages/macos/info_merge.plist new file mode 100644 index 000000000..75bd5abd5 --- /dev/null +++ b/src-tauri/packages/macos/info_merge.plist @@ -0,0 +1,10 @@ + + + + + AssociatedBundleIdentifiers + + io.github.clash-verge-rev.clash-verge-rev.service + + + \ No newline at end of file diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index ef471b8ee..2aa49bbf1 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -28,7 +28,8 @@ "x": 200, "y": 180 } - } + }, + "infoPlist": "packages/macos/info_merge.plist" } } } From 4fb8611e55d2ba38cd25b8ae50ccab68cc275e8e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:38:06 +0800 Subject: [PATCH 05/26] chore(deps): update npm dependencies (#5462) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +-- pnpm-lock.yaml | 96 +++++++++++++++++++++++++------------------------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index d7b3da756..019bd4a01 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "husky": "^9.1.7", "jiti": "^2.6.1", "lint-staged": "^16.2.6", - "meta-json-schema": "^1.19.14", + "meta-json-schema": "^1.19.16", "node-fetch": "^3.3.2", "prettier": "^3.6.2", "sass": "^1.94.0", @@ -120,7 +120,7 @@ "vite": "^7.2.2", "vite-plugin-monaco-editor-esm": "^2.0.2", "vite-plugin-svgr": "^4.5.0", - "vitest": "^4.0.8" + "vitest": "^4.0.9" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a4d1b722..b2b6aeb29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,8 +223,8 @@ importers: specifier: ^16.2.6 version: 16.2.6 meta-json-schema: - specifier: ^1.19.14 - version: 1.19.14 + specifier: ^1.19.16 + version: 1.19.16 node-fetch: specifier: ^3.3.2 version: 3.3.2 @@ -256,8 +256,8 @@ importers: specifier: ^4.5.0 version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) vitest: - specifier: ^4.0.8 - version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + specifier: ^4.0.9 + version: 4.0.9(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) packages: @@ -2029,11 +2029,11 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitest/expect@4.0.8': - resolution: {integrity: sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==} + '@vitest/expect@4.0.9': + resolution: {integrity: sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==} - '@vitest/mocker@4.0.8': - resolution: {integrity: sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==} + '@vitest/mocker@4.0.9': + resolution: {integrity: sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -2043,20 +2043,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.0.8': - resolution: {integrity: sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==} + '@vitest/pretty-format@4.0.9': + resolution: {integrity: sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==} - '@vitest/runner@4.0.8': - resolution: {integrity: sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==} + '@vitest/runner@4.0.9': + resolution: {integrity: sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==} - '@vitest/snapshot@4.0.8': - resolution: {integrity: sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==} + '@vitest/snapshot@4.0.9': + resolution: {integrity: sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==} - '@vitest/spy@4.0.8': - resolution: {integrity: sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==} + '@vitest/spy@4.0.9': + resolution: {integrity: sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==} - '@vitest/utils@4.0.8': - resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} + '@vitest/utils@4.0.9': + resolution: {integrity: sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -3299,8 +3299,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - meta-json-schema@1.19.14: - resolution: {integrity: sha512-A+NSHAfXWn2T225dawVuLXVXrSWhxjRNiG+nS+Cet1Zovslrq2lMqvkIrXhdaK6Gv+VYrEV8rAkYcqAz2pxKMw==} + meta-json-schema@1.19.16: + resolution: {integrity: sha512-Py3XR3VRXs3tAMg3sy7fmex8IU4p4FTxVbF86WTtssWpFcSNbBUjk0QjpdhGrh+9qPMSwCJY1drXnvgDq9XQ7Q==} engines: {node: '>=18', pnpm: '>=9'} micromark-core-commonmark@2.0.3: @@ -4198,18 +4198,18 @@ packages: yaml: optional: true - vitest@4.0.8: - resolution: {integrity: sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==} + vitest@4.0.9: + resolution: {integrity: sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.8 - '@vitest/browser-preview': 4.0.8 - '@vitest/browser-webdriverio': 4.0.8 - '@vitest/ui': 4.0.8 + '@vitest/browser-playwright': 4.0.9 + '@vitest/browser-preview': 4.0.9 + '@vitest/browser-webdriverio': 4.0.9 + '@vitest/ui': 4.0.9 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -6204,43 +6204,43 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitest/expect@4.0.8': + '@vitest/expect@4.0.9': dependencies: '@standard-schema/spec': 1.0.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.0.8 - '@vitest/utils': 4.0.8 + '@vitest/spy': 4.0.9 + '@vitest/utils': 4.0.9 chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': + '@vitest/mocker@4.0.9(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: - '@vitest/spy': 4.0.8 + '@vitest/spy': 4.0.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) - '@vitest/pretty-format@4.0.8': + '@vitest/pretty-format@4.0.9': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.8': + '@vitest/runner@4.0.9': dependencies: - '@vitest/utils': 4.0.8 + '@vitest/utils': 4.0.9 pathe: 2.0.3 - '@vitest/snapshot@4.0.8': + '@vitest/snapshot@4.0.9': dependencies: - '@vitest/pretty-format': 4.0.8 + '@vitest/pretty-format': 4.0.9 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.8': {} + '@vitest/spy@4.0.9': {} - '@vitest/utils@4.0.8': + '@vitest/utils@4.0.9': dependencies: - '@vitest/pretty-format': 4.0.8 + '@vitest/pretty-format': 4.0.9 tinyrainbow: 3.0.3 acorn-jsx@5.3.2(acorn@8.15.0): @@ -7795,7 +7795,7 @@ snapshots: merge2@1.4.1: {} - meta-json-schema@1.19.14: {} + meta-json-schema@1.19.16: {} micromark-core-commonmark@2.0.3: dependencies: @@ -8884,15 +8884,15 @@ snapshots: terser: 5.44.1 yaml: 2.8.1 - vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): + vitest@4.0.9(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): dependencies: - '@vitest/expect': 4.0.8 - '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) - '@vitest/pretty-format': 4.0.8 - '@vitest/runner': 4.0.8 - '@vitest/snapshot': 4.0.8 - '@vitest/spy': 4.0.8 - '@vitest/utils': 4.0.8 + '@vitest/expect': 4.0.9 + '@vitest/mocker': 4.0.9(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.9 + '@vitest/runner': 4.0.9 + '@vitest/snapshot': 4.0.9 + '@vitest/spy': 4.0.9 + '@vitest/utils': 4.0.9 debug: 4.4.3 es-module-lexer: 1.7.0 expect-type: 1.2.2 From 20cb3e49fd5460bae0c0f4de3e006e59d55608c4 Mon Sep 17 00:00:00 2001 From: oomeow Date: Fri, 14 Nov 2025 20:23:25 +0800 Subject: [PATCH 06/26] chore: don't record websocket logs --- src-tauri/src/utils/init.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index 66503ff63..12da3446f 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -68,11 +68,18 @@ pub async fn init_logger() -> Result<()> { Cleanup::KeepLogFiles(log_max_count), ); #[cfg(not(feature = "tracing"))] - let logger = logger.filter(Box::new(NoModuleFilter(&["wry", "tauri"]))); + let logger = logger.filter(Box::new(NoModuleFilter(&[ + "wry", + "tauri", + "tokio_tungstenite", + "tungstenite", + ]))); #[cfg(feature = "tracing")] let logger = logger.filter(Box::new(NoModuleFilter(&[ "wry", "tauri_plugin_mihomo", + "tokio_tungstenite", + "tungstenite", "kode_bridge", ]))); From 8190432903da8fac435c5bac52a61a626ccd3360 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:46:45 +0800 Subject: [PATCH 07/26] fix: update download links for Windows and macOS installers I hate tauri-action update :( --- .github/workflows/autobuild.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 0f978e8a3..3ce0a060f 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -77,20 +77,20 @@ jobs: ### Windows (不再支持Win7) #### 正常版本(推荐) - - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) ### macOS - - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg) + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) ### Linux #### DEB包(Debian系) 使用 apt ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) #### RPM包(Redhat系) 使用 dnf ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) ### FAQ - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) @@ -109,7 +109,7 @@ jobs: body_path: release.txt prerelease: true token: ${{ secrets.GITHUB_TOKEN }} - generate_release_notes: true + generate_release_notes: false clean_old_assets: name: Clean Old Release Assets @@ -566,20 +566,20 @@ jobs: ### Windows (不再支持Win7) #### 正常版本(推荐) - - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) ### macOS - - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg) + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) ### Linux #### DEB包(Debian系) 使用 apt ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) #### RPM包(Redhat系) 使用 dnf ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) ### FAQ - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) From c4e558d377e152b3a516f483c2fb4ee79aa43611 Mon Sep 17 00:00:00 2001 From: wonfen <96291150+wonfen@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:10:56 +0800 Subject: [PATCH 08/26] refactor: replace isahc with reqwest (#5463) --- src-tauri/Cargo.lock | 160 +++------------------------------ src-tauri/Cargo.toml | 4 - src-tauri/src/utils/network.rs | 154 +++++++++++++++---------------- 3 files changed, 88 insertions(+), 230 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 861530e0c..f763b7302 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -145,7 +145,7 @@ dependencies = [ "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", - "parking_lot 0.12.5", + "parking_lot", "percent-encoding", "windows-sys 0.60.2", "wl-clipboard-rs", @@ -967,12 +967,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "castaway" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" - [[package]] name = "castaway" version = "0.2.4" @@ -1133,14 +1127,13 @@ dependencies = [ "futures", "gethostname", "getrandom 0.3.4", - "isahc", "libc", "log", "nanoid", "network-interface", "once_cell", "open", - "parking_lot 0.12.5", + "parking_lot", "percent-encoding", "port_scanner", "regex", @@ -1276,7 +1269,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ - "castaway 0.2.4", + "castaway", "cfg-if", "itoa", "rkyv", @@ -1648,36 +1641,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "curl" -version = "0.4.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc" -dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2 0.6.1", - "windows-sys 0.59.0", -] - -[[package]] -name = "curl-sys" -version = "0.4.83+curl-8.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5830daf304027db10c82632a464879d46a3f7c4ba17a31592657ad16c719b483" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "windows-sys 0.59.0", -] - [[package]] name = "darling" version = "0.21.3" @@ -1723,7 +1686,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.12", + "parking_lot_core", ] [[package]] @@ -1737,7 +1700,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.12", + "parking_lot_core", ] [[package]] @@ -3730,34 +3693,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "isahc" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" -dependencies = [ - "async-channel 1.9.0", - "castaway 0.1.2", - "crossbeam-utils", - "curl", - "curl-sys", - "encoding_rs", - "event-listener 2.5.3", - "futures-lite 1.13.0", - "http 0.2.12", - "log", - "mime", - "once_cell", - "parking_lot 0.11.2", - "polling 2.8.0", - "slab", - "sluice", - "tracing", - "tracing-futures", - "url", - "waker-fn", -] - [[package]] name = "itertools" version = "0.11.0" @@ -3910,7 +3845,7 @@ dependencies = [ "httparse", "interprocess", "libc", - "parking_lot 0.12.5", + "parking_lot", "path-tree", "pin-project-lite", "rand 0.9.2", @@ -4008,7 +3943,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.5.18", + "redox_syscall", ] [[package]] @@ -4020,18 +3955,6 @@ dependencies = [ "zlib-rs", ] -[[package]] -name = "libz-sys" -version = "1.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -4917,7 +4840,7 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" dependencies = [ - "parking_lot_core 0.9.12", + "parking_lot_core", ] [[package]] @@ -5078,17 +5001,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -5096,21 +5008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -5121,7 +5019,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link 0.2.1", ] @@ -6038,15 +5936,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -6993,17 +6882,6 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" -[[package]] -name = "sluice" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" -dependencies = [ - "async-channel 1.9.0", - "futures-core", - "futures-io", -] - [[package]] name = "small_btree" version = "0.1.0" @@ -7084,7 +6962,7 @@ dependencies = [ "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", "raw-window-handle", - "redox_syscall 0.5.18", + "redox_syscall", "wasm-bindgen", "web-sys", "windows-sys 0.59.0", @@ -7141,7 +7019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", - "parking_lot 0.12.5", + "parking_lot", "phf_shared 0.11.3", "precomputed-hash", "serde", @@ -7354,7 +7232,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation 0.3.2", "once_cell", - "parking_lot 0.12.5", + "parking_lot", "raw-window-handle", "scopeguard", "tao-macros", @@ -8169,7 +8047,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot 0.12.5", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.6.1", @@ -8589,16 +8467,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 14ae5b2f0..158efb45a 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -70,10 +70,6 @@ gethostname = "1.1.0" scopeguard = "1.2.0" tauri-plugin-notification = "2.3.3" tokio-stream = "0.1.17" -isahc = { version = "1.7.2", default-features = false, features = [ - "text-decoding", - "parking_lot", -] } backoff = { version = "0.4.0", features = ["tokio"] } compact_str = { version = "0.9.0", features = ["serde"] } tauri-plugin-http = "2.5.4" diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index 400bf21c3..d48bb2a6f 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -1,22 +1,15 @@ use crate::config::Config; use anyhow::Result; use base64::{Engine as _, engine::general_purpose}; -use isahc::config::DnsCache; -use isahc::prelude::*; -use isahc::{HttpClient, config::SslOption}; -use isahc::{ - config::RedirectPolicy, - http::{ - StatusCode, Uri, - header::{HeaderMap, HeaderValue, USER_AGENT}, - }, +use reqwest::{ + Client, Proxy, StatusCode, + header::{HeaderMap, HeaderValue, USER_AGENT}, }; use smartstring::alias::String; use std::time::{Duration, Instant}; use sysproxy::Sysproxy; use tauri::Url; use tokio::sync::Mutex; -use tokio::time::timeout; #[derive(Debug)] pub struct HttpResponse { @@ -55,9 +48,9 @@ pub enum ProxyType { } pub struct NetworkManager { - self_proxy_client: Mutex>, - system_proxy_client: Mutex>, - no_proxy_client: Mutex>, + self_proxy_client: Mutex>, + system_proxy_client: Mutex>, + no_proxy_client: Mutex>, last_connection_error: Mutex>, connection_error_count: Mutex, } @@ -111,41 +104,42 @@ impl NetworkManager { fn build_client( &self, - proxy_uri: Option, + proxy_url: Option, default_headers: HeaderMap, accept_invalid_certs: bool, timeout_secs: Option, - ) -> Result { - { - let mut builder = HttpClient::builder(); + ) -> Result { + let mut builder = Client::builder() + .redirect(reqwest::redirect::Policy::limited(10)) + .tcp_keepalive(Duration::from_secs(60)) + .pool_max_idle_per_host(0) + .pool_idle_timeout(None); - builder = match proxy_uri { - Some(uri) => builder.proxy(Some(uri)), - None => builder.proxy(None), - }; - - for (name, value) in default_headers.iter() { - builder = builder.default_header(name, value); - } - - if accept_invalid_certs { - builder = builder.ssl_options(SslOption::DANGER_ACCEPT_INVALID_CERTS); - } - - if let Some(secs) = timeout_secs { - builder = builder.timeout(Duration::from_secs(secs)); - } - - builder = builder.redirect_policy(RedirectPolicy::Follow); - - // 禁用缓存,不关心连接复用 - builder = builder.connection_cache_size(0); - - // 禁用 DNS 缓存,避免因 DNS 变化导致的问题 - builder = builder.dns_cache(DnsCache::Disable); - - Ok(builder.build()?) + // 设置代理 + if let Some(proxy_str) = proxy_url { + let proxy = Proxy::all(proxy_str)?; + builder = builder.proxy(proxy); + } else { + builder = builder.no_proxy(); } + + builder = builder.default_headers(default_headers); + + // SSL/TLS + if accept_invalid_certs { + builder = builder + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true); + } + + // 超时设置 + if let Some(secs) = timeout_secs { + builder = builder + .timeout(Duration::from_secs(secs)) + .connect_timeout(Duration::from_secs(secs.min(30))); + } + + Ok(builder.build()?) } pub async fn create_request( @@ -154,8 +148,8 @@ impl NetworkManager { timeout_secs: Option, user_agent: Option, accept_invalid_certs: bool, - ) -> Result { - let proxy_uri = match proxy_type { + ) -> Result { + let proxy_url: Option = match proxy_type { ProxyType::None => None, ProxyType::Localhost => { let port = { @@ -165,13 +159,11 @@ impl NetworkManager { None => Config::clash().await.data_arc().get_mixed_port(), } }; - let proxy_scheme = format!("http://127.0.0.1:{port}"); - proxy_scheme.parse::().ok() + Some(format!("http://127.0.0.1:{port}")) } ProxyType::System => { if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { - let proxy_scheme = format!("http://{}:{}", p.host, p.port); - proxy_scheme.parse::().ok() + Some(format!("http://{}:{}", p.host, p.port)) } else { None } @@ -179,16 +171,18 @@ impl NetworkManager { }; let mut headers = HeaderMap::new(); - headers.insert( - USER_AGENT, - HeaderValue::from_str( - &user_agent.unwrap_or_else(|| { - format!("clash-verge/v{}", env!("CARGO_PKG_VERSION")).into() - }), - )?, - ); - let client = self.build_client(proxy_uri, headers, accept_invalid_certs, timeout_secs)?; + // 设置 User-Agent + if let Some(ua) = user_agent { + headers.insert(USER_AGENT, HeaderValue::from_str(ua.as_str())?); + } else { + headers.insert( + USER_AGENT, + HeaderValue::from_str(&format!("clash-verge/v{}", env!("CARGO_PKG_VERSION")))?, + ); + } + + let client = self.build_client(proxy_url, headers, accept_invalid_certs, timeout_secs)?; Ok(client) } @@ -226,37 +220,37 @@ impl NetworkManager { no_auth.to_string() }; + // 创建请求 let client = self .create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs) .await?; - let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20)); - let response = match timeout(timeout_duration, async { - let mut req = isahc::Request::get(&clean_url); + let mut request_builder = client.get(&clean_url); - for (k, v) in extra_headers.iter() { - req = req.header(k, v); - } + for (key, value) in extra_headers.iter() { + request_builder = request_builder.header(key, value); + } - let mut response = client.send_async(req.body(())?).await?; - let status = response.status(); - let headers = response.headers().clone(); - let body = response.text().await?; - Ok::<_, anyhow::Error>(HttpResponse::new(status, headers, body.into())) - }) - .await - { - Ok(res) => res?, - Err(_) => { - self.record_connection_error(&format!("Request interrupted: {}", url)) + let response = match request_builder.send().await { + Ok(resp) => resp, + Err(e) => { + self.record_connection_error(&format!("Request failed: {}", e)) .await; - return Err(anyhow::anyhow!( - "Request interrupted after {}s", - timeout_duration.as_secs() - )); + return Err(anyhow::anyhow!("Request failed: {}", e)); } }; - Ok(response) + let status = response.status(); + let headers = response.headers().clone(); + let body = match response.text().await { + Ok(text) => text.into(), + Err(e) => { + self.record_connection_error(&format!("Failed to read response body: {}", e)) + .await; + return Err(anyhow::anyhow!("Failed to read response body: {}", e)); + } + }; + + Ok(HttpResponse::new(status, headers, body)) } } From f6f288f02ba81a0cf4589188758db35f762da183 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 15 Nov 2025 01:09:22 +0800 Subject: [PATCH 09/26] fix: prevent save yaml return too early to failed get runtime config --- src-tauri/src/utils/help.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index a33b365a4..22361d986 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -75,7 +75,9 @@ pub async fn save_yaml( let path_str = path.as_os_str().to_string_lossy().to_string(); tokio::fs::write(path, yaml_str.as_bytes()) .await - .with_context(|| format!("failed to save file \"{path_str}\"")) + .with_context(|| format!("failed to save file \"{path_str}\""))?; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + Ok(()) } const ALPHABET: [char; 62] = [ From 2b0ba4dc95058ca335ba5acdfdd511c5d2874ca7 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 15 Nov 2025 02:46:34 +0800 Subject: [PATCH 10/26] refactor: streamline config handling and logging mechanisms --- src-tauri/src/cmd/runtime.rs | 14 ++---- src-tauri/src/constants.rs | 12 +---- src-tauri/src/core/manager/config.rs | 67 +++---------------------- src-tauri/src/core/manager/lifecycle.rs | 12 ++--- src-tauri/src/module/lightweight.rs | 5 +- src-tauri/src/utils/logging.rs | 15 ------ 6 files changed, 17 insertions(+), 108 deletions(-) diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index d75e470d0..9ad80f456 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -1,9 +1,6 @@ use super::CmdResult; use crate::{ - cmd::StringifyErr as _, - config::{Config, ConfigType}, - core::CoreManager, - log_err, + cmd::StringifyErr as _, config::Config, core::CoreManager, logging_error, utils::logging::Type, }; use anyhow::{Context as _, anyhow}; use serde_yaml_ng::Mapping; @@ -104,14 +101,9 @@ pub async fn update_proxy_chain_config_in_runtime( { let runtime = Config::runtime().await; runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config)); - runtime.apply(); + // 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply() } - - // 生成新的运行配置文件并通知 Clash 核心重新加载 - let run_path = Config::generate_file(ConfigType::Run) - .await - .stringify_err()?; - log_err!(CoreManager::global().put_configs_force(run_path).await); + logging_error!(Type::Core, CoreManager::global().update_config().await); Ok(()) } diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index a07da7c15..37f0750ad 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -35,8 +35,7 @@ pub mod bypass { pub mod timing { use super::Duration; - pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500); - pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300); + pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(300); pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20); pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2); pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300); @@ -58,15 +57,6 @@ pub mod files { pub const WINDOW_STATE: &str = "window_state.json"; } -pub mod error_patterns { - pub const CONNECTION_ERRORS: &[&str] = &[ - "Failed to create connection", - "The system cannot find the file specified", - "operation timed out", - "connection refused", - ]; -} - pub mod tun { pub const DEFAULT_STACK: &str = "gvisor"; diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index 64283a590..c2d4dca27 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -10,7 +10,6 @@ use anyhow::{Result, anyhow}; use smartstring::alias::String; use std::{path::PathBuf, time::Instant}; use tauri_plugin_mihomo::Error as MihomoError; -use tokio::time::sleep; impl CoreManager { pub async fn use_default_config(&self, error_key: &str, error_msg: &str) -> Result<()> { @@ -37,25 +36,25 @@ impl CoreManager { return Ok((true, String::new())); } - if !self.should_update_config()? { + if !self.should_update_config() { return Ok((true, String::new())); } self.perform_config_update().await } - fn should_update_config(&self) -> Result { + fn should_update_config(&self) -> bool { let now = Instant::now(); let last = self.get_last_update(); if let Some(last_time) = last && now.duration_since(*last_time) < timing::CONFIG_UPDATE_DEBOUNCE { - return Ok(false); + return false; } self.set_last_update(now); - Ok(true) + true } async fn perform_config_update(&self) -> Result<(bool, String)> { @@ -78,22 +77,14 @@ impl CoreManager { } } - pub async fn put_configs_force(&self, path: PathBuf) -> Result<()> { - self.apply_config(path).await - } - - pub(super) async fn apply_config(&self, path: PathBuf) -> Result<()> { - let path_str = dirs::path_to_str(&path)?; - - match self.reload_config(path_str).await { + async fn apply_config(&self, path: PathBuf) -> Result<()> { + let path = dirs::path_to_str(&path)?; + match self.reload_config(path).await { Ok(_) => { Config::runtime().await.apply(); logging!(info, Type::Core, "Configuration applied"); Ok(()) } - Err(err) if Self::should_restart_on_error(&err) => { - self.retry_with_restart(path_str).await - } Err(err) => { Config::runtime().await.discard(); Err(anyhow!("Failed to apply config: {}", err)) @@ -101,54 +92,10 @@ impl CoreManager { } } - async fn retry_with_restart(&self, config_path: &str) -> Result<()> { - if handle::Handle::global().is_exiting() { - return Err(anyhow!("Application exiting")); - } - - logging!(warn, Type::Core, "Restarting core for config reload"); - self.restart_core().await?; - sleep(timing::CONFIG_RELOAD_DELAY).await; - - self.reload_config(config_path).await?; - Config::runtime().await.apply(); - logging!(info, Type::Core, "Configuration applied after restart"); - Ok(()) - } - async fn reload_config(&self, path: &str) -> Result<(), MihomoError> { handle::Handle::mihomo() .await .reload_config(true, path) .await } - - fn should_restart_on_error(err: &MihomoError) -> bool { - match err { - MihomoError::ConnectionFailed | MihomoError::ConnectionLost => true, - MihomoError::Io(io_err) => Self::is_connection_io_error(io_err.kind()), - MihomoError::Reqwest(req_err) => { - req_err.is_connect() - || req_err.is_timeout() - || Self::contains_error_pattern(&req_err.to_string()) - } - MihomoError::FailedResponse(msg) => Self::contains_error_pattern(msg), - _ => false, - } - } - - const fn is_connection_io_error(kind: std::io::ErrorKind) -> bool { - matches!( - kind, - std::io::ErrorKind::ConnectionAborted - | std::io::ErrorKind::ConnectionRefused - | std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::NotFound - ) - } - - fn contains_error_pattern(text: &str) -> bool { - use crate::constants::error_patterns::CONNECTION_ERRORS; - CONNECTION_ERRORS.iter().any(|p| text.contains(p)) - } } diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 75574068c..fe524360a 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -1,5 +1,6 @@ use super::{CoreManager, RunningMode}; -use crate::config::{Config, ConfigType, IVerge}; +use crate::cmd::StringifyErr as _; +use crate::config::{Config, IVerge}; use crate::{ core::{ logger::CLASH_LOGGER, @@ -55,13 +56,8 @@ impl CoreManager { let verge_data = Config::verge().await.latest_arc(); verge_data.save_file().await.map_err(|e| e.to_string())?; - let run_path = Config::generate_file(ConfigType::Run) - .await - .map_err(|e| e.to_string())?; - - self.apply_config(run_path) - .await - .map_err(|e| e.to_string().into()) + self.update_config().await.stringify_err()?; + Ok(()) } async fn prepare_startup(&self) -> Result<()> { diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 25978b163..20e26b144 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -1,12 +1,11 @@ use crate::{ config::Config, core::{handle, timer::Timer, tray::Tray}, - log_err, logging, + logging, process::AsyncHandler, utils::logging::Type, }; -#[cfg(target_os = "macos")] use crate::logging_error; use crate::utils::window_manager::WindowManager; @@ -184,7 +183,7 @@ fn cancel_window_close_listener() { fn setup_webview_focus_listener() { if let Some(window) = handle::Handle::get_window() { let handler_id = window.listen("tauri://focus", move |_event| { - log_err!(cancel_light_weight_timer()); + logging_error!(Type::Lightweight, cancel_light_weight_timer()); logging!( debug, Type::Lightweight, diff --git a/src-tauri/src/utils/logging.rs b/src-tauri/src/utils/logging.rs index 7b5de419b..f78c9a55a 100644 --- a/src-tauri/src/utils/logging.rs +++ b/src-tauri/src/utils/logging.rs @@ -65,21 +65,6 @@ macro_rules! error { }; } -#[macro_export] -macro_rules! log_err { - ($result: expr) => { - if let Err(err) = $result { - log::error!(target: "app", "{err}"); - } - }; - - ($result: expr, $err_str: expr) => { - if let Err(_) = $result { - log::error!(target: "app", "{}", $err_str); - } - }; -} - /// wrap the anyhow error /// transform the error to String #[macro_export] From a6cb903fe67f8ace3acd14d4df2ed4717c5fc3ce Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:56:58 +0800 Subject: [PATCH 11/26] refactor: simplify function return types and remove unnecessary wraps --- src-tauri/Cargo.toml | 1 + src-tauri/src/cmd/app.rs | 10 ++++------ src-tauri/src/cmd/network.rs | 11 ++++------- src-tauri/src/cmd/system.rs | 8 ++++---- src-tauri/src/cmd/uwp.rs | 1 + src-tauri/src/config/config.rs | 4 +--- src-tauri/src/config/prfitem.rs | 1 + src-tauri/src/core/manager/lifecycle.rs | 5 ++++- src-tauri/src/core/manager/state.rs | 3 +-- src-tauri/src/core/service.rs | 6 +++--- src-tauri/src/core/sysopt.rs | 3 +-- src-tauri/src/core/tray/mod.rs | 10 +++++----- src-tauri/src/lib.rs | 8 ++------ src-tauri/src/module/sysinfo.rs | 2 +- src-tauri/src/process/guard.rs | 15 ++------------- src-tauri/src/utils/resolve/mod.rs | 4 ++-- 16 files changed, 37 insertions(+), 55 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 158efb45a..d66363973 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -254,3 +254,4 @@ cloned_instead_of_copied = "deny" unnecessary_self_imports = "deny" unused_trait_names = "deny" wildcard_imports = "deny" +unnecessary_wraps = "deny" diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 4a806acb7..f7cf77fb9 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -84,8 +84,8 @@ pub async fn restart_app() -> CmdResult<()> { /// 获取便携版标识 #[tauri::command] -pub fn get_portable_flag() -> CmdResult { - Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false)) +pub fn get_portable_flag() -> bool { + *dirs::PORTABLE_FLAG.get().unwrap_or(&false) } /// 获取应用目录 @@ -241,16 +241,14 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult CmdResult<()> { +pub fn notify_ui_ready() { logging!(info, Type::Cmd, "前端UI已准备就绪"); ui::mark_ui_ready(); - Ok(()) } /// UI加载阶段 #[tauri::command] -pub fn update_ui_stage(stage: UiReadyStage) -> CmdResult<()> { +pub fn update_ui_stage(stage: UiReadyStage) { logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage); ui::update_ui_ready_stage(stage); - Ok(()) } diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index be2fccff8..d6a9b5e2c 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -3,6 +3,7 @@ use crate::cmd::StringifyErr as _; use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery}; use crate::process::AsyncHandler; use crate::{logging, utils::logging::Type}; +use gethostname::gethostname; use network_interface::NetworkInterface; use serde_yaml_ng::Mapping; @@ -61,11 +62,9 @@ pub async fn get_auto_proxy() -> CmdResult { /// 获取系统主机名 #[tauri::command] -pub fn get_system_hostname() -> CmdResult { - use gethostname::gethostname; - +pub fn get_system_hostname() -> String { // 获取系统主机名,处理可能的非UTF-8字符 - let hostname = match gethostname().into_string() { + match gethostname().into_string() { Ok(name) => name, Err(os_string) => { // 对于包含非UTF-8的主机名,使用调试格式化 @@ -73,9 +72,7 @@ pub fn get_system_hostname() -> CmdResult { // 去掉可能存在的引号 fallback.trim_matches('"').to_string() } - }; - - Ok(hostname) + } } /// 获取网络接口列表 diff --git a/src-tauri/src/cmd/system.rs b/src-tauri/src/cmd/system.rs index 27a4f5f6c..8b282ff3f 100644 --- a/src-tauri/src/cmd/system.rs +++ b/src-tauri/src/cmd/system.rs @@ -53,12 +53,12 @@ pub async fn get_running_mode() -> Result, String> { /// 获取应用的运行时间(毫秒) #[tauri::command] -pub fn get_app_uptime() -> CmdResult { - Ok(APP_START_TIME.elapsed().as_millis()) +pub fn get_app_uptime() -> u128 { + APP_START_TIME.elapsed().as_millis() } /// 检查应用是否以管理员身份运行 #[tauri::command] -pub fn is_admin() -> CmdResult { - Ok(*APPS_RUN_AS_ADMIN) +pub fn is_admin() -> bool { + *APPS_RUN_AS_ADMIN } diff --git a/src-tauri/src/cmd/uwp.rs b/src-tauri/src/cmd/uwp.rs index 779029583..d87254880 100644 --- a/src-tauri/src/cmd/uwp.rs +++ b/src-tauri/src/cmd/uwp.rs @@ -17,6 +17,7 @@ mod platform { mod platform { use super::CmdResult; + #[allow(clippy::unnecessary_wraps)] pub const fn invoke_uwp_tool() -> CmdResult { Ok(()) } diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index ff4902262..25a454720 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -57,9 +57,7 @@ impl Config { Self::ensure_default_profile_items().await?; // init Tun mode - if !cmd::system::is_admin().unwrap_or_default() - && service::is_service_available().await.is_err() - { + if !cmd::system::is_admin() && service::is_service_available().await.is_err() { let verge = Self::verge().await; verge.edit_draft(|d| { d.enable_tun_mode = Some(false); diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index bdbac0183..f74fa0fda 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -581,6 +581,7 @@ impl PrfItem { } // 向前兼容,默认为订阅启用自动更新 +#[allow(clippy::unnecessary_wraps)] const fn default_allow_auto_update() -> Option { Some(true) } diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index fe524360a..a264f29e9 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -27,7 +27,10 @@ impl CoreManager { match *self.get_running_mode() { RunningMode::Service => self.stop_core_by_service().await, - RunningMode::Sidecar => self.stop_core_by_sidecar(), + RunningMode::Sidecar => { + self.stop_core_by_sidecar(); + Ok(()) + } RunningMode::NotRunning => Ok(()), } } diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index 520c1f2a1..ca2f7db35 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -96,7 +96,7 @@ impl CoreManager { Ok(()) } - pub(super) fn stop_core_by_sidecar(&self) -> Result<()> { + pub(super) fn stop_core_by_sidecar(&self) { logging!(info, Type::Core, "Stopping sidecar"); defer! { self.set_running_mode(RunningMode::NotRunning); @@ -106,7 +106,6 @@ impl CoreManager { drop(child); logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid); } - Ok(()) } pub(super) async fn start_core_by_service(&self) -> Result<()> { diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 892294d95..9307015ab 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -456,12 +456,12 @@ impl ServiceManager { Self(ServiceStatus::Unavailable("Need Checks".into())) } - pub const fn config() -> Option { - Some(clash_verge_service_ipc::IpcConfig { + pub const fn config() -> clash_verge_service_ipc::IpcConfig { + clash_verge_service_ipc::IpcConfig { default_timeout: Duration::from_millis(30), retry_delay: Duration::from_millis(250), max_retries: 6, - }) + } } pub async fn init(&mut self) -> Result<()> { diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index bd5383d97..e9e1ade55 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -100,13 +100,12 @@ impl Sysopt { self.initialed.load(Ordering::SeqCst) } - pub fn init_guard_sysproxy(&self) -> Result<()> { + pub fn init_guard_sysproxy(&self) { // 使用事件驱动代理管理器 let proxy_manager = EventDrivenProxyManager::global(); proxy_manager.notify_app_started(); logging!(info, Type::Core, "已启用事件驱动代理守卫"); - Ok(()) } /// init the sysproxy diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index ce29aecc0..c9820193d 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -301,8 +301,8 @@ impl Tray { let verge = Config::verge().await.latest_arc(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); - let tun_mode_available = cmd::system::is_admin().unwrap_or_default() - || service::is_service_available().await.is_ok(); + let tun_mode_available = + cmd::system::is_admin() || service::is_service_available().await.is_ok(); let mode = { Config::clash() .await @@ -640,7 +640,7 @@ fn create_subcreate_proxy_menu_item( current_profile_selected: &[PrfSelected], proxy_group_order_map: Option>, proxy_nodes_data: Result, -) -> Result>> { +) -> Vec> { let proxy_submenus: Vec> = { let mut submenus: Vec<(String, usize, Submenu)> = Vec::new(); @@ -767,7 +767,7 @@ fn create_subcreate_proxy_menu_item( .map(|(_, _, submenu)| submenu) .collect() }; - Ok(proxy_submenus) + proxy_submenus } fn create_proxy_menu_item( @@ -955,7 +955,7 @@ async fn create_tray_menu( ¤t_profile_selected, proxy_group_order_map, proxy_nodes_data.map_err(anyhow::Error::from), - )?; + ); let (proxies_menu, inline_proxy_items) = create_proxy_menu_item( app_handle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c3b374ca0..93c38d508 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -82,7 +82,7 @@ mod app_init { } /// Setup deep link handling - pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box> { + pub fn setup_deep_links(app: &tauri::App) { #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { logging!(info, Type::Setup, "注册深层链接..."); @@ -99,8 +99,6 @@ mod app_init { } }); }); - - Ok(()) } /// Setup autostart plugin @@ -246,9 +244,7 @@ pub fn run() { logging!(error, Type::Setup, "Failed to setup autostart: {}", e); } - if let Err(e) = app_init::setup_deep_links(app) { - logging!(error, Type::Setup, "Failed to setup deep links: {}", e); - } + app_init::setup_deep_links(app); if let Err(e) = app_init::setup_window_state(app) { logging!(error, Type::Setup, "Failed to setup window state: {}", e); diff --git a/src-tauri/src/module/sysinfo.rs b/src-tauri/src/module/sysinfo.rs index 74ebe1f3e..431eebaca 100644 --- a/src-tauri/src/module/sysinfo.rs +++ b/src-tauri/src/module/sysinfo.rs @@ -44,7 +44,7 @@ impl PlatformSpecification { // 使用默认值避免在同步上下文中执行异步操作 let running_mode = "NotRunning".to_string(); - let is_admin = system::is_admin().unwrap_or_default(); + let is_admin = system::is_admin(); Self { system_name, diff --git a/src-tauri/src/process/guard.rs b/src-tauri/src/process/guard.rs index d1a41ab50..73986cdb6 100644 --- a/src-tauri/src/process/guard.rs +++ b/src-tauri/src/process/guard.rs @@ -1,22 +1,12 @@ -use anyhow::Result; use tauri_plugin_shell::process::CommandChild; -use crate::{logging, utils::logging::Type}; - #[derive(Debug)] pub struct CommandChildGuard(Option); impl Drop for CommandChildGuard { #[inline] fn drop(&mut self) { - if let Err(err) = self.kill() { - logging!( - error, - Type::Service, - "Failed to kill child process: {}", - err - ); - } + self.kill(); } } @@ -27,11 +17,10 @@ impl CommandChildGuard { } #[inline] - pub fn kill(&mut self) -> Result<()> { + pub fn kill(&mut self) { if let Some(child) = self.0.take() { let _ = child.kill(); } - Ok(()) } #[inline] diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index b855b35c4..40971b0b4 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -153,7 +153,7 @@ pub(super) async fn init_verge_config() { } pub(super) async fn init_service_manager() { - clash_verge_service_ipc::set_config(ServiceManager::config()).await; + clash_verge_service_ipc::set_config(Some(ServiceManager::config())).await; if !is_service_ipc_path_exists() { return; } @@ -174,7 +174,7 @@ pub(super) async fn init_system_proxy() { } pub(super) fn init_system_proxy_guard() { - logging_error!(Type::Setup, sysopt::Sysopt::global().init_guard_sysproxy()); + sysopt::Sysopt::global().init_guard_sysproxy(); } pub(super) async fn refresh_tray_menu() { From 7f2bced0d410cc768ae79d7d7d23bee092fc88b7 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:58:52 +0800 Subject: [PATCH 12/26] fix: remove unnecessary exit statement from pre-push script --- .husky/pre-push | 1 - 1 file changed, 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index 43ffed41f..4f2a6a6b7 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -40,4 +40,3 @@ else fi echo "[pre-push] All checks passed." -exit 0 From 2eb5b5802d691bb2814010d5a15b53db6224d659 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:10:12 +0800 Subject: [PATCH 13/26] chore(deps): update dependency @types/react to v19.2.5 (#5465) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 150 ++++++++++++++++++++++++------------------------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 019bd4a01..b68ed8986 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/node": "^24.10.1", - "@types/react": "19.2.4", + "@types/react": "19.2.5", "@types/react-dom": "19.2.3", "@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-react-swc": "^4.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2b6aeb29..02de2a8dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: version: 3.2.2(react@19.2.0) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.4)(react@19.2.0) + version: 11.14.0(@types/react@19.2.5)(react@19.2.0) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) '@juggle/resize-observer': specifier: ^3.4.0 version: 3.4.0 @@ -31,16 +31,16 @@ importers: version: 4.7.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/icons-material': specifier: ^7.3.5 - version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) '@mui/lab': specifier: 7.0.0-beta.17 - version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/material': specifier: ^7.3.5 - version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/x-data-grid': specifier: ^8.18.0 - version: 8.18.0(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 8.18.0(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tauri-apps/api': specifier: 2.9.0 version: 2.9.0 @@ -118,7 +118,7 @@ importers: version: 16.3.3(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.2.4)(react@19.2.0) + version: 10.1.0(@types/react@19.2.5)(react@19.2.0) react-router: specifier: ^7.9.6 version: 7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -157,11 +157,11 @@ importers: specifier: ^24.10.1 version: 24.10.1 '@types/react': - specifier: 19.2.4 - version: 19.2.4 + specifier: 19.2.5 + version: 19.2.5 '@types/react-dom': specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.4) + version: 19.2.3(@types/react@19.2.5) '@vitejs/plugin-legacy': specifier: ^7.2.1 version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) @@ -1850,8 +1850,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@19.2.4': - resolution: {integrity: sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==} + '@types/react@19.2.5': + resolution: {integrity: sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -5077,7 +5077,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0)': + '@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 @@ -5089,7 +5089,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 transitivePeerDependencies: - supports-color @@ -5103,18 +5103,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 transitivePeerDependencies: - supports-color @@ -5397,39 +5397,39 @@ snapshots: '@mui/core-downloads-tracker@7.3.5': {} - '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.5) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@types/react': 19.2.5 - '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@mui/core-downloads-tracker': 7.3.5 - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.5) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@19.2.4) + '@types/react-transition-group': 4.4.12(@types/react@19.2.5) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 @@ -5438,20 +5438,20 @@ snapshots: react-is: 19.2.0 react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@types/react': 19.2.5 - '@mui/private-theming@7.3.5(@types/react@19.2.4)(react@19.2.0)': + '@mui/private-theming@7.3.5(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0)': + '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 @@ -5461,77 +5461,77 @@ snapshots: prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) - '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/private-theming': 7.3.5(@types/react@19.2.4)(react@19.2.0) - '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/private-theming': 7.3.5(@types/react@19.2.5)(react@19.2.0) + '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.5) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@types/react': 19.2.5 - '@mui/types@7.4.8(@types/react@19.2.4)': + '@mui/types@7.4.8(@types/react@19.2.5)': dependencies: '@babel/runtime': 7.28.4 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@mui/utils@7.3.5(@types/react@19.2.4)(react@19.2.0)': + '@mui/utils@7.3.5(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/types': 7.4.8(@types/react@19.2.4) + '@mui/types': 7.4.8(@types/react@19.2.5) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-is: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@mui/x-data-grid@8.18.0(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/x-data-grid@8.18.0(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) - '@mui/x-internals': 8.18.0(@types/react@19.2.4)(react@19.2.0) - '@mui/x-virtualizer': 0.2.8(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) + '@mui/x-internals': 8.18.0(@types/react@19.2.5)(react@19.2.0) + '@mui/x-virtualizer': 0.2.8(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.18.0(@types/react@19.2.4)(react@19.2.0)': + '@mui/x-internals@8.18.0(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) react: 19.2.0 reselect: 5.1.1 use-sync-external-store: 1.6.0(react@19.2.0) transitivePeerDependencies: - '@types/react' - '@mui/x-virtualizer@0.2.8(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/x-virtualizer@0.2.8(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) - '@mui/x-internals': 8.18.0(@types/react@19.2.4)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) + '@mui/x-internals': 8.18.0(@types/react@19.2.5)(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -6007,15 +6007,15 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.2.3(@types/react@19.2.4)': + '@types/react-dom@19.2.3(@types/react@19.2.5)': dependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@types/react-transition-group@4.4.12(@types/react@19.2.4)': + '@types/react-transition-group@4.4.12(@types/react@19.2.5)': dependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@types/react@19.2.4': + '@types/react@19.2.5': dependencies: csstype: 3.1.3 @@ -8215,11 +8215,11 @@ snapshots: react-is@19.2.0: {} - react-markdown@10.1.0(@types/react@19.2.4)(react@19.2.0): + react-markdown@10.1.0(@types/react@19.2.5)(react@19.2.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.4 + '@types/react': 19.2.5 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 From 40db9284365b606adee7e9ce692680ba0a3759ed Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:34:06 +0800 Subject: [PATCH 14/26] fix: suppress error from deep link registration in setup_plugins --- src-tauri/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 93c38d508..365afc15a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -86,7 +86,7 @@ mod app_init { #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { logging!(info, Type::Setup, "注册深层链接..."); - app.deep_link().register_all()?; + let _ = app.deep_link().register_all(); } app.deep_link().on_open_url(|event| { From 55ac25b1a69f51c48def07caccdacc45211d2fbf Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sat, 15 Nov 2025 15:51:03 +0800 Subject: [PATCH 15/26] fix(editor): restore syntax highlighting for JS/CSS in Monaco editor --- src/components/profile/editor-viewer.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx index 64f2e1675..e6962f321 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/src/components/profile/editor-viewer.tsx @@ -25,6 +25,15 @@ import { ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import pac from "types-pac/pac.d.ts?raw"; +// Ensure Monaco language contributions are registered for proper syntax highlighting. +// - basic-languages registers the language ids and tokenizers +// - language/* contributions wire up workers and rich language features +// Without these, non-YAML editors (e.g. javascript/css) may appear uncolored. +import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution"; +import "monaco-editor/esm/vs/language/typescript/monaco.contribution"; +import "monaco-editor/esm/vs/basic-languages/css/css.contribution"; +import "monaco-editor/esm/vs/language/css/monaco.contribution"; + import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import debounce from "@/utils/debounce"; From 0b641992e76dafe5ce3e554053ed23078ca6a761 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sat, 15 Nov 2025 16:37:53 +0800 Subject: [PATCH 16/26] refactor(async-proxy): remove unnecessary Result wrappers from Windows registry helpers --- src-tauri/src/core/async_proxy_query.rs | 34 +++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs index f0e579f6f..3e99eb44e 100644 --- a/src-tauri/src/core/async_proxy_query.rs +++ b/src-tauri/src/core/async_proxy_query.rs @@ -90,14 +90,12 @@ impl AsyncProxyQuery { #[cfg(target_os = "windows")] async fn get_auto_proxy_impl() -> Result { // Windows: 从注册表读取PAC配置 - AsyncHandler::spawn_blocking(move || -> Result { - Self::get_pac_config_from_registry() - }) - .await? + let proxy = AsyncHandler::spawn_blocking(Self::get_pac_config_from_registry).await?; + Ok(proxy) } #[cfg(target_os = "windows")] - fn get_pac_config_from_registry() -> Result { + fn get_pac_config_from_registry() -> AsyncAutoproxy { use std::ptr; use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; @@ -114,7 +112,7 @@ impl AsyncProxyQuery { if result != 0 { logging!(debug, Type::Network, "无法打开注册表项"); - return Ok(AsyncAutoproxy::default()); + return AsyncAutoproxy::default(); } // 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用) @@ -174,13 +172,13 @@ impl AsyncProxyQuery { pac_url = "auto-detect".into(); } - Ok(AsyncAutoproxy { + AsyncAutoproxy { enable: true, url: pac_url, - }) + } } else { logging!(debug, Type::Network, "PAC配置未启用"); - Ok(AsyncAutoproxy::default()) + AsyncAutoproxy::default() } } } @@ -286,14 +284,12 @@ impl AsyncProxyQuery { #[cfg(target_os = "windows")] async fn get_system_proxy_impl() -> Result { // Windows: 使用注册表直接读取代理设置 - AsyncHandler::spawn_blocking(move || -> Result { - Self::get_system_proxy_from_registry() - }) - .await? + let sys = AsyncHandler::spawn_blocking(Self::get_system_proxy_from_registry).await?; + Ok(sys) } #[cfg(target_os = "windows")] - fn get_system_proxy_from_registry() -> Result { + fn get_system_proxy_from_registry() -> AsyncSysproxy { use std::ptr; use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; @@ -309,7 +305,7 @@ impl AsyncProxyQuery { RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); if result != 0 { - return Ok(AsyncSysproxy::default()); + return AsyncSysproxy::default(); } // 检查代理是否启用 @@ -329,7 +325,7 @@ impl AsyncProxyQuery { if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 { RegCloseKey(hkey); - return Ok(AsyncSysproxy::default()); + return AsyncSysproxy::default(); } // 读取代理服务器设置 @@ -398,14 +394,14 @@ impl AsyncProxyQuery { "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}" ); - Ok(AsyncSysproxy { + AsyncSysproxy { enable: true, host, port, bypass: bypass_list, - }) + } } else { - Ok(AsyncSysproxy::default()) + AsyncSysproxy::default() } } } From 272c2f34673a5d47d883c4a596d63675e9743f86 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sat, 15 Nov 2025 16:43:56 +0800 Subject: [PATCH 17/26] Revert "fix(editor): restore syntax highlighting for JS/CSS in Monaco editor" This reverts commit 55ac25b1a69f51c48def07caccdacc45211d2fbf. vite-plugin-monaco-editor-esm has handled it --- src/components/profile/editor-viewer.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx index e6962f321..64f2e1675 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/src/components/profile/editor-viewer.tsx @@ -25,15 +25,6 @@ import { ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import pac from "types-pac/pac.d.ts?raw"; -// Ensure Monaco language contributions are registered for proper syntax highlighting. -// - basic-languages registers the language ids and tokenizers -// - language/* contributions wire up workers and rich language features -// Without these, non-YAML editors (e.g. javascript/css) may appear uncolored. -import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution"; -import "monaco-editor/esm/vs/language/typescript/monaco.contribution"; -import "monaco-editor/esm/vs/basic-languages/css/css.contribution"; -import "monaco-editor/esm/vs/language/css/monaco.contribution"; - import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import debounce from "@/utils/debounce"; From e021e45e9a13d4f757dc7ef71cc28cbe25d69a26 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sat, 15 Nov 2025 20:20:27 +0800 Subject: [PATCH 18/26] fix(backup): correctly restore CSS injection from verge.yaml backups --- src-tauri/src/feat/backup.rs | 71 ++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 055b1d680..f24925951 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -1,10 +1,11 @@ use crate::{ config::{Config, IVerge}, core::backup, - logging, logging_error, + logging, process::AsyncHandler, utils::{ - dirs::{PathBufExec as _, app_home_dir, local_backup_dir}, + dirs::{PathBufExec as _, app_home_dir, local_backup_dir, verge_path}, + help, logging::Type, }, }; @@ -24,6 +25,38 @@ pub struct LocalBackupFile { pub content_length: u64, } +/// Load restored verge.yaml from disk, merge back WebDAV creds, save, and sync memory. +async fn finalize_restored_verge_config( + webdav_url: Option, + webdav_username: Option, + webdav_password: Option, +) -> Result<()> { + // Do NOT silently fallback to defaults; a broken/missing verge.yaml means restore failed. + // Propagate the error so the UI/user can react accordingly. + let mut restored = help::read_yaml::(&verge_path()?).await?; + restored.webdav_url = webdav_url; + restored.webdav_username = webdav_username; + restored.webdav_password = webdav_password; + restored.save_file().await?; + + let verge_draft = Config::verge().await; + verge_draft.edit_draft(|d| { + *d = restored.clone(); + }); + verge_draft.apply(); + + // Ensure side-effects (flags, tray, sysproxy, hotkeys, auto-backup refresh, etc.) run. + // Use not_save_file = true to avoid extra I/O (we already persisted the restored file). + if let Err(err) = super::patch_verge(&restored, true).await { + logging!( + error, + Type::Backup, + "Failed to apply restored verge config: {err:#?}" + ); + } + Ok(()) +} + /// Create a backup and upload to WebDAV pub async fn create_backup_and_upload_webdav() -> Result<()> { let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| { @@ -103,22 +136,10 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??; let mut zip = zip::ZipArchive::new(file)?; zip.extract(app_home_dir()?)?; - logging_error!( - Type::Backup, - super::patch_verge( - &IVerge { - webdav_url, - webdav_username, - webdav_password, - ..IVerge::default() - }, - false - ) - .await - ); - // 最后删除临时文件 - backup_storage_path.remove_if_exists().await?; - Ok(()) + let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await; + // Finally remove the temp file (attempt cleanup even if finalize fails) + let _ = backup_storage_path.remove_if_exists().await; + res } /// Create a backup and save to local storage @@ -264,19 +285,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??; let mut zip = zip::ZipArchive::new(file)?; zip.extract(app_home_dir()?)?; - logging_error!( - Type::Backup, - super::patch_verge( - &IVerge { - webdav_url, - webdav_username, - webdav_password, - ..IVerge::default() - }, - false - ) - .await - ); + finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?; Ok(()) } From 8654bad6b0f7f0de6ca7aad7a88cad27115ea2e4 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Sat, 15 Nov 2025 20:51:36 +0800 Subject: [PATCH 19/26] refactor: migrate proxy setting guard to sysproxy-rs crate (#5287) * Refactor proxy management: Remove EventDrivenProxyManager and async_proxy_query - Removed the EventDrivenProxyManager and its related event-driven proxy management logic. - Replaced usages of AsyncProxyQuery with direct calls to sysproxy for fetching system and auto proxy configurations. - Cleaned up constants by removing unused default proxy host. - Updated sysopt to eliminate calls to the removed EventDrivenProxyManager. - Adjusted logging and proxy state management to reflect the removal of event-driven architecture. - Simplified the core module structure by removing unnecessary module imports. * refactor: remove bypass module for cleaner network configuration * feat: update sysproxy to version 0.4.0 and add guard feature; refactor sysopt for improved proxy management * feat(windows/sysproxy): unify guard-type handling and auto-restore drift * refactor(config): remove commented-out code for SysProxy update flag --------- Co-authored-by: Slinetrac --- src-tauri/Cargo.lock | 5 +- src-tauri/Cargo.toml | 4 +- src-tauri/src/cmd/network.rs | 34 +- src-tauri/src/constants.rs | 13 - src-tauri/src/core/async_proxy_query.rs | 561 ----------------------- src-tauri/src/core/event_driven_proxy.rs | 548 ---------------------- src-tauri/src/core/mod.rs | 4 +- src-tauri/src/core/sysopt.rs | 137 ++++-- src-tauri/src/feat/config.rs | 13 +- src-tauri/src/feat/window.rs | 2 - src-tauri/src/lib.rs | 3 +- src-tauri/src/utils/resolve/mod.rs | 6 +- 12 files changed, 132 insertions(+), 1198 deletions(-) delete mode 100644 src-tauri/src/core/async_proxy_query.rs delete mode 100644 src-tauri/src/core/event_driven_proxy.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f763b7302..80e7c3a14 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7151,13 +7151,14 @@ dependencies = [ [[package]] name = "sysproxy" -version = "0.3.1" -source = "git+https://github.com/clash-verge-rev/sysproxy-rs#ea6e5b5bcef32025e1df914d663eea8558afacb2" +version = "0.4.0" +source = "git+https://github.com/clash-verge-rev/sysproxy-rs#0f844dd2639b0ac74da4548b1325335844947420" dependencies = [ "interfaces", "iptools", "log", "thiserror 2.0.17", + "tokio", "url", "windows 0.62.2", "winreg 0.55.0", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d66363973..e3bcc8516 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,7 +42,9 @@ tokio = { version = "1.48.0", features = [ serde = { version = "1.0.228", features = ["derive"] } reqwest = { version = "0.12.24", features = ["json", "cookies"] } regex = "1.12.2" -sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } +sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [ + "guard", +] } tauri = { version = "2.9.3", features = [ "protocol-asset", "devtools", diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index d6a9b5e2c..ba780962a 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -1,7 +1,5 @@ use super::CmdResult; use crate::cmd::StringifyErr as _; -use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery}; -use crate::process::AsyncHandler; use crate::{logging, utils::logging::Type}; use gethostname::gethostname; use network_interface::NetworkInterface; @@ -12,23 +10,23 @@ use serde_yaml_ng::Mapping; pub async fn get_sys_proxy() -> CmdResult { logging!(debug, Type::Network, "异步获取系统代理配置"); - let current = AsyncProxyQuery::get_system_proxy().await; + let sys_proxy = sysproxy::Sysproxy::get_system_proxy().stringify_err()?; let mut map = Mapping::new(); - map.insert("enable".into(), current.enable.into()); + map.insert("enable".into(), sys_proxy.enable.into()); map.insert( "server".into(), - format!("{}:{}", current.host, current.port).into(), + format!("{}:{}", sys_proxy.host, sys_proxy.port).into(), ); - map.insert("bypass".into(), current.bypass.into()); + map.insert("bypass".into(), sys_proxy.bypass.into()); logging!( debug, Type::Network, "返回系统代理配置: enable={}, {}:{}", - current.enable, - current.host, - current.port + sys_proxy.enable, + sys_proxy.host, + sys_proxy.port ); Ok(map) } @@ -36,26 +34,18 @@ pub async fn get_sys_proxy() -> CmdResult { /// 获取自动代理配置 #[tauri::command] pub async fn get_auto_proxy() -> CmdResult { - logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)"); - - let proxy_manager = EventDrivenProxyManager::global(); - - let current = proxy_manager.get_auto_proxy_cached().await; - // 异步请求更新,立即返回缓存数据 - AsyncHandler::spawn(move || async move { - let _ = proxy_manager.get_auto_proxy_async().await; - }); + let auto_proxy = sysproxy::Autoproxy::get_auto_proxy().stringify_err()?; let mut map = Mapping::new(); - map.insert("enable".into(), current.enable.into()); - map.insert("url".into(), current.url.clone().into()); + map.insert("enable".into(), auto_proxy.enable.into()); + map.insert("url".into(), auto_proxy.url.clone().into()); logging!( debug, Type::Network, "返回自动代理配置(缓存): enable={}, url={}", - current.enable, - current.url + auto_proxy.enable, + auto_proxy.url ); Ok(map) } diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 37f0750ad..93ef8e872 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -1,7 +1,6 @@ use std::time::Duration; pub mod network { - pub const DEFAULT_PROXY_HOST: &str = "127.0.0.1"; pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097"; pub mod ports { @@ -20,18 +19,6 @@ pub mod network { } } -pub mod bypass { - #[cfg(target_os = "windows")] - pub const DEFAULT: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;"; - - #[cfg(target_os = "linux")] - pub const DEFAULT: &str = - "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1"; - - #[cfg(target_os = "macos")] - pub const DEFAULT: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; -} - pub mod timing { use super::Duration; diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs deleted file mode 100644 index 3e99eb44e..000000000 --- a/src-tauri/src/core/async_proxy_query.rs +++ /dev/null @@ -1,561 +0,0 @@ -#[cfg(target_os = "windows")] -use crate::process::AsyncHandler; -use crate::{logging, utils::logging::Type}; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use tokio::time::{Duration, timeout}; - -#[cfg(target_os = "linux")] -use anyhow::anyhow; -#[cfg(not(target_os = "windows"))] -use tokio::process::Command; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct AsyncAutoproxy { - pub enable: bool, - pub url: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AsyncSysproxy { - pub enable: bool, - pub host: String, - pub port: u16, - pub bypass: String, -} - -impl Default for AsyncSysproxy { - fn default() -> Self { - Self { - enable: false, - host: "127.0.0.1".into(), - port: 7897, - bypass: String::new(), - } - } -} - -pub struct AsyncProxyQuery; - -impl AsyncProxyQuery { - /// 异步获取自动代理配置(PAC) - pub async fn get_auto_proxy() -> AsyncAutoproxy { - match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await { - Ok(Ok(proxy)) => { - logging!( - debug, - Type::Network, - "异步获取自动代理成功: enable={}, url={}", - proxy.enable, - proxy.url - ); - proxy - } - Ok(Err(e)) => { - logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}"); - AsyncAutoproxy::default() - } - Err(_) => { - logging!(warn, Type::Network, "Warning: 异步获取自动代理超时"); - AsyncAutoproxy::default() - } - } - } - - /// 异步获取系统代理配置 - pub async fn get_system_proxy() -> AsyncSysproxy { - match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await { - Ok(Ok(proxy)) => { - logging!( - debug, - Type::Network, - "异步获取系统代理成功: enable={}, {}:{}", - proxy.enable, - proxy.host, - proxy.port - ); - proxy - } - Ok(Err(e)) => { - logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}"); - AsyncSysproxy::default() - } - Err(_) => { - logging!(warn, Type::Network, "Warning: 异步获取系统代理超时"); - AsyncSysproxy::default() - } - } - } - - #[cfg(target_os = "windows")] - async fn get_auto_proxy_impl() -> Result { - // Windows: 从注册表读取PAC配置 - let proxy = AsyncHandler::spawn_blocking(Self::get_pac_config_from_registry).await?; - Ok(proxy) - } - - #[cfg(target_os = "windows")] - fn get_pac_config_from_registry() -> AsyncAutoproxy { - use std::ptr; - use winapi::shared::minwindef::{DWORD, HKEY}; - use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; - use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}; - - unsafe { - let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" - .encode_utf16() - .collect::>(); - - let mut hkey: HKEY = ptr::null_mut(); - let result = - RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); - - if result != 0 { - logging!(debug, Type::Network, "无法打开注册表项"); - return AsyncAutoproxy::default(); - } - - // 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用) - let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::>(); - let mut url_buffer = vec![0u16; 1024]; - let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD; - let mut url_value_type: DWORD = 0; - - let url_query_result = RegQueryValueExW( - hkey, - auto_config_url_name.as_ptr(), - ptr::null_mut(), - &mut url_value_type, - url_buffer.as_mut_ptr() as *mut u8, - &mut url_buffer_size, - ); - - let mut pac_url = String::new(); - if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 { - let end_pos = url_buffer - .iter() - .position(|&x| x == 0) - .unwrap_or(url_buffer.len()); - pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]); - logging!(debug, Type::Network, "从注册表读取到PAC URL: {pac_url}"); - } - - // 2. 检查自动检测设置是否启用 - let auto_detect_name = "AutoDetect\0".encode_utf16().collect::>(); - let mut auto_detect: DWORD = 0; - let mut detect_buffer_size: DWORD = 4; - let mut detect_value_type: DWORD = 0; - - let detect_query_result = RegQueryValueExW( - hkey, - auto_detect_name.as_ptr(), - ptr::null_mut(), - &mut detect_value_type, - &mut auto_detect as *mut DWORD as *mut u8, - &mut detect_buffer_size, - ); - - RegCloseKey(hkey); - - // PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用 - let pac_enabled = !pac_url.is_empty() - || (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0); - - if pac_enabled { - logging!( - debug, - Type::Network, - "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}" - ); - - if pac_url.is_empty() && auto_detect != 0 { - pac_url = "auto-detect".into(); - } - - AsyncAutoproxy { - enable: true, - url: pac_url, - } - } else { - logging!(debug, Type::Network, "PAC配置未启用"); - AsyncAutoproxy::default() - } - } - } - - #[cfg(target_os = "macos")] - async fn get_auto_proxy_impl() -> Result { - // macOS: 使用 scutil --proxy 命令 - let output = Command::new("scutil").args(["--proxy"]).output().await?; - - if !output.status.success() { - return Ok(AsyncAutoproxy::default()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - crate::logging!( - debug, - crate::utils::logging::Type::Network, - "scutil output: {stdout}" - ); - - let mut pac_enabled = false; - let mut pac_url = String::new(); - - // 解析 scutil 输出 - for line in stdout.lines() { - let line = line.trim(); - if line.contains("ProxyAutoConfigEnable") && line.contains("1") { - pac_enabled = true; - } else if line.contains("ProxyAutoConfigURLString") { - // 正确解析包含冒号的URL - // 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac" - if let Some(colon_pos) = line.find(" : ") { - pac_url = line[colon_pos + 3..].trim().into(); - } - } - } - - crate::logging!( - debug, - crate::utils::logging::Type::Network, - "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}" - ); - - Ok(AsyncAutoproxy { - enable: pac_enabled && !pac_url.is_empty(), - url: pac_url, - }) - } - - #[cfg(target_os = "linux")] - async fn get_auto_proxy_impl() -> Result { - // Linux: 检查环境变量和GNOME设置 - - // 首先检查环境变量 - if let Ok(auto_proxy) = std::env::var("auto_proxy") - && !auto_proxy.is_empty() - { - return Ok(AsyncAutoproxy { - enable: true, - url: auto_proxy, - }); - } - - // 尝试使用 gsettings 获取 GNOME 代理设置 - let output = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy", "mode"]) - .output() - .await; - - if let Ok(output) = output - && output.status.success() - { - let mode: String = String::from_utf8_lossy(&output.stdout).trim().into(); - if mode.contains("auto") { - // 获取 PAC URL - let pac_output = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy", "autoconfig-url"]) - .output() - .await; - - if let Ok(pac_output) = pac_output - && pac_output.status.success() - { - let pac_url: String = String::from_utf8_lossy(&pac_output.stdout) - .trim() - .trim_matches('\'') - .trim_matches('"') - .into(); - - if !pac_url.is_empty() { - return Ok(AsyncAutoproxy { - enable: true, - url: pac_url, - }); - } - } - } - } - - Ok(AsyncAutoproxy::default()) - } - - #[cfg(target_os = "windows")] - async fn get_system_proxy_impl() -> Result { - // Windows: 使用注册表直接读取代理设置 - let sys = AsyncHandler::spawn_blocking(Self::get_system_proxy_from_registry).await?; - Ok(sys) - } - - #[cfg(target_os = "windows")] - fn get_system_proxy_from_registry() -> AsyncSysproxy { - use std::ptr; - use winapi::shared::minwindef::{DWORD, HKEY}; - use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; - use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}; - - unsafe { - let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" - .encode_utf16() - .collect::>(); - - let mut hkey: HKEY = ptr::null_mut(); - let result = - RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); - - if result != 0 { - return AsyncSysproxy::default(); - } - - // 检查代理是否启用 - let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::>(); - let mut proxy_enable: DWORD = 0; - let mut buffer_size: DWORD = 4; - let mut value_type: DWORD = 0; - - let enable_result = RegQueryValueExW( - hkey, - proxy_enable_name.as_ptr(), - ptr::null_mut(), - &mut value_type, - &mut proxy_enable as *mut DWORD as *mut u8, - &mut buffer_size, - ); - - if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 { - RegCloseKey(hkey); - return AsyncSysproxy::default(); - } - - // 读取代理服务器设置 - let proxy_server_name = "ProxyServer\0".encode_utf16().collect::>(); - let mut buffer = vec![0u16; 1024]; - let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD; - let mut value_type: DWORD = 0; - - let server_result = RegQueryValueExW( - hkey, - proxy_server_name.as_ptr(), - ptr::null_mut(), - &mut value_type, - buffer.as_mut_ptr() as *mut u8, - &mut buffer_size, - ); - - let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 { - let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len()); - String::from_utf16_lossy(&buffer[..end_pos]) - } else { - String::new() - }; - - // 读取代理绕过列表 - let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::>(); - let mut bypass_buffer = vec![0u16; 1024]; - let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD; - let mut bypass_value_type: DWORD = 0; - - let override_result = RegQueryValueExW( - hkey, - proxy_override_name.as_ptr(), - ptr::null_mut(), - &mut bypass_value_type, - bypass_buffer.as_mut_ptr() as *mut u8, - &mut bypass_buffer_size, - ); - - let bypass_list = - if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 { - let end_pos = bypass_buffer - .iter() - .position(|&x| x == 0) - .unwrap_or(bypass_buffer.len()); - String::from_utf16_lossy(&bypass_buffer[..end_pos]) - } else { - String::new() - }; - - RegCloseKey(hkey); - - if !proxy_server.is_empty() { - // 解析服务器地址和端口 - let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') { - let host = proxy_server[..colon_pos].into(); - let port = proxy_server[colon_pos + 1..].parse::().unwrap_or(8080); - (host, port) - } else { - (proxy_server, 8080) - }; - - logging!( - debug, - Type::Network, - "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}" - ); - - AsyncSysproxy { - enable: true, - host, - port, - bypass: bypass_list, - } - } else { - AsyncSysproxy::default() - } - } - } - - #[cfg(target_os = "macos")] - async fn get_system_proxy_impl() -> Result { - let output = Command::new("scutil").args(["--proxy"]).output().await?; - - if !output.status.success() { - return Ok(AsyncSysproxy::default()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - logging!(debug, Type::Network, "scutil proxy output: {stdout}"); - - let mut http_enabled = false; - let mut http_host = String::new(); - let mut http_port = 8080u16; - let mut exceptions: Vec = Vec::new(); - - for line in stdout.lines() { - let line = line.trim(); - if line.contains("HTTPEnable") && line.contains("1") { - http_enabled = true; - } else if line.contains("HTTPProxy") && !line.contains("Port") { - if let Some(host_part) = line.split(':').nth(1) { - http_host = host_part.trim().into(); - } - } else if line.contains("HTTPPort") { - if let Some(port_part) = line.split(':').nth(1) - && let Ok(port) = port_part.trim().parse::() - { - http_port = port; - } - } else if line.contains("ExceptionsList") { - // 解析异常列表 - if let Some(list_part) = line.split(':').nth(1) { - let list = list_part.trim(); - if !list.is_empty() { - exceptions.push(list.into()); - } - } - } - } - - Ok(AsyncSysproxy { - enable: http_enabled && !http_host.is_empty(), - host: http_host, - port: http_port, - bypass: exceptions.join(","), - }) - } - - #[cfg(target_os = "linux")] - async fn get_system_proxy_impl() -> Result { - // Linux: 检查环境变量和桌面环境设置 - - // 首先检查环境变量 - if let Ok(http_proxy) = std::env::var("http_proxy") - && let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) - { - return Ok(proxy_info); - } - - if let Ok(https_proxy) = std::env::var("https_proxy") - && let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) - { - return Ok(proxy_info); - } - - // 尝试使用 gsettings 获取 GNOME 代理设置 - let mode_output = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy", "mode"]) - .output() - .await; - - if let Ok(mode_output) = mode_output - && mode_output.status.success() - { - let mode: String = String::from_utf8_lossy(&mode_output.stdout).trim().into(); - if mode.contains("manual") { - // 获取HTTP代理设置 - let host_result = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy.http", "host"]) - .output() - .await; - - let port_result = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy.http", "port"]) - .output() - .await; - - if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) - && host_output.status.success() - && port_output.status.success() - { - let host: String = String::from_utf8_lossy(&host_output.stdout) - .trim() - .trim_matches('\'') - .trim_matches('"') - .into(); - - let port = String::from_utf8_lossy(&port_output.stdout) - .trim() - .parse::() - .unwrap_or(8080); - - if !host.is_empty() { - return Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: String::new(), - }); - } - } - } - } - - Ok(AsyncSysproxy::default()) - } - - #[cfg(target_os = "linux")] - fn parse_proxy_url(proxy_url: &str) -> Result { - // 解析形如 "http://proxy.example.com:8080" 的URL - let url = proxy_url.trim(); - - // 移除协议前缀 - let url = if let Some(stripped) = url.strip_prefix("http://") { - stripped - } else if let Some(stripped) = url.strip_prefix("https://") { - stripped - } else { - url - }; - - // 解析主机和端口 - let (host, port) = if let Some(colon_pos) = url.rfind(':') { - let host: String = url[..colon_pos].into(); - let port = url[colon_pos + 1..].parse::().unwrap_or(8080); - (host, port) - } else { - (url.into(), 8080) - }; - - if host.is_empty() { - return Err(anyhow!("无效的代理URL")); - } - - Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: std::env::var("no_proxy").unwrap_or_default(), - }) - } -} diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs deleted file mode 100644 index e03e9eeca..000000000 --- a/src-tauri/src/core/event_driven_proxy.rs +++ /dev/null @@ -1,548 +0,0 @@ -use std::sync::Arc; -use tokio::sync::RwLock; -use tokio::sync::{mpsc, oneshot}; -use tokio::time::{Duration, sleep, timeout}; -use tokio_stream::{StreamExt as _, wrappers::UnboundedReceiverStream}; - -use crate::config::{Config, IVerge}; -use crate::core::{async_proxy_query::AsyncProxyQuery, handle}; -use crate::process::AsyncHandler; -use crate::{logging, utils::logging::Type}; -use once_cell::sync::Lazy; -use smartstring::alias::String; -use sysproxy::{Autoproxy, Sysproxy}; - -#[derive(Debug, Clone)] -pub enum ProxyEvent { - /// 配置变更事件 - ConfigChanged, - /// 应用启动事件 - AppStarted, - /// 应用关闭事件 - AppStopping, -} - -#[derive(Debug, Clone)] -pub struct ProxyState { - pub sys_enabled: bool, - pub pac_enabled: bool, - pub auto_proxy: Autoproxy, - pub sys_proxy: Sysproxy, - pub last_updated: std::time::Instant, - pub is_healthy: bool, -} - -impl Default for ProxyState { - fn default() -> Self { - Self { - sys_enabled: false, - pac_enabled: false, - auto_proxy: Autoproxy { - enable: false, - url: "".into(), - }, - sys_proxy: Sysproxy { - enable: false, - host: "127.0.0.1".into(), - port: 7897, - bypass: "".into(), - }, - last_updated: std::time::Instant::now(), - is_healthy: true, - } - } -} - -pub struct EventDrivenProxyManager { - state: Arc>, - event_sender: mpsc::UnboundedSender, - query_sender: mpsc::UnboundedSender, -} - -#[derive(Debug)] -pub struct QueryRequest { - response_tx: oneshot::Sender, -} - -// 配置结构体移到外部 -struct ProxyConfig { - sys_enabled: bool, - pac_enabled: bool, - guard_enabled: bool, - guard_duration: u64, -} - -static PROXY_MANAGER: Lazy = Lazy::new(EventDrivenProxyManager::new); - -impl EventDrivenProxyManager { - pub fn global() -> &'static Self { - &PROXY_MANAGER - } - - fn new() -> Self { - let state = Arc::new(RwLock::new(ProxyState::default())); - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (query_tx, query_rx) = mpsc::unbounded_channel(); - - let state_clone = Arc::clone(&state); - AsyncHandler::spawn(move || Self::start_event_loop(state_clone, event_rx, query_rx)); - - Self { - state, - event_sender: event_tx, - query_sender: query_tx, - } - } - - /// 获取自动代理配置(缓存) - pub async fn get_auto_proxy_cached(&self) -> Autoproxy { - self.state.read().await.auto_proxy.clone() - } - - /// 异步获取最新的自动代理配置 - pub async fn get_auto_proxy_async(&self) -> Autoproxy { - let (tx, rx) = oneshot::channel(); - let query = QueryRequest { response_tx: tx }; - - if self.query_sender.send(query).is_err() { - logging!(error, Type::Network, "发送查询请求失败,返回缓存数据"); - return self.get_auto_proxy_cached().await; - } - - match timeout(Duration::from_secs(5), rx).await { - Ok(Ok(result)) => result, - _ => { - logging!(warn, Type::Network, "Warning: 查询超时,返回缓存数据"); - self.get_auto_proxy_cached().await - } - } - } - - /// 通知配置变更 - pub fn notify_config_changed(&self) { - self.send_event(ProxyEvent::ConfigChanged); - } - - /// 通知应用启动 - pub fn notify_app_started(&self) { - self.send_event(ProxyEvent::AppStarted); - } - - /// 通知应用即将关闭 - pub fn notify_app_stopping(&self) { - self.send_event(ProxyEvent::AppStopping); - } - - fn send_event(&self, event: ProxyEvent) { - if let Err(e) = self.event_sender.send(event) { - logging!(error, Type::Network, "发送代理事件失败: {e}"); - } - } - - pub async fn start_event_loop( - state: Arc>, - event_rx: mpsc::UnboundedReceiver, - query_rx: mpsc::UnboundedReceiver, - ) { - logging!(info, Type::Network, "事件驱动代理管理器启动"); - - // 将 mpsc 接收器包装成 Stream,避免每次循环创建 future - let mut event_stream = UnboundedReceiverStream::new(event_rx); - let mut query_stream = UnboundedReceiverStream::new(query_rx); - - // 初始化定时器,用于周期性检查代理设置 - let config = Self::get_proxy_config().await; - let mut guard_interval = tokio::time::interval(Duration::from_secs(config.guard_duration)); - // 防止首次立即触发 - guard_interval.tick().await; - - loop { - tokio::select! { - Some(event) = event_stream.next() => { - logging!(debug, Type::Network, "处理代理事件: {event:?}"); - let event_clone = event.clone(); // 保存一份副本用于后续检查 - Self::handle_event(&state, event).await; - - // 检查是否是配置变更事件,如果是,则可能需要更新定时器 - if matches!(event_clone, ProxyEvent::ConfigChanged | ProxyEvent::AppStarted) { - let new_config = Self::get_proxy_config().await; - // 重新设置定时器间隔 - guard_interval = tokio::time::interval(Duration::from_secs(new_config.guard_duration)); - // 防止首次立即触发 - guard_interval.tick().await; - } - } - Some(query) = query_stream.next() => { - let result = Self::handle_query(&state).await; - let _ = query.response_tx.send(result); - } - _ = guard_interval.tick() => { - // 定时检查代理设置 - let config = Self::get_proxy_config().await; - if config.guard_enabled && config.sys_enabled { - logging!(debug, Type::Network, "定时检查代理设置"); - Self::check_and_restore_proxy(&state).await; - } - } - else => { - // 两个通道都关闭时退出 - logging!(info, Type::Network, "事件或查询通道关闭,代理管理器停止"); - break; - } - } - } - } - - async fn handle_event(state: &Arc>, event: ProxyEvent) { - match event { - ProxyEvent::ConfigChanged => { - Self::update_proxy_config(state).await; - } - ProxyEvent::AppStarted => { - Self::initialize_proxy_state(state).await; - } - ProxyEvent::AppStopping => { - logging!(info, Type::Network, "清理代理状态"); - Self::update_state_timestamp(state, |s| { - s.sys_enabled = false; - s.pac_enabled = false; - s.is_healthy = false; - }) - .await; - } - } - } - - async fn handle_query(state: &Arc>) -> Autoproxy { - let auto_proxy = Self::get_auto_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.auto_proxy = auto_proxy.clone(); - }) - .await; - - auto_proxy - } - - async fn initialize_proxy_state(state: &Arc>) { - logging!(info, Type::Network, "初始化代理状态"); - - let config = Self::get_proxy_config().await; - let auto_proxy = Self::get_auto_proxy_with_timeout().await; - let sys_proxy = Self::get_sys_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.sys_enabled = config.sys_enabled; - s.pac_enabled = config.pac_enabled; - s.auto_proxy = auto_proxy; - s.sys_proxy = sys_proxy; - s.is_healthy = true; - }) - .await; - - logging!( - info, - Type::Network, - "代理状态初始化完成: sys={}, pac={}", - config.sys_enabled, - config.pac_enabled - ); - } - - async fn update_proxy_config(state: &Arc>) { - logging!(debug, Type::Network, "更新代理配置"); - - let config = Self::get_proxy_config().await; - - Self::update_state_timestamp(state, |s| { - s.sys_enabled = config.sys_enabled; - s.pac_enabled = config.pac_enabled; - }) - .await; - - if config.guard_enabled && config.sys_enabled { - Self::check_and_restore_proxy(state).await; - } - } - - async fn check_and_restore_proxy(state: &Arc>) { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过系统代理守卫检查"); - return; - } - let (sys_enabled, pac_enabled) = { - let s = state.read().await; - (s.sys_enabled, s.pac_enabled) - }; - - if !sys_enabled { - return; - } - - logging!(debug, Type::Network, "检查代理状态"); - - if pac_enabled { - Self::check_and_restore_pac_proxy(state).await; - } else { - Self::check_and_restore_sys_proxy(state).await; - } - } - - async fn check_and_restore_pac_proxy(state: &Arc>) { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复检查"); - return; - } - - let current = Self::get_auto_proxy_with_timeout().await; - let expected = Self::get_expected_pac_config().await; - - Self::update_state_timestamp(state, |s| { - s.auto_proxy = current.clone(); - }) - .await; - - if !current.enable || current.url != expected.url { - logging!(info, Type::Network, "PAC代理设置异常,正在恢复..."); - if let Err(e) = Self::restore_pac_proxy(&expected.url).await { - logging!(error, Type::Network, "恢复PAC代理失败: {}", e); - } - - sleep(Duration::from_millis(500)).await; - let restored = Self::get_auto_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.is_healthy = restored.enable && restored.url == expected.url; - s.auto_proxy = restored; - }) - .await; - } - } - - async fn check_and_restore_sys_proxy(state: &Arc>) { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复检查"); - return; - } - - let current = Self::get_sys_proxy_with_timeout().await; - let expected = Self::get_expected_sys_proxy().await; - - Self::update_state_timestamp(state, |s| { - s.sys_proxy = current.clone(); - }) - .await; - - if !current.enable || current.host != expected.host || current.port != expected.port { - logging!(info, Type::Network, "系统代理设置异常,正在恢复..."); - if let Err(e) = Self::restore_sys_proxy(&expected).await { - logging!(error, Type::Network, "恢复系统代理失败: {}", e); - } - - sleep(Duration::from_millis(500)).await; - let restored = Self::get_sys_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.is_healthy = restored.enable - && restored.host == expected.host - && restored.port == expected.port; - s.sys_proxy = restored; - }) - .await; - } - } - - async fn get_auto_proxy_with_timeout() -> Autoproxy { - let async_proxy = AsyncProxyQuery::get_auto_proxy().await; - - // 转换为兼容的结构 - Autoproxy { - enable: async_proxy.enable, - url: async_proxy.url, - } - } - - async fn get_sys_proxy_with_timeout() -> Sysproxy { - let async_proxy = AsyncProxyQuery::get_system_proxy().await; - - // 转换为兼容的结构 - Sysproxy { - enable: async_proxy.enable, - host: async_proxy.host, - port: async_proxy.port, - bypass: async_proxy.bypass, - } - } - - // 统一的状态更新方法 - async fn update_state_timestamp(state: &Arc>, update_fn: F) - where - F: FnOnce(&mut ProxyState), - { - let mut state_guard = state.write().await; - update_fn(&mut state_guard); - state_guard.last_updated = std::time::Instant::now(); - } - - async fn get_proxy_config() -> ProxyConfig { - let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = { - let verge_config = Config::verge().await; - let verge = verge_config.latest_arc(); - ( - verge.enable_system_proxy.unwrap_or(false), - verge.proxy_auto_config.unwrap_or(false), - verge.enable_proxy_guard.unwrap_or(false), - verge.proxy_guard_duration.unwrap_or(30), // 默认30秒 - ) - }; - ProxyConfig { - sys_enabled, - pac_enabled, - guard_enabled, - guard_duration, - } - } - - async fn get_expected_pac_config() -> Autoproxy { - let proxy_host = { - let verge_config = Config::verge().await; - let verge = verge_config.latest_arc(); - verge - .proxy_host - .clone() - .unwrap_or_else(|| "127.0.0.1".into()) - }; - let pac_port = IVerge::get_singleton_port(); - Autoproxy { - enable: true, - url: format!("http://{proxy_host}:{pac_port}/commands/pac"), - } - } - - async fn get_expected_sys_proxy() -> Sysproxy { - use crate::constants::network; - - let (verge_mixed_port, proxy_host) = { - let verge_config = Config::verge().await; - let verge_ref = verge_config.latest_arc(); - (verge_ref.verge_mixed_port, verge_ref.proxy_host.clone()) - }; - - let default_port = { - let clash_config = Config::clash().await; - clash_config.latest_arc().get_mixed_port() - }; - - let port = verge_mixed_port.unwrap_or(default_port); - let host = proxy_host - .unwrap_or_else(|| network::DEFAULT_PROXY_HOST.into()) - .into(); - - Sysproxy { - enable: true, - host, - port, - bypass: Self::get_bypass_config().await.into(), - } - } - - async fn get_bypass_config() -> String { - use crate::constants::bypass; - - let verge_config = Config::verge().await; - let verge = verge_config.latest_arc(); - let use_default = verge.use_default_bypass.unwrap_or(true); - let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); - - match (use_default, custom.is_empty()) { - (_, true) => bypass::DEFAULT.into(), - (true, false) => format!("{},{}", bypass::DEFAULT, custom).into(), - (false, false) => custom.into(), - } - } - - #[cfg(target_os = "windows")] - async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复"); - return Ok(()); - } - Self::execute_sysproxy_command(&["pac", expected_url]).await - } - - #[allow(clippy::unused_async)] - #[cfg(not(target_os = "windows"))] - async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> { - { - let new_autoproxy = Autoproxy { - enable: true, - url: expected_url.to_string(), - }; - // logging_error!(Type::System, true, new_autoproxy.set_auto_proxy()); - new_autoproxy - .set_auto_proxy() - .map_err(|e| anyhow::anyhow!("Failed to set auto proxy: {}", e)) - } - } - - #[cfg(target_os = "windows")] - async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复"); - return Ok(()); - } - let address = format!("{}:{}", expected.host, expected.port); - Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await - } - - #[allow(clippy::unused_async)] - #[cfg(not(target_os = "windows"))] - async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> { - { - // logging_error!(Type::System, true, expected.set_system_proxy()); - expected - .set_system_proxy() - .map_err(|e| anyhow::anyhow!("Failed to set system proxy: {}", e)) - } - } - - #[cfg(target_os = "windows")] - async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> { - if handle::Handle::global().is_exiting() { - logging!( - debug, - Type::Network, - "应用正在退出,取消调用 sysproxy.exe,参数: {:?}", - args - ); - return Ok(()); - } - - use crate::utils::dirs; - #[allow(unused_imports)] // creation_flags必须 - use std::os::windows::process::CommandExt as _; - use tokio::process::Command; - - let binary_path = match dirs::service_path() { - Ok(path) => path, - Err(e) => { - logging!(error, Type::Network, "获取服务路径失败: {e}"); - return Err(e); - } - }; - - let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); - if !sysproxy_exe.exists() { - logging!(error, Type::Network, "sysproxy.exe 不存在"); - } - anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist"); - - let _output = Command::new(sysproxy_exe) - .args(args) - .creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口 - .output() - .await?; - - Ok(()) - } -} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 6bdb2418b..9dc578796 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,6 +1,4 @@ -pub mod async_proxy_query; pub mod backup; -pub mod event_driven_proxy; pub mod handle; pub mod hotkey; pub mod logger; @@ -13,4 +11,4 @@ pub mod tray; pub mod validate; pub mod win_uwp; -pub use self::{event_driven_proxy::EventDrivenProxyManager, manager::CoreManager, timer::Timer}; +pub use self::{manager::CoreManager, timer::Timer}; diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index e9e1ade55..6af30efd1 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -2,22 +2,43 @@ use crate::utils::autostart as startup_shortcut; use crate::{ config::{Config, IVerge}, - core::{EventDrivenProxyManager, handle::Handle}, + core::handle::Handle, logging, logging_error, singleton_lazy, utils::logging::Type, }; use anyhow::Result; +use parking_lot::RwLock; use scopeguard::defer; use smartstring::alias::String; -use std::sync::atomic::{AtomicBool, Ordering}; -#[cfg(not(target_os = "windows"))] -use sysproxy::{Autoproxy, Sysproxy}; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; +use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy}; use tauri_plugin_autostart::ManagerExt as _; pub struct Sysopt { initialed: AtomicBool, update_sysproxy: AtomicBool, reset_sysproxy: AtomicBool, + guard: Arc>, +} + +impl Default for Sysopt { + fn default() -> Self { + Self { + initialed: AtomicBool::new(false), + update_sysproxy: AtomicBool::new(false), + reset_sysproxy: AtomicBool::new(false), + guard: Arc::new(RwLock::new(GuardMonitor::new( + GuardType::None, + Duration::from_secs(30), + ))), + } + } } #[cfg(target_os = "windows")] @@ -82,16 +103,6 @@ async fn execute_sysproxy_command(args: Vec) -> Result<()> Ok(()) } -impl Default for Sysopt { - fn default() -> Self { - Self { - initialed: AtomicBool::new(false), - update_sysproxy: AtomicBool::new(false), - reset_sysproxy: AtomicBool::new(false), - } - } -} - // Use simplified singleton_lazy macro singleton_lazy!(Sysopt, SYSOPT, Sysopt::default); @@ -100,30 +111,64 @@ impl Sysopt { self.initialed.load(Ordering::SeqCst) } - pub fn init_guard_sysproxy(&self) { - // 使用事件驱动代理管理器 - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_app_started(); + fn access_guard(&self) -> Arc> { + Arc::clone(&self.guard) + } - logging!(info, Type::Core, "已启用事件驱动代理守卫"); + pub async fn refresh_guard(&self) { + logging!(info, Type::Core, "Refreshing system proxy guard..."); + let verge = Config::verge().await.latest_arc(); + if !verge.enable_system_proxy.unwrap_or(false) { + logging!(info, Type::Core, "System proxy is disabled."); + self.access_guard().write().stop(); + return; + } + if !verge.enable_proxy_guard.unwrap_or(false) { + logging!(info, Type::Core, "System proxy guard is disabled."); + return; + } + logging!( + info, + Type::Core, + "Updating system proxy with duration: {} seconds", + verge.proxy_guard_duration.unwrap_or(30) + ); + { + let guard = self.access_guard(); + guard.write().set_interval(Duration::from_secs( + verge.proxy_guard_duration.unwrap_or(30), + )); + } + logging!(info, Type::Core, "Starting system proxy guard..."); + { + let guard = self.access_guard(); + guard.write().start(); + } } /// init the sysproxy pub async fn update_sysproxy(&self) -> Result<()> { self.initialed.store(true, Ordering::SeqCst); + if self.update_sysproxy.load(Ordering::Acquire) { + logging!(info, Type::Core, "Sysproxy update is already in progress."); + return Ok(()); + } if self .update_sysproxy - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .is_err() { + logging!(info, Type::Core, "Sysproxy update is already in progress."); return Ok(()); } defer! { - self.update_sysproxy.store(false, Ordering::SeqCst); + logging!(info, Type::Core, "Sysproxy update completed."); + self.update_sysproxy.store(false, Ordering::Release); } + let verge = Config::verge().await.latest_arc(); let port = { - let verge_port = Config::verge().await.latest_arc().verge_mixed_port; + let verge_port = verge.verge_mixed_port; match verge_port { Some(port) => port, None => Config::clash().await.latest_arc().get_mixed_port(), @@ -132,8 +177,6 @@ impl Sysopt { let pac_port = IVerge::get_singleton_port(); let (sys_enable, pac_enable, proxy_host) = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); ( verge.enable_system_proxy.unwrap_or(false), verge.proxy_auto_config.unwrap_or(false), @@ -160,8 +203,9 @@ impl Sysopt { if !sys_enable { sys.set_system_proxy()?; auto.set_auto_proxy()?; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); + self.access_guard() + .write() + .set_guard_type(GuardType::Sysproxy(sys)); return Ok(()); } @@ -170,8 +214,9 @@ impl Sysopt { auto.enable = true; sys.set_system_proxy()?; auto.set_auto_proxy()?; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); + self.access_guard() + .write() + .set_guard_type(GuardType::Autoproxy(auto)); return Ok(()); } @@ -180,33 +225,47 @@ impl Sysopt { sys.enable = true; auto.set_auto_proxy()?; sys.set_system_proxy()?; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); + self.access_guard() + .write() + .set_guard_type(GuardType::Sysproxy(sys)); return Ok(()); } } + #[cfg(target_os = "windows")] { if !sys_enable { - let result = self.reset_sysproxy().await; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); - return result; + self.access_guard().write().set_guard_type(GuardType::None); + return self.reset_sysproxy().await; } - let args: Vec = if pac_enable { + let (args, guard_type): (Vec, GuardType) = if pac_enable { let address = format!("http://{proxy_host}:{pac_port}/commands/pac"); - vec!["pac".into(), address] + ( + vec!["pac".into(), address.clone()], + GuardType::Autoproxy(Autoproxy { + enable: true, + url: address, + }), + ) } else { let address = format!("{proxy_host}:{port}"); let bypass = get_bypass().await; - vec!["global".into(), address, bypass.into()] + let bypass_for_guard = bypass.as_str().to_owned(); + ( + vec!["global".into(), address.clone(), bypass.into()], + GuardType::Sysproxy(Sysproxy { + enable: true, + host: proxy_host.clone().into(), + port, + bypass: bypass_for_guard, + }), + ) }; execute_sysproxy_command(args).await?; + self.access_guard().write().set_guard_type(guard_type); } - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); Ok(()) } diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index cfa9aef00..a127a5bb1 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -1,7 +1,7 @@ use crate::{ config::{Config, IVerge}, core::{CoreManager, handle, hotkey, sysopt, tray}, - logging_error, + logging, logging_error, module::{auto_backup::AutoBackupManager, lightweight}, utils::{draft::SharedBox, logging::Type}, }; @@ -107,6 +107,8 @@ fn determine_update_flags(patch: &IVerge) -> i32 { let enable_auto_light_weight = patch.enable_auto_light_weight_mode; let enable_external_controller = patch.enable_external_controller; let tray_inline_proxy_groups = patch.tray_inline_proxy_groups; + let enable_proxy_guard = patch.enable_proxy_guard; + let proxy_guard_duration = patch.proxy_guard_duration; if tun_mode.is_some() { update_flags |= UpdateFlags::ClashConfig as i32; @@ -144,7 +146,12 @@ fn determine_update_flags(patch: &IVerge) -> i32 { update_flags |= UpdateFlags::SystrayIcon as i32; } - if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() { + if proxy_bypass.is_some() + || pac_content.is_some() + || pac.is_some() + || enable_proxy_guard.is_some() + || proxy_guard_duration.is_some() + { update_flags |= UpdateFlags::SysProxy as i32; } @@ -207,6 +214,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( } if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 { sysopt::Sysopt::global().update_sysproxy().await?; + sysopt::Sysopt::global().refresh_guard().await; } if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 && let Some(hotkeys) = &patch.hotkeys @@ -258,6 +266,7 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { if !not_save_file { // 分离数据获取和异步调用 let verge_data = Config::verge().await.data_arc(); + logging!(info, Type::Setup, "Saving Verge configuration to file..."); verge_data.save_file().await?; } Ok(()) diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 876ba4432..9e741258d 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -1,5 +1,4 @@ use crate::config::Config; -use crate::core::event_driven_proxy::EventDrivenProxyManager; use crate::core::{CoreManager, handle, sysopt}; use crate::utils; use crate::utils::window_manager::WindowManager; @@ -24,7 +23,6 @@ pub async fn quit() { // 获取应用句柄并设置退出标志 let app_handle = handle::Handle::app_handle(); handle::Handle::global().set_is_exiting(); - EventDrivenProxyManager::global().notify_app_stopping(); logging!(info, Type::System, "开始异步清理资源"); let cleanup_result = clean_async().await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 365afc15a..e55613c5b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,7 +14,7 @@ use crate::constants::files; #[cfg(target_os = "linux")] use crate::utils::linux; use crate::{ - core::{EventDrivenProxyManager, handle}, + core::handle, process::AsyncHandler, utils::{resolve, server}, }; @@ -440,7 +440,6 @@ pub fn run() { let handle = core::handle::Handle::global(); if !handle.is_exiting() { handle.set_is_exiting(); - EventDrivenProxyManager::global().notify_app_stopping(); feat::clean(); } } diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 40971b0b4..6da25a439 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -54,7 +54,7 @@ pub fn resolve_setup_async() { init_service_manager().await; init_core_manager().await; init_system_proxy().await; - AsyncHandler::spawn_blocking(init_system_proxy_guard); + init_system_proxy_guard().await; }); let tray_init = async { @@ -173,8 +173,8 @@ pub(super) async fn init_system_proxy() { ); } -pub(super) fn init_system_proxy_guard() { - sysopt::Sysopt::global().init_guard_sysproxy(); +pub(super) async fn init_system_proxy_guard() { + sysopt::Sysopt::global().refresh_guard().await; } pub(super) async fn refresh_tray_menu() { From e66b8e7894822cba18183a0d604b3ad10ca6790e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:18:35 +0800 Subject: [PATCH 20/26] feat(timer): better subscription update timer after core startup --- src-tauri/src/core/timer.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 20c0bb6c0..08ee68395 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -1,5 +1,7 @@ use crate::{ - config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton, + config::Config, + core::{CoreManager, manager::RunningMode, sysopt::Sysopt}, + feat, logging, logging_error, singleton, utils::logging::Type, }; use anyhow::{Context as _, Result}; @@ -392,6 +394,7 @@ impl Timer { .spawn_async_routine(move || { let uid = uid.clone(); Box::pin(async move { + Self::wait_untile_core_manager(Duration::from_millis(1000)).await; Self::wait_until_sysopt(Duration::from_millis(1000)).await; Self::async_task(&uid).await; }) as Pin + Send>> @@ -523,10 +526,28 @@ impl Timer { Self::emit_update_event(uid, false); } + async fn wait_untile_core_manager(max_wait: Duration) { + let _ = timeout(max_wait, async { + while *CoreManager::global().get_running_mode() != RunningMode::NotRunning { + logging!( + debug, + Type::Timer, + "Waiting for CoreManager to be initialized..." + ); + sleep(Duration::from_millis(30)).await; + } + }) + .await; + } + async fn wait_until_sysopt(max_wait: Duration) { let _ = timeout(max_wait, async { while !Sysopt::global().is_initialed() { - logging!(warn, Type::Timer, "Waiting for Sysopt to be initialized..."); + logging!( + debug, + Type::Timer, + "Waiting for Sysopt to be initialized..." + ); sleep(Duration::from_millis(30)).await; } }) From dbb4877be666844ee782426e4790283d551e0836 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Sun, 16 Nov 2025 00:33:21 +0800 Subject: [PATCH 21/26] refactor(Draft): management as crate (#5470) * feat: implement draft functionality with apply and discard methods, and add benchmarks and tests * Refactor Draft management and integrate Tokio for asynchronous operations - Introduced a new `IVerge` struct for configuration management. - Updated `Draft` struct to use `Arc` for better concurrency handling. - Added asynchronous editing capabilities to `Draft` using Tokio. - Replaced synchronous editing methods with asynchronous counterparts. - Updated benchmark tests to reflect changes in the `Draft` API. - Removed redundant draft utility module and integrated its functionality into the main `Draft` implementation. - Adjusted tests to validate new behavior and ensure correctness of the `Draft` management flow. --- src-tauri/Cargo.lock | 11 ++ src-tauri/Cargo.toml | 42 ++++--- src-tauri/crates/draft/Cargo.toml | 17 +++ .../draft/bench/benche_me.rs} | 23 +++- src-tauri/crates/draft/src/lib.rs | 102 +++++++++++++++++ .../draft/tests/test_me.rs} | 105 +----------------- src-tauri/src/cmd/verge.rs | 3 +- src-tauri/src/config/config.rs | 3 +- src-tauri/src/feat/config.rs | 3 +- src-tauri/src/utils/mod.rs | 3 - 10 files changed, 181 insertions(+), 131 deletions(-) create mode 100644 src-tauri/crates/draft/Cargo.toml rename src-tauri/{benches/draft_benchmark.rs => crates/draft/bench/benche_me.rs} (90%) create mode 100644 src-tauri/crates/draft/src/lib.rs rename src-tauri/{src/utils/draft.rs => crates/draft/tests/test_me.rs} (71%) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 80e7c3a14..35cd86855 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1122,6 +1122,7 @@ dependencies = [ "criterion", "deelevate", "delay_timer", + "draft", "dunce", "flexi_logger", "futures", @@ -2016,6 +2017,16 @@ dependencies = [ "serde", ] +[[package]] +name = "draft" +version = "0.1.0" +dependencies = [ + "anyhow", + "criterion", + "parking_lot", + "tokio", +] + [[package]] name = "dtoa" version = "1.0.10" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e3bcc8516..67a142f6f 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,8 +17,11 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev" tauri-build = { version = "2.5.2", features = [] } [dependencies] +parking_lot = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +draft = { workspace = true } warp = { version = "0.4.2", features = ["server"] } -anyhow = "1.0.100" open = "5.3.2" log = "0.4.28" dunce = "1.0.5" @@ -31,14 +34,7 @@ serde_yaml_ng = "0.10.0" once_cell = { version = "1.21.3", features = ["parking_lot"] } port_scanner = "0.1.5" delay_timer = "0.11.6" -parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } percent-encoding = "2.3.2" -tokio = { version = "1.48.0", features = [ - "rt-multi-thread", - "macros", - "time", - "sync", -] } serde = { version = "1.0.228", features = ["derive"] } reqwest = { version = "0.12.24", features = ["json", "cookies"] } regex = "1.12.2" @@ -120,6 +116,28 @@ tauri-plugin-autostart = "2.5.1" tauri-plugin-global-shortcut = "2.3.1" tauri-plugin-updater = "2.9.0" +[dev-dependencies] +criterion = { workspace = true } + +[workspace.dependencies] +draft = { path = "crates/draft" } +parking_lot = { version = "0.12.5", features = [ + "hardware-lock-elision", + "send_guard", +] } +anyhow = "1.0.100" +criterion = { version = "0.7.0", features = ["async_tokio"] } +tokio = { version = "1.48.0", features = [ + "rt-multi-thread", + "macros", + "time", + "sync", +] } + +[workspace] +members = ["crates/*"] +resolver = "2" + [features] default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] @@ -129,11 +147,6 @@ tokio-trace = ["console-subscriber"] clippy = ["tauri/test"] tracing = [] -[[bench]] -name = "draft_benchmark" -path = "benches/draft_benchmark.rs" -harness = false - [profile.release] panic = "abort" codegen-units = 1 @@ -167,9 +180,6 @@ strip = false name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] -[dev-dependencies] -criterion = { version = "0.7.0", features = ["async_tokio"] } - [lints.clippy] # Core categories - most important for code safety and correctness correctness = { level = "deny", priority = -1 } diff --git a/src-tauri/crates/draft/Cargo.toml b/src-tauri/crates/draft/Cargo.toml new file mode 100644 index 000000000..b0d51e2cc --- /dev/null +++ b/src-tauri/crates/draft/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "draft" +version = "0.1.0" +edition = "2024" + +[[bench]] +name = "draft_bench" +path = "bench/benche_me.rs" +harness = false + +[dependencies] +parking_lot = { workspace = true } +anyhow = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +tokio = { workspace = true } diff --git a/src-tauri/benches/draft_benchmark.rs b/src-tauri/crates/draft/bench/benche_me.rs similarity index 90% rename from src-tauri/benches/draft_benchmark.rs rename to src-tauri/crates/draft/bench/benche_me.rs index c8a07c536..d5d8df7ea 100644 --- a/src-tauri/benches/draft_benchmark.rs +++ b/src-tauri/crates/draft/bench/benche_me.rs @@ -3,17 +3,30 @@ use std::hint::black_box; use std::process; use tokio::runtime::Runtime; -use app_lib::config::IVerge; -use app_lib::utils::Draft as DraftNew; +use draft::Draft; -/// 创建测试数据 -fn make_draft() -> DraftNew { +#[derive(Clone, Debug)] +struct IVerge { + enable_auto_launch: Option, + enable_tun_mode: Option, +} + +impl Default for IVerge { + fn default() -> Self { + Self { + enable_auto_launch: None, + enable_tun_mode: None, + } + } +} + +fn make_draft() -> Draft { let verge = IVerge { enable_auto_launch: Some(true), enable_tun_mode: Some(false), ..Default::default() }; - DraftNew::new(verge) + Draft::new(verge) } pub fn bench_draft(c: &mut Criterion) { diff --git a/src-tauri/crates/draft/src/lib.rs b/src-tauri/crates/draft/src/lib.rs new file mode 100644 index 000000000..5d548d091 --- /dev/null +++ b/src-tauri/crates/draft/src/lib.rs @@ -0,0 +1,102 @@ +use parking_lot::RwLock; +use std::sync::Arc; + +pub type SharedBox = Arc>; +type DraftInner = (SharedBox, Option>); + +/// Draft 管理:committed 与 optional draft 都以 Arc> 存储, +// (committed_snapshot, optional_draft_snapshot) +#[derive(Debug, Clone)] +pub struct Draft { + inner: Arc>>, +} + +impl Draft { + #[inline] + pub fn new(data: T) -> Self { + Self { + inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), + } + } + /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) + #[inline] + pub fn data_arc(&self) -> SharedBox { + let guard = self.inner.read(); + Arc::clone(&guard.0) + } + + /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 + /// 这也是零拷贝:只 clone Arc,不 clone T + #[inline] + pub fn latest_arc(&self) -> SharedBox { + let guard = self.inner.read(); + guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) + } + + /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) + /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; + /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 + #[inline] + pub fn edit_draft(&self, f: F) -> R + where + F: FnOnce(&mut T) -> R, + { + // 先获得写锁以创建或取出草稿 Arc 的可变引用位置 + let mut guard = self.inner.write(); + let mut draft_arc = if guard.1.is_none() { + Arc::clone(&guard.0) + } else { + #[allow(clippy::unwrap_used)] + guard.1.take().unwrap() + }; + drop(guard); + // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box(要求 T: Clone) + let boxed = Arc::make_mut(&mut draft_arc); // &mut Box + // 对 Box 解引用得到 &mut T + let result = f(&mut **boxed); + // 恢复修改后的草稿 Arc + self.inner.write().1 = Some(draft_arc); + result + } + + /// 将草稿提交到已提交位置(替换),并清除草稿 + #[inline] + pub fn apply(&self) { + let mut guard = self.inner.write(); + if let Some(d) = guard.1.take() { + guard.0 = d; + } + } + + /// 丢弃草稿(如果存在) + #[inline] + pub fn discard(&self) { + let mut guard = self.inner.write(); + guard.1 = None; + } + + /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, + /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 + #[inline] + pub async fn with_data_modify(&self, f: F) -> Result + where + T: Send + Sync + 'static, + F: FnOnce(Box) -> Fut + Send, + Fut: std::future::Future, R), anyhow::Error>> + Send, + { + // 读取已提交快照(cheap Arc clone, 然后得到 Box 所有权 via clone) + // 注意:为了让闭包接收 Box 所有权,我们需要 clone 底层 T(不可避免) + let local: Box = { + let guard = self.inner.read(); + // 将 Arc> 的 Box clone 出来(会调用 T: Clone) + (*guard.0).clone() + }; + + let (new_local, res) = f(local).await?; + + // 将新的 Box 放到已提交位置(包进 Arc) + self.inner.write().0 = Arc::new(new_local); + + Ok(res) + } +} diff --git a/src-tauri/src/utils/draft.rs b/src-tauri/crates/draft/tests/test_me.rs similarity index 71% rename from src-tauri/src/utils/draft.rs rename to src-tauri/crates/draft/tests/test_me.rs index 07782f4bd..448fc7945 100644 --- a/src-tauri/src/utils/draft.rs +++ b/src-tauri/crates/draft/tests/test_me.rs @@ -1,110 +1,7 @@ -use parking_lot::RwLock; -use std::sync::Arc; - -pub type SharedBox = Arc>; -type DraftInner = (SharedBox, Option>); - -/// Draft 管理:committed 与 optional draft 都以 Arc> 存储, -// (committed_snapshot, optional_draft_snapshot) -#[derive(Debug, Clone)] -pub struct Draft { - inner: Arc>>, -} - -impl Draft { - #[inline] - pub fn new(data: T) -> Self { - Self { - inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), - } - } - /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) - #[inline] - pub fn data_arc(&self) -> SharedBox { - let guard = self.inner.read(); - Arc::clone(&guard.0) - } - - /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 - /// 这也是零拷贝:只 clone Arc,不 clone T - #[inline] - pub fn latest_arc(&self) -> SharedBox { - let guard = self.inner.read(); - guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) - } - - /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) - /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; - /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 - #[inline] - pub fn edit_draft(&self, f: F) -> R - where - F: FnOnce(&mut T) -> R, - { - // 先获得写锁以创建或取出草稿 Arc 的可变引用位置 - let mut guard = self.inner.write(); - let mut draft_arc = if guard.1.is_none() { - Arc::clone(&guard.0) - } else { - #[allow(clippy::unwrap_used)] - guard.1.take().unwrap() - }; - drop(guard); - // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box(要求 T: Clone) - let boxed = Arc::make_mut(&mut draft_arc); // &mut Box - // 对 Box 解引用得到 &mut T - let result = f(&mut **boxed); - // 恢复修改后的草稿 Arc - self.inner.write().1 = Some(draft_arc); - result - } - - /// 将草稿提交到已提交位置(替换),并清除草稿 - #[inline] - pub fn apply(&self) { - let mut guard = self.inner.write(); - if let Some(d) = guard.1.take() { - guard.0 = d; - } - } - - /// 丢弃草稿(如果存在) - #[inline] - pub fn discard(&self) { - let mut guard = self.inner.write(); - guard.1 = None; - } - - /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, - /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 - #[inline] - pub async fn with_data_modify(&self, f: F) -> Result - where - T: Send + Sync + 'static, - F: FnOnce(Box) -> Fut + Send, - Fut: std::future::Future, R), anyhow::Error>> + Send, - { - // 读取已提交快照(cheap Arc clone, 然后得到 Box 所有权 via clone) - // 注意:为了让闭包接收 Box 所有权,我们需要 clone 底层 T(不可避免) - let local: Box = { - let guard = self.inner.read(); - // 将 Arc> 的 Box clone 出来(会调用 T: Clone) - (*guard.0).clone() - }; - - let (new_local, res) = f(local).await?; - - // 将新的 Box 放到已提交位置(包进 Arc) - self.inner.write().0 = Arc::new(new_local); - - Ok(res) - } -} - #[cfg(test)] mod tests { - use super::*; use anyhow::anyhow; + use draft::Draft; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; diff --git a/src-tauri/src/cmd/verge.rs b/src-tauri/src/cmd/verge.rs index 943eb9db6..b58e9e524 100644 --- a/src-tauri/src/cmd/verge.rs +++ b/src-tauri/src/cmd/verge.rs @@ -1,5 +1,6 @@ use super::CmdResult; -use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox}; +use crate::{cmd::StringifyErr as _, config::IVerge, feat}; +use draft::SharedBox; /// 获取Verge配置 #[tauri::command] diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index 25a454720..316d99c50 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -5,10 +5,11 @@ use crate::{ constants::{files, timing}, core::{CoreManager, handle, service, tray, validate::CoreConfigValidator}, enhance, logging, logging_error, - utils::{Draft, dirs, help, logging::Type}, + utils::{dirs, help, logging::Type}, }; use anyhow::{Result, anyhow}; use backoff::{Error as BackoffError, ExponentialBackoff}; +use draft::Draft; use smartstring::alias::String; use std::path::PathBuf; use tokio::sync::OnceCell; diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index a127a5bb1..d9932f79d 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -3,9 +3,10 @@ use crate::{ core::{CoreManager, handle, hotkey, sysopt, tray}, logging, logging_error, module::{auto_backup::AutoBackupManager, lightweight}, - utils::{draft::SharedBox, logging::Type}, + utils::logging::Type, }; use anyhow::Result; +use draft::SharedBox; use serde_yaml_ng::Mapping; /// Patch Clash configuration diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 7c3e1abac..c9c905443 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,6 +1,5 @@ pub mod autostart; pub mod dirs; -pub mod draft; pub mod format; pub mod help; pub mod i18n; @@ -15,5 +14,3 @@ pub mod server; pub mod singleton; pub mod tmpl; pub mod window_manager; - -pub use draft::Draft; From 29d8df40b9ea648e242a8d773a15a8b27ef713ac Mon Sep 17 00:00:00 2001 From: Su Guoyu Date: Sun, 16 Nov 2025 14:52:13 +0800 Subject: [PATCH 22/26] fix(linux): retry with sudo when pkexec execution failed (#5469) --- src-tauri/src/core/service.rs | 46 ++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 9307015ab..2245ea8ed 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -136,11 +136,28 @@ async fn uninstall_service() -> Result<()> { let status = if linux_running_as_root() { StdCommand::new(&uninstall_path).status()? } else { - StdCommand::new(elevator) + let result = StdCommand::new(&elevator) .arg("sh") .arg("-c") - .arg(uninstall_shell) - .status()? + .arg(&uninstall_shell) + .status()?; + + // 如果 pkexec 执行失败,回退到 sudo + if !result.success() && elevator.contains("pkexec") { + logging!( + warn, + Type::Service, + "pkexec failed with code {}, falling back to sudo", + result.code().unwrap_or(-1) + ); + StdCommand::new("sudo") + .arg("sh") + .arg("-c") + .arg(&uninstall_shell) + .status()? + } else { + result + } }; logging!( info, @@ -177,11 +194,28 @@ async fn install_service() -> Result<()> { let status = if linux_running_as_root() { StdCommand::new(&install_path).status()? } else { - StdCommand::new(elevator) + let result = StdCommand::new(&elevator) .arg("sh") .arg("-c") - .arg(install_shell) - .status()? + .arg(&install_shell) + .status()?; + + // 如果 pkexec 执行失败,回退到 sudo + if !result.success() && elevator.contains("pkexec") { + logging!( + warn, + Type::Service, + "pkexec failed with code {}, falling back to sudo", + result.code().unwrap_or(-1) + ); + StdCommand::new("sudo") + .arg("sh") + .arg("-c") + .arg(&install_shell) + .status()? + } else { + result + } }; logging!( info, From 1eb4a0d834abc13ac52d1cf68d8c9df8afa610e5 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:35:36 +0800 Subject: [PATCH 23/26] fix(macos): remove tproxy-port from config on macOS #5439, #5397, #5372 --- Changelog.md | 1 + src-tauri/src/enhance/mod.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 264ac6303..34363428e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ - Linux 无法切换 TUN 堆栈 - macOS service 启动项显示名称(试验性修改) +- macOS 非预期 Tproxy 端口设置
✨ 新增功能 diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 6b74b74a0..9d8c43856 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -425,7 +425,7 @@ async fn merge_default_config( } #[cfg(target_os = "windows")] { - if key.as_str() == Some("redir-port") || key.as_str() == Some("tproxy-port") { + if key.as_str() == Some("redir-port") { continue; } } @@ -443,6 +443,13 @@ async fn merge_default_config( continue; } } + #[cfg(not(target_os = "linux"))] + { + if key.as_str() == Some("tproxy-port") { + config.remove("tproxy-port"); + continue; + } + } // 处理 external-controller 键的开关逻辑 if key.as_str() == Some("external-controller") { let enable_external_controller = Config::verge() From cfd45324408a9867fb1f0658f080b312d99052eb Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 16 Nov 2025 15:42:14 +0800 Subject: [PATCH 24/26] fix(home/traffic): ensure traffic chart stretches on Windows maximize --- .../home/enhanced-canvas-traffic-graph.tsx | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/components/home/enhanced-canvas-traffic-graph.tsx b/src/components/home/enhanced-canvas-traffic-graph.tsx index 0cb5d43cf..e56f169b2 100644 --- a/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -795,32 +795,43 @@ export const EnhancedCanvasTrafficGraph = memo( const ctx = canvas.getContext("2d"); if (!ctx) return; - // Canvas尺寸设置 + // Compute CSS size and pixel buffer size. + // Note: WebView2 on Windows may return fractional CSS sizes after maximize. + // We round pixel buffer to integers to avoid 1px gaps/cropping artifacts. const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; - const width = rect.width; - const height = rect.height; + const cssWidth = rect.width; + const cssHeight = rect.height; + const pixelWidth = Math.max(1, Math.floor(cssWidth * dpr)); + const pixelHeight = Math.max(1, Math.floor(cssHeight * dpr)); - // 只在尺寸变化时重新设置Canvas - if (canvas.width !== width * dpr || canvas.height !== height * dpr) { - canvas.width = width * dpr; - canvas.height = height * dpr; - ctx.scale(dpr, dpr); - canvas.style.width = width + "px"; - canvas.style.height = height + "px"; + // Keep CSS-driven sizing so the canvas stretches with its container (e.g., on maximize). + if (canvas.style.width !== "100%") { + canvas.style.width = "100%"; + } + if (canvas.style.height !== "100%") { + canvas.style.height = "100%"; } - // 清空画布 - ctx.clearRect(0, 0, width, height); + if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + // Reset transform before scaling to avoid cumulative scaling offsets. + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(dpr, dpr); // map CSS units to device pixels + } + + // Clear using CSS dimensions; context is already scaled by DPR. + ctx.clearRect(0, 0, cssWidth, cssHeight); // 绘制Y轴刻度线(背景层) - drawYAxis(ctx, width, height, displayData); + drawYAxis(ctx, cssWidth, cssHeight, displayData); // 绘制网格 - drawGrid(ctx, width, height); + drawGrid(ctx, cssWidth, cssHeight); // 绘制时间轴 - drawTimeAxis(ctx, width, height, displayData); + drawTimeAxis(ctx, cssWidth, cssHeight, displayData); // 提取流量数据 const upValues = displayData.map((d) => d.up); @@ -830,8 +841,8 @@ export const EnhancedCanvasTrafficGraph = memo( drawTrafficLine( ctx, downValues, - width, - height, + cssWidth, + cssHeight, colors.down, true, displayData, @@ -841,8 +852,8 @@ export const EnhancedCanvasTrafficGraph = memo( drawTrafficLine( ctx, upValues, - width, - height, + cssWidth, + cssHeight, colors.up, true, displayData, @@ -851,7 +862,7 @@ export const EnhancedCanvasTrafficGraph = memo( // 绘制悬浮高亮线 if (tooltipData.visible && tooltipData.dataIndex >= 0) { const padding = GRAPH_CONFIG.padding; - const effectiveWidth = width - padding.left - padding.right; + const effectiveWidth = cssWidth - padding.left - padding.right; const dataX = padding.left + (tooltipData.dataIndex / (displayData.length - 1)) * effectiveWidth; @@ -865,13 +876,13 @@ export const EnhancedCanvasTrafficGraph = memo( // 绘制垂直指示线 ctx.beginPath(); ctx.moveTo(dataX, padding.top); - ctx.lineTo(dataX, height - padding.bottom); + ctx.lineTo(dataX, cssHeight - padding.bottom); ctx.stroke(); // 绘制水平指示线(高亮Y轴位置) ctx.beginPath(); ctx.moveTo(padding.left, tooltipData.highlightY); - ctx.lineTo(width - padding.right, tooltipData.highlightY); + ctx.lineTo(cssWidth - padding.right, tooltipData.highlightY); ctx.stroke(); ctx.restore(); From e1fc9547d8f42f1f46cc5769c27a8c0949145386 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 16 Nov 2025 16:05:25 +0800 Subject: [PATCH 25/26] docs: Changelog.md --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 34363428e..ec2cf4a5b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ - Linux 无法切换 TUN 堆栈 - macOS service 启动项显示名称(试验性修改) - macOS 非预期 Tproxy 端口设置 +- 流量图缩放异常
✨ 新增功能 From b0089d162bc8ada2cf43b5c0d372cdcd348c1647 Mon Sep 17 00:00:00 2001 From: oomeow Date: Sun, 16 Nov 2025 18:53:43 +0800 Subject: [PATCH 26/26] fix: dynamically modify pac content (#5468) * fix: dynamically modify pac content * docs: update Changelog.md --- Changelog.md | 1 + src-tauri/src/utils/server.rs | 62 ++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/Changelog.md b/Changelog.md index ec2cf4a5b..d0165c2a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ - macOS service 启动项显示名称(试验性修改) - macOS 非预期 Tproxy 端口设置 - 流量图缩放异常 +- PAC 自动代理脚本内容无法动态调整
✨ 新增功能 diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 4defbd759..eec96db86 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -73,20 +73,20 @@ pub fn embed_server() { .expect("failed to set shutdown signal for embedded server"); let port = IVerge::get_singleton_port(); - AsyncHandler::spawn(move || async move { - let visible = warp::path!("commands" / "visible").and_then(|| async { - logging!(info, Type::Window, "检测到从单例模式恢复应用窗口"); - if !lightweight::exit_lightweight_mode().await { - WindowManager::show_main_window().await; - } else { - logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口"); - }; - Ok::<_, warp::Rejection>(warp::reply::with_status::( - "ok".to_string(), - warp::http::StatusCode::OK, - )) - }); + let visible = warp::path!("commands" / "visible").and_then(|| async { + logging!(info, Type::Window, "检测到从单例模式恢复应用窗口"); + if !lightweight::exit_lightweight_mode().await { + WindowManager::show_main_window().await; + } else { + logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口"); + }; + Ok::<_, warp::Rejection>(warp::reply::with_status::( + "ok".to_string(), + warp::http::StatusCode::OK, + )) + }); + let pac = warp::path!("commands" / "pac").and_then(|| async move { let verge_config = Config::verge().await; let clash_config = Config::clash().await; @@ -100,29 +100,31 @@ pub fn embed_server() { .data_arc() .verge_mixed_port .unwrap_or_else(|| clash_config.data_arc().get_mixed_port()); - - let pac = warp::path!("commands" / "pac").map(move || { - let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); + let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); + Ok::<_, warp::Rejection>( warp::http::Response::builder() .header("Content-Type", "application/x-ns-proxy-autoconfig") .body(processed_content) - .unwrap_or_default() + .unwrap_or_default(), + ) + }); + + // Use map instead of and_then to avoid Send issues + let scheme = warp::path!("commands" / "scheme") + .and(warp::query::()) + .and_then(|query: QueryParam| async move { + AsyncHandler::spawn(|| async move { + logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); + }); + Ok::<_, warp::Rejection>(warp::reply::with_status::( + "ok".to_string(), + warp::http::StatusCode::OK, + )) }); - // Use map instead of and_then to avoid Send issues - let scheme = warp::path!("commands" / "scheme") - .and(warp::query::()) - .and_then(|query: QueryParam| async move { - AsyncHandler::spawn(|| async move { - logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); - }); - Ok::<_, warp::Rejection>(warp::reply::with_status::( - "ok".to_string(), - warp::http::StatusCode::OK, - )) - }); + let commands = visible.or(scheme).or(pac); - let commands = visible.or(scheme).or(pac); + AsyncHandler::spawn(move || async move { warp::serve(commands) .bind(([127, 0, 0, 1], port)) .await