Compare commits
2 Commits
chore/upda
...
refactor-p
42
.github/workflows/autobuild.yml
vendored
42
.github/workflows/autobuild.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
|||||||
|
|
||||||
### Windows (不再支持Win7)
|
### 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_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||||
|
|
||||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
#### 内置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)
|
- [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)
|
||||||
@@ -493,43 +493,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
publish-updater-manifests:
|
|
||||||
name: Publish Updater Manifests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
[
|
|
||||||
update_tag,
|
|
||||||
autobuild-x86-windows-macos-linux,
|
|
||||||
autobuild-arm-linux,
|
|
||||||
autobuild-x86-arm-windows_webview2,
|
|
||||||
]
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm i
|
|
||||||
|
|
||||||
- name: Publish updater manifests
|
|
||||||
run: pnpm updater
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish WebView2 updater manifests
|
|
||||||
run: pnpm updater-fixed-webview2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
notify-telegram:
|
notify-telegram:
|
||||||
name: Notify Telegram
|
name: Notify Telegram
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -539,7 +502,6 @@ jobs:
|
|||||||
autobuild-x86-windows-macos-linux,
|
autobuild-x86-windows-macos-linux,
|
||||||
autobuild-arm-linux,
|
autobuild-arm-linux,
|
||||||
autobuild-x86-arm-windows_webview2,
|
autobuild-x86-arm-windows_webview2,
|
||||||
publish-updater-manifests,
|
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -616,7 +578,7 @@ jobs:
|
|||||||
- [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_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||||
|
|
||||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
#### 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_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## v2.4.3
|
## v2.4.3
|
||||||
|
|
||||||
感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
|
感谢 @Slinetrac, @oomeow 以及 @Lythrilla 的出色贡献
|
||||||
|
|
||||||
### 🐞 修复问题
|
### 🐞 修复问题
|
||||||
|
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
||||||
- 修复 Linux 系统主题切换不生效
|
- 修复 Linux 系统主题切换不生效
|
||||||
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
||||||
- 修复轻量模式托盘状态不同步
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"@tauri-apps/cli": "2.9.2",
|
"@tauri-apps/cli": "2.9.2",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.9.2",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"@vitejs/plugin-legacy": "^7.2.1",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
|
|||||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -154,8 +154,8 @@ importers:
|
|||||||
specifier: ^4.17.12
|
specifier: ^4.17.12
|
||||||
version: 4.17.12
|
version: 4.17.12
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.10.0
|
specifier: ^24.9.2
|
||||||
version: 24.10.0
|
version: 24.9.2
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.2.2
|
specifier: 19.2.2
|
||||||
version: 19.2.2
|
version: 19.2.2
|
||||||
@@ -164,10 +164,10 @@ importers:
|
|||||||
version: 19.2.2(@types/react@19.2.2)
|
version: 19.2.2(@types/react@19.2.2)
|
||||||
'@vitejs/plugin-legacy':
|
'@vitejs/plugin-legacy':
|
||||||
specifier: ^7.2.1
|
specifier: ^7.2.1
|
||||||
version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@vitejs/plugin-react-swc':
|
'@vitejs/plugin-react-swc':
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
version: 4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
adm-zip:
|
adm-zip:
|
||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
@@ -248,16 +248,16 @@ importers:
|
|||||||
version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.1.12
|
specifier: ^7.1.12
|
||||||
version: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
vite-plugin-monaco-editor-esm:
|
vite-plugin-monaco-editor-esm:
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.2(monaco-editor@0.54.0)
|
version: 2.0.2(monaco-editor@0.54.0)
|
||||||
vite-plugin-svgr:
|
vite-plugin-svgr:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.6
|
specifier: ^4.0.6
|
||||||
version: 4.0.6(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
version: 4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1821,8 +1821,8 @@ packages:
|
|||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@24.10.0':
|
'@types/node@24.9.2':
|
||||||
resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==}
|
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
|
||||||
|
|
||||||
'@types/parse-json@4.0.2':
|
'@types/parse-json@4.0.2':
|
||||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||||
@@ -5982,7 +5982,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@24.10.0':
|
'@types/node@24.9.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
@@ -6160,7 +6160,7 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.4
|
||||||
'@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4)
|
'@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4)
|
||||||
@@ -6175,15 +6175,15 @@ snapshots:
|
|||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
systemjs: 6.15.1
|
systemjs: 6.15.1
|
||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
'@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.43
|
'@rolldown/pluginutils': 1.0.0-beta.43
|
||||||
'@swc/core': 1.14.0
|
'@swc/core': 1.14.0
|
||||||
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
|
|
||||||
@@ -6196,13 +6196,13 @@ snapshots:
|
|||||||
chai: 6.2.0
|
chai: 6.2.0
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
'@vitest/mocker@4.0.6(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
'@vitest/mocker@4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.0.6
|
'@vitest/spy': 4.0.6
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.19
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
|
|
||||||
'@vitest/pretty-format@4.0.6':
|
'@vitest/pretty-format@4.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8844,18 +8844,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
monaco-editor: 0.54.0
|
monaco-editor: 0.54.0
|
||||||
|
|
||||||
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)):
|
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
|
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
|
||||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
|
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
|
||||||
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -8864,17 +8864,17 @@ snapshots:
|
|||||||
rollup: 4.46.2
|
rollup: 4.46.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.10.0
|
'@types/node': 24.9.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
sass: 1.93.3
|
sass: 1.93.3
|
||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
yaml: 2.8.1
|
yaml: 2.8.1
|
||||||
|
|
||||||
vitest@4.0.6(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
vitest@4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.6
|
'@vitest/expect': 4.0.6
|
||||||
'@vitest/mocker': 4.0.6(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
'@vitest/mocker': 4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@vitest/pretty-format': 4.0.6
|
'@vitest/pretty-format': 4.0.6
|
||||||
'@vitest/runner': 4.0.6
|
'@vitest/runner': 4.0.6
|
||||||
'@vitest/snapshot': 4.0.6
|
'@vitest/snapshot': 4.0.6
|
||||||
@@ -8891,11 +8891,11 @@ snapshots:
|
|||||||
tinyexec: 0.3.2
|
tinyexec: 0.3.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 24.10.0
|
'@types/node': 24.9.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
* pnpm release-version <version>
|
* pnpm release-version <version>
|
||||||
*
|
*
|
||||||
* <version> can be:
|
* <version> can be:
|
||||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3-rc.1)
|
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
||||||
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
||||||
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
||||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3-autobuild.1022.r2+cc39b2)
|
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
||||||
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3-autobuild.1022.r2+a1b2c3d)
|
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
|
||||||
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3-deploytest.1022.r2+cc39b2)
|
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* pnpm release-version 1.2.3
|
* pnpm release-version 1.2.3
|
||||||
@@ -30,11 +30,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import fs from "fs/promises";
|
|
||||||
import process from "node:process";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
import { program } from "commander";
|
import { program } from "commander";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 git 短 commit hash
|
* 获取当前 git 短 commit hash
|
||||||
@@ -75,118 +73,41 @@ function getLatestTauriCommit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Asia/Shanghai 时区的日期片段
|
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
||||||
|
* 使用 Asia/Shanghai 时区
|
||||||
|
* @param {boolean} withCommit 是否带 commit
|
||||||
|
* @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit(仅当 withCommit 为 true 时有效)
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getLocalDatePart() {
|
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const dateFormatter = new Intl.DateTimeFormat("en-CA", {
|
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||||
timeZone: "Asia/Shanghai",
|
timeZone: "Asia/Shanghai",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
});
|
});
|
||||||
const dateParts = Object.fromEntries(
|
|
||||||
dateFormatter.formatToParts(now).map((part) => [part.type, part.value]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const month = dateParts.month ?? "00";
|
const parts = formatter.formatToParts(now);
|
||||||
const day = dateParts.day ?? "00";
|
const month = parts.find((part) => part.type === "month").value;
|
||||||
|
const day = parts.find((part) => part.type === "day").value;
|
||||||
|
|
||||||
|
if (withCommit) {
|
||||||
|
const gitShort = useTauriCommit
|
||||||
|
? getLatestTauriCommit()
|
||||||
|
: getGitShortCommit();
|
||||||
|
return `${month}${day}.${gitShort}`;
|
||||||
|
}
|
||||||
return `${month}${day}`;
|
return `${month}${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 GitHub Actions 运行编号(若存在)
|
|
||||||
* @returns {string|null}
|
|
||||||
*/
|
|
||||||
function getRunIdentifier() {
|
|
||||||
const runNumber = process.env.GITHUB_RUN_NUMBER;
|
|
||||||
if (runNumber && /^[0-9]+$/.test(runNumber)) {
|
|
||||||
const runNum = Number.parseInt(runNumber, 10);
|
|
||||||
if (!Number.isNaN(runNum)) {
|
|
||||||
const base = `r${runNum.toString(36)}`;
|
|
||||||
const attempt = process.env.GITHUB_RUN_ATTEMPT;
|
|
||||||
if (attempt && /^[0-9]+$/.test(attempt)) {
|
|
||||||
const attemptNumber = Number.parseInt(attempt, 10);
|
|
||||||
if (!Number.isNaN(attemptNumber) && attemptNumber > 1) {
|
|
||||||
return `${base}${attemptNumber.toString(36)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const attempt = process.env.GITHUB_RUN_ATTEMPT;
|
|
||||||
if (attempt && /^[0-9]+$/.test(attempt)) {
|
|
||||||
const attemptNumber = Number.parseInt(attempt, 10);
|
|
||||||
if (!Number.isNaN(attemptNumber)) {
|
|
||||||
return `r${attemptNumber.toString(36)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成用于自动构建类渠道的版本后缀
|
|
||||||
* @param {Object} options
|
|
||||||
* @param {boolean} [options.includeCommit=false]
|
|
||||||
* @param {"current"|"tauri"} [options.commitSource="current"]
|
|
||||||
* @param {boolean} [options.includeRun=true]
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function generateChannelSuffix({
|
|
||||||
includeCommit = false,
|
|
||||||
commitSource = "current",
|
|
||||||
includeRun = true,
|
|
||||||
} = {}) {
|
|
||||||
const segments = [];
|
|
||||||
const date = getLocalDatePart();
|
|
||||||
segments.push(date);
|
|
||||||
|
|
||||||
if (includeCommit) {
|
|
||||||
const commit =
|
|
||||||
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
|
|
||||||
segments.push(commit);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeRun) {
|
|
||||||
const run = getRunIdentifier();
|
|
||||||
if (run) {
|
|
||||||
segments.push(run);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments.join(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 为 autobuild 渠道构建版本片段
|
|
||||||
* @param {Object} options
|
|
||||||
* @param {"current"|"tauri"} [options.commitSource="current"]
|
|
||||||
* @returns {{date: string, run: string, metadata: string}}
|
|
||||||
*/
|
|
||||||
function generateAutobuildComponents({ commitSource = "current" } = {}) {
|
|
||||||
const date = getLocalDatePart();
|
|
||||||
const run = getRunIdentifier() ?? `manual${Date.now().toString(36)}`;
|
|
||||||
const commitHash =
|
|
||||||
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
|
|
||||||
|
|
||||||
return {
|
|
||||||
date,
|
|
||||||
run,
|
|
||||||
metadata: commitHash || "nogit",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证版本号格式
|
* 验证版本号格式
|
||||||
* @param {string} version
|
* @param {string} version
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function isValidVersion(version) {
|
function isValidVersion(version) {
|
||||||
return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
||||||
version,
|
version,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -201,14 +122,13 @@ function normalizeVersion(version) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取基础版本号(去掉所有 pre-release 和 build metadata)
|
* 提取基础版本号(去掉所有 -tag 和 +build 部分)
|
||||||
* @param {string} version
|
* @param {string} version
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getBaseVersion(version) {
|
function getBaseVersion(version) {
|
||||||
const cleaned = version.startsWith("v") ? version.slice(1) : version;
|
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
||||||
const withoutBuild = cleaned.split("+")[0];
|
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
||||||
const [base] = withoutBuild.split("-");
|
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,17 +273,17 @@ async function main(versionArg) {
|
|||||||
const baseVersion = getBaseVersion(currentVersion);
|
const baseVersion = getBaseVersion(currentVersion);
|
||||||
|
|
||||||
if (versionArg.toLowerCase() === "autobuild") {
|
if (versionArg.toLowerCase() === "autobuild") {
|
||||||
// 格式: 2.3.0-autobuild.1022.r2+cc39b2
|
// 格式: 2.3.0+autobuild.1004.cc39b27
|
||||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
// 使用 Tauri 相关的最新 commit hash
|
||||||
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
|
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
|
||||||
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
||||||
// 格式: 2.3.0-autobuild.1022.r2+a1b2c3d (使用最新 Tauri 提交)
|
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
|
||||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
const latestTauriCommit = getLatestTauriCommit();
|
||||||
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
|
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
||||||
} else if (versionArg.toLowerCase() === "deploytest") {
|
} else if (versionArg.toLowerCase() === "deploytest") {
|
||||||
// 格式: 2.3.0-deploytest.1022.r2+cc39b2
|
// 格式: 2.3.0+deploytest.1004.cc39b27
|
||||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
// 使用 Tauri 相关的最新 commit hash
|
||||||
newVersion = `${baseVersion}-deploytest.${parts.date}.${parts.run}+${parts.metadata}`;
|
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
|
||||||
} else {
|
} else {
|
||||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,7 @@
|
|||||||
import process from "node:process";
|
|
||||||
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
|
import { getOctokit, context } from "@actions/github";
|
||||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||||
|
|
||||||
const SEMVER_REGEX =
|
|
||||||
/v?\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/g;
|
|
||||||
const STRICT_SEMVER_REGEX =
|
|
||||||
/^\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
||||||
|
|
||||||
const stripLeadingV = (version) =>
|
|
||||||
typeof version === "string" && version.startsWith("v")
|
|
||||||
? version.slice(1)
|
|
||||||
: version;
|
|
||||||
|
|
||||||
const preferCandidate = (current, candidate) => {
|
|
||||||
if (!candidate) return current;
|
|
||||||
if (!current) return candidate;
|
|
||||||
|
|
||||||
const candidateHasPre = /[-+]/.test(candidate);
|
|
||||||
const currentHasPre = /[-+]/.test(current);
|
|
||||||
|
|
||||||
if (candidateHasPre && !currentHasPre) return candidate;
|
|
||||||
if (candidateHasPre === currentHasPre && candidate.length > current.length) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return current;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractBestSemver = (input) => {
|
|
||||||
if (typeof input !== "string") return null;
|
|
||||||
const matches = input.match(SEMVER_REGEX);
|
|
||||||
if (!matches) return null;
|
|
||||||
|
|
||||||
return matches
|
|
||||||
.map(stripLeadingV)
|
|
||||||
.reduce((best, candidate) => preferCandidate(best, candidate), null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const splitIdentifiers = (segment) =>
|
|
||||||
segment
|
|
||||||
.split(/[^0-9A-Za-z-]+/)
|
|
||||||
.map((part) => part.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const sanitizeSuffix = (value, fallbackLabel) => {
|
|
||||||
if (!value) return fallbackLabel;
|
|
||||||
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) return fallbackLabel;
|
|
||||||
|
|
||||||
const [preRelease = "", metadata] = trimmed.split("+", 2);
|
|
||||||
const normalizedPre = splitIdentifiers(preRelease).join(".") || fallbackLabel;
|
|
||||||
const normalizedMeta = metadata ? splitIdentifiers(metadata).join(".") : "";
|
|
||||||
|
|
||||||
return normalizedMeta ? `${normalizedPre}+${normalizedMeta}` : normalizedPre;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureSemverCompatibleVersion = (
|
|
||||||
version,
|
|
||||||
{ channel, releaseTag, fallbackLabel },
|
|
||||||
) => {
|
|
||||||
const trimmed = stripLeadingV(version ?? "").trim();
|
|
||||||
if (!trimmed) return null;
|
|
||||||
|
|
||||||
if (STRICT_SEMVER_REGEX.test(trimmed)) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channel === "autobuild") {
|
|
||||||
const normalizedSuffix = sanitizeSuffix(trimmed, fallbackLabel ?? channel);
|
|
||||||
const fallback = `0.0.0-${normalizedSuffix}`;
|
|
||||||
console.warn(
|
|
||||||
`[${channel}] Normalized non-semver version "${trimmed}" from release "${releaseTag}" to "${fallback}"`,
|
|
||||||
);
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`[${channel}] Derived version "${trimmed}" is not semver compatible for release "${releaseTag}"`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveReleaseVersion = (release) => {
|
|
||||||
const sources = [
|
|
||||||
release?.name,
|
|
||||||
release?.tag_name,
|
|
||||||
release?.body,
|
|
||||||
...(Array.isArray(release?.assets)
|
|
||||||
? release.assets.map((asset) => asset?.name)
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
return sources.reduce((best, source) => {
|
|
||||||
const candidate = extractBestSemver(source);
|
|
||||||
return preferCandidate(best, candidate);
|
|
||||||
}, null);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add stable update JSON filenames
|
// Add stable update JSON filenames
|
||||||
const UPDATE_TAG_NAME = "updater";
|
const UPDATE_TAG_NAME = "updater";
|
||||||
const UPDATE_JSON_FILE = "update.json";
|
const UPDATE_JSON_FILE = "update.json";
|
||||||
@@ -108,11 +10,6 @@ const UPDATE_JSON_PROXY = "update-proxy.json";
|
|||||||
const ALPHA_TAG_NAME = "updater-alpha";
|
const ALPHA_TAG_NAME = "updater-alpha";
|
||||||
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||||
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
// Add autobuild update JSON filenames
|
|
||||||
const AUTOBUILD_SOURCE_TAG_NAME = "autobuild";
|
|
||||||
const AUTOBUILD_TAG_NAME = "updater-autobuild";
|
|
||||||
const AUTOBUILD_UPDATE_JSON_FILE = "update.json";
|
|
||||||
const AUTOBUILD_UPDATE_JSON_PROXY = "update-proxy.json";
|
|
||||||
|
|
||||||
/// generate update.json
|
/// generate update.json
|
||||||
/// upload to update tag's release asset
|
/// upload to update tag's release asset
|
||||||
@@ -151,12 +48,12 @@ async function resolveUpdater() {
|
|||||||
|
|
||||||
// More flexible tag detection with regex patterns
|
// More flexible tag detection with regex patterns
|
||||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
||||||
|
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
||||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
||||||
|
|
||||||
// Get tags for known channels
|
// Get the latest stable tag and pre-release tag
|
||||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
||||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
||||||
const autobuildTag = tags.find((t) => t.name === AUTOBUILD_SOURCE_TAG_NAME);
|
|
||||||
|
|
||||||
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
||||||
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
||||||
@@ -164,106 +61,32 @@ async function resolveUpdater() {
|
|||||||
"Pre-release tag:",
|
"Pre-release tag:",
|
||||||
preReleaseTag ? preReleaseTag.name : "None found",
|
preReleaseTag ? preReleaseTag.name : "None found",
|
||||||
);
|
);
|
||||||
console.log(
|
|
||||||
"Autobuild tag:",
|
|
||||||
autobuildTag ? autobuildTag.name : "None found",
|
|
||||||
);
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const channels = [
|
// Process stable release
|
||||||
{
|
if (stableTag) {
|
||||||
name: "stable",
|
await processRelease(github, options, stableTag, false);
|
||||||
tagName: stableTag?.name,
|
}
|
||||||
updateReleaseTag: UPDATE_TAG_NAME,
|
|
||||||
jsonFile: UPDATE_JSON_FILE,
|
|
||||||
proxyFile: UPDATE_JSON_PROXY,
|
|
||||||
prerelease: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "alpha",
|
|
||||||
tagName: preReleaseTag?.name,
|
|
||||||
updateReleaseTag: ALPHA_TAG_NAME,
|
|
||||||
jsonFile: ALPHA_UPDATE_JSON_FILE,
|
|
||||||
proxyFile: ALPHA_UPDATE_JSON_PROXY,
|
|
||||||
prerelease: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "autobuild",
|
|
||||||
tagName: autobuildTag?.name ?? AUTOBUILD_SOURCE_TAG_NAME,
|
|
||||||
updateReleaseTag: AUTOBUILD_TAG_NAME,
|
|
||||||
jsonFile: AUTOBUILD_UPDATE_JSON_FILE,
|
|
||||||
proxyFile: AUTOBUILD_UPDATE_JSON_PROXY,
|
|
||||||
prerelease: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const channel of channels) {
|
// Process pre-release if found
|
||||||
if (!channel.tagName) {
|
if (preReleaseTag) {
|
||||||
console.log(`[${channel.name}] tag not found, skipping...`);
|
await processRelease(github, options, preReleaseTag, true);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await processRelease(github, options, channel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process a release and generate update files for the specified channel
|
// Process a release (stable or alpha) and generate update files
|
||||||
async function processRelease(github, options, channelConfig) {
|
async function processRelease(github, options, tag, isAlpha) {
|
||||||
if (!channelConfig) return;
|
if (!tag) return;
|
||||||
|
|
||||||
const {
|
|
||||||
tagName,
|
|
||||||
name: channelName,
|
|
||||||
updateReleaseTag,
|
|
||||||
jsonFile,
|
|
||||||
proxyFile,
|
|
||||||
prerelease,
|
|
||||||
} = channelConfig;
|
|
||||||
|
|
||||||
const channelLabel =
|
|
||||||
channelName.charAt(0).toUpperCase() + channelName.slice(1);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: tagName,
|
tag: tag.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const releaseTagName = release.tag_name ?? tagName;
|
|
||||||
const resolvedVersion = resolveReleaseVersion(release);
|
|
||||||
|
|
||||||
if (!resolvedVersion) {
|
|
||||||
throw new Error(
|
|
||||||
`[${channelName}] Failed to determine semver version from release "${releaseTagName}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[${channelName}] Preparing update metadata from release "${releaseTagName}"`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[${channelName}] Resolved release version: ${resolvedVersion}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const semverCompatibleVersion = ensureSemverCompatibleVersion(
|
|
||||||
resolvedVersion,
|
|
||||||
{
|
|
||||||
channel: channelName,
|
|
||||||
releaseTag: releaseTagName,
|
|
||||||
fallbackLabel: channelName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (semverCompatibleVersion !== resolvedVersion) {
|
|
||||||
console.log(
|
|
||||||
`[${channelName}] Normalized updater version: ${semverCompatibleVersion}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
version: semverCompatibleVersion,
|
name: tag.name,
|
||||||
original_version: resolvedVersion,
|
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||||
tag_name: releaseTagName,
|
|
||||||
notes: await resolveUpdateLog(releaseTagName).catch(() =>
|
|
||||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||||
),
|
),
|
||||||
pub_date: new Date().toISOString(),
|
pub_date: new Date().toISOString(),
|
||||||
@@ -363,15 +186,13 @@ async function processRelease(github, options, channelConfig) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
console.log(`[${channelName}] Update data snapshot:`, updateData);
|
console.log(updateData);
|
||||||
|
|
||||||
// maybe should test the signature as well
|
// maybe should test the signature as well
|
||||||
// delete the null field
|
// delete the null field
|
||||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||||
if (!value.url) {
|
if (!value.url) {
|
||||||
console.log(
|
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||||
`[${channelName}] [Error]: failed to parse release for "${key}"`,
|
|
||||||
);
|
|
||||||
delete updateData.platforms[key];
|
delete updateData.platforms[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -384,14 +205,15 @@ async function processRelease(github, options, channelConfig) {
|
|||||||
updateDataNew.platforms[key].url =
|
updateDataNew.platforms[key].url =
|
||||||
"https://download.clashverge.dev/" + value.url;
|
"https://download.clashverge.dev/" + value.url;
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||||
`[${channelName}] [Error]: updateDataNew.platforms.${key} is null`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get the appropriate updater release based on isAlpha flag
|
||||||
|
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
||||||
console.log(
|
console.log(
|
||||||
`[${channelName}] Processing update release target "${updateReleaseTag}"`,
|
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
||||||
|
releaseTag,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -401,28 +223,30 @@ async function processRelease(github, options, channelConfig) {
|
|||||||
// Try to get the existing release
|
// Try to get the existing release
|
||||||
const response = await github.rest.repos.getReleaseByTag({
|
const response = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: updateReleaseTag,
|
tag: releaseTag,
|
||||||
});
|
});
|
||||||
updateRelease = response.data;
|
updateRelease = response.data;
|
||||||
console.log(
|
console.log(
|
||||||
`[${channelName}] Found existing ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If release doesn't exist, create it
|
// If release doesn't exist, create it
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${channelName}] Release with tag ${updateReleaseTag} not found, creating new release...`,
|
`Release with tag ${releaseTag} not found, creating new release...`,
|
||||||
);
|
);
|
||||||
const createResponse = await github.rest.repos.createRelease({
|
const createResponse = await github.rest.repos.createRelease({
|
||||||
...options,
|
...options,
|
||||||
tag_name: updateReleaseTag,
|
tag_name: releaseTag,
|
||||||
name: `Auto-update ${channelLabel} Channel`,
|
name: isAlpha
|
||||||
body: `This release contains the update information for the ${channelName} channel.`,
|
? "Auto-update Alpha Channel"
|
||||||
prerelease,
|
: "Auto-update Stable Channel",
|
||||||
|
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
||||||
|
prerelease: isAlpha,
|
||||||
});
|
});
|
||||||
updateRelease = createResponse.data;
|
updateRelease = createResponse.data;
|
||||||
console.log(
|
console.log(
|
||||||
`[${channelName}] Created new ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If it's another error, throw it
|
// If it's another error, throw it
|
||||||
@@ -431,8 +255,11 @@ async function processRelease(github, options, channelConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// File names based on release type
|
// File names based on release type
|
||||||
|
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
||||||
|
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
||||||
|
|
||||||
// Delete existing assets with these names
|
// Delete existing assets with these names
|
||||||
for (const asset of updateRelease.assets) {
|
for (let asset of updateRelease.assets) {
|
||||||
if (asset.name === jsonFile) {
|
if (asset.name === jsonFile) {
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
@@ -443,12 +270,7 @@ async function processRelease(github, options, channelConfig) {
|
|||||||
if (asset.name === proxyFile) {
|
if (asset.name === proxyFile) {
|
||||||
await github.rest.repos
|
await github.rest.repos
|
||||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||||
.catch((deleteError) =>
|
.catch(console.error); // do not break the pipeline
|
||||||
console.error(
|
|
||||||
`[${channelName}] Failed to delete existing proxy asset:`,
|
|
||||||
deleteError.message,
|
|
||||||
),
|
|
||||||
); // do not break the pipeline
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,22 +290,20 @@ async function processRelease(github, options, channelConfig) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${channelName}] Successfully uploaded update files to ${updateReleaseTag}`,
|
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`[${channelName}] Failed to process update release:`,
|
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
console.log(
|
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
||||||
`[${channelName}] Release not found for tag: ${tagName}, skipping...`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
`[${channelName}] Failed to get release for tag: ${tagName}`,
|
`Failed to get release for tag: ${tag.name}`,
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
8
src-tauri/Cargo.lock
generated
8
src-tauri/Cargo.lock
generated
@@ -152,12 +152,6 @@ dependencies = [
|
|||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arc-swap"
|
|
||||||
version = "1.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arraydeque"
|
name = "arraydeque"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -1099,7 +1093,6 @@ version = "2.4.3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"backoff",
|
"backoff",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -1158,7 +1151,6 @@ dependencies = [
|
|||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"url",
|
|
||||||
"users",
|
"users",
|
||||||
"warp",
|
"warp",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ smartstring = { version = "1.0.1", features = ["serde"] }
|
|||||||
clash_verge_service_ipc = { version = "2.0.21", features = [
|
clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||||
"client",
|
"client",
|
||||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||||
arc-swap = "1.7.1"
|
|
||||||
url = "2.5.4"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ pub mod runtime;
|
|||||||
pub mod save_profile;
|
pub mod save_profile;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
pub mod updater;
|
|
||||||
pub mod uwp;
|
pub mod uwp;
|
||||||
pub mod validate;
|
pub mod validate;
|
||||||
pub mod verge;
|
pub mod verge;
|
||||||
@@ -35,7 +34,6 @@ pub use runtime::*;
|
|||||||
pub use save_profile::*;
|
pub use save_profile::*;
|
||||||
pub use service::*;
|
pub use service::*;
|
||||||
pub use system::*;
|
pub use system::*;
|
||||||
pub use updater::*;
|
|
||||||
pub use uwp::*;
|
pub use uwp::*;
|
||||||
pub use validate::*;
|
pub use validate::*;
|
||||||
pub use verge::*;
|
pub use verge::*;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::cmd::StringifyErr;
|
use crate::cmd::StringifyErr;
|
||||||
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
|
|
||||||
use crate::process::AsyncHandler;
|
|
||||||
use crate::{logging, utils::logging::Type};
|
use crate::{logging, utils::logging::Type};
|
||||||
use network_interface::NetworkInterface;
|
use network_interface::NetworkInterface;
|
||||||
use serde_yaml_ng::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
@@ -11,23 +9,23 @@ use serde_yaml_ng::Mapping;
|
|||||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||||
logging!(debug, Type::Network, "异步获取系统代理配置");
|
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();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), sys_proxy.enable.into());
|
||||||
map.insert(
|
map.insert(
|
||||||
"server".into(),
|
"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!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
Type::Network,
|
Type::Network,
|
||||||
"返回系统代理配置: enable={}, {}:{}",
|
"返回系统代理配置: enable={}, {}:{}",
|
||||||
current.enable,
|
sys_proxy.enable,
|
||||||
current.host,
|
sys_proxy.host,
|
||||||
current.port
|
sys_proxy.port
|
||||||
);
|
);
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
@@ -37,24 +35,18 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
|||||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||||
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
|
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
|
||||||
|
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
let auto_proxy = sysproxy::Autoproxy::get_auto_proxy().stringify_err()?;
|
||||||
|
|
||||||
let current = proxy_manager.get_auto_proxy_cached().await;
|
|
||||||
// 异步请求更新,立即返回缓存数据
|
|
||||||
AsyncHandler::spawn(move || async move {
|
|
||||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), auto_proxy.enable.into());
|
||||||
map.insert("url".into(), current.url.clone().into());
|
map.insert("url".into(), auto_proxy.url.clone().into());
|
||||||
|
|
||||||
logging!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
Type::Network,
|
Type::Network,
|
||||||
"返回自动代理配置(缓存): enable={}, url={}",
|
"返回自动代理配置(缓存): enable={}, url={}",
|
||||||
current.enable,
|
auto_proxy.enable,
|
||||||
current.url
|
auto_proxy.url
|
||||||
);
|
);
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,68 @@ use crate::{
|
|||||||
ret_err,
|
ret_err,
|
||||||
utils::{dirs, help, logging::Type},
|
utils::{dirs, help, logging::Type},
|
||||||
};
|
};
|
||||||
use scopeguard::defer;
|
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// 全局请求序列号跟踪,用于避免队列化执行
|
||||||
|
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
logging!(debug, Type::Cmd, "获取配置文件列表");
|
// 策略1: 尝试快速获取latest数据
|
||||||
let draft = Config::profiles().await;
|
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
|
||||||
let latest = draft.latest_ref();
|
let profiles = Config::profiles().await;
|
||||||
Ok((**latest).clone())
|
let latest = profiles.latest_ref();
|
||||||
|
IProfiles {
|
||||||
|
current: latest.current.clone(),
|
||||||
|
items: latest.items.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match latest_result {
|
||||||
|
Ok(profiles) => {
|
||||||
|
logging!(info, Type::Cmd, "快速获取配置列表成功");
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(warn, Type::Cmd, "快速获取配置超时(500ms)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略2: 如果快速获取失败,尝试获取data()
|
||||||
|
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let data = profiles.latest_ref();
|
||||||
|
IProfiles {
|
||||||
|
current: data.current.clone(),
|
||||||
|
items: data.items.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match data_result {
|
||||||
|
Ok(profiles) => {
|
||||||
|
logging!(info, Type::Cmd, "获取draft配置列表成功");
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
Err(join_err) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
"获取draft配置任务失败或超时: {}",
|
||||||
|
join_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略3: fallback,尝试重新创建配置
|
||||||
|
logging!(warn, Type::Cmd, "所有获取配置策略都失败,尝试fallback");
|
||||||
|
|
||||||
|
Ok(IProfiles::new().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 增强配置文件
|
/// 增强配置文件
|
||||||
@@ -283,7 +332,7 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
|
|||||||
Config::profiles()
|
Config::profiles()
|
||||||
.await
|
.await
|
||||||
.draft_mut()
|
.draft_mut()
|
||||||
.patch_config(&restore_profiles)
|
.patch_config(restore_profiles)
|
||||||
.stringify_err()?;
|
.stringify_err()?;
|
||||||
Config::profiles().await.apply();
|
Config::profiles().await.apply();
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
@@ -295,7 +344,26 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
async fn handle_success(current_sequence: u64, current_value: Option<String>) -> CmdResult<bool> {
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
Config::profiles().await.discard();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"配置更新成功,序列号: {}",
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
Config::profiles().await.apply();
|
Config::profiles().await.apply();
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
|
|
||||||
@@ -312,10 +380,17 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(current) = ¤t_value {
|
if let Some(current) = ¤t_value {
|
||||||
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,);
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||||
|
current,
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
handle::Handle::notify_profile_changed(current.clone());
|
handle::Handle::notify_profile_changed(current.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,31 +404,53 @@ async fn handle_validation_failure(
|
|||||||
restore_previous_profile(prev_profile).await?;
|
restore_previous_profile(prev_profile).await?;
|
||||||
}
|
}
|
||||||
handle::Handle::notice_message("config_validate::error", error_msg);
|
handle::Handle::notice_message("config_validate::error", error_msg);
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
|
async fn handle_update_error<E: std::fmt::Display>(e: E, current_sequence: u64) -> CmdResult<bool> {
|
||||||
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Cmd,
|
||||||
|
"更新过程发生错误: {}, 序列号: {}",
|
||||||
|
e,
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
Config::profiles().await.discard();
|
Config::profiles().await.discard();
|
||||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
|
async fn handle_timeout(current_profile: Option<String>, current_sequence: u64) -> CmdResult<bool> {
|
||||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||||
logging!(error, Type::Cmd, "{}", timeout_msg);
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
"{}, 序列号: {}",
|
||||||
|
timeout_msg,
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
Config::profiles().await.discard();
|
Config::profiles().await.discard();
|
||||||
if let Some(prev_profile) = current_profile {
|
if let Some(prev_profile) = current_profile {
|
||||||
restore_previous_profile(prev_profile).await?;
|
restore_previous_profile(prev_profile).await?;
|
||||||
}
|
}
|
||||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn perform_config_update(
|
async fn perform_config_update(
|
||||||
|
current_sequence: u64,
|
||||||
current_value: Option<String>,
|
current_value: Option<String>,
|
||||||
current_profile: Option<String>,
|
current_profile: Option<String>,
|
||||||
) -> CmdResult<bool> {
|
) -> CmdResult<bool> {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"开始内核配置更新,序列号: {}",
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
let update_result = tokio::time::timeout(
|
let update_result = tokio::time::timeout(
|
||||||
Duration::from_secs(30),
|
Duration::from_secs(30),
|
||||||
CoreManager::global().update_config(),
|
CoreManager::global().update_config(),
|
||||||
@@ -361,36 +458,46 @@ async fn perform_config_update(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
match update_result {
|
match update_result {
|
||||||
Ok(Ok((true, _))) => handle_success(current_value).await,
|
Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await,
|
||||||
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
|
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
|
||||||
Ok(Err(e)) => handle_update_error(e).await,
|
Ok(Err(e)) => handle_update_error(e, current_sequence).await,
|
||||||
Err(_) => handle_timeout(current_profile).await,
|
Err(_) => handle_timeout(current_profile, current_sequence).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改profiles的配置
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||||
if CURRENT_SWITCHING_PROFILE
|
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
|
||||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||||
return Err("switch_in_progress".into());
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
defer! {
|
// 为当前请求分配序列号
|
||||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
}
|
|
||||||
let target_profile = profiles.current.clone();
|
let target_profile = profiles.current.clone();
|
||||||
|
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Cmd,
|
Type::Cmd,
|
||||||
"开始修改配置文件,目标profile: {:?}",
|
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
||||||
|
current_sequence,
|
||||||
target_profile
|
target_profile
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
// 保存当前配置,以便在验证失败时恢复
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
let current_profile = Config::profiles().await.latest_ref().current.clone();
|
let current_profile = Config::profiles().await.latest_ref().current.clone();
|
||||||
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
|
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
|
||||||
@@ -400,13 +507,50 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
&& current_profile.as_ref() != Some(new_profile)
|
&& current_profile.as_ref() != Some(new_profile)
|
||||||
&& validate_new_profile(new_profile).await.is_err()
|
&& validate_new_profile(new_profile).await.is_err()
|
||||||
{
|
{
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = Config::profiles().await.draft_mut().patch_config(&profiles);
|
// 检查请求有效性
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新profiles配置
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"正在更新配置草稿,序列号: {}",
|
||||||
|
current_sequence
|
||||||
|
);
|
||||||
|
|
||||||
let current_value = profiles.current.clone();
|
let current_value = profiles.current.clone();
|
||||||
|
|
||||||
perform_config_update(current_value, current_profile).await
|
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
|
||||||
|
|
||||||
|
// 在调用内核前再次验证请求有效性
|
||||||
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
|
if current_sequence < latest_sequence {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||||
|
current_sequence,
|
||||||
|
latest_sequence
|
||||||
|
);
|
||||||
|
Config::profiles().await.discard();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
perform_config_update(current_sequence, current_value, current_profile).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据profile name修改profiles
|
/// 根据profile name修改profiles
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
use serde::Serialize;
|
|
||||||
use tauri::{Manager, ResourceId, Runtime, webview::Webview};
|
|
||||||
use tauri_plugin_updater::UpdaterExt;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use super::{CmdResult, String};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct UpdateMetadata {
|
|
||||||
rid: ResourceId,
|
|
||||||
current_version: String,
|
|
||||||
version: String,
|
|
||||||
date: Option<String>,
|
|
||||||
body: Option<String>,
|
|
||||||
raw_json: serde_json::Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum UpdateChannel {
|
|
||||||
Stable,
|
|
||||||
Autobuild,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for UpdateChannel {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
"stable" => Ok(Self::Stable),
|
|
||||||
"autobuild" => Ok(Self::Autobuild),
|
|
||||||
other => Err(String::from(format!("Unsupported channel \"{other}\""))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHANNEL_RELEASE_TAGS: &[(UpdateChannel, &str)] = &[
|
|
||||||
(UpdateChannel::Stable, "updater"),
|
|
||||||
(UpdateChannel::Autobuild, "updater-autobuild"),
|
|
||||||
];
|
|
||||||
|
|
||||||
const CHANNEL_ENDPOINT_TEMPLATES: &[&str] = &[
|
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
|
||||||
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update.json",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn resolve_release_tag(channel: UpdateChannel) -> CmdResult<&'static str> {
|
|
||||||
CHANNEL_RELEASE_TAGS
|
|
||||||
.iter()
|
|
||||||
.find_map(|(entry_channel, tag)| (*entry_channel == channel).then_some(*tag))
|
|
||||||
.ok_or_else(|| {
|
|
||||||
String::from(format!(
|
|
||||||
"No release tag registered for update channel \"{channel:?}\""
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_channel_endpoints(channel: UpdateChannel) -> CmdResult<Vec<Url>> {
|
|
||||||
let release_tag = resolve_release_tag(channel)?;
|
|
||||||
CHANNEL_ENDPOINT_TEMPLATES
|
|
||||||
.iter()
|
|
||||||
.map(|template| {
|
|
||||||
let endpoint = template.replace("{release}", release_tag);
|
|
||||||
Url::parse(&endpoint).map_err(|err| {
|
|
||||||
String::from(format!(
|
|
||||||
"Failed to parse updater endpoint \"{endpoint}\": {err}"
|
|
||||||
))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn check_update_channel<R: Runtime>(
|
|
||||||
webview: Webview<R>,
|
|
||||||
channel: String,
|
|
||||||
headers: Option<Vec<(String, String)>>,
|
|
||||||
timeout: Option<u64>,
|
|
||||||
proxy: Option<String>,
|
|
||||||
target: Option<String>,
|
|
||||||
allow_downgrades: Option<bool>,
|
|
||||||
) -> CmdResult<Option<UpdateMetadata>> {
|
|
||||||
let channel_enum = UpdateChannel::try_from(channel.as_str())?;
|
|
||||||
let endpoints = resolve_channel_endpoints(channel_enum)?;
|
|
||||||
|
|
||||||
let mut builder = webview
|
|
||||||
.updater_builder()
|
|
||||||
.endpoints(endpoints)
|
|
||||||
.map_err(|err| String::from(err.to_string()))?;
|
|
||||||
|
|
||||||
if let Some(headers) = headers {
|
|
||||||
for (key, value) in headers {
|
|
||||||
builder = builder
|
|
||||||
.header(key.as_str(), value.as_str())
|
|
||||||
.map_err(|err| String::from(err.to_string()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(timeout) = timeout {
|
|
||||||
builder = builder.timeout(std::time::Duration::from_millis(timeout));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(proxy) = proxy {
|
|
||||||
let proxy_url = Url::parse(&proxy)
|
|
||||||
.map_err(|err| String::from(format!("Invalid proxy URL \"{proxy}\": {err}")))?;
|
|
||||||
builder = builder.proxy(proxy_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(target) = target {
|
|
||||||
builder = builder.target(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
let allow_downgrades = allow_downgrades.unwrap_or(channel_enum != UpdateChannel::Stable);
|
|
||||||
|
|
||||||
if allow_downgrades {
|
|
||||||
builder = builder.version_comparator(|current, update| update.version != current);
|
|
||||||
}
|
|
||||||
|
|
||||||
let updater = builder
|
|
||||||
.build()
|
|
||||||
.map_err(|err| String::from(err.to_string()))?;
|
|
||||||
|
|
||||||
let update = updater
|
|
||||||
.check()
|
|
||||||
.await
|
|
||||||
.map_err(|err| String::from(err.to_string()))?;
|
|
||||||
|
|
||||||
let Some(update) = update else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatted_date = update
|
|
||||||
.date
|
|
||||||
.as_ref()
|
|
||||||
.map(|date| String::from(date.to_string()));
|
|
||||||
|
|
||||||
let metadata = UpdateMetadata {
|
|
||||||
rid: webview.resources_table().add(update.clone()),
|
|
||||||
current_version: String::from(update.current_version.clone()),
|
|
||||||
version: String::from(update.version.clone()),
|
|
||||||
date: formatted_date,
|
|
||||||
body: update.body.clone().map(Into::into),
|
|
||||||
raw_json: update.raw_json.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(metadata))
|
|
||||||
}
|
|
||||||
@@ -69,16 +69,23 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logging!(error, Type::Config, "{err}");
|
logging!(error, Type::Config, "{err}");
|
||||||
Self::default()
|
Self::template()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logging!(error, Type::Config, "{err}");
|
logging!(error, Type::Config, "{err}");
|
||||||
Self::default()
|
Self::template()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn template() -> Self {
|
||||||
|
Self {
|
||||||
|
items: Some(vec![]),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn save_file(&self) -> Result<()> {
|
pub async fn save_file(&self) -> Result<()> {
|
||||||
help::save_yaml(
|
help::save_yaml(
|
||||||
&dirs::profiles_path()?,
|
&dirs::profiles_path()?,
|
||||||
@@ -89,17 +96,17 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 只修改current,valid和chain
|
/// 只修改current,valid和chain
|
||||||
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> {
|
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
|
||||||
if self.items.is_none() {
|
if self.items.is_none() {
|
||||||
self.items = Some(vec![]);
|
self.items = Some(vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(current) = &patch.current
|
if let Some(current) = patch.current
|
||||||
&& let Some(items) = self.items.as_ref()
|
&& let Some(items) = self.items.as_ref()
|
||||||
{
|
{
|
||||||
let some_uid = Some(current);
|
let some_uid = Some(current);
|
||||||
if items.iter().any(|e| e.uid.as_ref() == some_uid) {
|
if items.iter().any(|e| e.uid == some_uid) {
|
||||||
self.current = some_uid.cloned();
|
self.current = some_uid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub mod network {
|
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 const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097";
|
||||||
|
|
||||||
pub mod ports {
|
pub mod ports {
|
||||||
@@ -20,18 +19,6 @@ pub mod network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod bypass {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub const DEFAULT: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<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 {
|
pub mod timing {
|
||||||
use super::Duration;
|
use super::Duration;
|
||||||
|
|
||||||
|
|||||||
@@ -1,562 +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 mut proxy_server = String::new();
|
|
||||||
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
|
||||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
|
||||||
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取代理绕过列表
|
|
||||||
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 mut bypass_list = String::new();
|
|
||||||
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());
|
|
||||||
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,8 @@ use crate::{
|
|||||||
utils::{dirs, logging::Type},
|
utils::{dirs, logging::Type},
|
||||||
};
|
};
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -56,24 +56,24 @@ impl Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct WebDavClient {
|
pub struct WebDavClient {
|
||||||
config: Arc<ArcSwapOption<WebDavConfig>>,
|
config: Arc<Mutex<Option<WebDavConfig>>>,
|
||||||
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
|
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebDavClient {
|
impl WebDavClient {
|
||||||
pub fn global() -> &'static WebDavClient {
|
pub fn global() -> &'static WebDavClient {
|
||||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||||
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
||||||
config: Arc::new(ArcSwapOption::new(None)),
|
config: Arc::new(Mutex::new(None)),
|
||||||
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
|
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
|
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
|
||||||
// 先尝试从缓存获取
|
// 先尝试从缓存获取
|
||||||
{
|
{
|
||||||
let clients_map = self.clients.load();
|
let clients = self.clients.lock();
|
||||||
if let Some(client) = clients_map.get(&op) {
|
if let Some(client) = clients.get(&op) {
|
||||||
return Ok(client.clone());
|
return Ok(client.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,10 +81,10 @@ impl WebDavClient {
|
|||||||
// 获取或创建配置
|
// 获取或创建配置
|
||||||
let config = {
|
let config = {
|
||||||
// 首先检查是否已有配置
|
// 首先检查是否已有配置
|
||||||
let existing_config = self.config.load();
|
let existing_config = self.config.lock().as_ref().cloned();
|
||||||
|
|
||||||
if let Some(cfg_arc) = existing_config.clone() {
|
if let Some(cfg) = existing_config {
|
||||||
(*cfg_arc).clone()
|
cfg
|
||||||
} else {
|
} else {
|
||||||
// 释放锁后获取异步配置
|
// 释放锁后获取异步配置
|
||||||
let verge = Config::verge().await.latest_ref().clone();
|
let verge = Config::verge().await.latest_ref().clone();
|
||||||
@@ -106,8 +106,8 @@ impl WebDavClient {
|
|||||||
password: verge.webdav_password.unwrap_or_default(),
|
password: verge.webdav_password.unwrap_or_default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 存储配置到 ArcSwapOption
|
// 重新获取锁并存储配置
|
||||||
self.config.store(Some(Arc::new(config.clone())));
|
*self.config.lock() = Some(config.clone());
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -161,19 +161,18 @@ impl WebDavClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
|
// 缓存客户端
|
||||||
{
|
{
|
||||||
let mut map = (**self.clients.load()).clone();
|
let mut clients = self.clients.lock();
|
||||||
map.insert(op, client.clone());
|
clients.insert(op, client.clone());
|
||||||
self.clients.store(map.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&self) {
|
pub fn reset(&self) {
|
||||||
self.config.store(None);
|
*self.config.lock() = None;
|
||||||
self.clients.store(Arc::new(HashMap::new()));
|
self.clients.lock().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
||||||
|
|||||||
@@ -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, 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 EventDrivenProxyManager {
|
|
||||||
&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_ref();
|
|
||||||
(
|
|
||||||
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_ref();
|
|
||||||
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_ref();
|
|
||||||
(verge_ref.verge_mixed_port, verge_ref.proxy_host.clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
let default_port = {
|
|
||||||
let clash_config = Config::clash().await;
|
|
||||||
clash_config.latest_ref().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_ref();
|
|
||||||
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;
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ use crate::{
|
|||||||
singleton_with_logging, utils::logging::Type,
|
singleton_with_logging, utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use arc_swap::ArcSwap;
|
use parking_lot::Mutex;
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
||||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||||
@@ -93,13 +93,13 @@ impl SystemHotkey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Hotkey {
|
pub struct Hotkey {
|
||||||
current: ArcSwap<Vec<String>>,
|
current: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hotkey {
|
impl Hotkey {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
current: ArcSwap::new(Arc::new(Vec::new())),
|
current: Arc::new(Mutex::new(Vec::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,9 +272,9 @@ impl Hotkey {
|
|||||||
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
|
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
|
||||||
|
|
||||||
impl Hotkey {
|
impl Hotkey {
|
||||||
pub async fn init(&self, skip: bool) -> Result<()> {
|
pub async fn init(&self) -> Result<()> {
|
||||||
let verge = Config::verge().await;
|
let verge = Config::verge().await;
|
||||||
let enable_global_hotkey = !skip && verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
||||||
|
|
||||||
logging!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
@@ -283,6 +283,10 @@ impl Hotkey {
|
|||||||
enable_global_hotkey
|
enable_global_hotkey
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if !enable_global_hotkey {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Extract hotkeys data before async operations
|
// Extract hotkeys data before async operations
|
||||||
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
|
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
|
||||||
|
|
||||||
@@ -340,7 +344,7 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.current.store(Arc::new(hotkeys));
|
self.current.lock().clone_from(&hotkeys);
|
||||||
} else {
|
} else {
|
||||||
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
||||||
}
|
}
|
||||||
@@ -371,8 +375,8 @@ impl Hotkey {
|
|||||||
|
|
||||||
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||||
// Extract current hotkeys before async operations
|
// Extract current hotkeys before async operations
|
||||||
let current_hotkeys = &*self.current.load();
|
let current_hotkeys = self.current.lock().clone();
|
||||||
let old_map = Self::get_map_from_vec(current_hotkeys);
|
let old_map = Self::get_map_from_vec(¤t_hotkeys);
|
||||||
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
||||||
|
|
||||||
let (del, add) = Self::get_diff(old_map, new_map);
|
let (del, add) = Self::get_diff(old_map, new_map);
|
||||||
@@ -386,7 +390,7 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the current hotkeys after all async operations
|
// Update the current hotkeys after all async operations
|
||||||
self.current.store(Arc::new(new_hotkeys));
|
*self.current.lock() = new_hotkeys;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,20 +39,25 @@ impl CoreManager {
|
|||||||
return Ok((true, String::new()));
|
return Ok((true, String::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _permit = self
|
||||||
|
.update_semaphore
|
||||||
|
.try_acquire()
|
||||||
|
.map_err(|_| anyhow!("Config update already in progress"))?;
|
||||||
|
|
||||||
self.perform_config_update().await
|
self.perform_config_update().await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_update_config(&self) -> Result<bool> {
|
fn should_update_config(&self) -> Result<bool> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let last = self.get_last_update();
|
let mut last = self.last_update.lock();
|
||||||
|
|
||||||
if let Some(last_time) = last
|
if let Some(last_time) = *last
|
||||||
&& now.duration_since(*last_time) < timing::CONFIG_UPDATE_DEBOUNCE
|
&& now.duration_since(last_time) < timing::CONFIG_UPDATE_DEBOUNCE
|
||||||
{
|
{
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_last_update(now);
|
*last = Some(now);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ mod lifecycle;
|
|||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
use parking_lot::Mutex;
|
||||||
use std::{fmt, sync::Arc, time::Instant};
|
use std::{fmt, sync::Arc, time::Instant};
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
use crate::process::CommandChildGuard;
|
use crate::process::CommandChildGuard;
|
||||||
use crate::singleton_lazy;
|
use crate::singleton_lazy;
|
||||||
@@ -28,21 +29,22 @@ impl fmt::Display for RunningMode {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CoreManager {
|
pub struct CoreManager {
|
||||||
state: ArcSwap<State>,
|
state: Arc<Mutex<State>>,
|
||||||
last_update: ArcSwapOption<Instant>,
|
update_semaphore: Arc<Semaphore>,
|
||||||
|
last_update: Arc<Mutex<Option<Instant>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct State {
|
struct State {
|
||||||
running_mode: ArcSwap<RunningMode>,
|
running_mode: Arc<RunningMode>,
|
||||||
child_sidecar: ArcSwapOption<CommandChildGuard>,
|
child_sidecar: Option<CommandChildGuard>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
running_mode: ArcSwap::new(Arc::new(RunningMode::NotRunning)),
|
running_mode: Arc::new(RunningMode::NotRunning),
|
||||||
child_sidecar: ArcSwapOption::new(None),
|
child_sidecar: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,41 +52,24 @@ impl Default for State {
|
|||||||
impl Default for CoreManager {
|
impl Default for CoreManager {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: ArcSwap::new(Arc::new(State::default())),
|
state: Arc::new(Mutex::new(State::default())),
|
||||||
last_update: ArcSwapOption::new(None),
|
update_semaphore: Arc::new(Semaphore::new(1)),
|
||||||
|
last_update: Arc::new(Mutex::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoreManager {
|
impl CoreManager {
|
||||||
pub fn get_running_mode(&self) -> Arc<RunningMode> {
|
pub fn get_running_mode(&self) -> Arc<RunningMode> {
|
||||||
Arc::clone(&self.state.load().running_mode.load())
|
Arc::clone(&self.state.lock().running_mode)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_child_sidecar(&self) -> Option<CommandChildGuard> {
|
|
||||||
self.state
|
|
||||||
.load()
|
|
||||||
.child_sidecar
|
|
||||||
.swap(None)
|
|
||||||
.and_then(|arc| Arc::try_unwrap(arc).ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_update(&self) -> Option<Arc<Instant>> {
|
|
||||||
self.last_update.load_full()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_running_mode(&self, mode: RunningMode) {
|
pub fn set_running_mode(&self, mode: RunningMode) {
|
||||||
let state = self.state.load();
|
self.state.lock().running_mode = Arc::new(mode);
|
||||||
state.running_mode.store(Arc::new(mode));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_running_child_sidecar(&self, child: CommandChildGuard) {
|
pub fn set_running_child_sidecar(&self, child: CommandChildGuard) {
|
||||||
let state = self.state.load();
|
self.state.lock().child_sidecar = Some(child);
|
||||||
state.child_sidecar.store(Some(Arc::new(child)));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_last_update(&self, time: Instant) {
|
|
||||||
self.last_update.store(Some(Arc::new(time)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init(&self) -> Result<()> {
|
pub async fn init(&self) -> Result<()> {
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ impl CoreManager {
|
|||||||
defer! {
|
defer! {
|
||||||
self.set_running_mode(RunningMode::NotRunning);
|
self.set_running_mode(RunningMode::NotRunning);
|
||||||
}
|
}
|
||||||
if let Some(child) = self.take_child_sidecar() {
|
let mut state = self.state.lock();
|
||||||
|
if let Some(child) = state.child_sidecar.take() {
|
||||||
let pid = child.pid();
|
let pid = child.pid();
|
||||||
drop(child);
|
drop(child);
|
||||||
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);
|
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
pub mod async_proxy_query;
|
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod event_driven_proxy;
|
|
||||||
pub mod handle;
|
pub mod handle;
|
||||||
pub mod hotkey;
|
pub mod hotkey;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
@@ -13,4 +11,4 @@ pub mod tray;
|
|||||||
pub mod validate;
|
pub mod validate;
|
||||||
pub mod win_uwp;
|
pub mod win_uwp;
|
||||||
|
|
||||||
pub use self::{event_driven_proxy::EventDrivenProxyManager, manager::CoreManager, timer::Timer};
|
pub use self::{manager::CoreManager, timer::Timer};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use crate::utils::autostart as startup_shortcut;
|
use crate::utils::autostart as startup_shortcut;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::{EventDrivenProxyManager, handle::Handle},
|
core::handle::Handle,
|
||||||
logging, logging_error, singleton_lazy,
|
logging, logging_error, singleton_lazy,
|
||||||
utils::logging::Type,
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
@@ -100,15 +100,6 @@ impl Sysopt {
|
|||||||
self.initialed.load(Ordering::SeqCst)
|
self.initialed.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
|
||||||
// 使用事件驱动代理管理器
|
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_app_started();
|
|
||||||
|
|
||||||
logging!(info, Type::Core, "已启用事件驱动代理守卫");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// init the sysproxy
|
/// init the sysproxy
|
||||||
pub async fn update_sysproxy(&self) -> Result<()> {
|
pub async fn update_sysproxy(&self) -> Result<()> {
|
||||||
self.initialed.store(true, Ordering::SeqCst);
|
self.initialed.store(true, Ordering::SeqCst);
|
||||||
@@ -161,8 +152,6 @@ impl Sysopt {
|
|||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +160,6 @@ impl Sysopt {
|
|||||||
auto.enable = true;
|
auto.enable = true;
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +168,6 @@ impl Sysopt {
|
|||||||
sys.enable = true;
|
sys.enable = true;
|
||||||
auto.set_auto_proxy()?;
|
auto.set_auto_proxy()?;
|
||||||
sys.set_system_proxy()?;
|
sys.set_system_proxy()?;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,8 +175,6 @@ impl Sysopt {
|
|||||||
{
|
{
|
||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
let result = self.reset_sysproxy().await;
|
let result = self.reset_sysproxy().await;
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,8 +189,6 @@ impl Sysopt {
|
|||||||
|
|
||||||
execute_sysproxy_command(args).await?;
|
execute_sysproxy_command(args).await?;
|
||||||
}
|
}
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
|
||||||
proxy_manager.notify_config_changed();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -241,15 +241,11 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
|
|||||||
return Err(anyhow!("Backup file not found: {}", filename));
|
return Err(anyhow!("Backup file not found: {}", filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (webdav_url, webdav_username, webdav_password) = {
|
let verge = Config::verge().await;
|
||||||
let verge = Config::verge().await;
|
let verge_data = verge.latest_ref().clone();
|
||||||
let verge = verge.latest_ref();
|
let webdav_url = verge_data.webdav_url.clone();
|
||||||
(
|
let webdav_username = verge_data.webdav_username.clone();
|
||||||
verge.webdav_url.clone(),
|
let webdav_password = verge_data.webdav_password.clone();
|
||||||
verge.webdav_username.clone(),
|
|
||||||
verge.webdav_password.clone(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
|
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
|
||||||
let mut zip = zip::ZipArchive::new(file)?;
|
let mut zip = zip::ZipArchive::new(file)?;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::core::event_driven_proxy::EventDrivenProxyManager;
|
|
||||||
use crate::core::{CoreManager, handle, sysopt};
|
use crate::core::{CoreManager, handle, sysopt};
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
@@ -24,7 +23,6 @@ pub async fn quit() {
|
|||||||
// 获取应用句柄并设置退出标志
|
// 获取应用句柄并设置退出标志
|
||||||
let app_handle = handle::Handle::app_handle();
|
let app_handle = handle::Handle::app_handle();
|
||||||
handle::Handle::global().set_is_exiting();
|
handle::Handle::global().set_is_exiting();
|
||||||
EventDrivenProxyManager::global().notify_app_stopping();
|
|
||||||
|
|
||||||
logging!(info, Type::System, "开始异步清理资源");
|
logging!(info, Type::System, "开始异步清理资源");
|
||||||
let cleanup_result = clean_async().await;
|
let cleanup_result = clean_async().await;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::utils::linux;
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{EventDrivenProxyManager, handle, hotkey},
|
core::{handle, hotkey},
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
utils::{resolve, server},
|
utils::{resolve, server},
|
||||||
};
|
};
|
||||||
@@ -174,7 +174,6 @@ mod app_init {
|
|||||||
cmd::get_runtime_logs,
|
cmd::get_runtime_logs,
|
||||||
cmd::get_runtime_proxy_chain_config,
|
cmd::get_runtime_proxy_chain_config,
|
||||||
cmd::update_proxy_chain_config_in_runtime,
|
cmd::update_proxy_chain_config_in_runtime,
|
||||||
cmd::check_update_channel,
|
|
||||||
cmd::invoke_uwp_tool,
|
cmd::invoke_uwp_tool,
|
||||||
cmd::copy_clash_env,
|
cmd::copy_clash_env,
|
||||||
cmd::sync_tray_proxy_selection,
|
cmd::sync_tray_proxy_selection,
|
||||||
@@ -335,7 +334,10 @@ pub fn run() {
|
|||||||
.register_system_hotkey(SystemHotkey::CmdW)
|
.register_system_hotkey(SystemHotkey::CmdW)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
let _ = hotkey::Hotkey::global().init(true).await;
|
|
||||||
|
if !is_enable_global_hotkey {
|
||||||
|
let _ = hotkey::Hotkey::global().init().await;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,18 +358,8 @@ pub fn run() {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
use crate::core::hotkey::SystemHotkey;
|
use crate::core::hotkey::SystemHotkey;
|
||||||
AsyncHandler::spawn(move || async move {
|
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
||||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
||||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
|
||||||
let is_enable_global_hotkey = Config::verge()
|
|
||||||
.await
|
|
||||||
.latest_ref()
|
|
||||||
.enable_global_hotkey
|
|
||||||
.unwrap_or(true);
|
|
||||||
if !is_enable_global_hotkey {
|
|
||||||
let _ = hotkey::Hotkey::global().reset();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,7 +429,6 @@ pub fn run() {
|
|||||||
let handle = core::handle::Handle::global();
|
let handle = core::handle::Handle::global();
|
||||||
if !handle.is_exiting() {
|
if !handle.is_exiting() {
|
||||||
handle.set_is_exiting();
|
handle.set_is_exiting();
|
||||||
EventDrivenProxyManager::global().notify_app_stopping();
|
|
||||||
feat::clean();
|
feat::clean();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ async fn initialize_config_files() -> Result<()> {
|
|||||||
if let Ok(path) = dirs::profiles_path()
|
if let Ok(path) = dirs::profiles_path()
|
||||||
&& !path.exists()
|
&& !path.exists()
|
||||||
{
|
{
|
||||||
let template = IProfiles::default();
|
let template = IProfiles::template();
|
||||||
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ pub fn resolve_setup_async() {
|
|||||||
init_service_manager().await;
|
init_service_manager().await;
|
||||||
init_core_manager().await;
|
init_core_manager().await;
|
||||||
init_system_proxy().await;
|
init_system_proxy().await;
|
||||||
AsyncHandler::spawn_blocking(init_system_proxy_guard);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let tray_init = async {
|
let tray_init = async {
|
||||||
@@ -120,7 +119,7 @@ pub(super) async fn init_timer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn init_hotkey() {
|
pub(super) async fn init_hotkey() {
|
||||||
logging_error!(Type::Setup, Hotkey::global().init(false).await);
|
logging_error!(Type::Setup, Hotkey::global().init().await);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn init_auto_lightweight_boot() {
|
pub(super) async fn init_auto_lightweight_boot() {
|
||||||
@@ -168,10 +167,6 @@ 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 refresh_tray_menu() {
|
pub(super) async fn refresh_tray_menu() {
|
||||||
logging_error!(Type::Setup, Tray::global().update_part().await);
|
logging_error!(Type::Setup, Tray::global().update_part().await);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { useServiceInstaller } from "@/hooks/useServiceInstaller";
|
|||||||
import { getSystemInfo } from "@/services/cmds";
|
import { getSystemInfo } from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||||
import { useUpdateChannel } from "@/services/updateChannel";
|
|
||||||
import { version as appVersion } from "@root/package.json";
|
import { version as appVersion } from "@root/package.json";
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
@@ -60,7 +59,6 @@ export const SystemInfoCard = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAdminMode, isSidecarMode } = useSystemState();
|
const { isAdminMode, isSidecarMode } = useSystemState();
|
||||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
const { installServiceAndRestartCore } = useServiceInstaller();
|
||||||
const [updateChannel] = useUpdateChannel();
|
|
||||||
|
|
||||||
// 系统信息状态
|
// 系统信息状态
|
||||||
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
||||||
@@ -119,7 +117,7 @@ export const SystemInfoCard = () => {
|
|||||||
|
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
if (verge?.auto_check_update) {
|
if (verge?.auto_check_update) {
|
||||||
checkUpdate(updateChannel).catch(console.error);
|
checkUpdate().catch(console.error);
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@@ -128,11 +126,11 @@ export const SystemInfoCard = () => {
|
|||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [verge?.auto_check_update, dispatchSystemState, updateChannel]);
|
}, [verge?.auto_check_update, dispatchSystemState]);
|
||||||
|
|
||||||
// 自动检查更新逻辑
|
// 自动检查更新逻辑
|
||||||
useSWR(
|
useSWR(
|
||||||
verge?.auto_check_update ? ["checkUpdate", updateChannel] : null,
|
verge?.auto_check_update ? "checkUpdate" : null,
|
||||||
async () => {
|
async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem("last_check_update", now.toString());
|
||||||
@@ -140,7 +138,7 @@ export const SystemInfoCard = () => {
|
|||||||
type: "set-last-check-update",
|
type: "set-last-check-update",
|
||||||
payload: new Date(now).toLocaleString(),
|
payload: new Date(now).toLocaleString(),
|
||||||
});
|
});
|
||||||
return await checkUpdate(updateChannel);
|
return await checkUpdate();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@@ -174,7 +172,7 @@ export const SystemInfoCard = () => {
|
|||||||
// 检查更新
|
// 检查更新
|
||||||
const onCheckUpdate = useLockFn(async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
const info = await checkUpdate(updateChannel);
|
const info = await checkUpdate();
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
showNotice("success", t("Currently on the Latest Version"));
|
showNotice("success", t("Currently on the Latest Version"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import useSWR from "swr";
|
|||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { checkUpdateSafe } from "@/services/update";
|
import { checkUpdateSafe } from "@/services/update";
|
||||||
import { useUpdateChannel } from "@/services/updateChannel";
|
|
||||||
|
|
||||||
import { DialogRef } from "../base";
|
import { DialogRef } from "../base";
|
||||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||||
@@ -17,14 +16,12 @@ export const UpdateButton = (props: Props) => {
|
|||||||
const { className } = props;
|
const { className } = props;
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { auto_check_update } = verge || {};
|
const { auto_check_update } = verge || {};
|
||||||
const [updateChannel] = useUpdateChannel();
|
|
||||||
|
|
||||||
const viewerRef = useRef<DialogRef>(null);
|
const viewerRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
const shouldCheck = auto_check_update || auto_check_update === null;
|
|
||||||
const { data: updateInfo } = useSWR(
|
const { data: updateInfo } = useSWR(
|
||||||
shouldCheck ? ["checkUpdate", updateChannel] : null,
|
auto_check_update || auto_check_update === null ? "checkUpdate" : null,
|
||||||
() => checkUpdateSafe(updateChannel),
|
checkUpdateSafe,
|
||||||
{
|
{
|
||||||
errorRetryCount: 2,
|
errorRetryCount: 2,
|
||||||
revalidateIfStale: false,
|
revalidateIfStale: false,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { portableFlag } from "@/pages/_layout";
|
|||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { useSetUpdateState, useUpdateState } from "@/services/states";
|
import { useSetUpdateState, useUpdateState } from "@/services/states";
|
||||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||||
import { useUpdateChannel } from "@/services/updateChannel";
|
|
||||||
|
|
||||||
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -27,17 +26,12 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
const updateState = useUpdateState();
|
const updateState = useUpdateState();
|
||||||
const setUpdateState = useSetUpdateState();
|
const setUpdateState = useSetUpdateState();
|
||||||
const { addListener } = useListen();
|
const { addListener } = useListen();
|
||||||
const [updateChannel] = useUpdateChannel();
|
|
||||||
|
|
||||||
const { data: updateInfo } = useSWR(
|
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||||
["checkUpdate", updateChannel],
|
errorRetryCount: 2,
|
||||||
() => checkUpdate(updateChannel),
|
revalidateIfStale: false,
|
||||||
{
|
focusThrottleInterval: 36e5, // 1 hour
|
||||||
errorRetryCount: 2,
|
});
|
||||||
revalidateIfStale: false,
|
|
||||||
focusThrottleInterval: 36e5, // 1 hour
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [downloaded, setDownloaded] = useState(0);
|
const [downloaded, setDownloaded] = useState(0);
|
||||||
const [buffer, setBuffer] = useState(0);
|
const [buffer, setBuffer] = useState(0);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||||
import { useUpdateChannel } from "@/services/updateChannel";
|
|
||||||
import { version } from "@root/package.json";
|
import { version } from "@root/package.json";
|
||||||
|
|
||||||
import { BackupViewer } from "./mods/backup-viewer";
|
import { BackupViewer } from "./mods/backup-viewer";
|
||||||
@@ -43,11 +42,10 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
|
|||||||
const updateRef = useRef<DialogRef>(null);
|
const updateRef = useRef<DialogRef>(null);
|
||||||
const backupRef = useRef<DialogRef>(null);
|
const backupRef = useRef<DialogRef>(null);
|
||||||
const liteModeRef = useRef<DialogRef>(null);
|
const liteModeRef = useRef<DialogRef>(null);
|
||||||
const [updateChannel] = useUpdateChannel();
|
|
||||||
|
|
||||||
const onCheckUpdate = async () => {
|
const onCheckUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
const info = await checkUpdate(updateChannel);
|
const info = await checkUpdate();
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
showNotice("success", t("Currently on the Latest Version"));
|
showNotice("success", t("Currently on the Latest Version"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { ContentCopyRounded } from "@mui/icons-material";
|
import { ContentCopyRounded } from "@mui/icons-material";
|
||||||
import {
|
import { Button, Input, MenuItem, Select } from "@mui/material";
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -17,11 +11,6 @@ import { navItems } from "@/pages/_routers";
|
|||||||
import { copyClashEnv } from "@/services/cmds";
|
import { copyClashEnv } from "@/services/cmds";
|
||||||
import { supportedLanguages } from "@/services/i18n";
|
import { supportedLanguages } from "@/services/i18n";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import {
|
|
||||||
UPDATE_CHANNEL_OPTIONS,
|
|
||||||
type UpdateChannel,
|
|
||||||
useUpdateChannel,
|
|
||||||
} from "@/services/updateChannel";
|
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
|
|
||||||
import { BackupViewer } from "./mods/backup-viewer";
|
import { BackupViewer } from "./mods/backup-viewer";
|
||||||
@@ -80,7 +69,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
const layoutRef = useRef<DialogRef>(null);
|
const layoutRef = useRef<DialogRef>(null);
|
||||||
const updateRef = useRef<DialogRef>(null);
|
const updateRef = useRef<DialogRef>(null);
|
||||||
const backupRef = useRef<DialogRef>(null);
|
const backupRef = useRef<DialogRef>(null);
|
||||||
const [updateChannel, setUpdateChannel] = useUpdateChannel();
|
|
||||||
|
|
||||||
const onChangeData = (patch: any) => {
|
const onChangeData = (patch: any) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false);
|
mutateVerge({ ...verge, ...patch }, false);
|
||||||
@@ -91,14 +79,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
showNotice("success", t("Copy Success"), 1000);
|
showNotice("success", t("Copy Success"), 1000);
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const onUpdateChannelChange = useCallback(
|
|
||||||
(event: SelectChangeEvent<UpdateChannel>) => {
|
|
||||||
const nextChannel = event.target.value as UpdateChannel;
|
|
||||||
setUpdateChannel(nextChannel);
|
|
||||||
},
|
|
||||||
[setUpdateChannel],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title={t("Verge Basic Setting")}>
|
<SettingList title={t("Verge Basic Setting")}>
|
||||||
<ThemeViewer ref={themeRef} />
|
<ThemeViewer ref={themeRef} />
|
||||||
@@ -109,21 +89,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
<UpdateViewer ref={updateRef} />
|
<UpdateViewer ref={updateRef} />
|
||||||
<BackupViewer ref={backupRef} />
|
<BackupViewer ref={backupRef} />
|
||||||
|
|
||||||
<SettingItem label={t("Update Channel")}>
|
|
||||||
<Select
|
|
||||||
size="small"
|
|
||||||
value={updateChannel}
|
|
||||||
onChange={onUpdateChannelChange}
|
|
||||||
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
|
||||||
>
|
|
||||||
{UPDATE_CHANNEL_OPTIONS.map((option) => (
|
|
||||||
<MenuItem key={option.value} value={option.value}>
|
|
||||||
{t(option.labelKey)}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
<SettingItem label={t("Language")}>
|
<SettingItem label={t("Language")}>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={language ?? "en"}
|
value={language ?? "en"}
|
||||||
|
|||||||
@@ -43,9 +43,6 @@
|
|||||||
"Proxy detail": "تفاصيل الوكيل",
|
"Proxy detail": "تفاصيل الوكيل",
|
||||||
"Profiles": "الملفات الشخصية",
|
"Profiles": "الملفات الشخصية",
|
||||||
"Update All Profiles": "تحديث جميع الملفات الشخصية",
|
"Update All Profiles": "تحديث جميع الملفات الشخصية",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "عرض تكوين وقت التشغيل",
|
"View Runtime Config": "عرض تكوين وقت التشغيل",
|
||||||
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
|
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
|
||||||
"Paste": "لصق",
|
"Paste": "لصق",
|
||||||
|
|||||||
@@ -45,9 +45,6 @@
|
|||||||
"Proxy detail": "Knotendetails anzeigen",
|
"Proxy detail": "Knotendetails anzeigen",
|
||||||
"Profiles": "Abonnement",
|
"Profiles": "Abonnement",
|
||||||
"Update All Profiles": "Alle Abonnements aktualisieren",
|
"Update All Profiles": "Alle Abonnements aktualisieren",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "Laufzeit-Abonnement anzeigen",
|
"View Runtime Config": "Laufzeit-Abonnement anzeigen",
|
||||||
"Reactivate Profiles": "Abonnement erneut aktivieren",
|
"Reactivate Profiles": "Abonnement erneut aktivieren",
|
||||||
"Paste": "Einfügen",
|
"Paste": "Einfügen",
|
||||||
|
|||||||
@@ -59,9 +59,6 @@
|
|||||||
"Proxy detail": "Proxy detail",
|
"Proxy detail": "Proxy detail",
|
||||||
"Profiles": "Profiles",
|
"Profiles": "Profiles",
|
||||||
"Update All Profiles": "Update All Profiles",
|
"Update All Profiles": "Update All Profiles",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "View Runtime Config",
|
"View Runtime Config": "View Runtime Config",
|
||||||
"Reactivate Profiles": "Reactivate Profiles",
|
"Reactivate Profiles": "Reactivate Profiles",
|
||||||
"Paste": "Paste",
|
"Paste": "Paste",
|
||||||
|
|||||||
@@ -45,9 +45,6 @@
|
|||||||
"Proxy detail": "Mostrar detalles del nodo",
|
"Proxy detail": "Mostrar detalles del nodo",
|
||||||
"Profiles": "Suscripciones",
|
"Profiles": "Suscripciones",
|
||||||
"Update All Profiles": "Actualizar todas las suscripciones",
|
"Update All Profiles": "Actualizar todas las suscripciones",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "Ver configuración en tiempo de ejecución",
|
"View Runtime Config": "Ver configuración en tiempo de ejecución",
|
||||||
"Reactivate Profiles": "Reactivar suscripciones",
|
"Reactivate Profiles": "Reactivar suscripciones",
|
||||||
"Paste": "Pegar",
|
"Paste": "Pegar",
|
||||||
|
|||||||
@@ -43,9 +43,6 @@
|
|||||||
"Proxy detail": "جزئیات پراکسی",
|
"Proxy detail": "جزئیات پراکسی",
|
||||||
"Profiles": "پروفایلها",
|
"Profiles": "پروفایلها",
|
||||||
"Update All Profiles": "بهروزرسانی همه پروفایلها",
|
"Update All Profiles": "بهروزرسانی همه پروفایلها",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
|
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
|
||||||
"Reactivate Profiles": "فعالسازی مجدد پروفایلها",
|
"Reactivate Profiles": "فعالسازی مجدد پروفایلها",
|
||||||
"Paste": "چسباندن",
|
"Paste": "چسباندن",
|
||||||
|
|||||||
@@ -43,9 +43,6 @@
|
|||||||
"Proxy detail": "Detail Proksi",
|
"Proxy detail": "Detail Proksi",
|
||||||
"Profiles": "Profil",
|
"Profiles": "Profil",
|
||||||
"Update All Profiles": "Perbarui Semua Profil",
|
"Update All Profiles": "Perbarui Semua Profil",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "Lihat Konfigurasi Runtime",
|
"View Runtime Config": "Lihat Konfigurasi Runtime",
|
||||||
"Reactivate Profiles": "Reaktivasi Profil",
|
"Reactivate Profiles": "Reaktivasi Profil",
|
||||||
"Paste": "Tempel",
|
"Paste": "Tempel",
|
||||||
|
|||||||
@@ -45,9 +45,6 @@
|
|||||||
"Proxy detail": "ノードの詳細を表示する",
|
"Proxy detail": "ノードの詳細を表示する",
|
||||||
"Profiles": "プロファイル",
|
"Profiles": "プロファイル",
|
||||||
"Update All Profiles": "すべてのプロファイルを更新",
|
"Update All Profiles": "すべてのプロファイルを更新",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "実行時のプロファイルを表示",
|
"View Runtime Config": "実行時のプロファイルを表示",
|
||||||
"Reactivate Profiles": "プロファイルを再アクティブ化",
|
"Reactivate Profiles": "プロファイルを再アクティブ化",
|
||||||
"Paste": "貼り付け",
|
"Paste": "貼り付け",
|
||||||
|
|||||||
@@ -46,9 +46,6 @@
|
|||||||
"Proxy detail": "프록시 상세",
|
"Proxy detail": "프록시 상세",
|
||||||
"Profiles": "프로필",
|
"Profiles": "프로필",
|
||||||
"Update All Profiles": "모든 프로필 업데이트",
|
"Update All Profiles": "모든 프로필 업데이트",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "런타임 설정 보기",
|
"View Runtime Config": "런타임 설정 보기",
|
||||||
"Reactivate Profiles": "프로필 재활성화",
|
"Reactivate Profiles": "프로필 재활성화",
|
||||||
"Paste": "붙여넣기",
|
"Paste": "붙여넣기",
|
||||||
|
|||||||
@@ -51,9 +51,6 @@
|
|||||||
"Proxy detail": "Отображать больше сведений о прокси",
|
"Proxy detail": "Отображать больше сведений о прокси",
|
||||||
"Profiles": "Профили",
|
"Profiles": "Профили",
|
||||||
"Update All Profiles": "Обновить все профили",
|
"Update All Profiles": "Обновить все профили",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||||
"Reactivate Profiles": "Перезапустить профиль",
|
"Reactivate Profiles": "Перезапустить профиль",
|
||||||
"Paste": "Вставить",
|
"Paste": "Вставить",
|
||||||
|
|||||||
@@ -46,9 +46,6 @@
|
|||||||
"Proxy detail": "Vekil detayı",
|
"Proxy detail": "Vekil detayı",
|
||||||
"Profiles": "Profiller",
|
"Profiles": "Profiller",
|
||||||
"Update All Profiles": "Tüm Profilleri Güncelle",
|
"Update All Profiles": "Tüm Profilleri Güncelle",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "Çalışma Zamanı Yapılandırmasını Görüntüle",
|
"View Runtime Config": "Çalışma Zamanı Yapılandırmasını Görüntüle",
|
||||||
"Reactivate Profiles": "Profilleri Yeniden Etkinleştir",
|
"Reactivate Profiles": "Profilleri Yeniden Etkinleştir",
|
||||||
"Paste": "Yapıştır",
|
"Paste": "Yapıştır",
|
||||||
|
|||||||
@@ -43,9 +43,6 @@
|
|||||||
"Proxy detail": "Прокси турында тулы мәгълүмат",
|
"Proxy detail": "Прокси турында тулы мәгълүмат",
|
||||||
"Profiles": "Профильләр",
|
"Profiles": "Профильләр",
|
||||||
"Update All Profiles": "Барлык профильләрне яңарту",
|
"Update All Profiles": "Барлык профильләрне яңарту",
|
||||||
"Update Channel": "Update Channel",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "Кулланылган конфигурацияне карау",
|
"View Runtime Config": "Кулланылган конфигурацияне карау",
|
||||||
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
|
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
|
||||||
"Paste": "Кую",
|
"Paste": "Кую",
|
||||||
|
|||||||
@@ -59,9 +59,6 @@
|
|||||||
"Proxy detail": "展示节点细节",
|
"Proxy detail": "展示节点细节",
|
||||||
"Profiles": "订阅",
|
"Profiles": "订阅",
|
||||||
"Update All Profiles": "更新所有订阅",
|
"Update All Profiles": "更新所有订阅",
|
||||||
"Update Channel": "更新通道",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "查看运行时订阅",
|
"View Runtime Config": "查看运行时订阅",
|
||||||
"Reactivate Profiles": "重新激活订阅",
|
"Reactivate Profiles": "重新激活订阅",
|
||||||
"Paste": "粘贴",
|
"Paste": "粘贴",
|
||||||
|
|||||||
@@ -59,9 +59,6 @@
|
|||||||
"Proxy detail": "展示節點細節",
|
"Proxy detail": "展示節點細節",
|
||||||
"Profiles": "訂閱",
|
"Profiles": "訂閱",
|
||||||
"Update All Profiles": "更新所有訂閱",
|
"Update All Profiles": "更新所有訂閱",
|
||||||
"Update Channel": "更新頻道",
|
|
||||||
"Update Channel Stable": "Stable",
|
|
||||||
"Update Channel Autobuild": "Autobuild",
|
|
||||||
"View Runtime Config": "查看執行時訂閱",
|
"View Runtime Config": "查看執行時訂閱",
|
||||||
"Reactivate Profiles": "重新啟用訂閱",
|
"Reactivate Profiles": "重新啟用訂閱",
|
||||||
"Paste": "貼上",
|
"Paste": "貼上",
|
||||||
@@ -220,7 +217,6 @@
|
|||||||
"Settings": "設定",
|
"Settings": "設定",
|
||||||
"System Setting": "系統設定",
|
"System Setting": "系統設定",
|
||||||
"Tun Mode": "虛擬網路介面卡模式",
|
"Tun Mode": "虛擬網路介面卡模式",
|
||||||
"TUN requires Service Mode or Admin Mode": "虛擬網路介面卡模式需要服務模式或管理員模式",
|
|
||||||
"Install Service": "安裝服務",
|
"Install Service": "安裝服務",
|
||||||
"Install Service failed": "安裝服務失敗",
|
"Install Service failed": "安裝服務失敗",
|
||||||
"Uninstall Service": "解除安裝服務",
|
"Uninstall Service": "解除安裝服務",
|
||||||
@@ -308,7 +304,6 @@
|
|||||||
"Socks Port": "SOCKS 代理連接埠",
|
"Socks Port": "SOCKS 代理連接埠",
|
||||||
"Http Port": "HTTP(S) 代理連接埠",
|
"Http Port": "HTTP(S) 代理連接埠",
|
||||||
"Redir Port": "Redir 透明代理連接埠",
|
"Redir Port": "Redir 透明代理連接埠",
|
||||||
"TPROXY Port": "TPROXY 透明代理連接埠",
|
|
||||||
"Port settings saved": "連結埠設定已儲存",
|
"Port settings saved": "連結埠設定已儲存",
|
||||||
"Failed to save port settings": "連結埠設定儲存失敗",
|
"Failed to save port settings": "連結埠設定儲存失敗",
|
||||||
"External": "外部控制",
|
"External": "外部控制",
|
||||||
@@ -429,6 +424,8 @@
|
|||||||
"Uninstalling Service...": "服務解除安裝中...",
|
"Uninstalling Service...": "服務解除安裝中...",
|
||||||
"Service Installed Successfully": "已成功安裝服務",
|
"Service Installed Successfully": "已成功安裝服務",
|
||||||
"Service Uninstalled Successfully": "已成功解除安裝服務",
|
"Service Uninstalled Successfully": "已成功解除安裝服務",
|
||||||
|
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守護間隔時間不得低於 1 秒",
|
||||||
|
"Invalid Bypass Format": "無效的代理繞過格式",
|
||||||
"Waiting for service to be ready...": "等待服務就緒...",
|
"Waiting for service to be ready...": "等待服務就緒...",
|
||||||
"Service not ready, retrying attempt {count}/{total}...": "服務未就緒,正在重試 {{count}}/{{total}} 次...",
|
"Service not ready, retrying attempt {count}/{total}...": "服務未就緒,正在重試 {{count}}/{{total}} 次...",
|
||||||
"Failed to check service status, retrying attempt {count}/{total}...": "檢查服務狀態失敗,正在重試 {{count}}/{{total}} 次...",
|
"Failed to check service status, retrying attempt {count}/{total}...": "檢查服務狀態失敗,正在重試 {{count}}/{{total}} 次...",
|
||||||
@@ -439,8 +436,6 @@
|
|||||||
"Fallback core restart also failed: {message}": "被園內核重新啟動也失敗了:{{message}}",
|
"Fallback core restart also failed: {message}": "被園內核重新啟動也失敗了:{{message}}",
|
||||||
"Service is ready and core restarted": "服務已就緒,內核已重啟",
|
"Service is ready and core restarted": "服務已就緒,內核已重啟",
|
||||||
"Core restarted. Service is now available.": "內核已重啟,服務已就緒",
|
"Core restarted. Service is now available.": "內核已重啟,服務已就緒",
|
||||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守護間隔時間不得低於 1 秒",
|
|
||||||
"Invalid Bypass Format": "無效的代理繞過格式",
|
|
||||||
"Clash Port Modified": "Clash 連結埠已修改",
|
"Clash Port Modified": "Clash 連結埠已修改",
|
||||||
"Port Conflict": "連結埠衝突",
|
"Port Conflict": "連結埠衝突",
|
||||||
"Restart Application to Apply Modifications": "重新啟動 Verge 以套用修改",
|
"Restart Application to Apply Modifications": "重新啟動 Verge 以套用修改",
|
||||||
@@ -716,6 +711,5 @@
|
|||||||
"Menu reorder mode": "選單排序模式",
|
"Menu reorder mode": "選單排序模式",
|
||||||
"Unlock menu order": "解鎖選單排序",
|
"Unlock menu order": "解鎖選單排序",
|
||||||
"Lock menu order": "鎖定選單排序",
|
"Lock menu order": "鎖定選單排序",
|
||||||
"Open App Log": "應用程式日誌",
|
"TPROXY Port": "TPROXY 透明代理連接埠"
|
||||||
"Open Core Log": "內核日誌"
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@@ -280,6 +280,7 @@ interface IProfileOption {
|
|||||||
|
|
||||||
interface IProfilesConfig {
|
interface IProfilesConfig {
|
||||||
current?: string;
|
current?: string;
|
||||||
|
valid?: string[];
|
||||||
items?: IProfileItem[];
|
items?: IProfileItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import {
|
|||||||
compareVersions,
|
compareVersions,
|
||||||
ensureSemver,
|
ensureSemver,
|
||||||
extractSemver,
|
extractSemver,
|
||||||
isPrereleaseVersion,
|
|
||||||
normalizeVersion,
|
normalizeVersion,
|
||||||
resolveRemoteVersion,
|
resolveRemoteVersion,
|
||||||
shouldRejectUpdate,
|
|
||||||
splitVersion,
|
splitVersion,
|
||||||
} from "@/services/update";
|
} from "@/services/update";
|
||||||
import type { VersionParts } from "@/services/update";
|
import type { VersionParts } from "@/services/update";
|
||||||
@@ -140,53 +138,3 @@ describe("resolveRemoteVersion", () => {
|
|||||||
expect(resolveRemoteVersion(update)).toBeNull();
|
expect(resolveRemoteVersion(update)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isPrereleaseVersion", () => {
|
|
||||||
it("returns true when version has prerelease identifiers", () => {
|
|
||||||
expect(isPrereleaseVersion("1.2.3-beta.1")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for release versions or missing input", () => {
|
|
||||||
expect(isPrereleaseVersion("1.2.3")).toBe(false);
|
|
||||||
expect(isPrereleaseVersion(null)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("shouldRejectUpdate", () => {
|
|
||||||
const localStable = "2.4.3";
|
|
||||||
const remoteAutobuild = "2.4.3-autobuild.1122.qwerty.r1a";
|
|
||||||
|
|
||||||
it("rejects when comparison cannot proceed in downgrade-safe way on stable channel", () => {
|
|
||||||
expect(shouldRejectUpdate("stable", -1, "2.4.2", localStable)).toBe(true);
|
|
||||||
expect(shouldRejectUpdate("stable", 0, "2.4.3", localStable)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows prerelease downgrade on autobuild channel", () => {
|
|
||||||
expect(
|
|
||||||
shouldRejectUpdate("autobuild", -1, remoteAutobuild, localStable),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects prerelease downgrade when base version is older", () => {
|
|
||||||
expect(
|
|
||||||
shouldRejectUpdate("autobuild", -1, "2.3.0-autobuild.1", localStable),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects downgrade when both versions are prereleases", () => {
|
|
||||||
expect(
|
|
||||||
shouldRejectUpdate(
|
|
||||||
"autobuild",
|
|
||||||
-1,
|
|
||||||
"2.4.3-autobuild.1122.qwerty.r1a",
|
|
||||||
"2.4.3-autobuild.1127.qwerty.r1a",
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects downgrade when remote release is older even on autobuild channel", () => {
|
|
||||||
expect(shouldRejectUpdate("autobuild", -1, "2.4.2", localStable)).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { Update, type CheckOptions } from "@tauri-apps/plugin-updater";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_UPDATE_CHANNEL,
|
check,
|
||||||
getStoredUpdateChannel,
|
type CheckOptions,
|
||||||
type UpdateChannel,
|
type Update,
|
||||||
} from "@/services/updateChannel";
|
} from "@tauri-apps/plugin-updater";
|
||||||
import { version as appVersion } from "@root/package.json";
|
|
||||||
|
|
||||||
type NativeUpdateMetadata = {
|
import { version as appVersion } from "@root/package.json";
|
||||||
rid: number;
|
|
||||||
currentVersion: string;
|
|
||||||
version: string;
|
|
||||||
date?: string;
|
|
||||||
body?: string | null;
|
|
||||||
rawJson: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type VersionParts = {
|
export type VersionParts = {
|
||||||
main: number[];
|
main: number[];
|
||||||
@@ -142,92 +131,16 @@ export const resolveRemoteVersion = (update: Update): string | null => {
|
|||||||
|
|
||||||
const localVersionNormalized = normalizeVersion(appVersion);
|
const localVersionNormalized = normalizeVersion(appVersion);
|
||||||
|
|
||||||
export const isPrereleaseVersion = (version: string | null): boolean => {
|
export const checkUpdateSafe = async (
|
||||||
const parts = splitVersion(version);
|
|
||||||
return Boolean(parts?.pre.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const shouldRejectUpdate = (
|
|
||||||
channel: UpdateChannel,
|
|
||||||
comparison: number | null,
|
|
||||||
remoteVersion: string | null,
|
|
||||||
localVersion: string | null,
|
|
||||||
): boolean => {
|
|
||||||
if (comparison === null) return false;
|
|
||||||
if (comparison === 0) return true;
|
|
||||||
if (comparison > 0) return false;
|
|
||||||
|
|
||||||
if (channel !== "stable") {
|
|
||||||
const remoteIsPrerelease = isPrereleaseVersion(remoteVersion);
|
|
||||||
const localIsPrerelease = isPrereleaseVersion(localVersion);
|
|
||||||
if (remoteIsPrerelease && !localIsPrerelease) {
|
|
||||||
const remoteParts = splitVersion(remoteVersion);
|
|
||||||
const localParts = splitVersion(localVersion);
|
|
||||||
if (!remoteParts || !localParts) return true;
|
|
||||||
|
|
||||||
const mainComparison = compareVersionParts(
|
|
||||||
{ main: remoteParts.main, pre: [] },
|
|
||||||
{ main: localParts.main, pre: [] },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mainComparison < 0) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeHeaders = (
|
|
||||||
headers?: HeadersInit,
|
|
||||||
): Array<[string, string]> | undefined => {
|
|
||||||
if (!headers) return undefined;
|
|
||||||
const pairs = Array.from(new Headers(headers).entries());
|
|
||||||
return pairs.length > 0 ? pairs : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkUpdateForChannel = async (
|
|
||||||
channel: UpdateChannel = DEFAULT_UPDATE_CHANNEL,
|
|
||||||
options?: CheckOptions,
|
options?: CheckOptions,
|
||||||
): Promise<Update | null> => {
|
): Promise<Update | null> => {
|
||||||
const allowDowngrades = channel !== "stable";
|
const result = await check({ ...(options ?? {}), allowDowngrades: false });
|
||||||
|
|
||||||
const metadata = await invoke<NativeUpdateMetadata | null>(
|
|
||||||
"check_update_channel",
|
|
||||||
{
|
|
||||||
channel,
|
|
||||||
headers: normalizeHeaders(options?.headers),
|
|
||||||
timeout: options?.timeout,
|
|
||||||
proxy: options?.proxy,
|
|
||||||
target: options?.target,
|
|
||||||
allowDowngrades,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!metadata) return null;
|
|
||||||
|
|
||||||
const result = new Update({
|
|
||||||
...metadata,
|
|
||||||
body:
|
|
||||||
typeof metadata.body === "string"
|
|
||||||
? metadata.body
|
|
||||||
: metadata.body === null
|
|
||||||
? undefined
|
|
||||||
: metadata.body,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
|
|
||||||
const remoteVersion = resolveRemoteVersion(result);
|
const remoteVersion = resolveRemoteVersion(result);
|
||||||
const comparison = compareVersions(remoteVersion, localVersionNormalized);
|
const comparison = compareVersions(remoteVersion, localVersionNormalized);
|
||||||
if (
|
|
||||||
shouldRejectUpdate(
|
if (comparison !== null && comparison <= 0) {
|
||||||
channel,
|
|
||||||
comparison,
|
|
||||||
remoteVersion,
|
|
||||||
localVersionNormalized,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
await result.close();
|
await result.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -239,13 +152,4 @@ export const checkUpdateForChannel = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkUpdateSafe = async (
|
|
||||||
channel?: UpdateChannel,
|
|
||||||
options?: CheckOptions,
|
|
||||||
): Promise<Update | null> => {
|
|
||||||
const resolvedChannel = channel ?? getStoredUpdateChannel();
|
|
||||||
return checkUpdateForChannel(resolvedChannel, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { CheckOptions };
|
export type { CheckOptions };
|
||||||
export type { UpdateChannel } from "@/services/updateChannel";
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { useLocalStorage } from "foxact/use-local-storage";
|
|
||||||
|
|
||||||
export type UpdateChannel = "stable" | "autobuild";
|
|
||||||
|
|
||||||
export const UPDATE_CHANNEL_STORAGE_KEY = "update-channel";
|
|
||||||
|
|
||||||
export const DEFAULT_UPDATE_CHANNEL: UpdateChannel = "stable";
|
|
||||||
|
|
||||||
export const UPDATE_CHANNEL_OPTIONS: Array<{
|
|
||||||
value: UpdateChannel;
|
|
||||||
labelKey: string;
|
|
||||||
}> = [
|
|
||||||
{ value: "stable", labelKey: "Update Channel Stable" },
|
|
||||||
{ value: "autobuild", labelKey: "Update Channel Autobuild" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const isValidChannel = (value: unknown): value is UpdateChannel => {
|
|
||||||
return value === "stable" || value === "autobuild";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateChannel = () =>
|
|
||||||
useLocalStorage<UpdateChannel>(
|
|
||||||
UPDATE_CHANNEL_STORAGE_KEY,
|
|
||||||
DEFAULT_UPDATE_CHANNEL,
|
|
||||||
{
|
|
||||||
serializer: JSON.stringify,
|
|
||||||
deserializer: (value) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value);
|
|
||||||
return isValidChannel(parsed) ? parsed : DEFAULT_UPDATE_CHANNEL;
|
|
||||||
} catch (ignoreErr) {
|
|
||||||
return DEFAULT_UPDATE_CHANNEL;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getStoredUpdateChannel = (): UpdateChannel => {
|
|
||||||
if (
|
|
||||||
typeof window === "undefined" ||
|
|
||||||
typeof window.localStorage === "undefined"
|
|
||||||
) {
|
|
||||||
return DEFAULT_UPDATE_CHANNEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = window.localStorage.getItem(UPDATE_CHANNEL_STORAGE_KEY);
|
|
||||||
if (raw === null) {
|
|
||||||
return DEFAULT_UPDATE_CHANNEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
return isValidChannel(parsed) ? parsed : DEFAULT_UPDATE_CHANNEL;
|
|
||||||
} catch (ignoreErr) {
|
|
||||||
return DEFAULT_UPDATE_CHANNEL;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user