Compare commits

...
72 Commits
Author SHA1 Message Date
Tunglies 45fce7db73 feat: enhance autobuild workflow with pre-release tagging and promotion steps 2025-09-20 16:12:48 +08:00
Tunglies 1cf8e2384e fix: Cargo.toml dependencies option (#4805)
* chore: update cargo lock

* refactor: streamline dependency declarations in Cargo.toml

* refactor: reorder import statements and comment out Cargo.toml version update

* fix: enable Cargo version update in release script and update tauri-plugin-devtools dependency
2025-09-20 15:15:24 +08:00
Tunglies d9a5c11d6a refactor: improve code readability and consistency in proxy-chain and uri-parser utilities
refactor: add keys to icons in routers for improved rendering and performance
refactor: optimize RegExp polyfill by using Object.prototype.hasOwnProperty.call
refactor: reorder imports in chain-proxy-provider for consistency
refactor: remove unused "obfs-opts" property from IProxySnellConfig interface

refactor: reorganize imports and enhance refresh logic in app data provider

refactor: re-enable prop-types linting for better type safety in BaseDialog component

refactor: update dependencies in effect hooks for improved stability and performance
2025-09-20 11:19:36 +08:00
Tunglies 7811714f89 refactor: enhance logging system and add new development commands (#4803)
* refactor: enhance logging system and add new development commands

* refactor: add cfg-if dependency and improve logging configuration
2025-09-20 00:04:46 +08:00
Tunglies e869da8d4c refactor: remove unused HTTP-specific structs to streamline service code 2025-09-19 00:04:51 +08:00
Tunglies e414b49879 Refactor imports across multiple components for consistency and clarity
- Reorganized import statements in various components to ensure consistent ordering and grouping.
- Removed unnecessary imports and added missing ones where applicable.
- Improved readability and maintainability of the codebase by standardizing import styles.
2025-09-19 00:01:04 +08:00
Tunglies 627119bb22 Refactor imports and improve code organization across multiple components and hooks
- Consolidated and reordered imports in various files for better readability and maintainability.
- Removed unused imports and ensured consistent import styles.
- Enhanced the structure of components by grouping related imports together.
- Updated the layout and organization of hooks to streamline functionality.
- Improved the overall code quality by following best practices in import management.
2025-09-18 23:34:38 +08:00
Tunglies 74ade3ee41 refactor: update ESLint configuration and improve lint command with cache 2025-09-18 23:13:13 +08:00
Tunglies 324628dd3d refactor: replace 'let' with 'const' for better variable scoping and immutability 2025-09-18 23:07:18 +08:00
Tunglies 9d96ac0f6a feat: Integrate HTTP plugin and update IP detection to use fetch API #4712 2025-09-18 19:35:14 +08:00
Tunglies 409571f54b refactor: remove unused notification permission hook and related code 2025-09-18 19:13:23 +08:00
TianHua Liu a995a13163 chore: use jsx-runtime presets of eslint-plugin-react (#4794) 2025-09-18 19:04:12 +08:00
Tunglies 7848d6b1de refactor: window handle usage (#4788)
* refactor: Remove unused UI reset function and streamline window creation logic

* refactor: Remove debug print statements and streamline lightweight mode initialization

* fix: Ensure tray status refresh during silent startup and lightweight mode entry is independent of window creation

* refactor: Simplify window creation process and remove debug print statements
2025-09-18 10:22:43 +08:00
Tunglies 5d2e114b4d fix: Update tray menu event handling to toggle lightweight mode based on current state #4785 2025-09-18 00:46:49 +08:00
Tunglies c207516b47 refactor: clash-verge-service management (#4674)
* refactor: clash-verge-service management

* fix: correct service state checks in ProxyControlSwitches component
refactor: improve logging in service state update functions

* fix: add missing async handler for Windows and adjust logging import for macOS

* fix: streamline logging imports and add missing async handler for Windows

* refactor: remove unused useServiceStateSync hook and update imports in _layout

* refactor: remove unused useServiceStateSync import and clean up code in ProxyControlSwitches and _layout

* refactor: simplify service status checks and reduce wait time in useServiceInstaller hook

* refactor: remove unnecessary logging statements in service checks and IPC connection

* refactor: extract SwitchRow component for better code organization and readability

* refactor: enhance service state management and update related mutations in layout

* refactor: streamline core stopping logic and improve IPC connection logging

* refactor: consolidate service uninstallation logic and improve error handling

* fix: simplify conditional statements in CoreManager and service functions

* feat: add backoff dependency and implement retry strategy for IPC requests

* refactor: remove redundant Windows conditional and improve error handling in IPC tests

* test: improve error handling in IPC tests for message signing and verification

* fix: adjust IPC backoff retry parameters

* refactor: Remove service state tracking and related logic from service management

* feat: Enhance service status handling with logging and running mode updates

* fix: Improve service status handling with enhanced error logging

* fix: Ensure proper handling of service operations with error propagation

* refactor: Simplify service operation execution and enhance service status handling

* fix: Improve error message formatting in service operation execution and simplify service status retrieval

* refactor: Replace Cache with CacheProxy in multiple modules and update CacheEntry to be generic

* fix: Remove unnecessary success message from config validation

* refactor: Comment out logging statements in service version check and IPC request handling
2025-09-17 22:59:02 +08:00
Tunglies 6724f1ae35 feat: Implement caching mechanism with Cache struct and update related commands 2025-09-17 19:37:42 +08:00
Tunglies 1787d5372e fix: Update OS_PLATFORM definition to reflect the current platform 2025-09-17 16:10:22 +08:00
Tunglies 4c41144dd0 fix: Update error message to include details of accumulated startup errors 2025-09-17 16:10:01 +08:00
Tunglies 27636c848f fix: update changelog to reflect removal of hidden groups in tray node switching #4765 2025-09-17 13:39:38 +08:00
Tunglies 8060d699f0 fix: enhance prebuild script to support shorthand for force update #4777 2025-09-17 13:36:31 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f36f31a636 chore(deps): update npm dependencies (#4686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 12:45:57 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d300fac3d9 chore(deps): update cargo dependencies (#4687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 12:45:50 +08:00
Thomas 8c2262dd95 stop showing hidden groups in the tray menu (#4765)
托盘菜单不显示隐藏代理组
2025-09-17 12:45:35 +08:00
wonfen c438e916ca perf: remove system-level unlock test timeout notice 2025-09-15 22:16:13 +08:00
ZShab NibaandTunglies 0855bd4896 fix: Fix icon to RGBA/sRGB Colorspace (#4753)
* fix: Fixed icon pixel size

* fix: fix icon to RGBA/sRGB Colorspace

* chore: update UPDATELOG

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-09-15 12:29:30 +08:00
Junkai W. f2073a2f83 Add Func 链式代理 (#4624)
* 添加链式代理gui和语言支持
在Iruntime中添跟新链式代理配置方法
同时添加了cmd

* 修复读取运行时代理链配置文件bug

* t

* 完成链式代理配置构造

* 修复获取链式代理运行时配置的bug

* 完整的链式代理功能
2025-09-15 07:44:54 +08:00
Tunglies a1f468202f Revert "fix: Fixed icon pixel size (#4698)"
revert due to runtime error [[Setup]] Error: failed to process image: Format error decoding Ico: The PNG is not in RGBA format!

This reverts commit a24bf4042c.
2025-09-15 00:56:59 +08:00
ZShab Niba a24bf4042c fix: Fixed icon pixel size (#4698) 2025-09-13 10:56:07 +08:00
❤是纱雾酱哟~ 15d22b4bf6 chore(issue template): disable blank issue template in GitHub config (#4731)
- Prevent users from creating issue without a template
- Always use a template for guiding users to provide necessary information for us

Signed-off-by: Dragon1573 <49941141+Dragon1573@users.noreply.github.com>
2025-09-13 10:11:40 +08:00
Tunglies 231d264652 fix: update alpha version description in README 2025-09-12 18:45:40 +08:00
Tunglies 67ac353fd5 chore: update cron schedule for auto build workflow 2025-09-12 18:43:13 +08:00
Tunglies 1c5534ad36 fix: remove deprecated updater for alpha endpoints in tauri configuration 2025-09-11 17:14:57 +08:00
Tunglies 251678493c edition 2024 (#4702)
* feat: update Cargo.toml for 2024 edition and optimize release profiles

* feat: refactor environment variable settings for Linux and improve code organization

* Refactor conditional statements to use `&&` for improved readability

- Updated multiple files to combine nested `if let` statements using `&&` for better clarity and conciseness.
- This change enhances the readability of the code by reducing indentation levels and making the conditions more straightforward.
- Affected files include: media_unlock_checker.rs, profile.rs, clash.rs, profiles.rs, async_proxy_query.rs, core.rs, handle.rs, hotkey.rs, service.rs, timer.rs, tray/mod.rs, merge.rs, seq.rs, config.rs, proxy.rs, window.rs, general.rs, dirs.rs, i18n.rs, init.rs, network.rs, and window.rs in the resolve module.

* refactor: streamline conditional checks using `&&` for improved readability

* fix: update release profile settings for panic behavior and optimization

* fix: adjust optimization level in Cargo.toml and reorder imports in lightweight.rs
2025-09-10 09:49:06 +08:00
Tunglies ccbffa14f0 fix: replace toggle with show for main window in lightweight mode exit #4697 2025-09-09 21:25:38 +08:00
Tunglies dfc1f736af fix: resolve from lightweight cause crash (#4682)
* refactor: streamline lightweight mode handling and improve window management

* refactor: replace mutex-based window creation lock with atomic operations for improved performance

* refactor: remove startup completed event handling and simplify initialization logic

* refactor: remove conditional compilation for emit_update_event function

* refactor: simplify return statements and clean up commented code in lightweight and window manager modules

* refactor: streamline lightweight mode handling by consolidating window management calls

* refactor: prevent unnecessary window toggle when exiting lightweight mode

* refactor: reorder imports for consistency in lightweight module

* refactor: move macOS specific logging_error import for clarity
2025-09-09 18:50:24 +08:00
Tunglies c54d89a465 feat: add support for Windows ARM64 in development workflow 2025-09-09 16:55:46 +08:00
Tunglies 55b95a1985 Revert "feat: update Cargo.toml for 2024 edition and optimize release profiles (#4681)"
This reverts commit 31e3104c7f.
2025-09-08 21:48:09 +08:00
Tunglies 31e3104c7f feat: update Cargo.toml for 2024 edition and optimize release profiles (#4681)
* feat: update Cargo.toml for 2024 edition and optimize release profiles

* feat: refactor environment variable settings for Linux and improve code organization

* Refactor conditional statements to use `&&` for improved readability

- Updated multiple files to combine nested `if let` statements using `&&` for better clarity and conciseness.
- This change enhances the readability of the code by reducing indentation levels and making the conditions more straightforward.
- Affected files include: media_unlock_checker.rs, profile.rs, clash.rs, profiles.rs, async_proxy_query.rs, core.rs, handle.rs, hotkey.rs, service.rs, timer.rs, tray/mod.rs, merge.rs, seq.rs, config.rs, proxy.rs, window.rs, general.rs, dirs.rs, i18n.rs, init.rs, network.rs, and window.rs in the resolve module.

* refactor: streamline conditional checks using `&&` for improved readability
2025-09-08 13:57:32 +08:00
wonfen 58a0089b19 fix: workflow file name 2025-09-07 15:24:14 +08:00
Tunglies 043ed4cb31 feat: add known issues section and update lightweight mode handling 2025-09-07 13:00:51 +08:00
Tunglies f64c01044c feat: bump version to 2.4.3 and update changelog for macOS intel Mihomo compatibility 2025-09-07 12:06:33 +08:00
Tunglies 5dca724017 feat: update Vite configuration for improved chunking and build options 2025-09-06 21:24:46 +08:00
Tunglies 579f9bd1f8 feat: add path and process dependencies; remove unused SCSS preprocessor options 2025-09-06 21:04:58 +08:00
Tunglies 7c9104a5b9 feat: optimize home page loading with lazy loading and improve card rendering logic 2025-09-06 20:38:33 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 14d1531469 chore(deps): update npm dependencies (#4567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 14:11:30 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 74e1e92607 chore(deps): update rust crate zip to v5 (#4653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 14:11:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f7a56c0eb3 chore(deps): update cargo dependencies (#4586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 14:10:35 +08:00
Tunglies e2fa76332a fix: remove unused ProxyRequestCache import and improve proxy update error handling 2025-09-06 14:10:09 +08:00
Tunglies 0daa8720cd feat: implement i18n lazy loading optimization
🚀 Performance improvements:
- Replace static language imports with dynamic imports
- Load only current language on startup instead of all 13 languages
- Implement on-demand loading when switching languages

📦 Bundle optimization:
- Reduce initial bundle size by avoiding preloading all language files
- Add resource caching to prevent reloading same language
- Support all 13 languages: en, ru, zh, fa, tt, id, ar, ko, tr, de, es, jp, zhtw

🔧 Technical changes:
- Convert i18n.ts to use dynamic import() for language resources
- Add async initializeLanguage() for app startup
- Create useI18n hook for language management with loading states
- Update main.tsx for async language initialization
- Fix language display labels in settings dropdown
- Maintain backward compatibility with existing language system

✅ Fixed issues:
- Resolve infinite loop in React components
- Fix missing language labels in settings UI
- Prevent circular dependencies in language loading
- Add proper error handling and fallback mechanisms
2025-09-06 14:05:36 +08:00
Tunglies f70b8b1213 Revert "fix: auto-detect KDE/Plasma and switch to X11 as backend to fix titlebar button freeze (#4523)"
This reverts commit d58c0a7df5.
2025-09-06 11:59:33 +08:00
wonfen feb3dfbe86 chore: use UTF+8 time to build 2025-09-05 11:21:34 +08:00
Tunglies f38e4a6cac fix: refactor proxy fetching to use command methods and improve error handling 2025-09-04 15:32:46 +08:00
Tunglies 893188d693 fix: add basic authorization header support for URL parsing in NetworkManager #4618 2025-09-03 01:07:45 +08:00
Tunglies b989aeb7b0 refactor: clean up imports and remove unused initialization in NetworkManager 2025-09-02 23:48:27 +08:00
Tunglies 40f87c834d fix: update ClientConfig settings for improved connection management 2025-09-02 23:21:05 +08:00
Tunglies 0bb9cb5097 fix: enhance startup speed and fix connection issues during initialization 2025-09-02 23:10:02 +08:00
Tunglies b51797e238 fix: update logging types and clean up ProxyRequestCache usage 2025-09-02 22:19:22 +08:00
Tunglies 926c095409 perf: update ProxyRequestCache to use boxed CacheEntry for improved memory management 2025-09-02 19:09:44 +08:00
Tunglies 0c65f8ebad fix: remove macOS specific conditional compilation for logging_error import 2025-09-02 18:36:13 +08:00
Tunglies 63f4295063 fix: update required service version to 1.1.2 2025-09-02 16:09:21 +08:00
Tunglies d2b38a8a3c fix: optimize async handler usage in singleton checks and resource initialization #4576, #4590, #4609 2025-09-02 13:37:14 +08:00
Tunglies 45ddb15d56 fix: remove redundant service stop call in CoreManager and clean up unused Mutex import 2025-09-02 12:12:19 +08:00
Tunglies 7aef9d2a5a fix: resolve lightweight mode state detection issues and improve logging #3814 2025-09-02 08:00:53 +08:00
wonfen 45fdebeaca style: simplify and improve proxy settings UI 2025-09-01 13:57:04 +08:00
wonfen 0ea875f7f7 fix: unify homepage node selection 2025-09-01 11:30:27 +08:00
Guanghui Qinandhiaoxui 1b54c9fc1b fix rpm/deb package naming issues (#4582)
* fix arm64 deb package name

* fix rpm package naming issue

* fix rpm package naming issue

---------

Co-authored-by: hiaoxui <hiaoxui@gmail.com>
2025-09-01 01:12:40 +08:00
Tunglies 89f3adcbef fix: add redirect policy to HTTP client builder 2025-09-01 00:45:39 +08:00
Tunglies b13fef5ad9 fix: add missing allow(dead_code) attribute to block_on method 2025-08-31 16:22:35 +08:00
Tunglies 9110955b63 fix: streamline service availability checks and improve logging for core startup 2025-08-31 16:19:31 +08:00
Tunglies 4508d062f1 chore: bump version to 2.4.2 2025-08-31 15:53:50 +08:00
Junkai W. bea0dde074 Win 下添加代理节点的系统托盘 (#4562)
* add proxy memu in tray

* 添加win下系统托盘 节点
代理->代理组->nodes
同时添加了对应gui同步

* 添加win 系统托盘显示代理节点
且gui和托盘刷新机制

* rust format

* 添加 win下系统托盘节点延迟

* Squashed commit of the following:

commit 44caaa62c5
Merge: 1916e539 3939741a
Author: Junkai W. <129588175+Be-Forever223@users.noreply.github.com>
Date:   Sat Aug 30 02:37:07 2025 +0800

    Merge branch 'dev' into dev

commit 3939741a06
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Sat Aug 30 02:24:47 2025 +0800

    refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling (#4568)

    * refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling

    * refactor: format code for better readability in DNS configuration

commit f86a1816e0
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Sat Aug 30 02:15:34 2025 +0800

    chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml (#4564)

    * chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml

    * chore(deps): remove libnghttp2-sys dependency and update isahc features in Cargo.toml

    * chore(deps): remove sysinfo and zip from ignoreDeps in renovate.json

commit 9cbd8b4529
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Sat Aug 30 01:30:48 2025 +0800

    feat: add x86 OpenSSL installation step for macOS in workflows

commit 5dea73fc2a
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sat Aug 30 01:21:53 2025 +0800

    chore(deps): update npm dependencies (#4542)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 01af1bea23
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sat Aug 30 01:21:46 2025 +0800

    chore(deps): update rust crate reqwest_dav to 0.2.2 (#4554)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 1227e86134
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Sat Aug 30 01:12:03 2025 +0800

    Remove unnecessary "rustls-tls" feature from reqwest dependency in Cargo.toml

commit c6a6ea48dd
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Fri Aug 29 23:51:09 2025 +0800

    refactor: enhance async initialization and streamline setup process (#4560)

    * feat: Implement DNS management for macOS

    - Added `set_public_dns` and `restore_public_dns` functions in `dns.rs` to manage system DNS settings.
    - Introduced `resolve` module to encapsulate DNS and scheme resolution functionalities.
    - Implemented `resolve_scheme` function in `scheme.rs` to handle deep links and profile imports.
    - Created UI readiness management in `ui.rs` to track and update UI loading states.
    - Developed window management logic in `window.rs` to handle window creation and visibility.
    - Added initial loading overlay script in `window_script.rs` for better user experience during startup.
    - Updated server handling in `server.rs` to integrate new resolve functionalities.
    - Refactored window creation calls in `window_manager.rs` to use the new window management logic.

    * refactor: streamline asynchronous handling in config and resolve setup

    * Revert "refactor: streamline asynchronous handling in config and resolve setup"

    This reverts commit 23d7dc86d5.

    * fix: optimize asynchronous memory handling

    * fix: enhance task logging by adding size check for special cases

    * refactor: enhance async initialization and streamline setup process

    * refactor: optimize async setup by consolidating initialization tasks

    * chore: update changelog for Mihomo(Meta) kernel upgrade to v1.19.13

    * fix: improve startup phase initialization performance

    * refactor: optimize file read/write performance to reduce application wait time

    * refactor: simplify app instance exit logic and adjust system proxy guard initialization

    * refactor: change resolve_setup_async to synchronous execution for improved performance

    * refactor: update resolve_setup_async to accept AppHandle for improved initialization flow

    * refactor: remove unnecessary initialization of portable flag in run function

    * refactor: consolidate async initialization tasks into a single blocking call for improved execution flow

    * refactor: optimize resolve_setup_async by restructuring async tasks for improved concurrency

    * refactor: streamline resolve_setup_async and embed_server for improved async handling

    * refactor: separate synchronous and asynchronous setup functions for improved clarity

    * refactor: simplify async notification handling and remove redundant network manager initialization

    * refactor: enhance async handling in proxy request cache and window creation logic

    * refactor: improve code formatting and readability in ProxyRequestCache

    * refactor: adjust singleton check timeout and optimize trace size conditions

    * refactor: update TRACE_SPECIAL_SIZE to include additional size condition

    * refactor: update kode-bridge dependency to version 0.2.1-rc2

    * refactor: replace RwLock with AtomicBool for UI readiness and implement event-driven monitoring

    * refactor: convert async functions to synchronous for window management

    * Update src-tauri/src/utils/resolve/window.rs

    * fix: handle missing app_handle in create_window function

    * Update src-tauri/src/module/lightweight.rs

* format
2025-08-31 14:20:57 +08:00
wonfen 3e674b186f fix: refine release workflow 2025-08-31 08:49:30 +08:00
223 changed files with 7298 additions and 7833 deletions
+1
View File
@@ -1,3 +1,4 @@
blank_issues_enabled: false
contact_links:
- name: 讨论交流 / Communication
url: https://t.me/clash_verge_rev
+66 -23
View File
@@ -3,12 +3,14 @@ name: Auto Build
on:
workflow_dispatch:
schedule:
# UTC+8 0,6,12,18
- cron: "0 16,22,4,10 * * *"
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
- cron: "0 4,10 * * *"
permissions: write-all
env:
TAG_NAME: autobuild
TAG_CHANNEL: AutoBuild
PRE_TAG_NAME: pre-autobuild
PRE_TAG_CHANNEL: PreAutoBuild
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
@@ -71,7 +73,7 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
VERSION=$(jq -r .version package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/${{ env.PRE_TAG_NAME }}" >> $GITHUB_ENV
shell: bash
- run: |
@@ -99,7 +101,7 @@ jobs:
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
@@ -113,11 +115,11 @@ jobs:
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
@@ -130,7 +132,7 @@ jobs:
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
with:
tag_name: autobuild
tag_name: pre-autobuild
dry_run: false
autobuild-x86-windows-macos-linux:
@@ -201,7 +203,7 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Tauri build for Windows-macOS-Linux
@@ -218,14 +220,13 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tagName: ${{ env.PRE_TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
autobuild-arm-linux:
name: Autobuild ARM Linux
@@ -276,7 +277,7 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Setup for linux
@@ -351,11 +352,11 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: |
@@ -408,7 +409,7 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Download WebView2 Runtime
@@ -429,7 +430,6 @@ jobs:
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
- name: Rename
run: |
@@ -451,20 +451,62 @@ jobs:
Rename-Item $file.FullName $newName
}
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.PRE_TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
promote-autobuild:
name: Promote PreAutoBuild to AutoBuild
runs-on: ubuntu-latest
needs:
[
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
]
if: >
${{
needs.autobuild-x86-windows-macos-linux.result == 'success' &&
needs.autobuild-arm-linux.result == 'success' &&
needs.autobuild-x86-arm-windows_webview2.result == 'success'
}}
steps:
- name: Install gh CLI
run: |
sudo apt-get update
sudo apt-get install -y gh jq
- name: Download PreAutoBuild Release Assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir pre-assets
release_id=$(gh api repos/${{ github.repository }}/releases/tags/${{ env.PRE_TAG_NAME }} | jq .id)
gh release download ${{ env.PRE_TAG_NAME }} --dir pre-assets
body=$(gh api repos/${{ github.repository }}/releases/$release_id | jq -r .body)
echo "$body" > release.txt
- name: Publish to autobuild tag
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: pre-assets/*
notify-telegram:
name: Notify Telegram
runs-on: ubuntu-latest
@@ -474,6 +516,7 @@ jobs:
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
promote-autobuild,
]
steps:
- name: Checkout repository
@@ -547,7 +590,7 @@ jobs:
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
@@ -104,8 +104,8 @@ jobs:
# Generate autobuild version using autobuild-latest format
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
MONTH=$(date +%m)
DAY=$(date +%d)
MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"
+2 -2
View File
@@ -88,8 +88,8 @@ jobs:
# Generate autobuild version for consistency
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
MONTH=$(date +%m)
DAY=$(date +%d)
MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"
+10
View File
@@ -13,6 +13,11 @@ on:
required: false
type: boolean
default: true
run_windows_arm64:
description: "运行 Windows ARM64"
required: false
type: boolean
default: true
permissions: write-all
env:
@@ -40,6 +45,11 @@ jobs:
bundle: dmg
id: macos-aarch64
input: run_macos_aarch64
- os: windows-latest
target: aarch64-pc-windows-msvc
bundle: nsis
id: windows-arm64
input: run_windows_arm64
runs-on: ${{ matrix.os }}
steps:
+6 -6
View File
@@ -122,10 +122,10 @@ jobs:
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -455,7 +455,7 @@ jobs:
release-update:
name: Release Update
runs-on: ubuntu-latest
needs: [release, release-for-linux-arm]
needs: [update_tag]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -480,7 +480,7 @@ jobs:
release-update-for-fixed-webview2:
runs-on: ubuntu-latest
needs: [release-for-fixed-webview2]
needs: [update_tag]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -605,10 +605,10 @@ jobs:
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
+1
View File
@@ -10,3 +10,4 @@ scripts/_env.sh
.tool-versions
.idea
.old
.eslintcache
+5 -2
View File
@@ -59,8 +59,9 @@ You have two options for downloading the clash binary:
```shell
pnpm run prebuild
# Use '--force' to force update to the latest version
# pnpm run prebuild --force
# Use '--force' or '-f' to update both the Mihomo core version
# and the Clash Verge Rev service version to the latest available.
pnpm run prebuild --force
```
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
@@ -73,6 +74,8 @@ To run the development server, use the following command:
pnpm dev
# If an app instance already exists, use a different command
pnpm dev:diff
# To using tauri built-in dev tool
pnpm dev:tauri
```
### Build the Project
+5 -5
View File
@@ -23,11 +23,11 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
#### 我应当怎样选择发行版
| 版本 | 特征 | 链接 |
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
| 版本 | 特征 | 链接 |
| :---------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha(废弃) | 测试发布流程。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | 滚动更新版,适合测试反馈,可能存在缺陷。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
+48
View File
@@ -1,3 +1,51 @@
## v2.4.3
### 🚀 优化改进
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
- 重构日志系统,单个日志默认最大 10 MB
- 优化前端资源占用
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1
### 🏆 重大改进
+97 -7
View File
@@ -1,16 +1,106 @@
import js from "@eslint/js";
import configPrettier from "eslint-config-prettier";
import pluginImport from "eslint-plugin-import";
import pluginPrettier from "eslint-plugin-prettier";
import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReactRefresh from "eslint-plugin-react-refresh";
import pluginUnusedImports from "eslint-plugin-unused-imports";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
plugins: {
js,
react: pluginReact,
"react-hooks": pluginReactHooks,
import: pluginImport,
"react-refresh": pluginReactRefresh,
"unused-imports": pluginUnusedImports,
prettier: pluginPrettier,
},
extends: [
"js/recommended",
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
pluginReact.configs.flat["jsx-runtime"],
configPrettier,
],
languageOptions: {
globals: globals.browser,
},
settings: {
react: {
version: "detect",
},
"import/resolver": {
typescript: {
project: "./tsconfig.json",
},
},
},
rules: {
// React
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
// TypeScript
"@typescript-eslint/no-explicit-any": "off",
// unused-imports 代替 no-unused-vars
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_+$",
args: "after-used",
argsIgnorePattern: "^_+$",
},
],
// Import
"import/no-unresolved": "error",
"import/order": [
"warn",
{
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
// 其他常见
"prefer-const": "warn",
"no-case-declarations": "error",
"no-fallthrough": "error",
"no-empty": ["warn", { allowEmptyCatch: true }],
// Prettier 格式化问题
"prettier/prettier": "warn",
},
},
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
]);
+39 -32
View File
@@ -1,11 +1,12 @@
{
"name": "clash-verge",
"version": "2.4.1",
"version": "2.4.3",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=1 RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
"dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
"dev:tauri": "cross-env RUST_BACKTRACE=full tauri dev -f tauri-dev",
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
"tauri": "tauri",
@@ -24,7 +25,7 @@
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint src",
"lint": "eslint -c eslint.config.ts --cache src",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
@@ -35,30 +36,28 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.3.1",
"@mui/lab": "7.0.0-beta.16",
"@mui/material": "^7.3.1",
"@mui/x-data-grid": "^8.11.0",
"@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.2",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-notification": "^2.3.1",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.4",
"axios": "^1.11.0",
"cli-color": "^2.0.4",
"dayjs": "1.11.16",
"ahooks": "^3.9.5",
"axios": "^1.12.2",
"dayjs": "1.11.18",
"foxact": "^0.2.49",
"glob": "^11.0.3",
"i18next": "^25.4.2",
"i18next": "^25.5.2",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",
"monaco-editor": "^0.53.0",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5",
"react": "19.1.1",
@@ -68,40 +67,48 @@
"react-i18next": "15.7.3",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.8.2",
"react-router-dom": "7.9.1",
"react-virtuoso": "^4.14.0",
"sockette": "^2.0.6",
"swr": "^2.3.6",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.8"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint/js": "^9.34.0",
"@tauri-apps/cli": "2.8.3",
"@eslint/js": "^9.35.0",
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.12",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.2",
"adm-zip": "^0.5.16",
"commander": "^14.0.0",
"cli-color": "^2.0.4",
"commander": "^14.0.1",
"cross-env": "^10.0.0",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-unused-imports": "^4.2.0",
"glob": "^11.0.3",
"globals": "^16.4.0",
"https-proxy-agent": "^7.0.6",
"jiti": "^2.5.1",
"meta-json-schema": "^1.19.12",
"meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2",
"prettier": "^3.6.2",
"sass": "^1.91.0",
"terser": "^5.43.1",
"sass": "^1.92.1",
"tar": "^7.4.3",
"terser": "^5.44.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.41.0",
"vite": "^7.1.3",
"typescript-eslint": "^8.44.0",
"vite": "^7.1.5",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.0"
},
+947 -2254
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -12,7 +12,7 @@ import { glob } from "glob";
const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
const FORCE = process.argv.includes("--force");
const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32",
@@ -43,7 +43,8 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
const target = arg1 === "--force" ? arg2 : arg1;
let target;
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
@@ -64,7 +65,7 @@ const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v2",
"darwin-x64": "mihomo-darwin-amd64-v1",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
+15 -5
View File
@@ -29,10 +29,10 @@
* Errors are logged and the process exits with code 1 on failure.
*/
import { execSync } from "child_process";
import { program } from "commander";
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
import { execSync } from "child_process";
/**
* 获取当前 git 短 commit hash
@@ -68,14 +68,24 @@ function getLatestTauriCommit() {
}
/**
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
* 使用 Asia/Shanghai 时区
* @param {boolean} withCommit 是否带 commit
* @returns {string}
*/
function generateShortTimestamp(withCommit = false) {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const formatter = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
});
const parts = formatter.formatToParts(now);
const month = parts.find((part) => part.type === "month").value;
const day = parts.find((part) => part.type === "day").value;
if (withCommit) {
const gitShort = getGitShortCommit();
return `${month}${day}.${gitShort}`;
+1 -1
View File
@@ -75,7 +75,7 @@ async function sendTelegramNotification() {
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
const encodedVersion = encodeURIComponent(version);
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
// 发送到 Telegram
try {
+596 -514
View File
File diff suppressed because it is too large Load Diff
+40 -29
View File
@@ -1,33 +1,32 @@
[package]
name = "clash-verge"
version = "2.4.1"
version = "2.4.3"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
edition = "2021"
edition = "2024"
build = "build.rs"
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.4.0", features = [] }
tauri-build = { version = "2.4.1", features = [] }
[dependencies]
warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.99"
dirs = "6.0"
open = "5.3.2"
log = "0.4.27"
log = "0.4.28"
dunce = "1.0.5"
log4rs = "1.3.0"
nanoid = "0.4"
chrono = "0.4.41"
chrono = "0.4.42"
sysinfo = { version = "0.37.0", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.143"
serde_json = "1.0.145"
serde_yaml_ng = "0.10.0"
once_cell = "1.21.3"
port_scanner = "0.1.5"
@@ -40,11 +39,11 @@ tokio = { version = "1.47.1", features = [
"time",
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
serde = { version = "1.0.225", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.8.4", features = [
tauri = { version = "2.8.5", features = [
"protocol-asset",
"devtools",
"tray-icon",
@@ -53,14 +52,13 @@ tauri = { version = "2.8.4", features = [
] }
network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.1"
tauri-plugin-dialog = "2.3.3"
tauri-plugin-dialog = "2.4.0"
tauri-plugin-fs = "2.4.2"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.2"
tauri-plugin-devtools = "2.0.1"
tauri-plugin-deep-link = "2.4.3"
tauri-plugin-window-state = "2.4.0"
zip = "4.5.0"
zip = "5.1.1"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
@@ -73,15 +71,21 @@ hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
kode-bridge = "0.2.1-rc2"
kode-bridge = "0.2.1"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1"
console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
"parking_lot",
] }
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2"
flexi_logger = "0.31.2"
cfg-if = "1.0.3"
nu-ansi-term = { version = "0.50.1", optional = true }
console-subscriber = { version = "0.4.1", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
[target.'cfg(windows)'.dependencies]
@@ -113,31 +117,38 @@ tauri-plugin-updater = "2.9.0"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = []
verge-dev = ["nu-ansi-term"]
tauri-dev = []
tokio-trace = ["console-subscriber"]
[profile.release]
panic = "abort"
codegen-units = 1
codegen-units = 16
lto = true
opt-level = "s"
opt-level = 2
debug = false
strip = true
overflow-checks = false
rpath = false
[profile.dev]
incremental = true
codegen-units = 256 # 增加编译单元,提升编译速度
opt-level = 0 # 禁用优化,进一步提升编译速度
debug = true # 保留调试信息
strip = false # 不剥离符号,保留调试信息
codegen-units = 64
opt-level = 0
debug = true
strip = "none"
overflow-checks = true
lto = false
rpath = false
[profile.fast-release]
inherits = "release" # 继承 release 的配置
panic = "abort" # 与 release 相同
codegen-units = 256 # 增加编译单元,提升编译速度
lto = false # 禁用 LTO,提升编译速度
opt-level = 0 # 禁用优化,大幅提升编译速度
debug = true # 保留调试信息
strip = false # 不剥离符号,保留调试信息
inherits = "release"
incremental = true
codegen-units = 64
lto = false
opt-level = 0
debug = true
strip = false
[lib]
name = "app_lib"
+1 -1
View File
@@ -1,4 +1,4 @@
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;
// 业务模型 & Draft
+7 -1
View File
@@ -18,6 +18,12 @@
"autostart:allow-disable",
"autostart:allow-is-enabled",
"core:window:allow-set-theme",
"notification:default"
"notification:default",
"http:default",
"http:allow-fetch",
{
"identifier": "http:default",
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+40 -13
View File
@@ -1,22 +1,25 @@
use crate::singleton;
use anyhow::Result;
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::OnceCell;
pub struct CacheEntry {
pub value: Arc<Value>,
pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250);
pub struct CacheEntry<T> {
pub value: Arc<T>,
pub expires_at: Instant,
}
pub struct ProxyRequestCache {
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
pub struct Cache<T> {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry<T>>>>>,
}
impl ProxyRequestCache {
impl<T> Cache<T> {
fn new() -> Self {
ProxyRequestCache {
Cache {
map: DashMap::new(),
}
}
@@ -25,10 +28,11 @@ impl ProxyRequestCache {
format!("{prefix}:{id}")
}
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<T>
where
F: Fn() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Value> + Send + 'static,
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
T: Send + Sync + 'static,
{
loop {
let now = Instant::now();
@@ -54,10 +58,10 @@ impl ProxyRequestCache {
// Try to set a new value
let value = fetch_fn().await;
let entry = CacheEntry {
let entry = Box::new(CacheEntry {
value: Arc::new(value),
expires_at: Instant::now() + ttl,
};
});
match cell.set(entry) {
Ok(_) => {
@@ -78,7 +82,30 @@ impl ProxyRequestCache {
}
}
}
// pub fn clean_key(&self, key: &str) {
// self.map.remove(key);
// }
// TODO
pub fn clean_default_keys(&self) {
// logging!(info, Type::Cache, "Cleaning proxies keys");
// let proxies_key = Self::make_key("proxies", "default");
// self.map.remove(&proxies_key);
// logging!(info, Type::Cache, "Cleaning providers keys");
// let providers_key = Self::make_key("providers", "default");
// self.map.remove(&providers_key);
// !The frontend goes crash if we clean the clash_config cache
// logging!(info, Type::Cache, "Cleaning clash config keys");
// let clash_config_key = Self::make_key("clash_config", "default");
// self.map.remove(&clash_config_key);
}
}
// Use singleton macro
singleton!(ProxyRequestCache, INSTANCE);
pub type CacheService = Cache<Result<String>>;
pub type CacheProxy = Cache<Value>;
singleton!(Cache<Value>, PROXY_INSTANCE);
singleton!(Cache<Result<String>>, SERVICE_INSTANCE);
-8
View File
@@ -235,11 +235,3 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
Ok(())
}
/// 重置UI就绪状态
#[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> {
log::info!(target: "app", "重置UI就绪状态");
crate::utils::resolve::ui::reset_ui_ready();
Ok(())
}
+7 -7
View File
@@ -1,14 +1,14 @@
use super::CmdResult;
use crate::{
cache::CacheProxy,
config::Config,
core::{handle, CoreManager},
core::{CoreManager, handle},
};
use crate::{
config::*,
feat,
ipc::{self, IpcManager},
logging,
state::proxy::ProxyRequestCache,
utils::logging::Type,
wrap_err,
};
@@ -166,7 +166,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{handle, CoreManager},
core::{CoreManager, handle},
utils::dirs,
};
@@ -317,8 +317,8 @@ pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
#[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| {
@@ -333,8 +333,8 @@ pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
/// 强制刷新Clash配置缓存
#[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
cache.map.remove(&key);
get_clash_config().await
}
+26 -30
View File
@@ -618,20 +618,20 @@ async fn check_netflix(client: &Client) -> UnlockItem {
{
Ok(response) => {
// 检查重定向位置
if let Some(location) = response.headers().get("location") {
if let Ok(location_str) = location.to_str() {
// 解析位置获取区域
let parts: Vec<&str> = location_str.split('/').collect();
if parts.len() >= 4 {
let region_code = parts[3].split('-').next().unwrap_or("unknown");
let emoji = country_code_to_emoji(region_code);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{region_code}")),
check_time: Some(get_local_date_string()),
};
}
if let Some(location) = response.headers().get("location")
&& let Ok(location_str) = location.to_str()
{
// 解析位置获取区域
let parts: Vec<&str> = location_str.split('/').collect();
if parts.len() >= 4 {
let region_code = parts[3].split('-').next().unwrap_or("unknown");
let emoji = country_code_to_emoji(region_code);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{region_code}")),
check_time: Some(get_local_date_string()),
};
}
}
// 如果没有重定向,假设是美国
@@ -691,22 +691,18 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
match response.json::<serde_json::Value>().await {
Ok(data) => {
// 尝试从数据中提取区域信息
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) {
if !targets.is_empty() {
if let Some(location) = targets[0].get("location") {
if let Some(country) =
location.get("country").and_then(|c| c.as_str())
{
let emoji = country_code_to_emoji(country);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{country}")),
check_time: Some(get_local_date_string()),
};
}
}
}
if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
&& !targets.is_empty()
&& let Some(location) = targets[0].get("location")
&& let Some(country) = location.get("country").and_then(|c| c.as_str())
{
let emoji = country_code_to_emoji(country);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{country}")),
check_time: Some(get_local_date_string()),
};
}
// 如果无法解析区域信息
+1 -1
View File
@@ -1,5 +1,5 @@
use super::CmdResult;
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
use crate::process::AsyncHandler;
use crate::wrap_err;
use network_interface::NetworkInterface;
+95 -96
View File
@@ -1,13 +1,14 @@
use super::CmdResult;
use crate::{
config::{
Config, IProfiles, PrfItem, PrfOption,
profiles::{
profiles_append_item_with_filedata_safe, profiles_delete_item_safe,
profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe,
},
profiles_append_item_safe, Config, IProfiles, PrfItem, PrfOption,
profiles_append_item_safe,
},
core::{handle, timer::Timer, tray::Tray, CoreManager},
core::{CoreManager, handle, timer::Timer, tray::Tray},
feat, logging,
process::AsyncHandler,
ret_err,
@@ -290,109 +291,106 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref() {
if current_profile.as_ref() != Some(new_profile) {
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
if let Some(new_profile) = profiles.current.as_ref()
&& current_profile.as_ref() != Some(new_profile)
{
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
path.ok()
} else {
None
}
}
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
path.ok()
} else {
None
}
}
};
// 如果获取到文件路径,检查YAML语法
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Ok(false);
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
None
}
}
};
// 超时保护
let file_read_result = tokio::time::timeout(
Duration::from_secs(5),
tokio::fs::read_to_string(&file_path),
)
.await;
// 如果获取到文件路径,检查YAML语法
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Ok(false);
}
match file_read_result {
Ok(Ok(content)) => {
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
})
.await;
// 超时保护
let file_read_result = tokio::time::timeout(
Duration::from_secs(5),
tokio::fs::read_to_string(&file_path),
)
.await;
match yaml_parse_result {
Ok(Ok(_)) => {
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
true,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
&error_msg,
);
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
&error_msg,
);
return Ok(false);
}
match file_read_result {
Ok(Ok(content)) => {
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
})
.await;
match yaml_parse_result {
Ok(Ok(_)) => {
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
true,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
&error_msg,
);
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
&error_msg,
);
return Ok(false);
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
&error_msg,
);
return Ok(false);
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
&error_msg,
);
return Ok(false);
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message("config_validate::file_read_error", &error_msg);
return Ok(false);
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
&error_msg,
);
return Ok(false);
}
}
}
@@ -663,8 +661,9 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command]
pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let file = {
wrap_err!(profiles.latest_ref().get_item(&index))?
wrap_err!(profiles_ref.get_item(&index))?
.file
.clone()
.ok_or("the file field is null")
+85 -9
View File
@@ -1,5 +1,13 @@
use tauri::Emitter;
use super::CmdResult;
use crate::{ipc::IpcManager, logging, state::proxy::ProxyRequestCache, utils::logging::Type};
use crate::{
cache::CacheProxy,
core::{handle::Handle, tray::Tray},
ipc::IpcManager,
logging,
utils::logging::Type,
};
use std::time::Duration;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
@@ -7,11 +15,11 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
@@ -24,19 +32,19 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
/// 强制刷新代理缓存用于profile切换
#[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key);
get_proxies().await
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("providers", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("providers", "default");
let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_providers_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
@@ -45,3 +53,71 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
.await;
Ok((*value).clone())
}
/// 同步托盘和GUI的代理选择状态
#[tauri::command]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
use crate::core::tray::Tray;
match Tray::global().update_menu().await {
Ok(_) => {
logging!(info, Type::Cmd, "Tray proxy selection synced successfully");
Ok(())
}
Err(e) => {
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}");
Err(e.to_string())
}
}
}
/// 更新代理选择并同步托盘和GUI状态
#[tauri::command]
pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
match IpcManager::global().update_proxy(&group, &proxy).await {
Ok(_) => {
// println!("Proxy updated successfully: {} -> {}", group,proxy);
logging!(
info,
Type::Cmd,
"Proxy updated successfully: {} -> {}",
group,
proxy
);
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key);
if let Err(e) = Tray::global().update_menu().await {
logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e);
}
if let Some(app_handle) = Handle::global().app_handle() {
let _ = app_handle.emit("verge://force-refresh-proxies", ());
let _ = app_handle.emit("verge://refresh-proxy-config", ());
}
logging!(
info,
Type::Cmd,
"Proxy and sync completed successfully: {} -> {}",
group,
proxy
);
Ok(())
}
Err(e) => {
println!("1111111111111111");
logging!(
error,
Type::Cmd,
"Failed to update proxy: {} -> {}, error: {}",
group,
proxy,
e
);
Err(e.to_string())
}
}
}
+95 -6
View File
@@ -1,5 +1,5 @@
use super::CmdResult;
use crate::{config::*, wrap_err};
use crate::{config::*, core::CoreManager, log_err, wrap_err};
use anyhow::Context;
use serde_yaml_ng::Mapping;
use std::collections::HashMap;
@@ -15,12 +15,14 @@ pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let config = runtime.config.as_ref();
wrap_err!(config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
.and_then(
|config| serde_yaml_ng::to_string(config).context("failed to convert config to yaml")
))
wrap_err!(
config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
.and_then(|config| serde_yaml_ng::to_string(config)
.context("failed to convert config to yaml"))
)
}
/// 获取运行时存在的键
@@ -34,3 +36,90 @@ pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
}
/// 读取运行时链式代理配置
#[tauri::command]
pub async fn get_runtime_proxy_chain_config() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let config = wrap_err!(
runtime
.config
.as_ref()
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
)?;
if let (
Some(serde_yaml_ng::Value::Sequence(proxies)),
Some(serde_yaml_ng::Value::Sequence(proxy_groups)),
) = (config.get("proxies"), config.get("proxy-groups"))
{
let mut proxy_name = None;
let mut proxies_chain = Vec::new();
let proxy_chain_groups = proxy_groups
.iter()
.filter_map(
|proxy_group| match proxy_group.get("name").and_then(|n| n.as_str()) {
Some("proxy_chain") => {
if let Some(serde_yaml_ng::Value::Sequence(ps)) = proxy_group.get("proxies")
&& let Some(x) = ps.first()
{
proxy_name = Some(x); //插入出口节点名字
}
Some(proxy_group.to_owned())
}
_ => None,
},
)
.collect::<Vec<serde_yaml_ng::Value>>();
while let Some(proxy) = proxies.iter().find(|proxy| {
if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy {
proxy_map.get("name") == proxy_name && proxy_map.get("dialer-proxy").is_some()
} else {
false
}
}) {
proxies_chain.push(proxy.to_owned());
proxy_name = proxy.get("dialer-proxy");
}
if let Some(entry_proxy) = proxies.iter().find(|proxy| proxy.get("name") == proxy_name) {
proxies_chain.push(entry_proxy.to_owned());
}
proxies_chain.reverse();
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
config.insert("proxies".to_string(), proxies_chain);
config.insert("proxy-groups".to_string(), proxy_chain_groups);
wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed"))
} else {
wrap_err!(Err(anyhow::anyhow!(
"failed to get proxies or proxy-groups".to_string()
)))
}
}
/// 更新运行时链式代理配置
#[tauri::command]
pub async fn update_proxy_chain_config_in_runtime(
proxy_chain_config: Option<serde_yaml_ng::Value>,
) -> CmdResult<()> {
{
let runtime = Config::runtime().await;
let mut draft = runtime.draft_mut();
draft.update_proxy_chain_config(proxy_chain_config);
drop(draft);
runtime.apply();
}
// 生成新的运行配置文件并通知 Clash 核心重新加载
let run_path = wrap_err!(Config::generate_file(ConfigType::Run).await)?;
log_err!(CoreManager::global().put_configs_force(run_path).await);
Ok(())
}
+18 -16
View File
@@ -1,45 +1,47 @@
use super::CmdResult;
use crate::{
core::{service, CoreManager},
core::{
CoreManager,
service::{self, SERVICE_MANAGER, ServiceStatus},
},
utils::i18n::t,
};
use anyhow::Result;
async fn execute_service_operation_sync<F, Fut, E>(service_op: F, op_type: &str) -> CmdResult
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<(), E>>,
E: ToString + std::fmt::Debug,
{
if let Err(e) = service_op().await {
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string());
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
if let Err(e) = SERVICE_MANAGER
.lock()
.await
.handle_service_status(&status)
.await
{
let emsg = format!("{} Service failed: {}", op_type, e);
return Err(t(emsg.as_str()).await);
}
if CoreManager::global().restart_core().await.is_err() {
let emsg = format!("{} {} failed", "Restart", "Core");
return Err(t(emsg.as_str()).await);
let emsg = "Restart Core failed";
return Err(t(emsg).await);
}
Ok(())
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
execute_service_operation_sync(service::install_service, "Install").await
execute_service_operation_sync(ServiceStatus::InstallRequired, "Install").await
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
execute_service_operation_sync(service::uninstall_service, "Uninstall").await
execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await
}
#[tauri::command]
pub async fn reinstall_service() -> CmdResult {
execute_service_operation_sync(service::reinstall_service, "Reinstall").await
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "Reinstall").await
}
#[tauri::command]
pub async fn repair_service() -> CmdResult {
execute_service_operation_sync(service::force_reinstall_service, "Repair").await
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "Repair").await
}
#[tauri::command]
+1 -1
View File
@@ -1,6 +1,6 @@
use super::CmdResult;
use crate::{
core::{handle, CoreManager},
core::{CoreManager, handle},
logging,
module::sysinfo::PlatformSpecification,
utils::logging::Type,
+8 -8
View File
@@ -25,17 +25,17 @@ impl IClashTemp {
match map_result {
Ok(mut map) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
if let Some(value) = template.0.get(key) {
map.insert(key.clone(), value.clone());
}
if !map.contains_key(key)
&& let Some(value) = template.0.get(key)
{
map.insert(key.clone(), value.clone());
}
});
// 确保 secret 字段存在且不为空
if let Some(Value::String(s)) = map.get_mut("secret") {
if s.is_empty() {
*s = "set-your-secret".to_string();
}
if let Some(Value::String(s)) = map.get_mut("secret")
&& s.is_empty()
{
*s = "set-your-secret".to_string();
}
Self(Self::guard(map))
}
+7 -5
View File
@@ -1,14 +1,14 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
config::{profiles_append_item_safe, PrfItem},
core::{handle, CoreManager},
config::{PrfItem, profiles_append_item_safe},
core::{CoreManager, handle},
enhance, logging,
utils::{dirs, help, logging::Type},
};
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use std::path::PathBuf;
use tokio::sync::OnceCell;
use tokio::time::{sleep, Duration};
use tokio::time::{Duration, sleep};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@@ -101,7 +101,9 @@ impl Config {
Some(("config_validate::boot_error", error_msg))
} else {
logging!(info, Type::Config, true, "配置验证成功");
Some(("config_validate::success", String::new()))
// 前端没有必要知道验证成功的消息,也没有事件驱动
// Some(("config_validate::success", String::new()))
None
}
}
Err(err) => {
+2 -2
View File
@@ -1,9 +1,9 @@
use crate::utils::dirs::get_encryption_key;
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
aead::{Aead, KeyInit},
};
use base64::{engine::general_purpose::STANDARD, Engine};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const NONCE_LENGTH: usize = 12;
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::utils::{
network::{NetworkManager, ProxyType},
tmpl,
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use std::{fs, time::Duration};
+162 -168
View File
@@ -1,10 +1,10 @@
use super::{prfitem::PrfItem, PrfOption};
use super::{PrfOption, prfitem::PrfItem};
use crate::{
logging_error,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use std::collections::HashSet;
@@ -88,12 +88,12 @@ impl IProfiles {
self.items = Some(vec![]);
}
if let Some(current) = patch.current {
if let Some(items) = self.items.as_ref() {
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
if let Some(current) = patch.current
&& let Some(items) = self.items.as_ref()
{
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
}
@@ -287,24 +287,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the merge index
for (i, _) in items.iter().enumerate() {
@@ -313,24 +313,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = merge_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = merge_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the script index
for (i, _) in items.iter().enumerate() {
@@ -339,24 +339,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = script_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = script_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the rules index
for (i, _) in items.iter().enumerate() {
@@ -365,24 +365,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = rules_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = rules_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the proxies index
for (i, _) in items.iter().enumerate() {
@@ -391,24 +391,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = proxies_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = proxies_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the groups index
for (i, _) in items.iter().enumerate() {
@@ -417,24 +417,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = groups_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = groups_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// delete the original uid
if current == uid {
@@ -595,25 +595,25 @@ impl IProfiles {
total_files += 1;
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
&& Self::is_profile_file(file_name)
{
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
// 检查是否为活跃文件
if !active_files.contains(file_name) {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
// 检查是否为活跃文件
if !active_files.contains(file_name) {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
}
}
@@ -659,50 +659,44 @@ impl IProfiles {
}
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
if let Some(itype) = &item.itype {
if itype == "remote" || itype == "local" {
if let Some(option) = &item.option {
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge {
if let Ok(merge_item) = self.get_item(merge_uid) {
if let Some(file) = &merge_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(itype) = &item.itype
&& (itype == "remote" || itype == "local")
&& let Some(option) = &item.option
{
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge
&& let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file
{
active_files.insert(file.clone());
}
if let Some(script_uid) = &option.script {
if let Ok(script_item) = self.get_item(script_uid) {
if let Some(file) = &script_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file
{
active_files.insert(file.clone());
}
if let Some(rules_uid) = &option.rules {
if let Ok(rules_item) = self.get_item(rules_uid) {
if let Some(file) = &rules_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file
{
active_files.insert(file.clone());
}
if let Some(proxies_uid) = &option.proxies {
if let Ok(proxies_item) = self.get_item(proxies_uid) {
if let Some(file) = &proxies_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file
{
active_files.insert(file.clone());
}
if let Some(groups_uid) = &option.groups {
if let Ok(groups_item) = self.get_item(groups_uid) {
if let Some(file) = &groups_item.file {
active_files.insert(file.clone());
}
}
}
}
if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file
{
active_files.insert(file.clone());
}
}
}
+135
View File
@@ -46,4 +46,139 @@ impl IRuntime {
}
}
}
//跟新链式代理配置文件
/// {
/// "proxies":[
/// {
/// name : 入口节点,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// },
/// {
/// name : hop_node_1_xxxx,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// dialer-proxy : "入口节点"
/// },
/// {
/// name : 出口节点,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// dialer-proxy : "hop_node_1_xxxx"
/// }
/// ],
/// "proxy-groups" : [
/// {
/// name : "proxy_chain",
/// type: "select",
/// proxies ["出口节点"]
/// }
/// ]
/// }
///
/// 传入none 为删除
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
if let Some(config) = self.config.as_mut() {
// 获取 默认第一的代理组的名字
let proxy_group_name =
if let Some(Value::Sequence(proxy_groups)) = config.get("proxy-groups") {
if let Some(Value::Mapping(proxy_group)) = proxy_groups.first() {
if let Some(Value::String(proxy_group_name)) = proxy_group.get("name") {
proxy_group_name.to_string()
} else {
"".to_string()
}
} else {
"".to_string()
}
} else {
"".to_string()
};
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
proxies.iter_mut().for_each(|proxy| {
if let Some(proxy) = proxy.as_mapping_mut()
&& proxy.get("dialer-proxy").is_some()
{
proxy.remove("dialer-proxy");
}
});
}
// 清除proxy_chain代理组
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
proxy_groups.retain(|proxy_group| {
!matches!(proxy_group.get("name").and_then(|n| n.as_str()), Some(name) if name== "proxy_chain")
});
}
// 清除rules
if let Some(Value::Sequence(rules)) = config.get_mut("rules") {
rules.retain(|rule| rule.as_str() != Some("MATCH,proxy_chain"));
rules.push(Value::String(format!("MATCH,{}", proxy_group_name)));
}
// some 则写入新配置
// 给proxy添加dialer-proxy字段
// 第一个proxy不添加dialer-proxy
// 然后第二个开始,dialer-proxy为上一个元素的name
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config {
let mut proxy_chain_group = Mapping::new();
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
if let Some(Value::Mapping(proxy)) = proxies
.iter_mut()
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
{
if i != 0
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
{
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
}
if i == dialer_proxies.len() - 1 {
// 添加proxy-groups
proxy_chain_group
.insert("name".into(), Value::String("proxy_chain".into()));
proxy_chain_group
.insert("type".into(), Value::String("select".into()));
proxy_chain_group.insert(
"proxies".into(),
Value::Sequence(vec![dialer_proxy.to_owned()]),
);
}
}
}
}
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
proxy_groups.push(Value::Mapping(proxy_chain_group));
}
// 添加rules
if let Some(Value::Sequence(rules)) = config.get_mut("rules")
&& let Ok(rule) = serde_yaml_ng::to_value("MATCH,proxy_chain")
{
rules.retain(|rule| {
rule.as_str() != Some(&format!("MATCH,{}", proxy_group_name))
});
rules.push(rule);
// *rules = vec![rule];
}
}
}
}
}
+1 -8
View File
@@ -1,5 +1,5 @@
use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
logging,
utils::{dirs, help, i18n, logging::Type},
};
@@ -203,9 +203,6 @@ pub struct IVerge {
/// 启用外部控制器
pub enable_external_controller: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -407,7 +404,6 @@ impl IVerge {
auto_light_weight_minutes: Some(10),
enable_dns_settings: Some(false),
home_cards: None,
service_state: None,
enable_external_controller: Some(false),
..Self::default()
}
@@ -495,7 +491,6 @@ impl IVerge {
patch!(auto_light_weight_minutes);
patch!(enable_dns_settings);
patch!(home_cards);
patch!(service_state);
patch!(enable_external_controller);
}
@@ -592,7 +587,6 @@ pub struct IVergeResponse {
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub enable_external_controller: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
}
impl From<IVerge> for IVergeResponse {
@@ -664,7 +658,6 @@ impl From<IVerge> for IVergeResponse {
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
enable_external_controller: verge.enable_external_controller,
service_state: verge.service_state,
}
}
}
+81 -80
View File
@@ -2,7 +2,7 @@
use crate::process::AsyncHandler;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::time::{timeout, Duration};
use tokio::time::{Duration, timeout};
#[cfg(target_os = "linux")]
use anyhow::anyhow;
@@ -87,7 +87,7 @@ impl AsyncProxyQuery {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -209,13 +209,13 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和GNOME设置
// 首先检查环境变量
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
if !auto_proxy.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: auto_proxy,
});
}
if let Ok(auto_proxy) = std::env::var("auto_proxy")
&& !auto_proxy.is_empty()
{
return Ok(AsyncAutoproxy {
enable: true,
url: auto_proxy,
});
}
// 尝试使用 gsettings 获取 GNOME 代理设置
@@ -224,31 +224,31 @@ impl AsyncProxyQuery {
.output()
.await;
if let Ok(output) = output {
if output.status.success() {
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
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(output) = output
&& output.status.success()
{
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
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 {
if pac_output.status.success() {
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if let Ok(pac_output) = pac_output
&& pac_output.status.success()
{
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if !pac_url.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
});
}
}
if !pac_url.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
});
}
}
}
@@ -271,7 +271,7 @@ impl AsyncProxyQuery {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -402,10 +402,10 @@ impl AsyncProxyQuery {
http_host = host_part.trim().to_string();
}
} else if line.contains("HTTPPort") {
if let Some(port_part) = line.split(':').nth(1) {
if let Ok(port) = port_part.trim().parse::<u16>() {
http_port = port;
}
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") {
// 解析异常列表
@@ -431,16 +431,16 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和桌面环境设置
// 首先检查环境变量
if let Ok(http_proxy) = std::env::var("http_proxy") {
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
return Ok(proxy_info);
}
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") {
if let Ok(proxy_info) = Self::parse_proxy_url(&https_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 代理设置
@@ -449,45 +449,46 @@ impl AsyncProxyQuery {
.output()
.await;
if let Ok(mode_output) = mode_output {
if mode_output.status.success() {
let mode = String::from_utf8_lossy(&mode_output.stdout)
.trim()
.to_string();
if mode.contains("manual") {
// 获取HTTP代理设置
let host_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "host"])
.output()
.await;
if let Ok(mode_output) = mode_output
&& mode_output.status.success()
{
let mode = String::from_utf8_lossy(&mode_output.stdout)
.trim()
.to_string();
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;
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) {
if host_output.status.success() && port_output.status.success() {
let host = String::from_utf8_lossy(&host_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result)
&& host_output.status.success()
&& port_output.status.success()
{
let host = String::from_utf8_lossy(&host_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
let port = String::from_utf8_lossy(&port_output.stdout)
.trim()
.parse::<u16>()
.unwrap_or(8080);
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(),
});
}
}
if !host.is_empty() {
return Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: String::new(),
});
}
}
}
+71 -265
View File
@@ -1,13 +1,13 @@
#[cfg(target_os = "windows")]
use crate::AsyncHandler;
use crate::{
config::*,
core::{
handle,
service::{self},
service::{self, SERVICE_MANAGER, ServiceStatus},
},
ipc::IpcManager,
logging, logging_error,
process::AsyncHandler,
singleton_lazy,
logging, logging_error, singleton_lazy,
utils::{
dirs,
help::{self},
@@ -19,12 +19,12 @@ use chrono::Local;
use parking_lot::Mutex;
use std::{
fmt,
fs::{create_dir_all, File},
fs::{File, create_dir_all},
io::Write,
path::PathBuf,
sync::Arc,
};
use tauri_plugin_shell::{process::CommandChild, ShellExt};
use tauri_plugin_shell::{ShellExt, process::CommandChild};
#[derive(Debug)]
pub struct CoreManager {
@@ -384,8 +384,6 @@ impl CoreManager {
return Ok((true, String::new()));
}
logging!(info, Type::Config, true, "开始更新配置");
// 1. 先生成新的配置内容
logging!(info, Type::Config, true, "生成新的配置内容");
Config::generate().await?;
@@ -393,9 +391,8 @@ impl CoreManager {
// 2. 验证配置
match self.validate_config().await {
Ok((true, _)) => {
logging!(info, Type::Config, true, "配置验证通过");
// 4. 验证通过后,生成正式的运行时配置
logging!(info, Type::Config, true, "生成运行时配置");
logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run).await?;
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
Ok((true, "something".into()))
@@ -467,18 +464,18 @@ impl CoreManager {
Ok((pids, process_name)) => {
for pid in pids {
// 跳过当前管理的进程
if let Some(current) = current_pid {
if pid == current {
logging!(
debug,
Type::Core,
true,
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
);
continue;
}
if let Some(current) = current_pid
&& pid == current
{
logging!(
debug,
Type::Core,
true,
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
);
continue;
}
pids_to_kill.push((pid, process_name.clone()));
}
@@ -527,7 +524,7 @@ impl CoreManager {
use std::mem;
use winapi::um::handleapi::CloseHandle;
use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
};
use winapi::um::winnt::HANDLE;
@@ -703,7 +700,7 @@ impl CoreManager {
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
let result = AsyncHandler::spawn_blocking(move || -> Result<bool> {
AsyncHandler::spawn_blocking(move || -> Result<bool> {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if process_handle.is_null() {
@@ -719,9 +716,7 @@ impl CoreManager {
Ok(exit_code == 259)
}
})
.await?;
result
.await?
}
#[cfg(not(windows))]
@@ -736,7 +731,8 @@ impl CoreManager {
}
async fn start_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by sidecar");
logging!(info, Type::Core, true, "Running core by sidecar");
let config_file = &Config::generate_file(ConfigType::Run).await?;
let app_handle = handle::Handle::global()
.app_handle()
@@ -765,21 +761,19 @@ impl CoreManager {
])
.spawn()?;
AsyncHandler::spawn(move || async move {
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
logging!(
error,
Type::Core,
true,
"[Sidecar] Failed to write stdout to file: {}",
e
);
}
}
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event
&& let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line))
{
logging!(
error,
Type::Core,
true,
"[Sidecar] Failed to write stdout to file: {}",
e
);
}
});
}
let pid = child.pid();
logging!(
@@ -794,7 +788,7 @@ impl CoreManager {
Ok(())
}
fn stop_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by sidecar");
logging!(info, Type::Core, true, "Stopping core by sidecar");
if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid();
@@ -814,14 +808,14 @@ impl CoreManager {
impl CoreManager {
async fn start_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by service");
logging!(info, Type::Core, true, "Running core by service");
let config_file = &Config::generate_file(ConfigType::Run).await?;
service::run_core_by_service(config_file).await?;
self.set_running_mode(RunningMode::Service);
Ok(())
}
async fn stop_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by service");
logging!(info, Type::Core, true, "Stopping core by service");
service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
@@ -837,58 +831,9 @@ impl Default for CoreManager {
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
impl CoreManager {
// 当服务安装失败时的回退逻辑
async fn attempt_service_init(&self) -> Result<()> {
if service::check_service_needs_reinstall().await {
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
if let Err(e) = service::reinstall_service().await {
logging!(
warn,
Type::Core,
true,
"服务重装失败 during attempt_service_init: {}",
e
);
return Err(e);
}
// 如果重装成功,还需要尝试启动服务
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务");
}
if let Err(e) = self.start_core_by_service().await {
logging!(
warn,
Type::Core,
true,
"通过服务启动核心失败 during attempt_service_init: {}",
e
);
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
let mut state = service::ServiceState::get().await;
if !state.prefer_sidecar {
state.prefer_sidecar = true;
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
if let Err(save_err) = state.save().await {
logging!(
error,
Type::Core,
true,
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}",
save_err
);
}
}
return Err(e);
}
Ok(())
}
pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core");
logging!(info, Type::Core, "Initializing core");
// 应用启动时先清理任何遗留的 mihomo 进程
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
@@ -901,155 +846,11 @@ impl CoreManager {
);
}
let mut core_started_successfully = false;
// 使用简化的启动流程
logging!(info, Type::Core, true, "开始核心初始化");
self.start_core().await?;
if service::is_service_available().await.is_ok() {
logging!(
info,
Type::Core,
true,
"服务当前可用或看似可用,尝试通过服务模式启动/重装"
);
match self.attempt_service_init().await {
Ok(_) => {
logging!(info, Type::Core, true, "服务模式成功启动核心");
core_started_successfully = true;
}
Err(_err) => {
logging!(
warn,
Type::Core,
true,
"服务模式启动或重装失败。将尝试Sidecar模式回退。"
);
}
}
} else {
logging!(
info,
Type::Core,
true,
"服务初始不可用 (is_service_available 调用失败)"
);
}
if !core_started_successfully {
logging!(
info,
Type::Core,
true,
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
);
let service_state = service::ServiceState::get().await;
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
);
self.start_core_by_sidecar().await?;
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
// 后续的 Tray::global().subscribe_traffic().await 仍然会执行
} else {
let has_service_install_record = service_state.last_install_time > 0;
if !has_service_install_record {
logging!(
info,
Type::Core,
true,
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
);
match service::install_service().await {
Ok(_) => {
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
let mut new_state = service::ServiceState::default();
new_state.record_install();
new_state.prefer_sidecar = false;
new_state.save().await?;
if service::is_service_available().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
if self.start_core_by_service().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务启动成功");
} else {
logging!(
warn,
Type::Core,
true,
"新安装的服务启动失败,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get().await;
final_state.prefer_sidecar = true;
final_state.last_error =
Some("Newly installed service failed to start".to_string());
final_state.save().await?;
self.start_core_by_sidecar().await?;
}
} else {
logging!(
warn,
Type::Core,
true,
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get().await;
final_state.prefer_sidecar = true;
final_state.last_error = Some(
"Newly installed service not immediately available/connectable"
.to_string(),
);
final_state.save().await?;
self.start_core_by_sidecar().await?;
}
}
Err(err) => {
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err);
let new_state = service::ServiceState {
last_error: Some(err.to_string()),
prefer_sidecar: true,
..Default::default()
};
new_state.save().await?;
self.start_core_by_sidecar().await?;
}
}
} else {
// 有安装记录,服务未成功启动,且初始不偏好sidecar
// 这意味着服务之前可能可用,但 attempt_service_init 失败了(并应已设置 prefer_sidecar),
// 或者服务初始不可用,无偏好,有记录。应强制使用 sidecar。
logging!(
info,
Type::Core,
true,
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
);
let mut final_state = service::ServiceState::get().await;
if !final_state.prefer_sidecar {
logging!(
warn,
Type::Core,
true,
"prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true"
);
final_state.prefer_sidecar = true;
final_state.last_error =
Some(final_state.last_error.unwrap_or_else(|| {
"Service startup failed or unavailable before sidecar fallback"
.to_string()
}));
final_state.save().await?;
}
self.start_core_by_sidecar().await?;
}
}
}
logging!(trace, Type::Core, "Initied core logic completed");
// #[cfg(target_os = "macos")]
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
logging!(info, Type::Core, true, "核心初始化完成");
Ok(())
}
@@ -1063,33 +864,35 @@ impl CoreManager {
(*guard).clone()
}
/// 启动核心
pub async fn start_core(&self) -> Result<()> {
if service::is_service_available().await.is_ok() {
if service::check_service_needs_reinstall().await {
service::reinstall_service().await?;
pub async fn prestart_core(&self) -> Result<()> {
SERVICE_MANAGER.lock().await.refresh().await?;
match SERVICE_MANAGER.lock().await.current() {
ServiceStatus::Ready => {
self.set_running_mode(RunningMode::Service);
}
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
self.start_core_by_service().await?;
} else {
// 服务不可用,检查用户偏好
let service_state = service::ServiceState::get().await;
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"服务不可用,根据用户偏好使用Sidecar模式"
);
self.start_core_by_sidecar().await?;
} else {
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
self.start_core_by_sidecar().await?;
_ => {
self.set_running_mode(RunningMode::Sidecar);
}
}
Ok(())
}
/// 启动核心
pub async fn start_core(&self) -> Result<()> {
self.prestart_core().await?;
match self.get_running_mode() {
RunningMode::Service => {
logging_error!(Type::Core, true, self.start_core_by_service().await);
}
RunningMode::NotRunning | RunningMode::Sidecar => {
logging_error!(Type::Core, true, self.start_core_by_sidecar().await);
}
};
Ok(())
}
/// 停止核心运行
pub async fn stop_core(&self) -> Result<()> {
match self.get_running_mode() {
@@ -1101,8 +904,8 @@ impl CoreManager {
/// 重启内核
pub async fn restart_core(&self) -> Result<()> {
logging!(info, Type::Core, true, "Restarting core");
self.stop_core().await?;
self.start_core().await?;
Ok(())
}
@@ -1143,3 +946,6 @@ impl CoreManager {
Ok(())
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
+2 -2
View File
@@ -1,8 +1,8 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep, timeout, Duration};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use tokio::time::{Duration, sleep, timeout};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery;
+20 -37
View File
@@ -2,8 +2,9 @@ use crate::singleton;
use parking_lot::RwLock;
use std::{
sync::{
Arc,
atomic::{AtomicU64, Ordering},
mpsc, Arc,
mpsc,
},
thread,
time::{Duration, Instant},
@@ -20,7 +21,6 @@ enum FrontendEvent {
NoticeMessage { status: String, message: String },
ProfileChanged { current_profile_id: String },
TimerUpdated { profile_index: String },
StartupCompleted,
ProfileUpdateStarted { uid: String },
ProfileUpdateCompleted { uid: String },
}
@@ -98,16 +98,14 @@ impl NotificationSystem {
let is_emergency = *system.emergency_mode.read();
if is_emergency {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
if is_emergency
&& let FrontendEvent::NoticeMessage { ref status, .. } = event
&& status == "info" {
log::warn!(
"Emergency mode active, skipping info message"
);
continue;
}
}
}
if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now();
@@ -134,9 +132,6 @@ impl NotificationSystem {
FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
}
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
}
@@ -203,13 +198,12 @@ impl NotificationSystem {
/// 发送事件到队列
fn send_event(&self, event: FrontendEvent) -> bool {
if *self.emergency_mode.read() {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
log::info!("Skipping info message in emergency mode");
return false;
}
}
if *self.emergency_mode.read()
&& let FrontendEvent::NoticeMessage { ref status, .. } = event
&& status == "info"
{
log::info!("Skipping info message in emergency mode");
return false;
}
if let Some(sender) = &self.sender {
@@ -366,22 +360,6 @@ impl Handle {
}
}
pub fn notify_startup_completed() {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::StartupCompleted);
} else {
log::warn!(
"Notification system not initialized when trying to send StartupCompleted event."
);
}
}
pub fn notify_profile_update_started(uid: String) {
let handle = Self::global();
if handle.is_exiting() {
@@ -392,7 +370,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
} else {
log::warn!("Notification system not initialized when trying to send ProfileUpdateStarted event.");
log::warn!(
"Notification system not initialized when trying to send ProfileUpdateStarted event."
);
}
}
@@ -406,7 +386,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
} else {
log::warn!("Notification system not initialized when trying to send ProfileUpdateCompleted event.");
log::warn!(
"Notification system not initialized when trying to send ProfileUpdateCompleted event."
);
}
}
@@ -471,8 +453,9 @@ impl Handle {
info,
Type::Frontend,
true,
"发送{}条启动时累积的错误消息",
errors.len()
"发送{}条启动时累积的错误消息: {:?}",
errors.len(),
errors
);
// 启动单独线程处理启动错误,避免阻塞主线程
+8 -8
View File
@@ -1,10 +1,10 @@
use crate::process::AsyncHandler;
use crate::utils::notification::{notify_event, NotificationEvent};
use crate::utils::notification::{NotificationEvent, notify_event};
use crate::{
config::Config, core::handle, feat, logging, logging_error,
module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use parking_lot::Mutex;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::{AppHandle, Manager};
@@ -109,7 +109,7 @@ impl Hotkey {
match function {
HotkeyFunction::OpenOrCloseDashboard => {
AsyncHandler::spawn(async move || {
crate::feat::open_or_close_dashboard_hotkey().await;
crate::feat::open_or_close_dashboard().await;
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
});
}
@@ -243,11 +243,11 @@ impl Hotkey {
);
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = app_handle_cloned.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned, &app_handle_cloned);
}
if let Some(window) = app_handle_cloned.get_webview_window("main")
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned, &app_handle_cloned);
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
File diff suppressed because it is too large Load Diff
+186 -179
View File
@@ -1,11 +1,15 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use backoff::{Error as BackoffError, ExponentialBackoff};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::windows::named_pipe::ClientOptions;
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\clash-verge-service"
@@ -112,170 +116,134 @@ pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
Ok(expected_signature == response.signature)
}
// IPC连接管理-win
#[cfg(target_os = "windows")]
fn create_backoff_strategy() -> ExponentialBackoff {
ExponentialBackoff {
initial_interval: Duration::from_millis(50),
max_interval: Duration::from_secs(1),
max_elapsed_time: Some(Duration::from_secs(3)),
multiplier: 1.5,
..Default::default()
}
}
pub async fn send_ipc_request(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
use std::{
ffi::CString,
fs::File,
io::{Read, Write},
os::windows::io::{FromRawHandle, RawHandle},
ptr,
};
use winapi::um::{
fileapi::{CreateFileA, OPEN_EXISTING},
handleapi::INVALID_HANDLE_VALUE,
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
};
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return Err(e);
let operation = || async {
match send_ipc_request_internal(command.clone(), payload.clone()).await {
Ok(response) => Ok(response),
Err(e) => {
logging!(
warn,
Type::Service,
true,
"IPC请求失败,准备重试: 命令={}, 错误={}",
command_type,
e
);
Err(BackoffError::transient(e))
}
}
};
let request_json = serde_json::to_string(&request)?;
let result = AsyncHandler::spawn_blocking(move || -> Result<IpcResponse> {
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
Ok(name) => name,
Err(e) => {
logging!(error, Type::Service, true, "创建CString失败: {}", e);
return Err(anyhow::anyhow!("创建CString失败: {}", e));
}
};
let handle = unsafe {
CreateFileA(
c_pipe_name.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
let error = std::io::Error::last_os_error();
match backoff::future::retry(create_backoff_strategy(), operation).await {
Ok(response) => {
// logging!(
// info,
// Type::Service,
// true,
// "IPC请求成功: 命令={}, 成功={}",
// command_type,
// response.success
// );
Ok(response)
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"连接到服务命名管道失败: {}",
error
"IPC请求最终失败,重试已耗尽: 命令={}, 错误={}",
command_type,
e
);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
Err(anyhow::anyhow!("IPC请求重试失败: {}", e))
}
}
}
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
// 内部IPC请求实现(不带重试)
async fn send_ipc_request_internal(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
#[cfg(target_os = "windows")]
{
send_ipc_request_windows(command, payload).await
}
#[cfg(target_family = "unix")]
{
send_ipc_request_unix(command, payload).await
}
}
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
// IPC连接管理-win
#[cfg(target_os = "windows")]
async fn send_ipc_request_windows(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
let request = create_signed_request(command, payload)?;
let request_json = serde_json::to_string(&request)?;
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = pipe.write_all(&len_bytes) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) {
Ok(p) => p,
Err(e) => {
logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e));
}
};
if let Err(e) = pipe.write_all(request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
if let Err(e) = pipe.flush() {
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
}
pipe.write_all(&len_bytes).await?;
pipe.write_all(request_bytes).await?;
pipe.flush().await?;
let mut response_len_bytes = [0u8; 4];
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
let mut response_len_bytes = [0u8; 4];
pipe.read_exact(&mut response_len_bytes).await?;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
pipe.read_exact(&mut response_bytes).await?;
let mut response_bytes = vec![0u8; response_len];
if let Err(e) = pipe.read_exact(&mut response_bytes) {
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
}
let response: IpcResponse = serde_json::from_slice(&response_bytes)
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
if !verify_response_signature(&response)? {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
logging!(
info,
Type::Service,
true,
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
Ok(response)
})
.await??;
Ok(result)
Ok(response)
}
// IPC连接管理-unix
#[cfg(target_family = "unix")]
pub async fn send_ipc_request(
async fn send_ipc_request_unix(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
use std::os::unix::net::UnixStream;
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return Err(e);
}
};
let request = create_signed_request(command, payload)?;
let request_json = serde_json::to_string(&request)?;
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
Ok(s) => {
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
s
}
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await {
Ok(s) => s,
Err(e) => {
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
@@ -285,58 +253,97 @@ pub async fn send_ipc_request(
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
}
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
stream.write_all(&len_bytes).await?;
stream.write_all(request_bytes).await?;
stream.flush().await?;
// 读取响应长度
let mut response_len_bytes = [0u8; 4];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
stream.read_exact(&mut response_len_bytes).await?;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
stream.read_exact(&mut response_bytes).await?;
let response: IpcResponse = serde_json::from_slice(&response_bytes)
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
if !verify_response_signature(&response)? {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
logging!(
info,
Type::Service,
true,
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_signed_request() {
let command = IpcCommand::GetVersion;
let payload = serde_json::json!({"test": "data"});
let result = create_signed_request(command, payload);
assert!(result.is_ok());
if let Ok(request) = result {
assert!(!request.id.is_empty());
assert!(!request.signature.is_empty());
assert_eq!(request.command, IpcCommand::GetVersion);
}
}
#[test]
fn test_sign_and_verify_message() {
let test_message = "test message for signing";
let signature_result = sign_message(test_message);
assert!(signature_result.is_ok());
if let Ok(signature) = signature_result {
assert!(!signature.is_empty());
// 测试相同消息产生相同签名
if let Ok(signature2) = sign_message(test_message) {
assert_eq!(signature, signature2);
}
}
}
#[test]
fn test_verify_response_signature() {
let response = IpcResponse {
id: "test-id".to_string(),
success: true,
data: Some(serde_json::json!({"result": "success"})),
error: None,
signature: String::new(),
};
// 创建正确的签名
let verification_response = IpcResponse {
id: response.id.clone(),
success: response.success,
data: response.data.clone(),
error: response.error.clone(),
signature: String::new(),
};
if let Ok(message) = serde_json::to_string(&verification_response)
&& let Ok(correct_signature) = sign_message(&message)
{
let signed_response = IpcResponse {
signature: correct_signature,
..response
};
let verification_result = verify_response_signature(&signed_response);
assert!(verification_result.is_ok());
if let Ok(is_valid) = verification_result {
assert!(is_valid);
}
}
}
}
+2 -3
View File
@@ -2,7 +2,7 @@
use crate::utils::autostart as startup_shortcut;
use crate::{
config::{Config, IVerge},
core::{handle::Handle, EventDrivenProxyManager},
core::{EventDrivenProxyManager, handle::Handle},
logging, logging_error, singleton_lazy,
utils::logging::Type,
};
@@ -24,8 +24,7 @@ static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;
static DEFAULT_BYPASS: &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")]
static DEFAULT_BYPASS: &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>";
static DEFAULT_BYPASS: &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>";
async fn get_bypass() -> String {
let use_default = Config::verge()
+15 -16
View File
@@ -6,8 +6,8 @@ use std::{
collections::HashMap,
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
@@ -242,19 +242,18 @@ impl Timer {
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
for item in items.iter() {
if let Some(option) = item.option.as_ref() {
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
if interval > 0 {
logging!(
debug,
Type::Timer,
"找到定时更新配置: uid={}, interval={}min",
uid,
interval
);
new_map.insert(uid.clone(), interval);
}
}
if let Some(option) = item.option.as_ref()
&& let (Some(interval), Some(uid)) = (option.update_interval, &item.uid)
&& interval > 0
{
logging!(
debug,
Type::Timer,
"找到定时更新配置: uid={}, interval={}min",
uid,
interval
);
new_map.insert(uid.clone(), interval);
}
}
}
@@ -390,7 +389,8 @@ impl Timer {
};
// Get the profile updated timestamp - now safe to await
let profiles = { Config::profiles().await.clone().data_ref() }.clone();
let config_profiles = Config::profiles().await;
let profiles = config_profiles.data_ref().clone();
let items = match profiles.get_items() {
Some(i) => i,
None => {
@@ -434,7 +434,6 @@ impl Timer {
/// Emit update events for frontend notification
fn emit_update_event(_uid: &str, _is_start: bool) {
#[cfg(any(feature = "verge-dev", feature = "default"))]
{
if _is_start {
super::handle::Handle::notify_profile_update_started(_uid.to_string());
+253 -71
View File
@@ -1,17 +1,21 @@
use once_cell::sync::OnceCell;
use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::ipc::Rate;
use crate::module::lightweight;
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
use crate::{
cmd,
Type, cmd,
config::Config,
feat, logging,
feat,
ipc::IpcManager,
logging,
module::lightweight::is_in_lightweight_mode,
singleton_lazy,
utils::{dirs::find_target_icons, i18n::t},
Type,
};
use super::handle;
@@ -24,9 +28,9 @@ use std::{
time::{Duration, Instant},
};
use tauri::{
AppHandle, Wry,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconEvent},
AppHandle, Wry,
};
#[derive(Clone)]
@@ -71,12 +75,11 @@ impl TrayState {
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon {
if let Ok(Some(common_icon_path)) = find_target_icons("common") {
if let Ok(icon_data) = fs::read(common_icon_path) {
return (true, icon_data);
}
}
if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
&& let Ok(icon_data) = fs::read(common_icon_path)
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
@@ -106,12 +109,11 @@ impl TrayState {
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon {
if let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") {
if let Ok(icon_data) = fs::read(sysproxy_icon_path) {
return (true, icon_data);
}
}
if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
&& let Ok(icon_data) = fs::read(sysproxy_icon_path)
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
@@ -141,12 +143,11 @@ impl TrayState {
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon {
if let Ok(Some(tun_icon_path)) = find_target_icons("tun") {
if let Ok(icon_data) = fs::read(tun_icon_path) {
return (true, icon_data);
}
}
if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
&& let Ok(icon_data) = fs::read(tun_icon_path)
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
@@ -281,6 +282,16 @@ impl Tray {
.unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode();
// 获取代理节点
let proxy_nodes_data = cmd::get_proxies().await.unwrap_or_else(|e| {
logging!(
error,
Type::Cmd,
"Failed to fetch proxies for tray menu: {e}"
);
serde_json::Value::Object(serde_json::Map::new())
});
match app_handle.tray_by_id("main") {
Some(tray) => {
let _ = tray.set_menu(Some(
@@ -291,6 +302,7 @@ impl Tray {
*tun_mode,
profile_uid_and_name,
is_lightweight_mode,
proxy_nodes_data,
)
.await?,
));
@@ -415,13 +427,13 @@ impl Tray {
{
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
if let Some(current_profile_uid) = profiles.get_current() {
if let Ok(profile) = profiles.get_item(&current_profile_uid) {
current_profile_name = match &profile.name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
}
if let Some(current_profile_uid) = profiles.get_current()
&& let Ok(profile) = profiles.get_item(&current_profile_uid)
{
current_profile_name = match &profile.name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
}
}
@@ -518,14 +530,9 @@ impl Tray {
feat::toggle_tun_mode(None).await;
}),
"main_window" => Box::pin(async move {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray点击事件: 显示主窗口");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
crate::module::lightweight::exit_lightweight_mode().await;
}
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "窗口显示结果: {result:?}");
if !lightweight::exit_lightweight_mode().await {
WindowManager::show_main_window().await;
};
}),
_ => Box::pin(async move {}),
};
@@ -557,9 +564,21 @@ async fn create_tray_menu(
tun_mode_enabled: bool,
profile_uid_and_name: Vec<(String, String)>,
is_lightweight_mode: bool,
proxy_nodes_data: serde_json::Value,
) -> Result<tauri::menu::Menu<Wry>> {
let mode = mode.unwrap_or("");
// 获取当前配置文件的选中代理组信息
let current_profile_selected = {
let profiles_config = Config::profiles().await;
let profiles_ref = profiles_config.latest_ref();
profiles_ref
.get_current()
.and_then(|uid| profiles_ref.get_item(&uid).ok())
.and_then(|profile| profile.selected.clone())
.unwrap_or_default()
};
let version = env!("CARGO_PKG_VERSION");
let hotkeys = Config::verge()
@@ -606,12 +625,119 @@ async fn create_tray_menu(
results.into_iter().collect::<Result<Vec<_>, _>>()?
};
// 代理组子菜单
let proxy_submenus: Vec<Submenu<Wry>> = {
let mut submenus = Vec::new();
if let Some(proxies) = proxy_nodes_data.get("proxies").and_then(|v| v.as_object()) {
for (group_name, group_data) in proxies.iter() {
// Filter groups based on mode
let should_show = match mode {
"global" => group_name == "GLOBAL",
_ => group_name != "GLOBAL",
} &&
// Check if the group is hidden
!group_data.get("hidden").and_then(|v| v.as_bool( )).unwrap_or(false);
if !should_show {
continue;
}
let Some(all_proxies) = group_data.get("all").and_then(|v| v.as_array()) else {
continue;
};
let now_proxy = group_data.get("now").and_then(|v| v.as_str()).unwrap_or("");
// Create proxy items
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
.iter()
.filter_map(|proxy_name| proxy_name.as_str())
.filter_map(|proxy_str| {
let is_selected = proxy_str == now_proxy;
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
// Get delay for display
let delay_text = proxies
.get(proxy_str)
.and_then(|p| p.get("history"))
.and_then(|h| h.as_array())
.and_then(|h| h.last())
.and_then(|r| r.get("delay"))
.and_then(|d| d.as_i64())
.map(|delay| match delay {
-1 => "-ms".to_string(),
delay if delay >= 10000 => "-ms".to_string(),
_ => format!("{}ms", delay),
})
.unwrap_or_else(|| "-ms".to_string());
let display_text = format!("{} | {}", proxy_str, delay_text);
CheckMenuItem::with_id(
app_handle,
item_id,
display_text,
true,
is_selected,
None::<&str>,
)
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
.ok()
})
.collect();
if group_items.is_empty() {
continue;
}
// Determine if group is active
let is_group_active = match mode {
"global" => group_name == "GLOBAL" && !now_proxy.is_empty(),
"direct" => false,
_ => {
current_profile_selected
.iter()
.any(|s| s.name.as_deref() == Some(group_name))
&& !now_proxy.is_empty()
}
};
let group_display_name = if is_group_active {
format!("✓ {}", group_name)
} else {
group_name.to_string()
};
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> = group_items
.iter()
.map(|item| item as &dyn IsMenuItem<Wry>)
.collect();
if let Ok(submenu) = Submenu::with_id_and_items(
app_handle,
format!("proxy_group_{}", group_name),
group_display_name,
true,
&group_items_refs,
) {
submenus.push(submenu);
} else {
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
}
}
}
submenus
};
// Pre-fetch all localized strings
let dashboard_text = t("Dashboard").await;
let rule_mode_text = t("Rule Mode").await;
let global_mode_text = t("Global Mode").await;
let direct_mode_text = t("Direct Mode").await;
let profiles_text = t("Profiles").await;
let proxies_text = t("Proxies").await;
let system_proxy_text = t("System Proxy").await;
let tun_mode_text = t("TUN Mode").await;
let lightweight_mode_text = t("LightWeight Mode").await;
@@ -675,6 +801,24 @@ async fn create_tray_menu(
&profile_menu_items_refs,
)?;
// 创建代理主菜单
let proxies_submenu = if !proxy_submenus.is_empty() {
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
.iter()
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
.collect();
Some(Submenu::with_id_and_items(
app_handle,
"proxies",
proxies_text,
true,
&proxy_submenu_refs,
)?)
} else {
None
};
let system_proxy = &CheckMenuItem::with_id(
app_handle,
"system_proxy",
@@ -772,26 +916,37 @@ async fn create_tray_menu(
let separator = &PredefinedMenuItem::separator(app_handle)?;
// 动态构建菜单项
let mut menu_items: Vec<&dyn IsMenuItem<Wry>> = vec![
open_window,
separator,
rule_mode,
global_mode,
direct_mode,
separator,
profiles,
];
// 如果有代理节点,添加代理节点菜单
if let Some(ref proxies_menu) = proxies_submenu {
menu_items.push(proxies_menu);
}
menu_items.extend_from_slice(&[
separator,
system_proxy as &dyn IsMenuItem<Wry>,
tun_mode as &dyn IsMenuItem<Wry>,
separator,
lighteweight_mode as &dyn IsMenuItem<Wry>,
copy_env as &dyn IsMenuItem<Wry>,
open_dir as &dyn IsMenuItem<Wry>,
more as &dyn IsMenuItem<Wry>,
separator,
quit as &dyn IsMenuItem<Wry>,
]);
let menu = tauri::menu::MenuBuilder::new(app_handle)
.items(&[
open_window,
separator,
rule_mode,
global_mode,
direct_mode,
separator,
profiles,
separator,
system_proxy,
tun_mode,
separator,
lighteweight_mode,
copy_env,
open_dir,
more,
separator,
quit,
])
.items(&menu_items)
.build()?;
Ok(menu)
}
@@ -811,19 +966,14 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into()).await; // Await async function
}
"open_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() {
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出");
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
}
let result = WindowManager::show_main_window().await; // Await async function
log::info!(target: "app", "窗口显示结果: {result:?}");
if !lightweight::exit_lightweight_mode().await {
WindowManager::show_main_window().await;
};
}
"system_proxy" => {
feat::toggle_system_proxy().await; // Await async function
@@ -848,14 +998,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
return;
}
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
if was_lightweight {
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
use crate::utils::window_manager::WindowManager;
let result = WindowManager::show_main_window().await; // Await async function
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
if !is_in_lightweight_mode() {
lightweight::entry_lightweight_mode().await; // Await async function
} else {
crate::module::lightweight::entry_lightweight_mode().await; // Remove .await as it's not async
lightweight::exit_lightweight_mode().await; // Await async function
}
}
"quit" => {
@@ -865,6 +1011,42 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
let profile_index = &id["profiles_".len()..];
feat::toggle_proxy_profile(profile_index.into()).await; // Await async function
}
id if id.starts_with("proxy_") => {
// proxy_{group_name}_{proxy_name}
let parts: Vec<&str> = id.splitn(3, '_').collect();
if parts.len() == 3 && parts[0] == "proxy" {
let group_name = parts[1];
let proxy_name = parts[2];
match cmd::proxy::update_proxy_and_sync(
group_name.to_string(),
proxy_name.to_string(),
)
.await
{
Ok(_) => {
log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name);
}
Err(e) => {
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
// Fallback to IPC update
if (IpcManager::global()
.update_proxy(group_name, proxy_name)
.await)
.is_ok()
{
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
if let Some(app_handle) = handle::Handle::global().app_handle() {
let _ = app_handle.emit("verge://force-refresh-proxies", ());
}
}
}
}
}
}
_ => {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
#![cfg(target_os = "windows")]
use crate::utils::dirs;
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
+2 -4
View File
@@ -18,12 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
deep_merge(&mut config, &Value::from(merge));
let config = config.as_mapping().cloned().unwrap_or_else(|| {
config.as_mapping().cloned().unwrap_or_else(|| {
log::error!("Failed to convert merged config to mapping, using empty mapping");
Mapping::new()
});
config
})
}
#[test]
+18 -21
View File
@@ -384,29 +384,26 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join("dns_config.yaml");
if dns_path.exists() {
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
if let Ok(dns_config) =
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
// 处理hosts配置
if let Some(hosts_value) = dns_config.get("hosts") {
if hosts_value.is_mapping() {
config.insert("hosts".into(), hosts_value.clone());
log::info!(target: "app", "apply hosts configuration");
}
}
if dns_path.exists()
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path)
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
// 处理hosts配置
if let Some(hosts_value) = dns_config.get("hosts")
&& hosts_value.is_mapping()
{
config.insert("hosts".into(), hosts_value.clone());
log::info!(target: "app", "apply hosts configuration");
}
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
log::info!(target: "app", "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
}
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
log::info!(target: "app", "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ pub fn use_script(
config: Mapping,
name: String,
) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsString, JsValue, Source};
use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction};
use std::{cell::RefCell, rc::Rc};
let mut context = Context::default();
+30 -30
View File
@@ -44,39 +44,39 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
// If this is proxies field, we also need to filter proxy-groups
if field == "proxies" {
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") {
let mut new_groups = Sequence::new();
for group in groups {
if let Value::Mapping(group_map) = group {
let mut new_group = group_map.clone();
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
let filtered_proxies: Sequence = proxies
.iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.contains(name)
} else {
true
}
})
.cloned()
.collect();
new_group.insert(
Value::String("proxies".into()),
Value::Sequence(filtered_proxies),
);
}
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
if field == "proxies"
&& let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups")
{
let mut new_groups = Sequence::new();
for group in groups {
if let Value::Mapping(group_map) = group {
let mut new_group = group_map.clone();
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
let filtered_proxies: Sequence = proxies
.iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.contains(name)
} else {
true
}
})
.cloned()
.collect();
new_group.insert(
Value::String("proxies".into()),
Value::Sequence(filtered_proxies),
);
}
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
}
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
}
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
}
config
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::{
config::Config,
core::{handle, tray, CoreManager},
core::{CoreManager, handle, tray},
ipc::IpcManager,
logging_error,
process::AsyncHandler,
+5 -5
View File
@@ -1,6 +1,6 @@
use crate::{
config::{Config, IVerge},
core::{handle, hotkey, sysopt, tray, CoreManager},
core::{CoreManager, handle, hotkey, sysopt, tray},
logging_error,
module::lightweight,
utils::logging::Type,
@@ -202,10 +202,10 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys).await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
&& let Some(hotkeys) = patch.hotkeys
{
hotkey::Hotkey::global().update(hotkeys).await?;
}
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
tray::Tray::global().update_menu().await?;
+3 -3
View File
@@ -1,11 +1,11 @@
use crate::{
cmd,
config::{profiles::profiles_draft_update_item_safe, Config, PrfItem, PrfOption},
core::{handle, tray, CoreManager},
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{CoreManager, handle, tray},
logging,
utils::logging::Type,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
+9 -8
View File
@@ -13,21 +13,22 @@ pub async fn toggle_system_proxy() {
// 获取当前系统代理状态
let enable = {
let verge = Config::verge().await;
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
enable
verge.latest_ref().enable_system_proxy.unwrap_or(false)
};
// 获取自动关闭连接设置
let auto_close_connection = {
let verge = Config::verge().await;
let auto_close = verge.latest_ref().auto_close_connection.unwrap_or(false);
auto_close
verge.latest_ref().auto_close_connection.unwrap_or(false)
};
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable && auto_close_connection {
if let Err(err) = IpcManager::global().close_all_connections().await {
log::error!(target: "app", "Failed to close all connections: {err}");
}
if enable
&& auto_close_connection
&& let Err(err) = IpcManager::global().close_all_connections().await
{
log::error!(target: "app", "Failed to close all connections: {err}");
}
let patch_result = super::patch_verge(
+11 -47
View File
@@ -1,57 +1,21 @@
use crate::utils::window_manager::WindowManager;
use crate::{
config::Config,
core::{handle, sysopt, CoreManager},
core::{CoreManager, handle, sysopt},
ipc::IpcManager,
logging,
module::lightweight,
utils::logging::Type,
};
/// Open or close the dashboard window
#[allow(dead_code)]
pub async fn open_or_close_dashboard() {
open_or_close_dashboard_internal(false).await
}
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
#[allow(dead_code)]
pub async fn open_or_close_dashboard_hotkey() {
open_or_close_dashboard_internal(true).await
open_or_close_dashboard_internal().await
}
/// Internal implementation for opening/closing dashboard
async fn open_or_close_dashboard_internal(bypass_debounce: bool) {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
// 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce {
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
log::info!(target: "app", "主线程中执行热键窗口操作");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode().await;
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
let result = WindowManager::toggle_main_window().await;
log::info!(target: "app", "Window toggle result: {result:?}");
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode().await;
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
async fn open_or_close_dashboard_internal() {
let _ = lightweight::exit_lightweight_mode().await;
let result = WindowManager::toggle_main_window().await;
log::info!(target: "app", "Window toggle result: {result:?}");
}
@@ -92,7 +56,7 @@ pub async fn quit() {
}
async fn clean_async() -> bool {
use tokio::time::{timeout, Duration};
use tokio::time::{Duration, timeout};
logging!(info, Type::System, true, "开始执行异步清理操作...");
@@ -252,10 +216,10 @@ pub async fn hide() {
add_light_weight_timer().await;
}
if let Some(window) = handle::Handle::global().get_window() {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
}
if let Some(window) = handle::Handle::global().get_window()
&& window.is_visible().unwrap_or(false)
{
let _ = window.hide();
}
handle::Handle::global().set_activation_policy_accessory();
}
+29 -45
View File
@@ -1,11 +1,15 @@
use std::time::Duration;
use kode_bridge::{
errors::{AnyError, AnyResult},
pool::PoolConfig,
ClientConfig, IpcHttpClient, LegacyResponse,
errors::{AnyError, AnyResult},
};
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use crate::{
logging, singleton_with_logging,
utils::{dirs::ipc_path, logging::Type},
};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
// 定义用于URL路径的编码集合,只编码真正必要的字符
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
@@ -16,57 +20,38 @@ const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b'&') // 和号
.add(b'%'); // 百分号
use crate::{logging, singleton_with_logging, utils::dirs::ipc_path};
// Helper function to create AnyError from string
fn create_error(msg: impl Into<String>) -> AnyError {
Box::new(std::io::Error::other(msg.into()))
}
pub struct IpcManager {
ipc_path: String,
config: ClientConfig,
client: IpcHttpClient,
}
impl IpcManager {
fn new() -> Self {
pub fn new() -> Self {
logging!(info, Type::Ipc, true, "Creating new IpcManager instance");
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!(
error,
crate::utils::logging::Type::Ipc,
true,
"Failed to get IPC path: {}",
e
);
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
});
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
Self {
ipc_path: ipc_path.to_string(),
config: ClientConfig {
default_timeout: Duration::from_secs(5),
enable_pooling: true,
max_retries: 3,
max_concurrent_requests: 32,
max_requests_per_second: Some(5.0),
pool_config: PoolConfig {
max_size: 32,
min_idle: 2,
max_idle_time_ms: 10_000,
max_retries: 3,
max_concurrent_requests: 32,
max_requests_per_second: Some(5.0),
..Default::default()
},
..Default::default()
},
}
let config = ClientConfig {
default_timeout: Duration::from_secs(5),
enable_pooling: false,
max_retries: 4,
retry_delay: Duration::from_millis(125),
max_concurrent_requests: 16,
max_requests_per_second: Some(64.0),
..Default::default()
};
#[allow(clippy::unwrap_used)]
let client = IpcHttpClient::with_config(ipc_path, config).unwrap();
Self { client }
}
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
impl IpcManager {
pub async fn request(
&self,
@@ -74,8 +59,7 @@ impl IpcManager {
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<LegacyResponse> {
let client = IpcHttpClient::with_config(&self.ipc_path, self.config.clone())?;
client.request(method, path, body).await
self.client.request(method, path, body).await
}
}
@@ -213,8 +197,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
self.send_request("GET", &url, None).await
}
// 版本和配置相关
@@ -293,7 +276,6 @@ impl IpcManager {
let payload = serde_json::json!({
"name": proxy
});
match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => Ok(()),
Err(e) => {
@@ -354,8 +336,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
self.send_request("GET", &url, None).await
}
// 调试相关
@@ -384,3 +365,6 @@ impl IpcManager {
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
+34 -30
View File
@@ -1,6 +1,7 @@
#![allow(non_snake_case)]
#![recursion_limit = "512"]
mod cache;
mod cmd;
pub mod config;
mod core;
@@ -9,7 +10,6 @@ mod feat;
mod ipc;
mod module;
mod process;
mod state;
mod utils;
#[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager;
@@ -20,14 +20,13 @@ use crate::{
utils::{resolve, server},
};
use config::Config;
use parking_lot::Mutex;
use tauri::AppHandle;
#[cfg(target_os = "macos")]
use tauri::Manager;
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration};
use tokio::time::{Duration, timeout};
use utils::logging::Type;
/// Application initialization helper functions
@@ -36,7 +35,7 @@ mod app_init {
/// Initialize singleton monitoring for other instances
pub fn init_singleton_check() {
AsyncHandler::spawn(move || async move {
AsyncHandler::spawn_blocking(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_millis(500), server::check_singleton()).await {
Ok(result) => {
@@ -75,14 +74,16 @@ mod app_init {
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init());
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_http::init());
#[cfg(all(debug_assertions, not(feature = "tokio-trace")))]
// Devtools plugin only in debug mode with feature tauri-dev
// to avoid duplicated registering of logger since the devtools plugin also registers a logger
#[cfg(all(debug_assertions, not(feature = "tokio-trace"), feature = "tauri-dev"))]
{
builder = builder.plugin(tauri_plugin_devtools::init());
}
builder.manage(Mutex::new(state::lightweight::LightWeightState::default()))
builder
}
/// Setup deep link handling
@@ -96,7 +97,7 @@ mod app_init {
app.deep_link().on_open_url(|event| {
let url = event.urls().first().map(|u| u.to_string());
if let Some(url) = url {
tokio::task::spawn_local(async move {
AsyncHandler::spawn(|| async {
if let Err(e) = resolve::resolve_scheme(url).await {
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
}
@@ -136,8 +137,8 @@ mod app_init {
}
/// Generate all command handlers for the application
pub fn generate_handlers(
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
pub fn generate_handlers()
-> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![
// Common commands
cmd::get_sys_proxy,
@@ -157,7 +158,6 @@ mod app_init {
// Application lifecycle
cmd::notify_ui_ready,
cmd::update_ui_stage,
cmd::reset_ui_ready_state,
cmd::get_running_mode,
cmd::get_app_uptime,
cmd::get_auto_launch_status,
@@ -180,11 +180,15 @@ mod app_init {
cmd::get_runtime_yaml,
cmd::get_runtime_exists,
cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::get_proxies,
cmd::force_refresh_proxies,
cmd::get_providers_proxies,
cmd::sync_tray_proxy_selection,
cmd::update_proxy_and_sync,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::check_dns_config_exists,
@@ -269,28 +273,30 @@ pub fn run() {
// Setup singleton check
app_init::init_singleton_check();
// We don't need init_portable_flag here anymore due to the init_config will do the things
// let _ = utils::dirs::init_portable_flag();
let _ = utils::dirs::init_portable_flag();
// Set Linux environment variable
#[cfg(target_os = "linux")]
{
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.to_uppercase();
let session_env = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
let is_kde_desktop = desktop_env.contains("KDE");
let is_wayland_session = session_env.contains("wayland");
let is_plasma_desktop = desktop_env.contains("PLASMA");
if is_kde_desktop && is_wayland_session {
std::env::set_var("GDK_BACKEND", "x11");
if is_kde_desktop || is_plasma_desktop {
unsafe {
std::env::set_var("GTK_CSD", "0");
}
logging!(
info,
Type::Setup,
true,
"KDE Wayland detected: Switched to X11 backend for better titlebar stability."
"KDE detected: Disabled GTK CSD for better titlebar stability."
);
}
}
@@ -331,9 +337,9 @@ pub fn run() {
logging!(info, Type::Setup, true, "执行主要设置操作...");
logging!(info, Type::Setup, true, "异步执行应用设置...");
resolve::resolve_setup_sync(app_handle);
resolve::resolve_setup_handle(app_handle);
resolve::resolve_setup_async();
resolve::resolve_setup_sync();
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())
@@ -439,10 +445,10 @@ pub fn run() {
}
}
if !is_enable_global_hotkey {
if let Err(e) = hotkey::Hotkey::global().init().await {
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
}
if !is_enable_global_hotkey
&& let Err(e) = hotkey::Hotkey::global().init().await
{
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
}
return;
}
@@ -475,10 +481,8 @@ pub fn run() {
}
}
if !is_enable_global_hotkey {
if let Err(e) = hotkey::Hotkey::global().reset() {
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
}
if !is_enable_global_hotkey && let Err(e) = hotkey::Hotkey::global().reset() {
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
}
});
}
+148 -145
View File
@@ -1,42 +1,88 @@
use crate::{
cache::CacheProxy,
config::Config,
core::{handle, timer::Timer, tray::Tray},
log_err, logging,
process::AsyncHandler,
state::lightweight::LightWeightState,
utils::logging::Type,
};
#[cfg(target_os = "macos")]
use crate::logging_error;
use crate::utils::window_manager::WindowManager;
use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{Listener, Manager};
use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
use tauri::Listener;
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 添加退出轻量模式的锁,防止并发调用
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LightweightState {
Normal = 0,
In = 1,
Exiting = 2,
}
fn with_lightweight_status<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut LightWeightState) -> R,
{
if let Some(app_handle) = handle::Handle::global().app_handle() {
// Try to get state, but don't panic if it's not managed yet
if let Some(state) = app_handle.try_state::<Mutex<LightWeightState>>() {
let mut guard = state.lock();
Some(f(&mut guard))
} else {
// State not managed yet, return None
None
impl From<u8> for LightweightState {
fn from(v: u8) -> Self {
match v {
1 => LightweightState::In,
2 => LightweightState::Exiting,
_ => LightweightState::Normal,
}
} else {
// App handle not available yet
None
}
}
impl LightweightState {
fn as_u8(self) -> u8 {
self as u8
}
}
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0);
static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0);
fn set_state(new: LightweightState) {
LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
match new {
LightweightState::Normal => {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
LightweightState::In => {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
}
LightweightState::Exiting => {
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
}
}
}
fn get_state() -> LightweightState {
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
}
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
get_state() == LightweightState::In
}
// 设置轻量模式状态(仅 Normal <-> In)
async fn set_lightweight_mode(value: bool) {
let current = get_state();
if value && current != LightweightState::In {
set_state(LightweightState::In);
} else if !value && current != LightweightState::Normal {
set_state(LightweightState::Normal);
}
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
@@ -61,86 +107,35 @@ pub async fn run_once_auto_lightweight() {
return;
}
logging!(
info,
Type::Lightweight,
true,
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
);
if with_lightweight_status(|_| ()).is_some() {
set_lightweight_mode(false).await;
enable_auto_light_weight_mode().await;
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
set_lightweight_mode(true).await;
enable_auto_light_weight_mode().await;
}
pub async fn auto_lightweight_mode_init() -> Result<()> {
if let Some(app_handle) = handle::Handle::global().app_handle() {
// Check if state is available before accessing it
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() {
logging!(
warn,
Type::Lightweight,
true,
"LightWeightState 尚未初始化,跳过自动轻量模式初始化"
);
return Err(anyhow::anyhow!("LightWeightState has not been initialized"));
}
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto = {
Config::verge()
.await
.latest_ref()
.enable_auto_light_weight_mode
}
.unwrap_or(false);
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto = {
Config::verge()
.await
.latest_ref()
.enable_auto_light_weight_mode
}
.unwrap_or(false);
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_lightweight_mode(true).await;
enable_auto_light_weight_mode().await;
// 确保托盘状态更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
return Err(e);
}
}
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_state(LightweightState::Normal);
enable_auto_light_weight_mode().await;
}
Ok(())
}
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
with_lightweight_status(|state| state.is_lightweight).unwrap_or(false)
}
// 设置轻量模式状态
pub async fn set_lightweight_mode(value: bool) {
if with_lightweight_status(|state| {
state.set_lightweight_mode(value);
})
.is_some()
{
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
}
pub async fn enable_auto_light_weight_mode() {
if let Err(e) = Timer::global().init().await {
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
@@ -155,72 +150,67 @@ pub fn disable_auto_light_weight_mode() {
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
let _ = cancel_light_weight_timer();
cancel_window_close_listener();
cancel_webview_focus_listener();
}
pub async fn entry_lightweight_mode() {
use crate::utils::window_manager::WindowManager;
let result = WindowManager::hide_main_window();
logging!(
info,
Type::Lightweight,
true,
"轻量模式隐藏窗口结果: {:?}",
result
);
if let Some(window) = handle::Handle::global().get_window() {
if let Some(webview) = window.get_webview_window("main") {
let _ = webview.destroy();
}
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_accessory();
pub async fn entry_lightweight_mode() -> bool {
// 尝试从 Normal -> In
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::Normal as u8,
LightweightState::In as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
logging!(info, Type::Lightweight, true, "无需进入轻量模式,跳过调用");
return false;
}
WindowManager::destroy_main_window();
set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
// 回到 In
set_state(LightweightState::In);
CacheProxy::global().clean_default_keys();
true
}
// 添加从轻量模式恢复的函数
pub async fn exit_lightweight_mode() {
// 使用原子操作检查是否已经在退出过程中,防止并发调用
if EXITING_LIGHTWEIGHT
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
pub async fn exit_lightweight_mode() -> bool {
// 尝试从 In -> Exiting
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::In as u8,
LightweightState::Exiting as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
logging!(
info,
Type::Lightweight,
true,
"轻量模式退出操作已在进行中,跳过重复调用"
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
return;
return false;
}
// 使用defer确保无论如何都会重置标志
let _guard = scopeguard::guard((), |_| {
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
});
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
return;
}
WindowManager::show_main_window().await;
set_lightweight_mode(false).await;
let _ = cancel_light_weight_timer();
// macOS激活策略
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
// 回到 Normal
set_state(LightweightState::Normal);
// 重置UI就绪状态
crate::utils::resolve::ui::reset_ui_ready();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
logging!(info, Type::Lightweight, true, "轻量模式退出完成");
true
}
#[cfg(target_os = "macos")]
@@ -228,7 +218,7 @@ pub async fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer().await);
}
fn setup_window_close_listener() -> u32 {
fn setup_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
@@ -243,12 +233,22 @@ fn setup_window_close_listener() -> u32 {
"监听到关闭请求,开始轻量模式计时"
);
});
return handler;
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
}
0
}
fn setup_webview_focus_listener() -> u32 {
fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
}
}
}
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer());
@@ -258,15 +258,18 @@ fn setup_webview_focus_listener() -> u32 {
"监听到窗口获得焦点,取消轻量模式计时"
);
});
return handler;
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
}
0
}
fn cancel_window_close_listener() {
fn cancel_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() {
window.unlisten(setup_window_close_listener());
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口焦点监听");
}
}
}
+8 -2
View File
@@ -1,6 +1,6 @@
use crate::{
cmd::system,
core::{handle, CoreManager},
core::{CoreManager, handle},
};
use std::fmt::{self, Debug, Formatter};
use sysinfo::System;
@@ -20,7 +20,13 @@ impl Debug for PlatformSpecification {
write!(
f,
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode, self.is_admin
self.system_name,
self.system_version,
self.system_kernel_version,
self.system_arch,
self.verge_version,
self.running_mode,
self.is_admin
)
}
}
+10 -3
View File
@@ -34,6 +34,7 @@ impl AsyncHandler {
async_runtime::spawn_blocking(f)
}
#[allow(dead_code)]
#[track_caller]
pub fn block_on<Fut>(fut: Fut) -> Fut::Output
where
@@ -66,12 +67,18 @@ impl AsyncHandler {
location.column()
);
println!("┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐");
println!(
"┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐"
);
println!("│ {:<18} │ {:<80} │", "Field", "Value");
println!("├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤");
println!(
"├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤"
);
println!("│ {:<18} │ {:<80} │", "Type of task", type_str);
println!("│ {:<18} │ {:<80} │", "Size of task", size_str);
println!("│ {:<18} │ {:<80} │", "Called from", loc_str);
println!("└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘");
println!(
"└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘"
);
}
}
-36
View File
@@ -1,36 +0,0 @@
use std::sync::{Arc, Once, OnceLock};
use crate::{logging, utils::logging::Type};
#[derive(Clone)]
pub struct LightWeightState {
#[allow(unused)]
once: Arc<Once>,
pub is_lightweight: bool,
}
impl LightWeightState {
pub fn new() -> Self {
Self {
once: Arc::new(Once::new()),
is_lightweight: false,
}
}
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
self.is_lightweight = value;
if value {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} else {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
self
}
}
impl Default for LightWeightState {
fn default() -> Self {
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
INSTANCE.get_or_init(LightWeightState::new).clone()
}
}
-5
View File
@@ -1,5 +0,0 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc
pub mod lightweight;
pub mod proxy;
+1 -1
View File
@@ -1,5 +1,5 @@
#[cfg(target_os = "windows")]
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
#[cfg(target_os = "windows")]
use log::info;
+5 -6
View File
@@ -151,12 +151,11 @@ pub fn find_target_icons(target: &str) -> Result<Option<String>> {
let entry = entry?;
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with(target)
&& (file_name.ends_with(".ico") || file_name.ends_with(".png"))
{
matching_files.push(path);
}
if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
&& file_name.starts_with(target)
&& (file_name.ends_with(".ico") || file_name.ends_with(".png"))
{
matching_files.push(path);
}
}
+3 -7
View File
@@ -1,7 +1,7 @@
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize};
use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::Mapping;
use std::{path::PathBuf, str::FromStr};
@@ -154,10 +154,6 @@ macro_rules! ret_err {
#[macro_export]
macro_rules! t {
($en:expr, $zh:expr, $use_zh:expr) => {
if $use_zh {
$zh
} else {
$en
}
if $use_zh { $zh } else { $en }
};
}
+16 -17
View File
@@ -15,14 +15,14 @@ fn get_locales_dir() -> Option<PathBuf> {
pub fn get_supported_languages() -> Vec<String> {
let mut languages = Vec::new();
if let Some(locales_dir) = get_locales_dir() {
if let Ok(entries) = fs::read_dir(locales_dir) {
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str() {
if let Some(lang) = file_name.strip_suffix(".json") {
languages.push(lang.to_string());
}
}
if let Some(locales_dir) = get_locales_dir()
&& let Ok(entries) = fs::read_dir(locales_dir)
{
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str()
&& let Some(lang) = file_name.strip_suffix(".json")
{
languages.push(lang.to_string());
}
}
}
@@ -39,10 +39,10 @@ static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
if let Some(locales_dir) = get_locales_dir() {
for lang in get_supported_languages() {
let file_path = locales_dir.join(format!("{lang}.json"));
if let Ok(content) = fs::read_to_string(file_path) {
if let Ok(json) = serde_json::from_str(&content) {
translations.insert(lang.to_string(), json);
}
if let Ok(content) = fs::read_to_string(file_path)
&& let Ok(json) = serde_json::from_str(&content)
{
translations.insert(lang.to_string(), json);
}
}
}
@@ -76,14 +76,13 @@ pub async fn t(key: &str) -> String {
return text.to_string();
}
if current_lang != DEFAULT_LANGUAGE {
if let Some(text) = TRANSLATIONS
if current_lang != DEFAULT_LANGUAGE
&& let Some(text) = TRANSLATIONS
.get(DEFAULT_LANGUAGE)
.and_then(|trans| trans.get(&key))
.and_then(|val| val.as_str())
{
return text.to_string();
}
{
return text.to_string();
}
key
+82 -105
View File
@@ -1,3 +1,11 @@
cfg_if::cfg_if! {
if #[cfg(not(feature = "tauri-dev"))] {
use crate::utils::logging::{console_colored_format, file_format};
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, LogSpecification, Logger};
use log::LevelFilter;
}
}
use crate::{
config::*,
core::handle,
@@ -7,64 +15,47 @@ use crate::{
};
use anyhow::Result;
use chrono::{Local, TimeZone};
use log::LevelFilter;
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Logger, Root},
encode::pattern::PatternEncoder,
};
use std::{path::PathBuf, str::FromStr};
use tauri_plugin_shell::ShellExt;
use tokio::fs;
use tokio::fs::DirEntry;
/// initialize this instance's log file
async fn init_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?;
if !log_dir.exists() {
let _ = tokio::fs::create_dir_all(&log_dir).await;
}
#[cfg(not(feature = "tauri-dev"))]
pub async fn init_logger() -> Result<()> {
let log_level = Config::verge().await.latest_ref().get_log_level();
if log_level == LevelFilter::Off {
return Ok(());
return Err(anyhow::anyhow!("logging is disabled"));
}
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{local_time}.log");
let log_file = log_dir.join(log_file);
let log_dir = dirs::app_logs_dir()?;
let logger = Logger::with(LogSpecification::from(log_level))
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(Duplicate::Debug)
.format(console_colored_format)
.format_for_files(file_format)
.rotate(
// ? 总是保持单个日志最大 10 MB
Criterion::Size(10 * 1024 * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
// TODO 提供前端设置最大保留文件数量
Cleanup::Never,
);
let log_pattern = match log_level {
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
};
let _handle = logger.start()?;
let encode = Box::new(PatternEncoder::new(log_pattern));
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
let mut logger_builder = Logger::builder();
let mut root_builder = Root::builder();
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
logger_builder = logger_builder.appenders(["file"]);
if log_more {
root_builder = root_builder.appenders(["file"]);
}
let (config, _) = log4rs::config::Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile)))
.logger(logger_builder.additive(false).build("app", log_level))
.build_lossy(root_builder.build(log_level));
log4rs::init_config(config)?;
// TODO 全局 logger handle 控制
// GlobalLoggerProxy::global().set_inner(handle);
// TODO 提供前端设置等级,热更新等级
// logger.parse_new_spec(spec)
Ok(())
}
// TODO flexi_logger 提供了最大保留天数,或许我们应该用内置删除log文件
/// 删除log文件
pub async fn delete_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?;
@@ -296,52 +287,52 @@ async fn ensure_directories() -> Result<()> {
/// 初始化配置文件
async fn initialize_config_files() -> Result<()> {
if let Ok(path) = dirs::clash_path() {
if !path.exists() {
let template = IClashTemp::template().0;
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created clash config at {:?}",
path
);
}
if let Ok(path) = dirs::clash_path()
&& !path.exists()
{
let template = IClashTemp::template().0;
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created clash config at {:?}",
path
);
}
if let Ok(path) = dirs::verge_path() {
if !path.exists() {
let template = IVerge::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created verge config at {:?}",
path
);
}
if let Ok(path) = dirs::verge_path()
&& !path.exists()
{
let template = IVerge::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created verge config at {:?}",
path
);
}
if let Ok(path) = dirs::profiles_path() {
if !path.exists() {
let template = IProfiles::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created profiles config at {:?}",
path
);
}
if let Ok(path) = dirs::profiles_path()
&& !path.exists()
{
let template = IProfiles::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created profiles config at {:?}",
path
);
}
// 验证并修正verge配置
@@ -355,11 +346,13 @@ async fn initialize_config_files() -> Result<()> {
/// Initialize all the config files
/// before tauri setup
pub async fn init_config() -> Result<()> {
let _ = dirs::init_portable_flag();
// We do not need init_portable_flag here anymore due to lib.rs will to the things
// let _ = dirs::init_portable_flag();
if let Err(e) = init_log().await {
eprintln!("Failed to initialize logging: {}", e);
}
// We do not need init_log here anymore due to resolve will to the things
// if let Err(e) = init_log().await {
// eprintln!("Failed to initialize logging: {}", e);
// }
ensure_directories().await?;
@@ -405,14 +398,6 @@ pub async fn init_resources() -> Result<()> {
for file in file_list.iter() {
let src_path = res_dir.join(file);
let dest_path = app_dir.join(file);
logging!(
debug,
Type::Setup,
true,
"src_path: {:?}, dest_path: {:?}",
src_path,
dest_path
);
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
match fs::copy(&src, &dest).await {
@@ -445,14 +430,6 @@ pub async fn init_resources() -> Result<()> {
(Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified {
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
} else {
logging!(
debug,
Type::Setup,
true,
"skipping resource copy '{}'",
file
);
}
}
_ => {
@@ -475,7 +452,7 @@ pub async fn init_resources() -> Result<()> {
#[cfg(target_os = "windows")]
pub fn init_scheme() -> Result<()> {
use tauri::utils::platform::current_exe;
use winreg::{enums::*, RegKey};
use winreg::{RegKey, enums::*};
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
+68 -2
View File
@@ -1,4 +1,14 @@
use std::fmt;
cfg_if::cfg_if! {
if #[cfg(feature = "tauri-dev")] {
use std::fmt;
} else {
#[cfg(feature = "verge-dev")]
use nu_ansi_term::Color;
use std::{fmt, io::Write, thread};
use flexi_logger::DeferredNow;
use log::{LevelFilter, Record};
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
@@ -18,6 +28,7 @@ pub enum Type {
Network,
ProxyMode,
Ipc,
// Cache,
}
impl fmt::Display for Type {
@@ -39,6 +50,7 @@ impl fmt::Display for Type {
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
Type::Ipc => write!(f, "[IPC]"),
// Type::Cache => write!(f, "[Cache]"),
}
}
}
@@ -105,7 +117,8 @@ macro_rules! wrap_err {
macro_rules! logging {
// 带 println 的版本(支持格式化参数)
($level:ident, $type:expr, true, $($arg:tt)*) => {
println!("{} {}", $type, format_args!($($arg)*));
// We dont need println here anymore
// println!("{} {}", $type, format_args!($($arg)*));
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
@@ -155,3 +168,56 @@ macro_rules! logging_error {
logging_error!($type, false, $fmt $(, $arg)*);
};
}
#[cfg(not(feature = "tauri-dev"))]
pub fn get_log_level(log_level: &LevelFilter) -> String {
#[cfg(feature = "verge-dev")]
match log_level {
LevelFilter::Off => Color::Fixed(8).paint("OFF").to_string(),
LevelFilter::Error => Color::Red.paint("ERROR").to_string(),
LevelFilter::Warn => Color::Yellow.paint("WARN ").to_string(),
LevelFilter::Info => Color::Green.paint("INFO ").to_string(),
LevelFilter::Debug => Color::Blue.paint("DEBUG").to_string(),
LevelFilter::Trace => Color::Purple.paint("TRACE").to_string(),
}
#[cfg(not(feature = "verge-dev"))]
log_level.to_string()
}
#[cfg(not(feature = "tauri-dev"))]
pub fn console_colored_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &log::Record,
) -> std::io::Result<()> {
let current_thread = thread::current();
let thread_name = current_thread.name().unwrap_or("unnamed");
let level = get_log_level(&record.level().to_level_filter());
let line = record.line().unwrap_or(0);
write!(
w,
"[{}] {} [{}:{}] T[{}] {}",
now.format("%H:%M:%S%.3f"),
level,
record.module_path().unwrap_or("<unnamed>"),
line,
thread_name,
record.args(),
)
}
#[cfg(not(feature = "tauri-dev"))]
pub fn file_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &Record,
) -> std::io::Result<()> {
write!(
w,
"[{}] {} {}",
now.format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(),
record.args(),
)
}
+48 -23
View File
@@ -1,13 +1,17 @@
use anyhow::Result;
use isahc::http::{
header::{HeaderMap, HeaderValue, USER_AGENT},
StatusCode, Uri,
};
use base64::{Engine as _, engine::general_purpose};
use isahc::prelude::*;
use isahc::{config::SslOption, HttpClient};
use std::sync::Once;
use isahc::{HttpClient, config::SslOption};
use isahc::{
config::RedirectPolicy,
http::{
StatusCode, Uri,
header::{HeaderMap, HeaderValue, USER_AGENT},
},
};
use std::time::{Duration, Instant};
use sysproxy::Sysproxy;
use tauri::Url;
use tokio::sync::Mutex;
use tokio::time::timeout;
@@ -53,7 +57,6 @@ pub struct NetworkManager {
self_proxy_client: Mutex<Option<HttpClient>>,
system_proxy_client: Mutex<Option<HttpClient>>,
no_proxy_client: Mutex<Option<HttpClient>>,
init: Once,
last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: Mutex<usize>,
}
@@ -64,16 +67,11 @@ impl NetworkManager {
self_proxy_client: Mutex::new(None),
system_proxy_client: Mutex::new(None),
no_proxy_client: Mutex::new(None),
init: Once::new(),
last_connection_error: Mutex::new(None),
connection_error_count: Mutex::new(0),
}
}
pub fn init(&self) {
self.init.call_once(|| {});
}
async fn record_connection_error(&self, error: &str) {
let mut last_error = self.last_connection_error.lock().await;
*last_error = Some((Instant::now(), error.to_string()));
@@ -90,10 +88,11 @@ impl NetworkManager {
return true;
}
if let Some((time, _)) = &*last_error_guard {
if time.elapsed() < Duration::from_secs(30) && count > 2 {
return true;
}
if let Some((time, _)) = &*last_error_guard
&& time.elapsed() < Duration::from_secs(30)
&& count > 2
{
return true;
}
false
@@ -115,7 +114,8 @@ impl NetworkManager {
) -> Result<HttpClient> {
let proxy_uri_clone = proxy_uri.clone();
let headers_clone = default_headers.clone();
let client = {
{
let mut builder = HttpClient::builder();
builder = match proxy_uri_clone {
@@ -135,10 +135,10 @@ impl NetworkManager {
builder = builder.timeout(Duration::from_secs(secs));
}
Ok(builder.build()?)
};
builder = builder.redirect_policy(RedirectPolicy::Follow);
client
Ok(builder.build()?)
}
}
pub async fn create_request(
@@ -197,15 +197,40 @@ impl NetworkManager {
self.reset_clients().await;
}
let parsed = Url::parse(url)?;
let mut extra_headers = HeaderMap::new();
if !parsed.username().is_empty()
&& let Some(pass) = parsed.password()
{
let auth_str = format!("{}:{}", parsed.username(), pass);
let encoded = general_purpose::STANDARD.encode(auth_str);
extra_headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded))?,
);
}
let clean_url = {
let mut no_auth = parsed.clone();
no_auth.set_username("").ok();
no_auth.set_password(None).ok();
no_auth.to_string()
};
let client = self
.create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs)
.await?;
let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20));
let url_owned = url.to_string();
let response = match timeout(timeout_duration, async {
let mut response = client.get_async(&url_owned).await?;
let mut req = isahc::Request::get(&clean_url);
for (k, v) in extra_headers.iter() {
req = req.header(k, v);
}
let mut response = client.send_async(req.body(())?).await?;
let status = response.status();
let headers = response.headers().clone();
let body = response.text().await?;
+57 -18
View File
@@ -3,11 +3,13 @@ use tauri::AppHandle;
use crate::{
config::Config,
core::{handle, hotkey::Hotkey, sysopt, tray::Tray, CoreManager, Timer},
core::{
CoreManager, Timer, handle, hotkey::Hotkey, service::SERVICE_MANAGER, sysopt, tray::Tray,
},
logging, logging_error,
module::lightweight::auto_lightweight_mode_init,
module::lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
process::AsyncHandler,
utils::{init, logging::Type, network::NetworkManager, resolve::window::create_window, server},
utils::{init, logging::Type, server, window_manager::WindowManager},
};
pub mod dns;
@@ -16,11 +18,15 @@ pub mod ui;
pub mod window;
pub mod window_script;
pub fn resolve_setup_sync(app_handle: AppHandle) {
pub fn resolve_setup_handle(app_handle: AppHandle) {
init_handle(app_handle);
init_scheme();
init_embed_server();
NetworkManager::new().init();
}
pub fn resolve_setup_sync() {
AsyncHandler::spawn(|| async {
AsyncHandler::spawn_blocking(init_scheme);
AsyncHandler::spawn_blocking(init_embed_server);
});
}
pub fn resolve_setup_async() {
@@ -33,28 +39,35 @@ pub fn resolve_setup_async() {
std::thread::current().id()
);
// AsyncHandler::spawn_blocking(|| AsyncHandler::block_on(init_work_config()));
AsyncHandler::spawn(|| async {
init_work_config().await;
init_resources().await;
init_startup_script().await;
#[cfg(not(feature = "tauri-dev"))]
resolve_setup_logger().await;
init_service_manager().await;
futures::join!(
init_work_config(),
init_resources(),
init_startup_script(),
init_hotkey(),
);
init_timer().await;
init_hotkey().await;
init_once_auto_lightweight().await;
init_auto_lightweight_mode().await;
init_verge_config().await;
init_core_manager().await;
init_system_proxy().await;
init_system_proxy().await;
AsyncHandler::spawn_blocking(|| {
init_system_proxy_guard();
});
init_window().await;
init_tray().await;
refresh_tray_menu().await
let tray_and_refresh = async {
init_tray().await;
refresh_tray_menu().await;
};
futures::join!(init_window(), tray_and_refresh,);
});
let elapsed = start_time.elapsed();
@@ -108,6 +121,12 @@ pub(super) fn init_scheme() {
logging_error!(Type::Setup, true, init::init_scheme());
}
#[cfg(not(feature = "tauri-dev"))]
pub(super) async fn resolve_setup_logger() {
logging!(info, Type::Setup, true, "Initializing global logger...");
logging_error!(Type::Setup, true, init::init_logger().await);
}
pub async fn resolve_scheme(param: String) -> Result<()> {
logging!(
info,
@@ -144,6 +163,16 @@ pub(super) async fn init_hotkey() {
logging_error!(Type::Setup, true, Hotkey::global().init().await);
}
pub(super) async fn init_once_auto_lightweight() {
logging!(
info,
Type::Lightweight,
true,
"Running auto lightweight mode check..."
);
run_once_auto_lightweight().await;
}
pub(super) async fn init_auto_lightweight_mode() {
logging!(
info,
@@ -179,6 +208,15 @@ pub(super) async fn init_verge_config() {
logging_error!(Type::Setup, true, Config::init_config().await);
}
pub(super) async fn init_service_manager() {
logging!(info, Type::Setup, true, "Initializing service manager...");
logging_error!(
Type::Setup,
true,
SERVICE_MANAGER.lock().await.refresh().await
);
}
pub(super) async fn init_core_manager() {
logging!(info, Type::Setup, true, "Initializing core manager...");
logging_error!(Type::Setup, true, CoreManager::global().init().await);
@@ -213,6 +251,7 @@ pub(super) async fn refresh_tray_menu() {
}
pub(super) async fn init_window() {
logging!(info, Type::Setup, true, "Initializing main window...");
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
@@ -223,5 +262,5 @@ pub(super) async fn init_window() {
Handle::global().set_activation_policy_accessory();
}
}
create_window(!is_silent_start).await;
WindowManager::create_window(!is_silent_start).await;
}
+1 -2
View File
@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use percent_encoding::percent_decode_str;
use tauri::Url;
@@ -44,7 +44,6 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
// create_window(false).await;
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = match item.uid.clone() {
+1 -34
View File
@@ -1,8 +1,8 @@
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
};
use tokio::sync::Notify;
@@ -51,26 +51,6 @@ fn get_ui_ready_notify() -> &'static Arc<Notify> {
UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new()))
}
/// 等待 UI 就绪的异步函数,使用事件驱动而非轮询
pub async fn wait_for_ui_ready(timeout_seconds: u64) -> bool {
// 首先检查是否已经就绪
if get_ui_ready().load(Ordering::Acquire) {
return true;
}
// 使用 tokio::select! 同时等待通知和超时
tokio::select! {
_ = get_ui_ready_notify().notified() => {
// 收到通知后再次确认状态(防止虚假通知)
get_ui_ready().load(Ordering::Acquire)
}
_ = tokio::time::sleep(std::time::Duration::from_secs(timeout_seconds)) => {
// 超时,返回当前状态
get_ui_ready().load(Ordering::Acquire)
}
}
}
// 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
@@ -91,16 +71,3 @@ pub fn mark_ui_ready() {
// 通知所有等待的任务
get_ui_ready_notify().notify_waiters();
}
// 重置UI就绪状态
pub fn reset_ui_ready() {
get_ui_ready().store(false, Ordering::Release);
{
let state = get_ui_ready_state();
let mut stage = state.stage.write();
*stage = UiReadyStage::NotStarted;
}
logging!(info, Type::Window, true, "UI就绪状态已重置");
// 注意:这里不需要通知,因为重置后状态变为 false
}
+10 -256
View File
@@ -1,20 +1,11 @@
use std::time::{Duration, Instant};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use tauri::{Manager, WebviewWindow};
use tauri::WebviewWindow;
use crate::{
core::handle,
logging,
module::lightweight,
process::AsyncHandler,
logging, logging_error,
utils::{
logging::Type,
resolve::{
ui::{get_ui_ready, update_ui_ready_stage, wait_for_ui_ready, UiReadyStage},
window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
},
resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
},
};
@@ -25,68 +16,8 @@ const DEFAULT_HEIGHT: f64 = 700.0;
const MINIMAL_WIDTH: f64 = 520.0;
const MINIMAL_HEIGHT: f64 = 520.0;
// 窗口创建锁,防止并发创建窗口
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
}
/// 检查是否已存在窗口,如果存在则显示并返回 true
fn check_existing_window(is_show: bool) -> Option<bool> {
if let Some(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
}
return Some(true);
}
}
None
}
/// 获取窗口创建锁,防止并发创建
fn acquire_window_creation_lock() -> Result<(), bool> {
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
let (is_creating, last_time) = *creating;
let elapsed = last_time.elapsed();
if is_creating && elapsed < Duration::from_secs(2) {
logging!(
info,
Type::Window,
true,
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
elapsed.as_millis()
);
return Err(false);
}
*creating = (true, Instant::now());
Ok(())
}
/// 重置窗口创建锁
fn reset_window_creation_lock() {
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
*creating = (false, Instant::now());
logging!(debug, Type::Window, true, "窗口创建状态已重置");
}
/// 构建新的 WebView 窗口
fn build_new_window() -> Result<WebviewWindow, String> {
pub fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
logging!(
error,
@@ -97,7 +28,7 @@ fn build_new_window() -> Result<WebviewWindow, String> {
"无法获取app_handle".to_string()
})?;
tauri::WebviewWindowBuilder::new(
match tauri::WebviewWindowBuilder::new(
&app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
@@ -111,188 +42,11 @@ fn build_new_window() -> Result<WebviewWindow, String> {
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(WINDOW_INITIAL_SCRIPT)
.build()
.map_err(|e| {
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
e.to_string()
})
}
/// 窗口创建后的初始设置
async fn setup_window_post_creation() {
update_ui_ready_stage(UiReadyStage::NotStarted);
handle::Handle::global().mark_startup_completed();
logging!(
debug,
Type::Window,
true,
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
lightweight::run_once_auto_lightweight().await;
// 发送启动完成事件,触发前端开始加载
logging!(
debug,
Type::Window,
true,
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
}
/// 通过窗口标签处理窗口显示逻辑(减少任务大小的优化版本)
fn handle_window_display_by_label(window_label: String, is_show: bool) {
if !is_show {
logging!(
debug,
Type::Window,
true,
"is_show为false,窗口保持隐藏状态"
);
return;
}
// 通过标签重新获取窗口实例
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(error, Type::Window, true, "无法获取app handle");
return;
}
};
let window = match app_handle.get_webview_window(&window_label) {
Some(window) => window,
None => {
logging!(
error,
Type::Window,
true,
"无法通过标签获取窗口: {}",
window_label
);
return;
}
};
// 立即显示窗口
let _ = window.show();
let _ = window.set_focus();
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
3
} else {
8
};
logging!(
info,
Type::Window,
true,
"开始监控UI加载状态 (最多{}秒)...",
timeout_seconds
);
// 异步监控UI状态
AsyncHandler::spawn(move || async move {
monitor_ui_loading(timeout_seconds).await;
});
logging!(info, Type::Window, true, "窗口显示流程完成");
}
/// 监控 UI 加载状态 - 优雅的事件驱动版本
async fn monitor_ui_loading(timeout_seconds: u64) {
logging!(
debug,
Type::Window,
true,
"启动UI状态监控,使用事件驱动方式,超时{}秒",
timeout_seconds
);
// 使用事件驱动的方式等待 UI 就绪,完全消除轮询
let ui_ready = wait_for_ui_ready(timeout_seconds).await;
if ui_ready {
logging!(info, Type::Window, true, "UI已完全加载就绪(事件驱动)");
handle::Handle::global()
.get_window()
.map(|window| window.eval(INITIAL_LOADING_OVERLAY));
} else {
logging!(
warn,
Type::Window,
true,
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
// 超时后手动设置 UI 就绪状态(保持向后兼容性)
get_ui_ready().store(true, std::sync::atomic::Ordering::Release);
logging!(info, Type::Window, true, "超时后成功设置UI就绪状态");
}
}
pub async fn create_window(is_show: bool) -> bool {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
if !is_show {
lightweight::set_lightweight_mode(true).await;
handle::Handle::notify_startup_completed();
return false;
}
// 检查是否已存在窗口
if let Some(result) = check_existing_window(is_show) {
return result;
}
// 检查 app_handle 是否存在
if handle::Handle::global().app_handle().is_none() {
logging!(error, Type::Window, true, "No window found in app_handle");
return false;
}
// 获取窗口创建锁
if let Err(should_return) = acquire_window_creation_lock() {
return should_return;
}
// 构建新窗口
let newly_created_window = match build_new_window() {
{
Ok(window) => {
// 窗口创建成功,重置锁状态
reset_window_creation_lock();
window
logging_error!(Type::Window, true, window.eval(INITIAL_LOADING_OVERLAY));
Ok(window)
}
Err(_) => {
// 窗口创建失败,重置锁状态
reset_window_creation_lock();
return false;
}
};
logging!(debug, Type::Window, true, "主窗口实例创建成功");
// 获取窗口标签,减少闭包捕获的大小
let window_label = newly_created_window.label().to_string();
// 异步处理窗口后续设置,只捕获必要的小数据
setup_window_post_creation().await;
handle_window_display_by_label(window_label, is_show);
true
Err(e) => Err(e.to_string()),
}
}
+3 -3
View File
@@ -1,11 +1,11 @@
use super::resolve;
use crate::{
config::{Config, IVerge, DEFAULT_PAC},
config::{Config, DEFAULT_PAC, IVerge},
logging_error,
process::AsyncHandler,
utils::logging::Type,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use port_scanner::local_port_available;
use warp::Filter;
@@ -44,7 +44,7 @@ pub async fn check_singleton() -> Result<()> {
pub fn embed_server() {
let port = IVerge::get_singleton_port();
AsyncHandler::spawn_blocking(move || async move {
AsyncHandler::spawn(move || async move {
let visible = warp::path!("commands" / "visible").and_then(|| async {
Ok::<_, warp::Rejection>(warp::reply::with_status(
"ok".to_string(),
+58 -28
View File
@@ -1,4 +1,10 @@
use crate::{core::handle, logging, utils::logging::Type};
use crate::{
core::handle,
logging,
utils::{logging::Type, resolve::window::build_new_window},
};
use std::future::Future;
use std::pin::Pin;
use tauri::{Manager, WebviewWindow, Wry};
use once_cell::sync::OnceCell;
@@ -18,6 +24,8 @@ pub enum WindowOperationResult {
Hidden,
/// 创建了新窗口
Created,
/// 摧毁了窗口
Destroyed,
/// 操作失败
Failed,
/// 无需操作
@@ -138,7 +146,7 @@ impl WindowManager {
match current_state {
WindowState::NotExist => {
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
if Self::create_new_window().await {
if Self::create_window(true).await {
logging!(info, Type::Window, true, "窗口创建成功");
std::thread::sleep(std::time::Duration::from_millis(100));
WindowOperationResult::Created
@@ -198,7 +206,7 @@ impl WindowManager {
// 窗口不存在,创建新窗口
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
// 由于已经有防抖保护,直接调用内部方法
if Self::create_new_window().await {
if Self::create_window(true).await {
WindowOperationResult::Created
} else {
WindowOperationResult::Failed
@@ -316,28 +324,6 @@ impl WindowManager {
}
}
/// 隐藏主窗口
pub fn hide_main_window() -> WindowOperationResult {
logging!(info, Type::Window, true, "开始隐藏主窗口");
match Self::get_main_window() {
Some(window) => match window.hide() {
Ok(_) => {
logging!(info, Type::Window, true, "窗口已隐藏");
WindowOperationResult::Hidden
}
Err(e) => {
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed
}
},
None => {
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
WindowOperationResult::NoAction
}
}
}
/// 检查窗口是否可见
pub fn is_main_window_visible() -> bool {
Self::get_main_window()
@@ -360,10 +346,54 @@ impl WindowManager {
}
/// 创建新窗口,防抖避免重复调用
async fn create_new_window() -> bool {
use crate::utils::resolve;
pub fn create_window(is_show: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
Box::pin(async move {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
resolve::window::create_window(true).await
if !is_show {
return false;
}
match build_new_window() {
Ok(_) => {
logging!(info, Type::Window, true, "新窗口创建成功");
}
Err(e) => {
logging!(error, Type::Window, true, "新窗口创建失败: {}", e);
return false;
}
}
if WindowOperationResult::Failed != Self::show_main_window().await {
return false;
}
handle::Handle::global().mark_startup_completed();
true
})
}
/// 摧毁窗口
pub fn destroy_main_window() -> WindowOperationResult {
if let Some(window) = Self::get_main_window() {
let _ = window.destroy();
logging!(info, Type::Window, true, "窗口已摧毁");
#[cfg(target_os = "macos")]
{
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
handle::Handle::global().set_activation_policy_accessory();
}
return WindowOperationResult::Destroyed;
}
logging!(warn, Type::Window, true, "窗口摧毁失败");
WindowOperationResult::Failed
}
/// 获取详细的窗口状态信息
+2 -5
View File
@@ -1,5 +1,5 @@
{
"version": "2.4.1",
"version": "2.4.3",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
@@ -33,10 +33,7 @@
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
],
"windows": {
"installMode": "basicUi"
+1 -3
View File
@@ -1,9 +1,7 @@
import { AppDataProvider } from "./providers/app-data-provider";
import Layout from "./pages/_layout";
import { useNotificationPermission } from "./hooks/useNotificationPermission";
import { AppDataProvider } from "./providers/app-data-provider";
function App() {
useNotificationPermission();
return (
<AppDataProvider>
<Layout />
+3 -2
View File
@@ -1,6 +1,7 @@
import React, { useSyncExternalStore } from "react";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import React, { useSyncExternalStore } from "react";
import {
subscribeNotices,
hideNotice,
+3 -2
View File
@@ -1,4 +1,5 @@
import { ReactNode } from "react";
/* eslint-disable react/prop-types */
import { LoadingButton } from "@mui/lab";
import {
Button,
Dialog,
@@ -8,7 +9,7 @@ import {
type SxProps,
type Theme,
} from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { ReactNode } from "react";
interface Props {
title: ReactNode;
+1 -1
View File
@@ -1,5 +1,5 @@
import { alpha, Box, Typography } from "@mui/material";
import { InboxRounded } from "@mui/icons-material";
import { alpha, Box, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
interface Props {
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import { Box, styled } from "@mui/material";
import React from "react";
type Props = {
label: string;
+2 -4
View File
@@ -1,7 +1,7 @@
import React from "react";
import { Box, CircularProgress } from "@mui/material";
import React from "react";
export interface BaseLoadingOverlayProps {
interface BaseLoadingOverlayProps {
isLoading: boolean;
}
@@ -29,5 +29,3 @@ export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
</Box>
);
};
export default BaseLoadingOverlay;
+3 -2
View File
@@ -1,7 +1,8 @@
import React, { ReactNode } from "react";
import { Typography } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary";
import { useTheme } from "@mui/material/styles";
import React, { ReactNode } from "react";
import { BaseErrorBoundary } from "./base-error-boundary";
interface Props {
title?: React.ReactNode; // the page title
+4 -3
View File
@@ -1,10 +1,11 @@
import { Box, SvgIcon, TextField, styled } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
import { useTranslation } from "react-i18next";
export type SearchState = {
text: string;
@@ -81,8 +82,8 @@ export const BaseSearchBox = (props: SearchProps) => {
return (content: string) => {
if (!searchText) return true;
let item = !matchCase ? content.toLowerCase() : content;
let searchItem = !matchCase ? searchText.toLowerCase() : searchText;
const item = !matchCase ? content.toLowerCase() : content;
const searchItem = !matchCase ? searchText.toLowerCase() : searchText;
if (useRegularExpression) {
return new RegExp(searchItem).test(item);
+1 -1
View File
@@ -1,10 +1,10 @@
import { InfoRounded } from "@mui/icons-material";
import {
Tooltip,
IconButton,
IconButtonProps,
SvgIconProps,
} from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
interface Props extends IconButtonProps {
title?: string;
@@ -1,10 +1,10 @@
import React, { Component, ErrorInfo, ReactNode } from "react";
import { Box, Typography, Button, Alert, Collapse } from "@mui/material";
import {
ErrorOutlineRounded,
RefreshRounded,
BugReportRounded,
} from "@mui/icons-material";
import { Box, Typography, Button, Alert, Collapse } from "@mui/material";
import React, { Component, ErrorInfo, ReactNode } from "react";
import { useTranslation } from "react-i18next";
interface Props {
@@ -1,10 +1,11 @@
import dayjs from "dayjs";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, Snackbar, useTheme } from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { t } from "i18next";
import { forwardRef, useImperativeHandle, useState } from "react";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void;
@@ -1,5 +1,4 @@
import dayjs from "dayjs";
import { useLockFn } from "ahooks";
import { CloseRounded } from "@mui/icons-material";
import {
styled,
ListItem,
@@ -8,7 +7,9 @@ import {
Box,
alpha,
} from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
@@ -1,10 +1,11 @@
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
import dayjs from "dayjs";
import { useLocalStorage } from "foxact/use-local-storage";
import { t } from "i18next";
import { useMemo, useState } from "react";
import parseTraffic from "@/utils/parse-traffic";
import { truncateStr } from "@/utils/truncate-str";
interface Props {
connections: IConnectionsItem[];
+6 -4
View File
@@ -1,11 +1,13 @@
import { useTranslation } from "react-i18next";
import { Typography, Stack, Divider } from "@mui/material";
import { DeveloperBoardOutlined } from "@mui/icons-material";
import { useClash } from "@/hooks/use-clash";
import { EnhancedCard } from "./enhanced-card";
import { Typography, Stack, Divider } from "@mui/material";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useClash } from "@/hooks/use-clash";
import { useAppData } from "@/providers/app-data-provider";
import { EnhancedCard } from "./enhanced-card";
// 将毫秒转换为时:分:秒格式的函数
const formatUptime = (uptimeMs: number) => {
const hours = Math.floor(uptimeMs / 3600000);
+7 -6
View File
@@ -1,16 +1,17 @@
import { useTranslation } from "react-i18next";
import { Box, Typography, Paper, Stack } from "@mui/material";
import { useLockFn } from "ahooks";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import {
LanguageRounded,
MultipleStopRounded,
DirectionsRounded,
} from "@mui/icons-material";
import { Box, Typography, Paper, Stack } from "@mui/material";
import { useLockFn } from "ahooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
export const ClashModeCard = () => {
const { t } = useTranslation();
+33 -49
View File
@@ -1,4 +1,15 @@
import { useTranslation } from "react-i18next";
import {
SignalWifi4Bar as SignalStrong,
SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium,
SignalWifi1Bar as SignalWeak,
SignalWifi0Bar as SignalNone,
WifiOff as SignalError,
ChevronRight,
SortRounded,
AccessTimeRounded,
SortByAlphaRounded,
} from "@mui/icons-material";
import {
Box,
Typography,
@@ -15,24 +26,13 @@ import {
IconButton,
} from "@mui/material";
import { useEffect, useState, useMemo, useCallback } from "react";
import {
SignalWifi4Bar as SignalStrong,
SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium,
SignalWifi1Bar as SignalWeak,
SignalWifi0Bar as SignalNone,
WifiOff as SignalError,
ChevronRight,
SortRounded,
AccessTimeRounded,
SortByAlphaRounded,
} from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { EnhancedCard } from "@/components/home/enhanced-card";
import { updateProxy, deleteConnection } from "@/services/cmds";
import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useAppData } from "@/providers/app-data-provider";
import delayManager from "@/services/delay";
// 本地存储的键名
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
@@ -45,7 +45,7 @@ interface ProxyOption {
}
// 排序类型: 默认 | 按延迟 | 按字母
export type ProxySortType = 0 | 1 | 2;
type ProxySortType = 0 | 1 | 2;
function convertDelayColor(delayValue: number) {
const colorStr = delayManager.formatDelayColor(delayValue);
@@ -94,8 +94,18 @@ export const CurrentProxyCard = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const { verge } = useVerge();
const { proxies, connections, clashConfig, refreshProxy } = useAppData();
const { proxies, clashConfig, refreshProxy } = useAppData();
// 统一代理选择器
const { handleSelectChange } = useProxySelection({
onSuccess: () => {
refreshProxy();
},
onError: (error) => {
console.error("代理切换失败", error);
refreshProxy();
},
});
// 判断模式
const mode = clashConfig?.mode?.toLowerCase() || "rule";
@@ -113,8 +123,6 @@ export const CurrentProxyCard = () => {
proxyData: {
groups: { name: string; now: string; all: string[] }[];
records: Record<string, any>;
globalProxy: string;
directProxy: any;
};
selection: {
group: string;
@@ -127,8 +135,6 @@ export const CurrentProxyCard = () => {
proxyData: {
groups: [],
records: {},
globalProxy: "",
directProxy: { name: "DIRECT" }, // 默认值避免 undefined
},
selection: {
group: "",
@@ -253,8 +259,6 @@ export const CurrentProxyCard = () => {
proxyData: {
groups: filteredGroups,
records: proxies.records || {},
globalProxy: proxies.global?.now || "",
directProxy: proxies.records?.DIRECT || { name: "DIRECT" },
},
selection: {
group: newGroup,
@@ -310,7 +314,7 @@ export const CurrentProxyCard = () => {
// 处理代理节点变更
const handleProxyChange = useCallback(
async (event: SelectChangeEvent) => {
(event: SelectChangeEvent) => {
if (isDirectMode) return;
const newProxy = event.target.value;
@@ -330,35 +334,15 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
try {
await updateProxy(currentGroup, newProxy);
// 自动关闭连接设置
if (verge?.auto_close_connection && previousProxy) {
connections.data.forEach((conn: any) => {
if (conn.chains.includes(previousProxy)) {
deleteConnection(conn.id);
}
});
}
// 延长刷新延迟时间
setTimeout(() => {
refreshProxy();
}, 500);
} catch (error) {
console.error("更新代理失败", error);
}
const skipConfigSave = isGlobalMode || isDirectMode;
handleSelectChange(currentGroup, previousProxy, skipConfigSave)(event);
},
[
isDirectMode,
isGlobalMode,
state.proxyData.records,
state.selection,
verge?.auto_close_connection,
refreshProxy,
debouncedSetState,
connections.data,
handleSelectChange,
],
);
@@ -1,3 +1,4 @@
import { Box, useTheme } from "@mui/material";
import {
forwardRef,
useImperativeHandle,
@@ -8,16 +9,16 @@ import {
useRef,
memo,
} from "react";
import { Box, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import parseTraffic from "@/utils/parse-traffic";
import {
useTrafficGraphDataEnhanced,
type ITrafficDataPoint,
} from "@/hooks/use-traffic-monitor";
import parseTraffic from "@/utils/parse-traffic";
// 流量数据项接口
export interface ITrafficItem {
interface ITrafficItem {
up: number;
down: number;
timestamp?: number;
+1 -1
View File
@@ -2,7 +2,7 @@ import { Box, Typography, alpha, useTheme } from "@mui/material";
import { ReactNode } from "react";
// 自定义卡片组件接口
export interface EnhancedCardProps {
interface EnhancedCardProps {
title: ReactNode;
icon: ReactNode;
action?: ReactNode;
+22 -20
View File
@@ -1,14 +1,3 @@
import { useRef, useCallback, memo, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
Typography,
Paper,
alpha,
useTheme,
PaletteColor,
Grid,
Box,
} from "@mui/material";
import {
ArrowUpwardRounded,
ArrowDownwardRounded,
@@ -17,19 +6,32 @@ import {
CloudUploadRounded,
CloudDownloadRounded,
} from "@mui/icons-material";
import {
Typography,
Paper,
alpha,
useTheme,
PaletteColor,
Grid,
Box,
} from "@mui/material";
import { useRef, useCallback, memo, useMemo } from "react";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider";
import { isDebugEnabled, gc } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import {
EnhancedCanvasTrafficGraph,
type EnhancedCanvasTrafficGraphRef,
} from "./enhanced-canvas-traffic-graph";
import { useVisibility } from "@/hooks/use-visibility";
import { useVerge } from "@/hooks/use-verge";
import parseTraffic from "@/utils/parse-traffic";
import { isDebugEnabled, gc } from "@/services/cmds";
import { ReactNode } from "react";
import { useAppData } from "@/providers/app-data-provider";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import useSWR from "swr";
interface StatCardProps {
icon: ReactNode;
+21 -19
View File
@@ -1,4 +1,12 @@
import { useTranslation } from "react-i18next";
import {
CloudUploadOutlined,
StorageOutlined,
UpdateOutlined,
DnsOutlined,
SpeedOutlined,
EventOutlined,
LaunchOutlined,
} from "@mui/icons-material";
import {
Box,
Typography,
@@ -10,24 +18,18 @@ import {
Link,
keyframes,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import {
CloudUploadOutlined,
StorageOutlined,
UpdateOutlined,
DnsOutlined,
SpeedOutlined,
EventOutlined,
LaunchOutlined,
} from "@mui/icons-material";
import dayjs from "dayjs";
import parseTraffic from "@/utils/parse-traffic";
import { useMemo, useCallback, useState } from "react";
import { openWebUrl, updateProfile } from "@/services/cmds";
import { useLockFn } from "ahooks";
import { showNotice } from "@/services/noticeService";
import { EnhancedCard } from "./enhanced-card";
import dayjs from "dayjs";
import { useMemo, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAppData } from "@/providers/app-data-provider";
import { openWebUrl, updateProfile } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import parseTraffic from "@/utils/parse-traffic";
import { EnhancedCard } from "./enhanced-card";
// 定义旋转动画
const round = keyframes`
@@ -55,7 +57,7 @@ interface ProfileExtra {
expire: number;
}
export interface ProfileItem {
interface ProfileItem {
uid: string;
type?: "local" | "remote" | "merge" | "script";
name?: string;
@@ -68,7 +70,7 @@ export interface ProfileItem {
option?: any;
}
export interface HomeProfileCardProps {
interface HomeProfileCardProps {
current: ProfileItem | null | undefined;
onProfileUpdated?: () => void;
}

Some files were not shown because too many files have changed in this diff Show More