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) 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 diff --git a/Changelog.md b/Changelog.md index 082960a0b..d0165c2a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,10 @@ ### 🐞 修复问题 - Linux 无法切换 TUN 堆栈 +- macOS service 启动项显示名称(试验性修改) +- macOS 非预期 Tproxy 端口设置 +- 流量图缩放异常 +- PAC 自动代理脚本内容无法动态调整
✨ 新增功能 diff --git a/package.json b/package.json index db4e538b0..b68ed8986 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", @@ -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", @@ -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 314a64ef5..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 @@ -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) + version: 10.1.0(@types/react@19.2.5)(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) @@ -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)) @@ -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: @@ -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==} @@ -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: @@ -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' @@ -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: @@ -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 @@ -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: @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b9c98fa50..aa46886f1 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" @@ -1128,19 +1122,19 @@ dependencies = [ "criterion", "deelevate", "delay_timer", + "draft", "dunce", "flexi_logger", "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", @@ -1258,7 +1252,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]] @@ -1277,7 +1271,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", @@ -1649,36 +1643,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" @@ -1724,7 +1688,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.12", + "parking_lot_core", ] [[package]] @@ -1738,7 +1702,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.12", + "parking_lot_core", ] [[package]] @@ -2054,6 +2018,16 @@ dependencies = [ "serde", ] +[[package]] +name = "draft" +version = "0.1.0" +dependencies = [ + "anyhow", + "criterion", + "parking_lot", + "tokio", +] + [[package]] name = "dtoa" version = "1.0.10" @@ -3416,7 +3390,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -3731,34 +3705,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" @@ -3911,7 +3857,7 @@ dependencies = [ "httparse", "interprocess", "libc", - "parking_lot 0.12.5", + "parking_lot", "path-tree", "pin-project-lite", "rand 0.9.2", @@ -4009,7 +3955,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.5.18", + "redox_syscall", ] [[package]] @@ -4021,18 +3967,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" @@ -4918,7 +4852,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]] @@ -5031,7 +4965,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]] @@ -5079,17 +5013,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" @@ -5097,21 +5020,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]] @@ -5122,7 +5031,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link 0.2.1", ] @@ -6039,15 +5948,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" @@ -6417,7 +6317,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6994,17 +6894,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" @@ -7085,7 +6974,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", @@ -7142,7 +7031,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", @@ -7274,13 +7163,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", @@ -7355,7 +7245,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", @@ -7403,9 +7293,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", @@ -7457,9 +7347,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", @@ -7479,9 +7369,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", @@ -7506,9 +7396,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", @@ -8186,7 +8076,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot 0.12.5", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.6.1", @@ -8606,16 +8496,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" @@ -9338,7 +9218,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 65fc7abcc..0385a1a3f 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -14,11 +14,14 @@ 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] +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,19 +34,14 @@ 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" -sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } -tauri = { version = "2.9.2", features = [ +sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [ + "guard", +] } +tauri = { version = "2.9.3", features = [ "protocol-asset", "devtools", "tray-icon", @@ -70,10 +68,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" @@ -123,6 +117,28 @@ tauri-plugin-global-shortcut = "2.3.1" tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } 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"] @@ -132,11 +148,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 @@ -170,9 +181,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 } @@ -259,3 +267,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/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/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/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..ba780962a 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -1,8 +1,7 @@ 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; use serde_yaml_ng::Mapping; @@ -11,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) } @@ -35,37 +34,27 @@ 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) } /// 获取系统主机名 #[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 +62,7 @@ pub fn get_system_hostname() -> CmdResult { // 去掉可能存在的引号 fallback.trim_matches('"').to_string() } - }; - - Ok(hostname) + } } /// 获取网络接口列表 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/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/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 ff4902262..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; @@ -57,9 +58,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/constants.rs b/src-tauri/src/constants.rs index a07da7c15..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,23 +19,10 @@ 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; - 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 +44,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/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs deleted file mode 100644 index f0e579f6f..000000000 --- a/src-tauri/src/core/async_proxy_query.rs +++ /dev/null @@ -1,565 +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配置 - AsyncHandler::spawn_blocking(move || -> Result { - Self::get_pac_config_from_registry() - }) - .await? - } - - #[cfg(target_os = "windows")] - fn get_pac_config_from_registry() -> Result { - 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 Ok(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(); - } - - Ok(AsyncAutoproxy { - enable: true, - url: pac_url, - }) - } else { - logging!(debug, Type::Network, "PAC配置未启用"); - Ok(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: 使用注册表直接读取代理设置 - AsyncHandler::spawn_blocking(move || -> Result { - Self::get_system_proxy_from_registry() - }) - .await? - } - - #[cfg(target_os = "windows")] - fn get_system_proxy_from_registry() -> Result { - 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 Ok(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 Ok(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}" - ); - - Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: bypass_list, - }) - } else { - Ok(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/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..a264f29e9 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, @@ -26,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(()), } } @@ -55,13 +59,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/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/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/service.rs b/src-tauri/src/core/service.rs index 892294d95..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, @@ -456,12 +490,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..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,31 +111,64 @@ impl Sysopt { self.initialed.load(Ordering::SeqCst) } - pub fn init_guard_sysproxy(&self) -> Result<()> { - // 使用事件驱动代理管理器 - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_app_started(); + fn access_guard(&self) -> Arc> { + Arc::clone(&self.guard) + } - logging!(info, Type::Core, "已启用事件驱动代理守卫"); - Ok(()) + 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(), @@ -133,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), @@ -161,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(()); } @@ -171,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(()); } @@ -181,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/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; } }) 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/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() 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(()) } diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index cfa9aef00..d9932f79d 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -1,11 +1,12 @@ 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}, + utils::logging::Type, }; use anyhow::Result; +use draft::SharedBox; use serde_yaml_ng::Mapping; /// Patch Clash configuration @@ -107,6 +108,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 +147,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 +215,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 +267,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 c36cdbd7d..139765441 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,11 +13,7 @@ pub mod utils; use crate::constants::files; #[cfg(target_os = "linux")] use crate::utils::linux; -use crate::{ - core::{EventDrivenProxyManager, handle}, - process::AsyncHandler, - utils::resolve, -}; +use crate::{core::handle, process::AsyncHandler, utils::resolve}; use anyhow::Result; use once_cell::sync::OnceCell; use rust_i18n::i18n; @@ -85,9 +81,9 @@ 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)))] - app.deep_link().register_all()?; + let _ = app.deep_link().register_all(); app.deep_link().on_open_url(|event| { let urls = event.urls(); @@ -99,8 +95,6 @@ mod app_init { } }); }); - - Ok(()) } /// Setup autostart plugin @@ -242,9 +236,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); @@ -440,7 +432,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/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/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/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] = [ diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index c7ab73095..809b97cc6 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", ]))); 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] 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; 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)) } } diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 4ea2d376f..fa0871371 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -53,7 +53,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 { @@ -148,7 +148,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; } @@ -168,8 +168,8 @@ pub(super) async fn init_system_proxy() { ); } -pub(super) fn init_system_proxy_guard() { - logging_error!(Type::Setup, 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() { diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index d50af28dd..d8fc783af 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -21,7 +21,7 @@ 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 pac = warp::path!("commands" / "pac").and_then(|| async move { let verge_config = Config::verge().await; let clash_config = Config::clash().await; @@ -35,15 +35,16 @@ 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(), + ) + }); + AsyncHandler::spawn(move || async move { warp::serve(pac) .bind(([127, 0, 0, 1], port)) .await 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" } } } 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(); 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) => {