Merge remote-tracking branch 'origin/dev' into refactor/single-instance

This commit is contained in:
oomeow 2025-11-16 20:00:58 +08:00
commit 3bada1393e
No known key found for this signature in database
GPG Key ID: 1E1E69B3EC8F6EA7
49 changed files with 799 additions and 1966 deletions

View File

@ -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)

View File

@ -40,4 +40,3 @@ else
fi
echo "[pre-push] All checks passed."
exit 0

View File

@ -3,6 +3,10 @@
### 🐞 修复问题
- Linux 无法切换 TUN 堆栈
- macOS service 启动项显示名称(试验性修改)
- macOS 非预期 Tproxy 端口设置
- 流量图缩放异常
- PAC 自动代理脚本内容无法动态调整
<details>
<summary><strong> ✨ 新增功能 </strong></summary>

View File

@ -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}": [

266
pnpm-lock.yaml generated
View File

@ -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

202
src-tauri/Cargo.lock generated
View File

@ -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]]

View File

@ -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"

View File

@ -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 }

View File

@ -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<IVerge> {
#[derive(Clone, Debug)]
struct IVerge {
enable_auto_launch: Option<bool>,
enable_tun_mode: Option<bool>,
}
impl Default for IVerge {
fn default() -> Self {
Self {
enable_auto_launch: None,
enable_tun_mode: None,
}
}
}
fn make_draft() -> Draft<IVerge> {
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) {

View File

@ -0,0 +1,102 @@
use parking_lot::RwLock;
use std::sync::Arc;
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
/// Draft 管理committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone> {
inner: Arc<RwLock<DraftInner<T>>>,
}
impl<T: Clone> Draft<T> {
#[inline]
pub fn new(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
}
}
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc
#[inline]
pub fn data_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc不 clone T
#[inline]
pub fn latest_arc(&self) -> SharedBox<T> {
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<F, R>(&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>(要求 T: Clone
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &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<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
#[inline]
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
{
// 读取已提交快照cheap Arc clone, 然后得到 Box<T> 所有权 via clone
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T不可避免
let local: Box<T> = {
let guard = self.inner.read();
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone
(*guard.0).clone()
};
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 Arc
self.inner.write().0 = Arc::new(new_local);
Ok(res)
}
}

View File

@ -1,110 +1,7 @@
use parking_lot::RwLock;
use std::sync::Arc;
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
/// Draft 管理committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone> {
inner: Arc<RwLock<DraftInner<T>>>,
}
impl<T: Clone> Draft<T> {
#[inline]
pub fn new(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
}
}
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc
#[inline]
pub fn data_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc不 clone T
#[inline]
pub fn latest_arc(&self) -> SharedBox<T> {
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<F, R>(&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>(要求 T: Clone
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &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<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
#[inline]
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
{
// 读取已提交快照cheap Arc clone, 然后得到 Box<T> 所有权 via clone
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T不可避免
let local: Box<T> = {
let guard = self.inner.read();
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone
(*guard.0).clone()
};
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 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};

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>io.github.clash-verge-rev.clash-verge-rev.service</string>
</array>
</dict>
</plist>

View File

@ -84,8 +84,8 @@ pub async fn restart_app() -> CmdResult<()> {
/// 获取便携版标识
#[tauri::command]
pub fn get_portable_flag() -> CmdResult<bool> {
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<Stri
/// 通知UI已准备就绪
#[tauri::command]
pub fn notify_ui_ready() -> 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(())
}

View File

@ -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<Mapping> {
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<Mapping> {
/// 获取自动代理配置
#[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
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<String> {
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<String> {
// 去掉可能存在的引号
fallback.trim_matches('"').to_string()
}
};
Ok(hostname)
}
}
/// 获取网络接口列表

View File

@ -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(())
}

View File

@ -53,12 +53,12 @@ pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
/// 获取应用的运行时间(毫秒)
#[tauri::command]
pub fn get_app_uptime() -> CmdResult<u128> {
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<bool> {
Ok(*APPS_RUN_AS_ADMIN)
pub fn is_admin() -> bool {
*APPS_RUN_AS_ADMIN
}

View File

@ -17,6 +17,7 @@ mod platform {
mod platform {
use super::CmdResult;
#[allow(clippy::unnecessary_wraps)]
pub const fn invoke_uwp_tool() -> CmdResult {
Ok(())
}

View File

@ -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]

View File

@ -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);

View File

@ -581,6 +581,7 @@ impl PrfItem {
}
// 向前兼容,默认为订阅启用自动更新
#[allow(clippy::unnecessary_wraps)]
const fn default_allow_auto_update() -> Option<bool> {
Some(true)
}

View File

@ -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.*;<local>";
#[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,<local>";
}
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";

View File

@ -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<AsyncAutoproxy> {
// Windows: 从注册表读取PAC配置
AsyncHandler::spawn_blocking(move || -> Result<AsyncAutoproxy> {
Self::get_pac_config_from_registry()
})
.await?
}
#[cfg(target_os = "windows")]
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
.encode_utf16()
.collect::<Vec<u16>>();
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::<Vec<u16>>();
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::<Vec<u16>>();
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<AsyncAutoproxy> {
// 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<AsyncAutoproxy> {
// 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<AsyncSysproxy> {
// Windows: 使用注册表直接读取代理设置
AsyncHandler::spawn_blocking(move || -> Result<AsyncSysproxy> {
Self::get_system_proxy_from_registry()
})
.await?
}
#[cfg(target_os = "windows")]
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
.encode_utf16()
.collect::<Vec<u16>>();
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::<Vec<u16>>();
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::<Vec<u16>>();
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::<Vec<u16>>();
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::<u16>().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<AsyncSysproxy> {
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<String> = 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::<u16>()
{
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<AsyncSysproxy> {
// 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::<u16>()
.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<AsyncSysproxy> {
// 解析形如 "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::<u16>().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(),
})
}
}

View File

@ -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<RwLock<ProxyState>>,
event_sender: mpsc::UnboundedSender<ProxyEvent>,
query_sender: mpsc::UnboundedSender<QueryRequest>,
}
#[derive(Debug)]
pub struct QueryRequest {
response_tx: oneshot::Sender<Autoproxy>,
}
// 配置结构体移到外部
struct ProxyConfig {
sys_enabled: bool,
pac_enabled: bool,
guard_enabled: bool,
guard_duration: u64,
}
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = 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<RwLock<ProxyState>>,
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) {
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<RwLock<ProxyState>>, 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<RwLock<ProxyState>>) -> 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<RwLock<ProxyState>>) {
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<RwLock<ProxyState>>) {
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<RwLock<ProxyState>>) {
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<RwLock<ProxyState>>) {
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<RwLock<ProxyState>>) {
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<F>(state: &Arc<RwLock<ProxyState>>, 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(())
}
}

View File

@ -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<bool> {
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))
}
}

View File

@ -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<()> {

View File

@ -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<()> {

View File

@ -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};

View File

@ -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<clash_verge_service_ipc::IpcConfig> {
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<()> {

View File

@ -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<RwLock<GuardMonitor>>,
}
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<std::string::String>) -> 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<RwLock<GuardMonitor>> {
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<std::string::String> = if pac_enable {
let (args, guard_type): (Vec<std::string::String>, 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(())
}

View File

@ -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<Box<dyn std::future::Future<Output = ()> + 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;
}
})

View File

@ -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<HashMap<String, usize>>,
proxy_nodes_data: Result<Proxies>,
) -> Result<Vec<Submenu<Wry>>> {
) -> Vec<Submenu<Wry>> {
let proxy_submenus: Vec<Submenu<Wry>> = {
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = 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(
&current_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,

View File

@ -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()

View File

@ -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<String>,
webdav_username: Option<String>,
webdav_password: Option<String>,
) -> 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::<IVerge>(&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(())
}

View File

@ -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(())

View File

@ -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;

View File

@ -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<dyn std::error::Error>> {
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();
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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<CommandChild>);
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]

View File

@ -75,7 +75,9 @@ pub async fn save_yaml<T: Serialize + Sync>(
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] = [

View File

@ -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",
])));

View File

@ -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]

View File

@ -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;

View File

@ -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<Option<HttpClient>>,
system_proxy_client: Mutex<Option<HttpClient>>,
no_proxy_client: Mutex<Option<HttpClient>>,
self_proxy_client: Mutex<Option<Client>>,
system_proxy_client: Mutex<Option<Client>>,
no_proxy_client: Mutex<Option<Client>>,
last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: Mutex<usize>,
}
@ -111,41 +104,42 @@ impl NetworkManager {
fn build_client(
&self,
proxy_uri: Option<Uri>,
proxy_url: Option<std::string::String>,
default_headers: HeaderMap,
accept_invalid_certs: bool,
timeout_secs: Option<u64>,
) -> Result<HttpClient> {
{
let mut builder = HttpClient::builder();
) -> Result<Client> {
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<u64>,
user_agent: Option<String>,
accept_invalid_certs: bool,
) -> Result<HttpClient> {
let proxy_uri = match proxy_type {
) -> Result<Client> {
let proxy_url: Option<std::string::String> = 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::<Uri>().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::<Uri>().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))
}
}

View File

@ -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() {

View File

@ -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

View File

@ -28,7 +28,8 @@
"x": 200,
"y": 180
}
}
},
"infoPlist": "packages/macos/info_merge.plist"
}
}
}

View File

@ -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();

View File

@ -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<string, string> = Object.fromEntries(
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
const PROXY_POLICY_LABEL_KEYS: Record<string, string> =
const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
builtinProxyPolicies.reduce(
(acc, policy) => {
acc[policy] = `proxy.policies.${policy}`;
acc[policy] =
`proxies.components.enums.policies.${policy}` as TranslationKey;
return acc;
},
{} as Record<string, string>,
{} as Record<string, TranslationKey>,
);
export const RulesEditorViewer = (props: Props) => {