Compare commits

...

4 Commits

Author SHA1 Message Date
renovate[bot]
d8216ac337
chore(deps): update npm dependencies 2026-03-31 13:09:37 +00:00
Tunglies
9bcb79465c
fix: resolve frontend data race conditions in hooks
- use-system-state: convert module-level `disablingTunMode` to useRef
  to isolate state per hook instance, fix no-op clearTimeout, add
  proper effect cleanup
- use-profiles: convert forEach to for..of so selectNodeForGroup is
  properly awaited, remove fire-and-forget setTimeout around mutate
- use-clash: add useLockFn to patchInfo for concurrency safety
2026-03-31 20:26:34 +08:00
renovate[bot]
b62d89e163
chore(deps): update github/gh-aw-actions action to v0.64.4 (#6665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-30 17:12:08 +00:00
wysha-object
b7230967b4
feat: show detailed results in hotkey notifications (#6639)
* feat: show detailed results in hotkey notifications

* fix: Japanese locale appears to have a truncated translation key label

* fix: variable naming

* Update documentation to English

* Remove unnecessary mut

* feat: enhance system proxy notifications with toggle state

* chore: update changelog to include new shortcut notification feature

* fix: remove unnecessary quotes from system proxy toggle messages in localization files

* fix: tun mode toggled hotkey notifications

* fix: correct toggle_tun_mode logic to handle current state and errors

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-03-30 17:06:45 +00:00
23 changed files with 154 additions and 91 deletions

View File

@ -65,7 +65,7 @@ jobs:
title: ${{ steps.sanitized.outputs.title }} title: ${{ steps.sanitized.outputs.title }}
steps: steps:
- name: Setup Scripts - name: Setup Scripts
uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 uses: github/gh-aw-actions/setup@dc2e3faa962b8cd6219ca125f4e3989bf731e535 # v0.64.4
with: with:
destination: ${{ runner.temp }}/gh-aw/actions destination: ${{ runner.temp }}/gh-aw/actions
- name: Generate agentic run info - name: Generate agentic run info
@ -277,7 +277,7 @@ jobs:
output_types: ${{ steps.collect_output.outputs.output_types }} output_types: ${{ steps.collect_output.outputs.output_types }}
steps: steps:
- name: Setup Scripts - name: Setup Scripts
uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 uses: github/gh-aw-actions/setup@dc2e3faa962b8cd6219ca125f4e3989bf731e535 # v0.64.4
with: with:
destination: ${{ runner.temp }}/gh-aw/actions destination: ${{ runner.temp }}/gh-aw/actions
- name: Set runtime paths - name: Set runtime paths
@ -800,7 +800,7 @@ jobs:
total_count: ${{ steps.missing_tool.outputs.total_count }} total_count: ${{ steps.missing_tool.outputs.total_count }}
steps: steps:
- name: Setup Scripts - name: Setup Scripts
uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 uses: github/gh-aw-actions/setup@dc2e3faa962b8cd6219ca125f4e3989bf731e535 # v0.64.4
with: with:
destination: ${{ runner.temp }}/gh-aw/actions destination: ${{ runner.temp }}/gh-aw/actions
- name: Download agent output artifact - name: Download agent output artifact
@ -895,7 +895,7 @@ jobs:
detection_success: ${{ steps.detection_conclusion.outputs.success }} detection_success: ${{ steps.detection_conclusion.outputs.success }}
steps: steps:
- name: Setup Scripts - name: Setup Scripts
uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 uses: github/gh-aw-actions/setup@dc2e3faa962b8cd6219ca125f4e3989bf731e535 # v0.64.4
with: with:
destination: ${{ runner.temp }}/gh-aw/actions destination: ${{ runner.temp }}/gh-aw/actions
- name: Download agent output artifact - name: Download agent output artifact
@ -1056,7 +1056,7 @@ jobs:
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
steps: steps:
- name: Setup Scripts - name: Setup Scripts
uses: github/gh-aw-actions/setup@0048fdad270986610dd71c966d15f7abd7de037a # v0.64.2 uses: github/gh-aw-actions/setup@dc2e3faa962b8cd6219ca125f4e3989bf731e535 # v0.64.4
with: with:
destination: ${{ runner.temp }}/gh-aw/actions destination: ${{ runner.temp }}/gh-aw/actions
- name: Download agent output artifact - name: Download agent output artifact

View File

@ -11,6 +11,8 @@
### ✨ 新增功能 ### ✨ 新增功能
- 快捷键操作通知操作结果
### 🚀 优化改进 ### 🚀 优化改进
- 优化 macOS 读取系统代理性能 - 优化 macOS 读取系统代理性能

View File

@ -8,10 +8,12 @@ notifications:
body: تم التبديل إلى {mode}. body: تم التبديل إلى {mode}.
systemProxyToggled: systemProxyToggled:
title: وكيل النظام title: وكيل النظام
body: تم تحديث حالة وكيل النظام. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: وضع TUN title: وضع TUN
body: تم تحديث حالة وضع TUN. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: الوضع الخفيف title: الوضع الخفيف
body: تم الدخول إلى الوضع الخفيف. body: تم الدخول إلى الوضع الخفيف.

View File

@ -8,10 +8,12 @@ notifications:
body: Auf {mode} umgeschaltet. body: Auf {mode} umgeschaltet.
systemProxyToggled: systemProxyToggled:
title: Systemproxy title: Systemproxy
body: Der Status des Systemproxys wurde aktualisiert. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: TUN-Modus title: TUN-Modus
body: Der Status des TUN-Modus wurde aktualisiert. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: Leichtmodus title: Leichtmodus
body: Leichtmodus aktiviert. body: Leichtmodus aktiviert.

View File

@ -8,10 +8,12 @@ notifications:
body: Switched to {mode}. body: Switched to {mode}.
systemProxyToggled: systemProxyToggled:
title: System Proxy title: System Proxy
body: System proxy status has been updated. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: TUN Mode title: TUN Mode
body: TUN mode status has been updated. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: Lightweight Mode title: Lightweight Mode
body: Entered lightweight mode. body: Entered lightweight mode.

View File

@ -8,10 +8,12 @@ notifications:
body: Cambiado a {mode}. body: Cambiado a {mode}.
systemProxyToggled: systemProxyToggled:
title: Proxy del sistema title: Proxy del sistema
body: El estado del proxy del sistema se ha actualizado. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: Modo TUN title: Modo TUN
body: El estado del modo TUN se ha actualizado. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: Modo ligero title: Modo ligero
body: Se ha entrado en el modo ligero. body: Se ha entrado en el modo ligero.

View File

@ -8,10 +8,12 @@ notifications:
body: به {mode} تغییر کرد. body: به {mode} تغییر کرد.
systemProxyToggled: systemProxyToggled:
title: پروکسی سیستم title: پروکسی سیستم
body: وضعیت پروکسی سیستم به‌روزرسانی شد. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: حالت TUN title: حالت TUN
body: وضعیت حالت TUN به‌روزرسانی شد. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: حالت سبک title: حالت سبک
body: به حالت سبک وارد شد. body: به حالت سبک وارد شد.

View File

@ -8,10 +8,12 @@ notifications:
body: Beralih ke {mode}. body: Beralih ke {mode}.
systemProxyToggled: systemProxyToggled:
title: Proksi Sistem title: Proksi Sistem
body: Status proksi sistem telah diperbarui. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: Mode TUN title: Mode TUN
body: Status mode TUN telah diperbarui. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: Mode Ringan title: Mode Ringan
body: Masuk ke mode ringan. body: Masuk ke mode ringan.

View File

@ -8,10 +8,12 @@ notifications:
body: '{mode} に切り替えました。' body: '{mode} に切り替えました。'
systemProxyToggled: systemProxyToggled:
title: システムプロキシ title: システムプロキシ
body: システムプロキシの状態が更新されました。 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: TUN モード title: TUN モード
body: TUN モードの状態が更新されました。 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: 軽量モード title: 軽量モード
body: 軽量モードに入りました。 body: 軽量モードに入りました。

View File

@ -8,10 +8,12 @@ notifications:
body: '{mode}(으)로 전환되었습니다.' body: '{mode}(으)로 전환되었습니다.'
systemProxyToggled: systemProxyToggled:
title: 시스템 프록시 title: 시스템 프록시
body: 시스템 프록시 상태가 업데이트되었습니다. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: TUN 모드 title: TUN 모드
body: TUN 모드 상태가 업데이트되었습니다. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: 경량 모드 title: 경량 모드
body: 경량 모드에 진입했습니다. body: 경량 모드에 진입했습니다.

View File

@ -8,10 +8,12 @@ notifications:
body: Переключено на {mode}. body: Переключено на {mode}.
systemProxyToggled: systemProxyToggled:
title: Системный прокси title: Системный прокси
body: Статус системного прокси обновлен. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: Режим TUN title: Режим TUN
body: Статус режима TUN обновлен. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: Легкий режим title: Легкий режим
body: Включен легкий режим. body: Включен легкий режим.

View File

@ -8,10 +8,12 @@ notifications:
body: '{mode} moduna geçildi.' body: '{mode} moduna geçildi.'
systemProxyToggled: systemProxyToggled:
title: Sistem Vekil'i title: Sistem Vekil'i
body: Sistem vekil'i durumu güncellendi. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: TUN Modu title: TUN Modu
body: TUN modu durumu güncellendi. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: Hafif Mod title: Hafif Mod
body: Hafif moda geçildi. body: Hafif moda geçildi.

View File

@ -8,10 +8,12 @@ notifications:
body: '{mode} режимына күчтел.' body: '{mode} режимына күчтел.'
systemProxyToggled: systemProxyToggled:
title: Системалы прокси title: Системалы прокси
body: Системалы прокси хәле яңартылды. 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: TUN режимы title: TUN режимы
body: TUN режимы хәле яңартылды. 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: Җиңел режим title: Җиңел режим
body: Җиңел режимга күчелде. body: Җиңел режимга күчелде.

View File

@ -8,10 +8,12 @@ notifications:
body: 已切换至 {mode}。 body: 已切换至 {mode}。
systemProxyToggled: systemProxyToggled:
title: 系统代理 title: 系统代理
body: 系统代理状态已更新。 'on': 系统代理已启用。
'off': 系统代理已禁用。
tunModeToggled: tunModeToggled:
title: TUN 模式 title: TUN 模式
body: TUN 模式状态已更新。 'on': TUN 模式已开启。
'off': TUN 模式已关闭。
lightweightModeEntered: lightweightModeEntered:
title: 轻量模式 title: 轻量模式
body: 已进入轻量模式。 body: 已进入轻量模式。

View File

@ -8,10 +8,12 @@ notifications:
body: 已切換至 {mode}。 body: 已切換至 {mode}。
systemProxyToggled: systemProxyToggled:
title: 系統代理 title: 系統代理
body: 系統代理狀態已更新。 'on': System proxy has been enabled.
'off': System proxy has been disabled.
tunModeToggled: tunModeToggled:
title: 虛擬網路介面卡模式 title: 虛擬網路介面卡模式
body: 已更新虛擬網路介面卡模式狀態。 'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
lightweightModeEntered: lightweightModeEntered:
title: 輕量模式 title: 輕量模式
body: 已進入輕量模式。 body: 已進入輕量模式。

View File

@ -60,7 +60,7 @@
"dayjs": "1.11.20", "dayjs": "1.11.20",
"foxact": "^0.3.0", "foxact": "^0.3.0",
"foxts": "^5.3.0", "foxts": "^5.3.0",
"i18next": "^25.10.4", "i18next": "^26.0.0",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.23",
"meta-json-schema": "^1.19.21", "meta-json-schema": "^1.19.21",
@ -71,7 +71,7 @@
"react-dom": "19.2.4", "react-dom": "19.2.4",
"react-error-boundary": "6.1.1", "react-error-boundary": "6.1.1",
"react-hook-form": "^7.72.0", "react-hook-form": "^7.72.0",
"react-i18next": "16.6.6", "react-i18next": "17.0.2",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-router": "^7.13.1", "react-router": "^7.13.1",
"react-virtuoso": "^4.18.3", "react-virtuoso": "^4.18.3",
@ -120,7 +120,7 @@
"typescript": "^6.0.0", "typescript": "^6.0.0",
"typescript-eslint": "^8.57.1", "typescript-eslint": "^8.57.1",
"vite": "^8.0.1", "vite": "^8.0.1",
"vite-plugin-svgr": "^4.5.0" "vite-plugin-svgr": "^5.0.0"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx}": [ "*.{ts,tsx}": [

36
pnpm-lock.yaml generated
View File

@ -87,8 +87,8 @@ importers:
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.4.0 version: 5.4.0
i18next: i18next:
specifier: ^25.10.4 specifier: ^26.0.0
version: 25.10.10(typescript@6.0.2) version: 26.0.3(typescript@6.0.2)
js-yaml: js-yaml:
specifier: ^4.1.1 specifier: ^4.1.1
version: 4.1.1 version: 4.1.1
@ -120,8 +120,8 @@ importers:
specifier: ^7.72.0 specifier: ^7.72.0
version: 7.72.0(react@19.2.4) version: 7.72.0(react@19.2.4)
react-i18next: react-i18next:
specifier: 16.6.6 specifier: 17.0.2
version: 16.6.6(i18next@25.10.10(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2) version: 17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2)
react-markdown: react-markdown:
specifier: 10.1.0 specifier: 10.1.0
version: 10.1.0(@types/react@19.2.14)(react@19.2.4) version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
@ -262,8 +262,8 @@ importers:
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3) version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3)
vite-plugin-svgr: vite-plugin-svgr:
specifier: ^4.5.0 specifier: ^5.0.0
version: 4.5.0(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3)) version: 5.0.0(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3))
packages: packages:
@ -2494,8 +2494,8 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
i18next@25.10.10: i18next@26.0.3:
resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} resolution: {integrity: sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==}
peerDependencies: peerDependencies:
typescript: ^5 || ^6 typescript: ^5 || ^6
peerDependenciesMeta: peerDependenciesMeta:
@ -3059,10 +3059,10 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19 react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@16.6.6: react-i18next@17.0.2:
resolution: {integrity: sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==} resolution: {integrity: sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==}
peerDependencies: peerDependencies:
i18next: '>= 25.10.9' i18next: '>= 26.0.1'
react: '>= 16.8.0' react: '>= 16.8.0'
react-dom: '*' react-dom: '*'
react-native: '*' react-native: '*'
@ -3438,10 +3438,10 @@ packages:
vfile@6.0.3: vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite-plugin-svgr@4.5.0: vite-plugin-svgr@5.0.0:
resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==} resolution: {integrity: sha512-CZFWDtbWSLnF6C+uv8u7E5Ao6UVQYBpJrS6212XsEod/Lm4ErhOoFc01/po4ie5hqvMCr5KYrlMrSGQQEtMtBg==}
peerDependencies: peerDependencies:
vite: '>=2.6.0' vite: '>=3.0.0'
vite@8.0.3: vite@8.0.3:
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
@ -6023,7 +6023,7 @@ snapshots:
husky@9.1.7: {} husky@9.1.7: {}
i18next@25.10.10(typescript@6.0.2): i18next@26.0.3(typescript@6.0.2):
dependencies: dependencies:
'@babel/runtime': 7.29.2 '@babel/runtime': 7.29.2
optionalDependencies: optionalDependencies:
@ -6663,11 +6663,11 @@ snapshots:
dependencies: dependencies:
react: 19.2.4 react: 19.2.4
react-i18next@16.6.6(i18next@25.10.10(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2): react-i18next@17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2):
dependencies: dependencies:
'@babel/runtime': 7.29.2 '@babel/runtime': 7.29.2
html-parse-stringify: 3.0.1 html-parse-stringify: 3.0.1
i18next: 25.10.10(typescript@6.0.2) i18next: 26.0.3(typescript@6.0.2)
react: 19.2.4 react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4) use-sync-external-store: 1.6.0(react@19.2.4)
optionalDependencies: optionalDependencies:
@ -7088,7 +7088,7 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.3 vfile-message: 4.0.3
vite-plugin-svgr@4.5.0(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3)): vite-plugin-svgr@5.0.0(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3)):
dependencies: dependencies:
'@rollup/pluginutils': 5.3.0 '@rollup/pluginutils': 5.3.0
'@svgr/core': 8.1.0(typescript@6.0.2) '@svgr/core': 8.1.0(typescript@6.0.2)

View File

@ -135,14 +135,14 @@ impl Hotkey {
} }
HotkeyFunction::ToggleSystemProxy => { HotkeyFunction::ToggleSystemProxy => {
AsyncHandler::spawn(async move || { AsyncHandler::spawn(async move || {
feat::toggle_system_proxy().await; let is_proxy_enabled = feat::toggle_system_proxy().await;
notify_event(NotificationEvent::SystemProxyToggled).await; notify_event(NotificationEvent::SystemProxyToggled(is_proxy_enabled)).await;
}); });
} }
HotkeyFunction::ToggleTunMode => { HotkeyFunction::ToggleTunMode => {
AsyncHandler::spawn(async move || { AsyncHandler::spawn(async move || {
feat::toggle_tun_mode(None).await; let is_tun_enable = feat::toggle_tun_mode(None).await;
notify_event(NotificationEvent::TunModeToggled).await; notify_event(NotificationEvent::TunModeToggled(is_tun_enable)).await;
}); });
} }
HotkeyFunction::EntryLightweightMode => { HotkeyFunction::EntryLightweightMode => {

View File

@ -7,7 +7,7 @@ use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt as _; use tauri_plugin_clipboard_manager::ClipboardExt as _;
/// Toggle system proxy on/off /// Toggle system proxy on/off
pub async fn toggle_system_proxy() { pub async fn toggle_system_proxy() -> bool {
let verge = Config::verge().await; let verge = Config::verge().await;
let enable = verge.latest_arc().enable_system_proxy.unwrap_or(false); let enable = verge.latest_arc().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_arc().auto_close_connection.unwrap_or(false); let auto_close_connection = verge.latest_arc().auto_close_connection.unwrap_or(false);
@ -20,9 +20,10 @@ pub async fn toggle_system_proxy() {
logging!(error, Type::ProxyMode, "Failed to close all connections: {err}"); logging!(error, Type::ProxyMode, "Failed to close all connections: {err}");
} }
let enable = !enable;
let patch_result = super::patch_verge( let patch_result = super::patch_verge(
&IVerge { &IVerge {
enable_system_proxy: Some(!enable), enable_system_proxy: Some(enable),
..IVerge::default() ..IVerge::default()
}, },
false, false,
@ -33,24 +34,33 @@ pub async fn toggle_system_proxy() {
Ok(_) => handle::Handle::refresh_verge(), Ok(_) => handle::Handle::refresh_verge(),
Err(err) => logging!(error, Type::ProxyMode, "{err}"), Err(err) => logging!(error, Type::ProxyMode, "{err}"),
} }
enable
} }
/// Toggle TUN mode on/off /// Toggle TUN mode on/off
pub async fn toggle_tun_mode(not_save_file: Option<bool>) { /// Returns the updated toggle state
let enable = Config::verge().await.latest_arc().enable_tun_mode; pub async fn toggle_tun_mode(not_save_file: Option<bool>) -> bool {
let enable = enable.unwrap_or(false); let current = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
let enable = !current;
match super::patch_verge( match super::patch_verge(
&IVerge { &IVerge {
enable_tun_mode: Some(!enable), enable_tun_mode: Some(enable),
..IVerge::default() ..IVerge::default()
}, },
not_save_file.unwrap_or(false), not_save_file.unwrap_or(false),
) )
.await .await
{ {
Ok(_) => handle::Handle::refresh_verge(), Ok(_) => {
Err(err) => logging!(error, Type::ProxyMode, "{err}"), handle::Handle::refresh_verge();
enable
}
Err(err) => {
logging!(error, Type::ProxyMode, "{err}");
current
}
} }
} }

View File

@ -9,8 +9,8 @@ pub enum NotificationEvent<'a> {
ClashModeChanged { ClashModeChanged {
mode: &'a str, mode: &'a str,
}, },
SystemProxyToggled, SystemProxyToggled(bool),
TunModeToggled, TunModeToggled(bool),
LightweightModeEntered, LightweightModeEntered,
ProfilesReactivated, ProfilesReactivated,
AppQuit, AppQuit,
@ -37,14 +37,25 @@ pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
.into(); .into();
notify(title, body); notify(title, body);
} }
NotificationEvent::SystemProxyToggled => { NotificationEvent::SystemProxyToggled(enabled) => {
let title = clash_verge_i18n::t!("notifications.systemProxyToggled.title"); let title = clash_verge_i18n::t!("notifications.systemProxyToggled.title");
let body = clash_verge_i18n::t!("notifications.systemProxyToggled.body"); let key = if enabled {
"notifications.systemProxyToggled.on"
} else {
"notifications.systemProxyToggled.off"
};
let body = clash_verge_i18n::t!(key);
notify(title, body); notify(title, body);
} }
NotificationEvent::TunModeToggled => { NotificationEvent::TunModeToggled(enabled) => {
let title = clash_verge_i18n::t!("notifications.tunModeToggled.title"); let title = clash_verge_i18n::t!("notifications.tunModeToggled.title");
let body = clash_verge_i18n::t!("notifications.tunModeToggled.body"); let key = if enabled {
"notifications.tunModeToggled.on"
} else {
"notifications.tunModeToggled.off"
};
let body = clash_verge_i18n::t!(key);
notify(title, body); notify(title, body);
} }
NotificationEvent::LightweightModeEntered => { NotificationEvent::LightweightModeEntered => {

View File

@ -87,7 +87,7 @@ export const useClashInfo = () => {
getClashInfo, getClashInfo,
) )
const patchInfo = async (patch: ClashInfoPatch) => { const patchInfo = useLockFn(async (patch: ClashInfoPatch) => {
if (!hasClashInfoPayload(patch)) return if (!hasClashInfoPayload(patch)) return
validatePorts(patch) validatePorts(patch)
@ -95,7 +95,7 @@ export const useClashInfo = () => {
await patchClashConfig(patch) await patchClashConfig(patch)
mutateInfo() mutateInfo()
mutate('getClashConfig') mutate('getClashConfig')
} })
return { return {
clashInfo, clashInfo,

View File

@ -120,9 +120,9 @@ export const useProfiles = () => {
]) ])
// 处理所有代理组 // 处理所有代理组
;[global, ...groups].forEach((group) => { for (const group of [global, ...groups]) {
if (!group) { if (!group) {
return continue
} }
const { type, name, now } = group const { type, name, now } = group
@ -134,14 +134,14 @@ export const useProfiles = () => {
const preferredProxy = now ? now : savedProxy const preferredProxy = now ? now : savedProxy
newSelected.push({ name, now: preferredProxy }) newSelected.push({ name, now: preferredProxy })
} }
return continue
} }
if (savedProxy == null) { if (savedProxy == null) {
if (now != null) { if (now != null) {
newSelected.push({ name, now }) newSelected.push({ name, now })
} }
return continue
} }
const existsInGroup = availableProxies.some((proxy) => { const existsInGroup = availableProxies.some((proxy) => {
@ -158,7 +158,7 @@ export const useProfiles = () => {
) )
hasChange = true hasChange = true
newSelected.push({ name, now: now ?? savedProxy }) newSelected.push({ name, now: now ?? savedProxy })
return continue
} }
if (savedProxy !== now) { if (savedProxy !== now) {
@ -166,11 +166,18 @@ export const useProfiles = () => {
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${savedProxy}`, `[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${savedProxy}`,
) )
hasChange = true hasChange = true
selectNodeForGroup(name, savedProxy) try {
await selectNodeForGroup(name, savedProxy)
} catch (error: any) {
console.warn(
`[ActivateSelected] 切换代理组 ${name} 失败:`,
error.message,
)
}
} }
newSelected.push({ name, now: savedProxy }) newSelected.push({ name, now: savedProxy })
}) }
if (!hasChange) { if (!hasChange) {
debugLog('[ActivateSelected] 所有代理选择已经是目标状态,无需更新') debugLog('[ActivateSelected] 所有代理选择已经是目标状态,无需更新')
@ -183,9 +190,7 @@ export const useProfiles = () => {
await patchProfile(profileData.current!, { selected: newSelected }) await patchProfile(profileData.current!, { selected: newSelected })
debugLog('[ActivateSelected] 代理选择配置保存成功') debugLog('[ActivateSelected] 代理选择配置保存成功')
setTimeout(() => { await mutate('getProxies', calcuProxies())
mutate('getProxies', calcuProxies())
}, 100)
} catch (error: any) { } catch (error: any) {
console.error('[ActivateSelected] 保存代理选择配置失败:', error.message) console.error('[ActivateSelected] 保存代理选择配置失败:', error.message)
} }

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react' import { useEffect, useRef } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds' import { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds'
@ -18,14 +18,13 @@ const defaultSystemState = {
isServiceOk: false, isServiceOk: false,
} as SystemState } as SystemState
let disablingTunMode = false
/** /**
* hook * hook
* *
*/ */
export function useSystemState() { export function useSystemState() {
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
const disablingTunRef = useRef(false)
const { const {
data: systemState, data: systemState,
@ -53,16 +52,18 @@ export function useSystemState() {
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk
const enable_tun_mode = verge?.enable_tun_mode const enable_tun_mode = verge?.enable_tun_mode
const cooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { useEffect(() => {
if (enable_tun_mode === undefined) return if (enable_tun_mode === undefined) return
if ( if (
!disablingTunMode && !disablingTunRef.current &&
enable_tun_mode && enable_tun_mode &&
!isTunModeAvailable && !isTunModeAvailable &&
!isLoading !isLoading
) { ) {
disablingTunMode = true disablingTunRef.current = true
patchVerge({ enable_tun_mode: false }) patchVerge({ enable_tun_mode: false })
.then(() => { .then(() => {
showNotice.info( showNotice.info(
@ -76,13 +77,21 @@ export function useSystemState() {
) )
}) })
.finally(() => { .finally(() => {
const tid = setTimeout(() => {
// 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式 // 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式
disablingTunMode = false cooldownTimerRef.current = setTimeout(() => {
clearTimeout(tid) disablingTunRef.current = false
cooldownTimerRef.current = null
}, 1000) }, 1000)
}) })
} }
return () => {
if (cooldownTimerRef.current != null) {
clearTimeout(cooldownTimerRef.current)
cooldownTimerRef.current = null
disablingTunRef.current = false
}
}
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading]) }, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading])
return { return {