mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-13 05:20:28 +08:00
Merge remote-tracking branch 'origin/dev' into refactor/single-instance
This commit is contained in:
commit
3bada1393e
18
.github/workflows/autobuild.yml
vendored
18
.github/workflows/autobuild.yml
vendored
@ -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)
|
||||
|
||||
@ -40,4 +40,3 @@ else
|
||||
fi
|
||||
|
||||
echo "[pre-push] All checks passed."
|
||||
exit 0
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
### 🐞 修复问题
|
||||
|
||||
- Linux 无法切换 TUN 堆栈
|
||||
- macOS service 启动项显示名称(试验性修改)
|
||||
- macOS 非预期 Tproxy 端口设置
|
||||
- 流量图缩放异常
|
||||
- PAC 自动代理脚本内容无法动态调整
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
10
package.json
10
package.json
@ -71,9 +71,9 @@
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-i18next": "16.3.1",
|
||||
"react-i18next": "16.3.3",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^7.9.5",
|
||||
"react-router": "^7.9.6",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"swr": "^2.3.6",
|
||||
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main",
|
||||
@ -87,7 +87,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "19.2.4",
|
||||
"@types/react": "19.2.5",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
@ -109,7 +109,7 @@
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
"lint-staged": "^16.2.6",
|
||||
"meta-json-schema": "^1.19.14",
|
||||
"meta-json-schema": "^1.19.16",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.94.0",
|
||||
@ -120,7 +120,7 @@
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-monaco-editor-esm": "^2.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vitest": "^4.0.8"
|
||||
"vitest": "^4.0.9"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
|
||||
266
pnpm-lock.yaml
generated
266
pnpm-lock.yaml
generated
@ -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
202
src-tauri/Cargo.lock
generated
@ -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]]
|
||||
|
||||
@ -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"
|
||||
|
||||
17
src-tauri/crates/draft/Cargo.toml
Normal file
17
src-tauri/crates/draft/Cargo.toml
Normal 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 }
|
||||
@ -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) {
|
||||
102
src-tauri/crates/draft/src/lib.rs
Normal file
102
src-tauri/crates/draft/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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};
|
||||
10
src-tauri/packages/macos/info_merge.plist
Normal file
10
src-tauri/packages/macos/info_merge.plist
Normal 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>
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取网络接口列表
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ mod platform {
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub const fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -581,6 +581,7 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
// 向前兼容,默认为订阅启用自动更新
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
const fn default_allow_auto_update() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<()> {
|
||||
|
||||
@ -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<()> {
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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<()> {
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
})
|
||||
|
||||
@ -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(
|
||||
¤t_profile_selected,
|
||||
proxy_group_order_map,
|
||||
proxy_nodes_data.map_err(anyhow::Error::from),
|
||||
)?;
|
||||
);
|
||||
|
||||
let (proxies_menu, inline_proxy_items) = create_proxy_menu_item(
|
||||
app_handle,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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] = [
|
||||
|
||||
@ -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",
|
||||
])));
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -28,7 +28,8 @@
|
||||
"x": 200,
|
||||
"y": 180
|
||||
}
|
||||
}
|
||||
},
|
||||
"infoPlist": "packages/macos/info_merge.plist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user