Compare commits

..
752 Commits
Author SHA1 Message Date
Tunglies 20e93ecc8e Revert "chore: updater channel combined"
This reverts commit aeaab10fcb.
2025-03-25 01:38:52 +08:00
Tunglies aeaab10fcb chore: updater channel combined 2025-03-25 01:32:37 +08:00
wonfen cbbd99a748 refactor: service reinstallation logic on detection failure 2025-03-25 00:51:38 +08:00
Tunglies 228b013249 chore: remove update endpoints temporary 2025-03-25 00:28:28 +08:00
Tunglies 55fdce58d0 feat: add default update log resolution and improve error handling 2025-03-25 00:23:19 +08:00
Tunglies c1abb78cf9 refactor: remove deprecated lightweight module and adjust macOS activation policy 2025-03-24 20:11:38 +08:00
Tunglies 53023fa189 fix: update lightweight mode implementation and fix MacOS dock icon visibility 2025-03-24 19:16:21 +08:00
Tunglies 827ab4b1c5 fix: correct spelling of "Lightweight" in tray menu 2025-03-24 18:54:39 +08:00
Skrepysh 2fbcbafc53 Update russian translation (#3109) 2025-03-24 18:34:50 +08:00
wonfen ab140d6990 chore: update required service version 2025-03-24 03:59:55 +08:00
Tunglies 0e38ed7112 chore: alpha update cron 2025-03-23 23:04:40 +08:00
Tunglies 5e3ff37602 chore: update bug_report.yml 2025-03-23 22:56:42 +08:00
Tunglies 3f84368fc0 feat: add option to enable/disable tray icon display on MacOS 2025-03-23 16:37:27 +08:00
Tunglies 6ded03c616 chore: remove unused import 2025-03-23 14:28:28 +08:00
wonfen 15fd8b17c3 perf: improve Clash mode switch responsiveness on home card 2025-03-23 04:54:18 +08:00
Tunglies 7cb0c8173a feat: add lightweight mode entry and related hotkey support 2025-03-23 03:10:48 +08:00
wonfen 9b04721b3d perf: simplify code logic and improve efficiency 2025-03-23 02:16:06 +08:00
wonfen bc39d56b0f chore: update required service version 2025-03-23 00:16:32 +08:00
Tunglies a1f671f380 feat: enhance tray icon handling with caching and speed rate rendering 2025-03-22 23:00:45 +08:00
Tunglies 147e7d7390 chore: update UPDATELOG 2025-03-22 17:23:10 +08:00
Tunglies e9684fd639 fix: update alpha workflow to correctly extract alpha logs from UPDATELOG.md 2025-03-22 17:21:45 +08:00
Tunglies d7e9d9498d feat: update versioning in package.json, Cargo.toml, and tauri.conf.json to append '-alpha' 2025-03-22 17:19:11 +08:00
Tunglies b8161b01c1 feat: ensure Mihomo and Verge services are running before executing commands 2025-03-22 17:05:42 +08:00
wonfen 079591da59 feat: add singleton check after core startup in sidecar mode 2025-03-22 15:01:55 +08:00
wonfen 1461ab0d37 chore: update preview img 2025-03-22 11:07:20 +08:00
wonfen c37a69ea1b fix: alpha workflow 2025-03-22 08:45:55 +08:00
wonfen f78977e267 chore: remove unused code of current proxy card 2025-03-22 06:25:10 +08:00
Tunglies 85124549d7 feat: add scripts for managing alpha versioning and update workflow 2025-03-22 05:11:32 +08:00
Tunglies 90f99a3879 chore: update UPDATELOG 2025-03-22 04:53:27 +08:00
Tunglies 8336c43e86 fix: linux hanlding mihomo-core and verge-service communication 2025-03-22 04:51:38 +08:00
wonfen dbbd7a1ed7 perf: optimize proxy refresh mechanism for home page current proxy card 2025-03-22 04:34:19 +08:00
wonfen e246cf33da fix: theme color on connection detail card
fix: home page clash info card proxy address
2025-03-22 04:26:28 +08:00
wonfen 22ae6bc583 fix: port setting sync problem 2025-03-21 12:28:51 +08:00
wonfen 9d13063d53 chore: add missing i18n 2025-03-21 10:29:20 +08:00
wonfen 47d581f042 refactor: auto-truncate long text on home profile card
fix: sync system proxy and TUN mode status indicators on home proxy mode card
2025-03-21 05:23:45 +08:00
Tunglies 6a1587ec73 fix: add libxslt1.1 dependency to Ubuntu installation in workflows 2025-03-20 23:36:51 +08:00
Tunglies fe891d23d2 v2.2.1 2025-03-20 23:22:34 +08:00
Tunglies 07c151b287 v2.2.1-alpha.1 2025-03-20 23:20:07 +08:00
Tunglies 51d61a4bb7 fix: homepage entry lightweight mode exiting Macos tray icon
fix: lightweight mode better handling and logging logic
2025-03-20 23:17:37 +08:00
Tunglies 37e63ec67c chore: v2.2.1 2025-03-20 22:33:20 +08:00
Tunglies 106861f761 fix: homepage proxy card handle direct mode 2025-03-20 21:51:12 +08:00
Tunglies edb9ccdca9 feat: enhance alpha release workflow to fetch update logs and generate release notes 2025-03-20 19:42:04 +08:00
Tunglies 6dfa53fc23 fix: update workflow to delete old release assets instead of the release itself 2025-03-20 19:33:26 +08:00
Tunglies abe0750d07 chore: update version to 2.2.1-alpha and add alpha update logs
refactor: simplify version change detection in alpha workflow

chore: alpha delete old release then release new one

fix: update alpha workflow to handle missing ALPHA_LOGS and improve release notes generation

fix: update job dependencies in alpha workflow to include delete_otld_release
2025-03-20 19:11:48 +08:00
Tunglies 2195576553 fix: remove macOS application menu setup due to CMD+C/V/A issues 2025-03-20 18:14:18 +08:00
Tunglies 1dc015f411 chore: bug report template remove os label 2025-03-20 16:39:29 +08:00
Tunglies 49d13c9ecb chore: UPDATELOG update 2025-03-20 15:49:00 +08:00
Tunglies d8cd91b725 fix: ensure main window title is set correctly on macOS 2025-03-20 15:43:59 +08:00
wonfen 40eede325b release 2.2.0 2025-03-20 14:44:26 +08:00
Tunglies 59e07a3e55 Revert "fix: update permission config with app icon and name"
This reverts commit 46af096cb1.
2025-03-20 14:17:41 +08:00
Tunglies 3d32d2d08f feat(hotkey): macos support CMD+W to close window as default 2025-03-20 13:02:26 +08:00
wonfen 46af096cb1 fix: update permission config with app icon and name 2025-03-20 12:43:22 +08:00
Tunglies 486f33fd7a fix: update system architecture retrieval method in PlatformSpecification 2025-03-20 06:20:19 +08:00
Tunglies 5ebfcc6f98 fix: update service response check for correct status code and message 2025-03-20 06:18:14 +08:00
Tunglies 7fa1767645 feat: implement auto lightweight mode timer functionality
This commit implements the automatic lightweight mode feature with timer functionality:

- Rename configuration properties from auto_enter_lite_mode to enable_auto_light_weight_mode and auto_enter_lite_mode_delay to auto_light_weight_minutes for better clarity
- Add window event listeners to detect when window is closed or gets focus
- Implement timer system to automatically enter lightweight mode after configured time
- Remove exit_lightweight_mode function as it's no longer needed with the new implementation
- Update UI components to reflect the new property names
- Add logging for lightweight mode operations
- Initialize lightweight mode based on user configuration at startup

The feature now allows users to set a timer that will automatically enter lightweight mode
after closing the main window, which can be cancelled by focusing the window again.
2025-03-20 06:01:38 +08:00
Tunglies 33d54b27f4 feat: implement lightweight mode functionality and update related settings 2025-03-20 03:23:14 +08:00
wonfen f8dbc7bea0 feat: lite mode settings 2025-03-20 01:44:43 +08:00
Tunglies 83d338eaa4 fix: hotkeys on windows crash 2025-03-19 18:41:26 +08:00
wonfen 9732b2be14 chore: update deps 2025-03-19 11:07:57 +08:00
wonfen 8a5232df20 feat: add up/down name to home traffic graph card 2025-03-19 10:38:21 +08:00
wonfen 5f0f6db0d5 chore: change defaut start page to home 2025-03-19 05:48:20 +08:00
wonfen 7a86e4bd85 fix: cannot detect service mode on home card 2025-03-19 05:25:38 +08:00
Tunglies f18a8c901f feat: add lite mode toggle to home page 2025-03-19 02:18:20 +08:00
Tunglies 23d161c298 refactor: improve proxy retrieval and add window destruction method 2025-03-19 02:04:01 +08:00
Tunglies 3e2148e236 feat: add macOS application menu integration 2025-03-18 18:40:53 +08:00
Tunglies 70a4bd1074 chore: remove tray icon configuration from webview JSON files 2025-03-18 15:31:23 +08:00
Tunglies e13d3e7f83 Revert "feat: add Rust installation step and configure alpha release details"
This reverts commit 1d5471e70d.
2025-03-18 14:37:56 +08:00
wonfen e21bc22fa3 chore: add missing colorful svg for home and unlock menu 2025-03-18 10:37:00 +08:00
wonfen d8e85c9cfc refactor: revise data retrieval for homepage traffic stats 2025-03-18 09:05:44 +08:00
Tunglies 1d5471e70d feat: add Rust installation step and configure alpha release details 2025-03-18 01:08:46 +08:00
wonfen a2a8dd182a feat: persist graph data after page reload 2025-03-18 00:37:10 +08:00
wonfen 511fb9e2cf feat: limit max url lenght on home profile card 2025-03-18 00:18:26 +08:00
wonfen 14f90ac925 fix: auto launch 2025-03-17 13:51:52 +08:00
wonfen 8e09bf525e fix: resolve Netflix detection error 2025-03-17 11:57:12 +08:00
wonfen 7e7ef5766e perf: optimize all home page components 2025-03-17 11:47:02 +08:00
wonfen b078560ac1 feat: sync auto-start status 2025-03-17 09:48:44 +08:00
wonfen 1e481ed1c1 chore: update i18n for unlock test 2025-03-17 07:45:49 +08:00
wonfen e957c10138 perf: optimize CPU and memory usage of homepage traffic chart 2025-03-16 14:34:29 +08:00
wonfen 79bb0f29f9 fix: sync proxy node selection 2025-03-16 14:24:58 +08:00
wonfen f4cb978118 feat: unlock test page 2025-03-16 12:15:35 +08:00
Tunglies e16b23946a refactor(timer): improve timer management with robust error handling
This commit improves the timer management system with the following enhancements:

Replace Mutex with RwLock for better read concurrency in timer state
Add structured TimerTask type to store task metadata
Use atomic boolean for initialization flag instead of mutex
Implement comprehensive error handling with detailed logging
Add rollback capability when task operations fail
Reduce lock contention by generating task diffs outside locks
Add timing metrics for task execution
Improve code organization and documentation
2025-03-15 18:58:12 +08:00
Tunglies df538ca9a3 refactor(config): use bitflags for tracking update operations
Replace multiple boolean variables with a bitflag approach for tracking
required update operations in the patch_verge function. This improves
code maintainability and potentially performance by:

1. Using a single integer variable with bit operations instead of multiple booleans
2. Defining clear flags as enum variants for better code readability
3. Simplifying flag checks with bitwise operations

The UpdateFlags enum provides a clear and type-safe way to represent
different types of updates needed when patching Verge configuration.
2025-03-15 18:42:57 +08:00
Tunglies b97f0734d0 Optimize hotkey management to reduce lock contention and improve performance
- Minimize mutex lock durations in update() by processing data outside critical sections
- Pre-allocate collections to avoid unnecessary reallocations
- Replace forEach-style loops with more efficient for loops
- Add defensive null checks when accessing app_handle
- Improve error handling with more robust Option unwrapping
- Enhance code readability with descriptive comments
2025-03-15 17:52:14 +08:00
Tunglies cbd2dd972a structure: move out crate_mihomo_api 2025-03-15 14:47:02 +08:00
Tunglies 00d6bcc00f refactor: update request method handling to use reqwest::Method enum
fix: duplicated checks tray menu
2025-03-15 13:23:17 +08:00
Tunglies 43ffb3c9e0 fix: optimize speed rate update logic for small values 2025-03-14 22:40:56 +08:00
wonfen 9fd75f0efb feat: home page 2025-03-14 13:31:34 +08:00
Tunglies 49f0ac407e Revert "feat: add trigger updater workflow to GitHub actions for release and alpha workflows"
This reverts commit ff8426d95e.
2025-03-14 02:00:55 +08:00
Tunglies ff8426d95e feat: add trigger updater workflow to GitHub actions for release and alpha workflows 2025-03-14 01:31:19 +08:00
GKarbon 3ca4ac2726 chore: correct typo in src/locales/en.json (#2978)
This pull request corrects a typo in src/locales/en.json by replacing “than” with “then”, gives more accurate instructions.
2025-03-14 01:04:44 +08:00
Christine. 07441ba95e fix: build failed due to vite unexpected output dir config. (#2981) 2025-03-14 00:57:17 +08:00
Tunglies e74fd068d5 chore: remove tray icon configuration from Linux app settings 2025-03-13 19:03:27 +08:00
Tunglies 965e281348 fix: increase request timeout to 60 seconds for better reliability 2025-03-13 15:16:54 +08:00
Tunglies a7f66c3d65 chore: git hooks for linter and formatter 2025-03-13 12:51:20 +08:00
Mimi f7f15544ba feat: additional macos tray event handling option for menu display (#2958) 2025-03-13 10:08:38 +08:00
MaqicXu 6ea79daf73 refactor(timer): enhance timer initialization and task handling (#2956)
- Add initialization flag to prevent duplicate timer initialization
- Improve logging for better debugging and monitoring
- Refactor async task function with proper error handling
- Add more detailed log messages throughout the timer lifecycle
2025-03-12 22:36:25 +08:00
Christine. 99abbfbc4c chore: Change default TUN stack from 'mixed' to 'gvisor' (#2967) 2025-03-12 21:45:14 +08:00
Tunglies 24df5e02d6 fix: windows unmatched tray 2025-03-12 13:55:11 +08:00
Tunglies 6aaec3de5d fix: windows different tray icon display 2025-03-12 13:28:04 +08:00
Tunglies 39d811cc62 feat: update systray creation to use TrayIconBuilder and pass app reference
fix: macos systray duplicated icon
2025-03-12 13:04:15 +08:00
Tunglies 832a0ac281 feat: add alpha update endpoints to tauri configuration 2025-03-11 01:31:31 +08:00
Tunglies 7b0df043be feat: add support for alpha updates and enhance updater functionality
feat: improve release handling by adding creation logic for non-existent releases
2025-03-11 01:27:17 +08:00
Tunglies d470bf2dd9 rm: label issues workflow 2025-03-10 01:01:12 +08:00
Tunglies b5e8a1ddb5 chore: rename "DNS Settings" to "DNS Overwrite" in UI for consistency
This change updates the label for the DNS toggle setting from "DNS Settings" to "DNS Overwrite"
in the ClashVerge settings interface. The change provides better consistency with the translation
keys and more clearly communicates the function of the setting, which is to override system DNS.
The corresponding translation keys have been updated in both English and Chinese localization files.
2025-03-10 00:57:55 +08:00
Tunglies 08f3357d9a feat: add scheduled workflow and commit change check to alpha build 2025-03-09 17:42:13 +08:00
Tunglies 107c54921d refactor: update MihomoManager to handle traffic WebSocket URL and authorization 2025-03-09 14:44:15 +08:00
wonfen ca1f7a2b31 feat: enhance latency test logging and error handling 2025-03-09 04:22:34 +08:00
wonfen 95e8fe38f2 refactor: improve webSocket connection handling and error recovery 2025-03-09 04:22:01 +08:00
wonfen cd6eb26a41 feat: add icon file content check to prevent saving failed downloads 2025-03-09 02:34:57 +08:00
Tunglies 9f2c4b4f35 refactor: migrate clash client info retrieval to MihomoManager 2025-03-09 00:40:16 +08:00
Tunglies adbf98cb15 rm: clash-api dead code 2025-03-09 00:29:14 +08:00
Tunglies c9b3fec09c refactor: improve request handling and response processing in MihomoManager 2025-03-09 00:27:12 +08:00
Tunglies 364fb16b7b refactor: streamline clash delay test and improve API interactions 2025-03-09 00:04:48 +08:00
Tunglies 634ed9cb46 refactor: streamline clash mode handling and improve API interactions 2025-03-08 22:41:14 +08:00
wonfen cddb117402 feat: optimize icon cache download and DNS view styling 2025-03-08 13:31:20 +08:00
wonfen bd6c4e00c4 refactor: restructure DNS setting logic 2025-03-08 11:25:00 +08:00
wonfen bbcfb4901f refactor: refine DNS handling to follow config and merge settings 2025-03-08 03:34:25 +08:00
wonfen 230a0eeb64 fix: regex for first character matching 2025-03-07 14:07:10 +08:00
wonfen c4c49f61df perf: faster app exit 2025-03-07 13:44:07 +08:00
TianHua Liu 6d5c4f86d3 fix: Notice @ts-ignore (#2896)
thx
2025-03-07 12:46:30 +08:00
0XE 59f22b9cd2 feat: Add x-data-grid component localization (#2925)
thx
2025-03-07 12:45:56 +08:00
wonfen 6caf2cd057 test: remove entitlements.plist items 2025-03-07 06:49:24 +08:00
Tunglies 684128ca68 Update dependencies and refactor encryption logic
Updates multiple dependencies to their latest versions in Cargo.lock and Cargo.toml.
Refactors encryption logic to use updated getrandom API.
Improves tray speed rate display by using ab_glyph for font rendering.
2025-03-06 18:56:31 +08:00
wonfen 6925adc66f feat: enable dns settings by default 2025-03-06 14:40:35 +08:00
wonfen e1e9d91d86 feat: add dns settings 2025-03-06 14:30:43 +08:00
Christine. 881c136b1e add: missing i18n text. (#2917) 2025-03-05 22:28:28 +08:00
0XE 5bc699b19c fix: ISSUES #2727 (#2913) 2025-03-05 14:20:07 +08:00
Christine. 52ba1c5a30 workflow: remove renaming behavior. (#2909) 2025-03-05 11:41:15 +08:00
Tunglies 817d68546e feat(mihomo): refactor MihomoManager for global access and improve proxy retrieval (#2906) 2025-03-05 10:58:54 +08:00
wonfen c0f5c231ad feat: add admin permission prompt for system service 2025-03-05 10:22:57 +08:00
Tunglies 9677d8670b test: crate_mihomo_api additional headers 2025-03-05 08:14:37 +08:00
Tunglies b01630d31c fix: extern controler api secert with headers 2025-03-05 08:09:42 +08:00
Tunglies f771f4720f refacture: Mihomo API integration (#2900)
* feat: add mihomo_api crate as a workspace member

Added a new mihomo_api crate to handle interactions with the Mihomo API. This modular approach provides a dedicated interface for fetching and managing proxy data from Mihomo servers. The implementation includes functionality to refresh and retrieve both proxies and provider proxies with proper error handling and timeouts. Added this crate as a workspace member and included it as a dependency in the main project.

* Refactors Mihomo API integration

Simplifies proxy fetching by removing the MihomoManager structure.

Updates the get_proxies and get_providers_proxies functions to directly use the mihomo_api module.

Removes unused Mihomo API related files and modules for cleaner codebase.

Enhances overall maintainability and performance.
2025-03-05 00:45:08 +08:00
0XE f0becb5003 fix: issues #2838 (#2886) 2025-03-04 20:46:17 +08:00
Tunglies 405e8df825 feat(sysinfo): Add diagnostic information enhancements (#2880)
Enhanced the PlatformSpecification struct with additional diagnostic information including:
- Added Verge version information to diagnostic output
- Added running mode information (Service/Sidecar/Not Running)
- Improved Debug implementation to display all diagnostic fields
- Implemented asynchronous detection of core running mode

This change helps users provide more complete system information when reporting issues.
2025-03-04 11:52:22 +08:00
wonfen 49fc7ff7ca fix: sync system proxy status indicator with hotkey 2025-03-04 06:57:42 +08:00
wonfen fde9c8aaee fix: correct type declarations for getProxiesInner and getProxyProviders 2025-03-04 02:26:26 +08:00
Tunglies 8f95c28050 feat(proxy): add proxy commands and integrate with API
Add new proxy.rs module with get_proxies and get_providers_proxies commands.
Update mod.rs and lib.rs to re-export and register proxy commands.
Update API.ts to use invoke for proxy commands.
Minor formatting improvements in module/mihomo.rs.
2025-03-04 01:01:24 +08:00
Tunglies 2cd97c7785 feat: add Mihomo API modules and manager (#2869)
• Introduce new API caller implementations for Mihomo in model and module layers.
• Add configuration and API integration files under /src-tauri/src/config/api and /src-tauri/src/model/api.
• Implement a singleton MihomoAPICaller with async API call support and integration tests.
• Create a new MihomoManager module to refresh and fetch proxies from the API.
• Update Cargo.lock and Cargo.toml with additional dependencies (async-trait, env_logger, mockito, tempfile, etc.) related to the Mihomo API support.
2025-03-03 19:31:44 +08:00
wonfen 6e48781687 Revert "refactor: improve proxy group UI and spacing (#2835)"
This reverts commit b31cbda615.
2025-03-03 14:47:05 +08:00
wonfen dd510b2ee9 feat: show service mode installation prompts in user mode 2025-03-03 14:42:31 +08:00
Tunglies fadae3f7dc fix: speed format runns by docs 2025-03-03 11:36:21 +08:00
Tunglies fdf8a6b2ba Revert "style: update box styling in settings page for improved layout (#2857)"
This reverts commit 4382df7951.
2025-03-03 08:00:10 +08:00
Tunglies 4382df7951 style: update box styling in settings page for improved layout (#2857) 2025-03-03 06:38:32 +08:00
Tunglies 6cb7d48530 feat: add export diagnostic info functionality (#2856) 2025-03-03 05:58:12 +08:00
wonfen 9d74b93ee0 feat: Add sidecar mode as an alternative to service mode
- Auto-fallback to sidecar mode if service mode fails
2025-03-03 03:34:34 +08:00
Tunglies d370868222 fix: windows/linux runtime crash 2025-03-03 02:27:45 +08:00
Christine. 9278f9e193 workflow: remove 32-bit platform (#2855)
* chore: build portable by self

* chore: remove 32bit platform

* Update CONTRIBUTING.md

* update alpha version
2025-03-03 01:16:33 +08:00
Tunglies a708db6f2b Revert "chore: alpha ci should remove old builds"
This reverts commit d2c4a44297.
2025-03-03 00:16:17 +08:00
Tunglies d2c4a44297 chore: alpha ci should remove old builds 2025-03-02 23:46:20 +08:00
Tunglies 3a24623e4b feat: fish env export support 2025-03-02 23:20:10 +08:00
Tunglies d2084d8d21 version: 2.1.3 a 2025-03-02 20:46:53 +08:00
Tunglies d9b4ca91e7 version: 2.1.3 alpha 2025-03-02 20:43:54 +08:00
Tunglies 13da571ad2 version: 2.1.3 alpha (#2851)
* version: 2.1.3 alpha
2025-03-02 19:08:27 +08:00
Tunglies 662d60c0f5 chore: automatically label issues (#2844) 2025-03-02 18:50:31 +08:00
Tunglies 368095d2d4 docs: add fast build and clean commands to contributing guide (#2842)
docs: add fast build and clean commands to contributing guide

- Added documentation for the `pnpm build:fast` command which uses Rust's fast-release profile to reduce compilation time
- Added explanation that fast builds disable optimization and LTO, resulting in larger but quicker builds
- Added documentation for the `pnpm clean` command to clean Rust build files
2025-03-02 15:13:17 +08:00
Tunglies 496aeeb06d chore: fast-dev and fast-build profile (#2841)
* refactor: improve proxy group UI and spacing

- Increased spacing in proxy-groups.tsx by adjusting the right position
  of the alphabet selector to provide better visual separation
- Enhanced spacing in proxy-render.tsx with larger margins and padding
  - Increased group item margins from 8px to 10px with 16px horizontal spacing
  - Expanded border radius from 8px to 10px for smoother appearance
  - Improved ProxyHead component spacing with pl: 3, pr: 3.5
  - Enhanced grid spacing in proxy collection items from 1 to 1.5
  - Adjusted padding for better visual hierarchy

These changes create a more polished, spacious layout with improved
readability and touch targets.

* - Update package.json with improved dev and build scripts:
  - Add fast-dev profile to development scripts
  - Configure build:fast with fast-release profile
  - Add clean command for cargo cleaning
2025-03-02 14:58:59 +08:00
Tunglies b31cbda615 refactor: improve proxy group UI and spacing (#2835)
- Increased spacing in proxy-groups.tsx by adjusting the right position
  of the alphabet selector to provide better visual separation
- Enhanced spacing in proxy-render.tsx with larger margins and padding
  - Increased group item margins from 8px to 10px with 16px horizontal spacing
  - Expanded border radius from 8px to 10px for smoother appearance
  - Improved ProxyHead component spacing with pl: 3, pr: 3.5
  - Enhanced grid spacing in proxy collection items from 1 to 1.5
  - Adjusted padding for better visual hierarchy

These changes create a more polished, spacious layout with improved
readability and touch targets.
2025-03-02 05:36:18 +08:00
Tunglies 7898f3a119 fix: macos frameless title 2025-03-02 04:30:58 +08:00
wonfen ba0a291d97 feat: refactor logging system into a global service 2025-03-02 04:20:38 +08:00
wonfen b3e4defc0f fix: remove macos window title 2025-03-02 04:09:50 +08:00
wonfen 1ae0ad8a0e fix: add bottom padding to prevent jitter 2025-03-02 04:08:13 +08:00
Tunglies 79b20694c7 feat: add fast compilation options for development and release (#2831)
- Added fast compilation profiles in Cargo.toml
  - fast-dev profile with max codegen units and disabled optimizations
  - fast-release profile with debugging support and faster build time
- Added new npm scripts for quick development iterations
  - dev:fast command for standard development without extra features
  - build:fast command for quick release builds
- Updated default dev command to use verge-dev feature flag
- Both profiles retain debug symbols and disable stripping for better debugging
2025-03-02 00:40:07 +08:00
Tunglies 1ad4941ed8 refactor: rename cmds module to cmd for better consistency (#2830)
- Renamed `cmds` module to `cmd` for better naming consistency
- Reorganized command modules into separate files under src/cmd/
- Updated all imports and references to use the new module name
- Fixed missing dependency in webdav.rs to reference core::backup
- Updated tray module to use new cmd namespace
- Improved uwp.rs module structure using platform-specific implementations
- Removed unnecessary imports from various command files
2025-03-01 22:52:43 +08:00
Tunglies b3fd44d165 refactor: reorganize feat.rs into modular structure (#2827)
Split the monolithic feat.rs file into specialized modules:
- backup.rs: WebDAV backup and restore functions
- clash.rs: Core management and testing functions
- config.rs: Configuration handling
- profile.rs: Profile management
- proxy.rs: Proxy and TUN mode controls
- window.rs: Dashboard window management

This improves code organization, readability, and maintainability
by grouping related functionality into logical modules.
2025-03-01 20:44:35 +08:00
Christine. 391a494af6 fix: build failed with Windows (#2825) 2025-03-01 19:52:42 +08:00
Tunglies 623d075ab8 add: issues type template 2025-03-01 19:12:12 +08:00
Tunglies 5d70b77316 Issues label options template (#2820)
* fix: macos dock display icon and text

* add: issues label option template
2025-03-01 02:46:16 -08:00
Tunglies f5f54c0f0e fix: macos dock display icon and text (#2818) 2025-03-01 02:29:40 -08:00
wonfen 91b0b1f279 perf: improve scrolling performance and interaction in proxy group list 2025-03-01 08:31:31 +08:00
wonfen e0fa1f3efe fix: v2 action file rename 2025-03-01 08:04:31 +08:00
wonfen b00abf3337 chore: updatelog 2025-03-01 03:45:03 +08:00
wonfen a5d846ce4e feat: add exit status check in core config validation 2025-03-01 03:39:13 +08:00
wonfen 7743c42dd1 Release - 2.1.2 真·臻 2025-03-01 01:49:01 +08:00
Christine. 3f3dab9495 add: i18n text for settings page. (#2815) 2025-03-01 01:36:36 +08:00
Tunglies 2dc9672b20 feat: better setting UI layout (#2814) 2025-03-01 01:29:23 +08:00
wonfen 2c90d1ca69 chore: v2 updater 2025-03-01 01:29:23 +08:00
Tunglies 1555e2910d feat: better setting layout ui (#2809) 2025-03-01 01:29:23 +08:00
Tunglies 19b6bd35f5 feat: quiting when enable tun mode no more blocking system network (#2805) 2025-03-01 01:29:23 +08:00
wonfen 3d4d60edc8 test updater 2025-03-01 01:29:23 +08:00
wonfen db0a000ecb chore: updater v2 Capabilities 2025-02-28 10:55:53 +08:00
wonfen 24ff48b41c fix: updater 2025-02-28 09:33:50 +08:00
wonfen 18a67d0b2f chore: updatelog 2025-02-28 07:34:56 +08:00
wonfen e23af1ad58 feat: add ability to check service version and auto-reinstall 2025-02-28 06:45:30 +08:00
wonfen 70af392059 feat: enhance updater script with comprehensive platform support and logging 2025-02-27 17:19:04 +08:00
Christine. d255df23ee fix: connection details (#2778) 2025-02-27 14:53:17 +08:00
wonfen 1317a8b2db feat: enhance merge config validation and error handling 2025-02-27 14:49:55 +08:00
wonfen 4cac118442 style: UI tweak 2025-02-27 13:04:46 +08:00
wonfen 2ce43ccd23 Release - 2.1.1 臻fix 2025-02-27 03:18:23 +08:00
Tunglies d23b2949d8 feat: macos display colorful icon with speed rate (#2771) 2025-02-27 01:51:52 +08:00
wonfen 3471476ba2 feat: improve file type detection for better script recognition 2025-02-26 15:59:19 +08:00
wonfen cee61e5619 chore: downgrade Tauri updater plugin and add i18n for core switching 2025-02-26 15:04:47 +08:00
wonfen 5026e2bade feat: optimize hotkey behavior and window management logic 2025-02-26 11:03:50 +08:00
wonfen 83f005c256 fix: CI 2025-02-26 09:52:15 +08:00
wonfen 80843c5ee3 Release 2.1.0 - 臻 2025-02-26 08:36:02 +08:00
wonfen ee00defe43 feat: enhance script validation and error handling 2025-02-26 05:21:14 +08:00
wonfen 7696504d97 feat: Add window state monitoring and auto-save in real-time 2025-02-26 00:36:02 +08:00
wonfen 7a0e38a1b4 perf: Improve config validation error messages and handling 2025-02-25 13:47:29 +08:00
wonfen bc5d577553 chore: revise translation 2025-02-24 23:26:04 +08:00
wonfen cfed2b7236 chore: Change default TUN stack from 'mixed' to 'gvisor' 2025-02-24 23:10:09 +08:00
Christine. e34e6654bc add: i18n text for config check (#2750) 2025-02-24 22:23:42 +08:00
wonfen d6b85f1a01 fix: remove node related group info when deleting a node 2025-02-24 11:40:28 +08:00
wonfen 16af7b53cb fix: menu switching issue 2025-02-24 11:37:23 +08:00
wonfen befc856207 feat: Improve core change configuration validation and error handling 2025-02-24 07:34:03 +08:00
wonfen 5ab8e7a7c7 feat: Enhance configuration validation and error handling during app startup 2025-02-24 06:21:32 +08:00
zhaoyuanandzymouse 443bfa5928 feat: 通过添加CLASH_VERGE_REV_IP环境变量的方式,修改复制环境变量按钮的IP (#2734)
Co-authored-by: zymouse <zymouse@pixmoving.net>
2025-02-24 03:42:40 +08:00
wonfen 52627575ff feat: Enhance configuration validation and error handling
- Improve config validation process with detailed logging and error tracking
- Add more robust error handling in profile updates and config patches
- Implement comprehensive config validation using clash core subprocess
2025-02-23 10:53:09 +08:00
Christine. 39973f2d24 feat: auto rename alpha version (#2740) 2025-02-23 02:30:15 +08:00
wonfen 2b01857d15 chore: Disable automatic alpha tag update in workflow 2025-02-20 14:25:58 +08:00
wonfen 2650aa845f refactor: Optimize proxy rendering and layout calculation 2025-02-20 14:21:55 +08:00
wonfen 8400a61bf0 refactor: Simplify tray icon event handling across platforms 2025-02-20 07:03:28 +08:00
wonfen 763b6b9003 chore: Update Node.js version and remove updater file generation 2025-02-20 02:55:47 +08:00
wonfen 053f414f3c feat: Add system proxy status indicators 2025-02-19 13:56:22 +08:00
wonfen 2defa2cc56 refactor: Simplify log data management and improve search functionality 2025-02-19 13:06:15 +08:00
wonfen bb1dbfcfc3 fix: connections page traffic calculation 2025-02-19 02:24:21 +08:00
Tunglies 1378068a30 rm dead code (#2718)
* rm: verge service takes full control of mihomo process. no more required.

* rm: dead code
2025-02-19 01:13:52 +08:00
wonfen 5d9dce7d10 style: tweak 2025-02-18 11:40:41 +08:00
wonfen acb66a3012 feat: Add persistent column width settings for connection table & process filtering 2025-02-18 11:04:22 +08:00
wonfen 054f902cc6 refactor: Remove unused routing select component from settings page 2025-02-18 09:04:23 +08:00
wonfen 4333153a59 feat: Improve letter item hover interaction with debounce mechanism 2025-02-18 09:00:03 +08:00
wonfen 19e623d7c2 feat: Add pause/resume functionality for connection tracking 2025-02-18 08:14:09 +08:00
wonfen 2dd646537e fix: resolve deprecated warnings in console 2025-02-18 07:10:28 +08:00
wonfen 1ece079978 perf: Improve proxy list interaction and UI responsiveness 2025-02-18 05:54:16 +08:00
wonfen 0d8f779634 feat: Enhance alphabet selector with dynamic tooltip and scrolling 2025-02-18 01:55:44 +08:00
wonfen a7ffc9fc38 refactor: Improve proxy data update mechanism with optimistic UI and error handling 2025-02-18 00:37:55 +08:00
wonfen 932b29e66f fix: Refine TypeScript types for proxy groups component 2025-02-17 16:27:06 +08:00
wonfen 6be7a3b94c feat: Enhance proxy groups with Initials navigation and performance optimizations 2025-02-17 16:07:46 +08:00
wonfen 8e8dd1ec03 feat: Optimize tray speed rate rendering and update logic 2025-02-17 15:08:19 +08:00
wonfen 772f3268ce feat: Improve Virtuoso list rendering for proxy groups 2025-02-17 14:30:21 +08:00
wonfen a179591ac2 feat: Add persistent scroll position for proxy groups 2025-02-17 14:24:33 +08:00
wonfen d071e5971f fix: alpha build win webview pack rename issue 2025-02-17 12:06:57 +08:00
wonfen a9032f5f20 chore: Update updater artifacts configuration 2025-02-17 11:22:14 +08:00
Akioe Yu 0190702616 fix: check script error (#2453)
(cherry picked from commit 97bde64fd6516019a190d52f05af54ade623d57c)
2025-02-17 03:10:19 +08:00
wonfen c35bf38420 chore: Add macOS-specific test for format_bytes_speed function 2025-02-17 03:10:03 +08:00
wonfen 89d20e564a Revert "perf: Improve kernel management logic & add more dev mode logs"
This reverts commit 18c7ed1ccc.
2025-02-15 05:51:46 +08:00
Christine. 3c24ff3afc add: i18n text for lightweight mode (#2692) 2025-02-13 14:27:12 +08:00
Tunglies daf0398750 fix: build front cannot find IvergeConfig.enable_lite_mode and macos port switching causes crash (#2691)
* fix: macos switch protocol port or enable protocol port causes crash

* fix: build time front cannot find IVergeConfig attribute enable_lite_mode
2025-02-12 18:14:13 -08:00
wonfen 6b14c2b763 feat: add lightweight mode 2025-02-13 02:18:17 +08:00
wonfen 5bf2f9b8ed fix(macos): add missing required dependencies 2025-02-12 15:06:42 +08:00
wonfen 10a2655288 fix: remove unused imports to resolve compile warnings 2025-02-12 14:35:49 +08:00
wonfen ac07397818 refactor: improve hotkey management, logging, and error handling; fix tray freeze and hotkey failure on silent startup 2025-02-12 14:23:42 +08:00
wonfen 30d061d00f fix(tray): resolve blank icon issue on Windows and optimize creation logic 2025-02-12 13:56:33 +08:00
Christine. 360d8a5143 fix: use remoteDestination replace DestinationIP in connection page, #2668 (#2679)
* fix: use remoteDestination replace DestinationIP  in connection page, #2668

* add: missing i18n text

* fix: display the target address details in connection page
2025-02-11 14:52:27 +08:00
wonfen 70e0a5adc8 refactor: Improve tray icon and event handling across platforms & unify click behavior 2025-02-11 14:48:31 +08:00
wonfen ef52f81494 fix: app crash without a default global_hotkey vaule 2025-02-11 01:50:48 +08:00
wonfen e5c1de3ad3 fix: deprecated CLI warnings 2025-02-11 01:05:51 +08:00
wonfen 576fd700ae feat: Add independent alpha update channel 2025-02-11 00:53:44 +08:00
wonfen 476df65a1b fix: try to fix linux CI updater problem 2025-02-11 00:53:44 +08:00
Christine. bd0a4863a8 add: i18n text (#2675) 2025-02-10 00:29:43 -08:00
wonfen d25fbc05e2 perf: Optimize kernel shutdown speed & logic 2025-02-10 13:18:00 +08:00
wonfen b8a0b6f1f4 fix: crash caused by global_hotkey 2025-02-10 12:39:07 +08:00
wonfen 18c7ed1ccc perf: Improve kernel management logic & add more dev mode logs 2025-02-09 14:19:15 +08:00
Tunglies b14db06955 perf: imporve clash mode switching performance on the main window (#2667) 2025-02-09 07:45:46 +08:00
Tunglies 215dcee3f1 feat: option to enable global hotkey (#2665) 2025-02-09 07:45:22 +08:00
Tunglies 37e5c22a5a fix: duplicate checked tray menu when profile name are same (#2660) 2025-02-08 09:36:04 +08:00
wonfen bb0b6c3f77 feat: Optimize UI layout and page hierarchy 2025-02-07 09:13:30 +08:00
wonfen a37df46ce7 perf: optimize node latency refresh rate for faster updates 2025-02-06 07:58:42 +08:00
wonfen 73fbec2514 chore: update deps 2025-02-05 14:19:43 +08:00
Tunglies f66fa08b2c Feature: Switch Proxy Profile from Tray Menu (#2644) 2025-02-05 08:52:47 +08:00
wonfen bae606bc9d fix: restore hotkey functionality after silent startup 2025-02-02 11:37:10 +08:00
wonfen 1a2d5e988a feat: allow users to customize enhanced-mode and fakeip-range in tun mode by wonfen 2025-02-02 05:12:16 +08:00
wonfen d20bd62b90 style: refine tray speed display 2025-02-02 03:43:15 +08:00
wonfen 204bf43b9a fix: windows rounded corners 2025-01-19 09:47:43 +08:00
wonfen 3ea0d20e2c add donation 2025-01-18 16:08:24 +08:00
wonfen 6d527a1cdb release 2.0.3 2025-01-16 03:30:07 +08:00
白铭骢 (Mingcong Bai) 9c1ece754e fix(locales): clean up typesetting (#2539)
Following localisation guidelines from 《大陆简中自由软件本地化工作指南》
1.5.4 版 (Simplified Chinese - Mainland China FOSS Localization
Guidelines, version 1.5.4).

Link: https://repo.aosc.io/aosc-l10n/zh_CN_l10n_1.5.4.pdf
2025-01-14 13:00:30 +08:00
huzibaca 9070260d41 chore: update 2025-01-14 12:56:08 +08:00
huzibaca cd8df52aad revert: update deps 2025-01-14 12:41:52 +08:00
huzibaca fa86efcdfb chore: avoid duplicate updates when tray rate is off 2025-01-14 11:48:43 +08:00
huzibaca 1a61fab79a feat: maoos tray speed can be closed 2025-01-13 20:48:25 +08:00
huzibaca 41a27641df fix: fix: try to fix the language pack issue(2) 2025-01-13 17:09:38 +08:00
huzibaca a39696151d fix: windows tray icon color not updated 2025-01-13 13:49:56 +08:00
huzibaca 4a4ca5c409 chore: update husky/pre-commit 2025-01-13 13:03:46 +08:00
huzibaca 5bff7ea469 fix: tray tooltip not updating 2025-01-13 13:01:12 +08:00
huzibaca f2cc116ff9 chore: update 2025-01-13 12:41:07 +08:00
huzibaca 80a18c9172 chore: update deps 2025-01-12 23:10:15 +08:00
huzibaca a56732e0a3 fix: try to fix the language pack issue 2025-01-12 22:22:06 +08:00
huzibaca 9655f7712b Merge branch 'languagefixes' into dev
# Conflicts:
#	src-tauri/Cargo.lock
#	src-tauri/Cargo.toml
#	src-tauri/src/core/tray/mod.rs
2025-01-11 15:07:30 +08:00
huzibaca 13b63b5d96 fix: fixes #2502 2025-01-11 12:55:20 +08:00
lucidhz c5989d2735 fix: put_configs response add detail error message (#2492)
感谢pr
2025-01-05 01:27:12 -08:00
huzibaca fb4032d6ce fix: modify the external control access key, the tray rate display is abnormal 2025-01-02 15:27:28 +08:00
huzibaca a29c2d4b14 chore: update deps 2025-01-02 15:02:07 +08:00
huzibaca aa7e911c63 chore: update 2025-01-01 08:25:31 +08:00
huzibaca eeff4d41f4 feat: the tray displays the shortcut keys that have been set 2025-01-01 08:14:15 +08:00
huzibaca 086f023ebc chore: update 2024-12-31 11:11:29 +08:00
huzibaca b70336c026 fix: unused code 2024-12-31 08:08:52 +08:00
huzibaca 7d84279370 fix: syntax issues 2024-12-31 04:50:12 +08:00
huzibaca 1b2f1b6106 Merge branch 'feat-macos-spped-rate-icon' into dev 2024-12-31 04:43:54 +08:00
huzibaca fb41c915cc feat: macos system tray addition rate display 2024-12-31 04:42:55 +08:00
blagodaren bab291a60b fix: small refactor 2024-12-28 13:57:30 +03:00
blagodaren 8461046a4f fix: add more lang and lang fix for tray 2024-12-28 10:05:30 +03:00
blagodaren fc925ea032 fix: add system language sup 2024-12-28 08:12:46 +03:00
huzibaca 542baf9d69 fix: fixes #2460 2024-12-28 06:05:09 +08:00
huzibaca 3f02859203 chore:add macos entitlements.list 2024-12-27 04:38:34 +08:00
huzibaca af97bd15a9 chore: update 2024-12-27 02:42:35 +08:00
huzibaca 2c081e5a04 chore: secret is empty and no parameters are passed 2024-12-25 02:15:06 +08:00
huzibaca 2d2521e434 chore: update 2024-12-24 06:03:23 +08:00
huzibaca d9291d4f79 refactor: when updating the tray, the logic is split to improve performance. 2024-12-24 04:52:14 +08:00
huzibaca 0669f7a10b chore: after saving the configuration file, restart the core 2024-12-24 02:22:46 +08:00
huzibaca f0d953ff59 chore: update default_bypass
1. add 172.29.0.0/16
2024-12-23 06:06:46 +08:00
huzibaca 4f6ca40811 fix: turn off window shadow fixer, fixes #2425 2024-12-23 04:36:41 +08:00
huzibaca 47848099be chore: update 2024-12-15 01:14:16 +08:00
huzibaca f839d3bc88 fix: fixer #2346 2024-12-14 17:30:58 +08:00
huzibaca 800f994b10 chore: update deps 2024-12-14 16:21:16 +08:00
Langning Chen b149084e39 Fix l10n issues (#2315)
* Complete the missing translation

* Match the Chinese language file order with others
2024-12-11 01:02:21 +08:00
huzibaca e6580f2f05 chorea: update deps 2024-12-10 22:36:13 +08:00
huzibaca c5c840d378 feat: improve system bypass settings 2024-12-10 14:37:11 +08:00
huzibaca 2e0be4b426 chore: enable default proxy bypass and hide custom bypass settings 2024-12-10 13:59:13 +08:00
huzibaca 460d72f392 chore: update deps(tauri-plugin-autostart) 2024-12-08 22:10:15 +08:00
huzibaca aa18c4870d chore: deeplink uses the latest API 2024-12-08 15:54:46 +08:00
huzibaca 61533646ad chore: update deps 2024-12-08 09:58:59 +08:00
huzibaca b42d13f573 fix: when the window is hidden, close the websocket connection, reduce the risk of memory leaks 2024-12-07 16:47:41 +08:00
wonfen 0e3b631118 chore: change fakeip range 2024-12-06 15:34:41 +08:00
huzibaca 38745d4513 chore:set the sysproxy_rs version to a fixed version 2024-12-06 15:03:04 +08:00
huzibaca d22b37c7bf fix: extension script dns is overwritten fixer #2235 2024-12-06 12:35:09 +08:00
Myles Mo d233a84362 feat: add nullshell env variable (#2285) 2024-12-04 19:01:13 +08:00
huzibaca 589324b582 fix: shift hotkey conversion fixer #2278 2024-12-04 13:19:56 +08:00
huzibaca c11efcb9be chore: update version 2024-12-04 00:30:46 +08:00
huzibaca 6197249377 fix: set fontLigatures to false fixer #2267 2024-12-04 00:28:33 +08:00
lollapalooza c71e18e97e feat: add bypass check feature (#2272) 2024-12-03 16:18:07 +08:00
Christine. f400f900e6 fix: #2126 (#2233) 2024-12-03 16:03:21 +08:00
wonfen ae5b2cfb79 release 2.0.2 2024-12-01 11:00:09 +08:00
huzibaca 0bb8786ef2 fix: resource file initialization failed 2024-12-01 09:38:29 +08:00
huzibaca f7d5be774d fix: resolve service permission failed(2) 2024-11-30 10:45:53 +08:00
huzibaca c0a0b82fa6 chore: update 2024-11-30 10:38:15 +08:00
huzibaca 277ef51375 fix: resolve service permission failed 2024-11-30 10:29:16 +08:00
huzibaca 67b1cf9e1b feat: user uploaded icons can use templates, provided they are monochrome icons fixer #2213 2024-11-30 09:49:43 +08:00
huzibaca 4f797eb7b8 chore: update 2024-11-30 07:04:09 +08:00
huzibaca 29ccabc054 refactor: update_core_config, simplify logic and delete invalid notifications 2024-11-30 05:43:59 +08:00
yyhhyy 857574cbc1 🐛 fix: Dynamically set IPv6 DNS configuration based on existing config (#2198)
thanks
2024-11-30 00:55:15 +08:00
wonfen acc97f28b5 chore: change mac tun dns 2024-11-29 11:10:41 +08:00
huzibaca 02e32aec41 chore: update 2024-11-29 03:46:37 +08:00
huzibaca fe0fdc5603 chore: remove debug code 2024-11-28 14:44:13 +08:00
Yu-Haifeng 66941a18c0 fix: macOS tray icon not changed when use system proxy or tun mode (#2176) 2024-11-28 14:42:05 +08:00
huzibaca f772f92b88 fix: the deadlock caused by incorrect call of window_state due to document error 2024-11-28 12:54:55 +08:00
huzibaca d80db8c91e chore: clean startup registry keys for older versions(2) 2024-11-28 06:40:44 +08:00
huzibaca a1e67820c7 chore: clean startup registry keys for older versions 2024-11-28 05:34:56 +08:00
wonfen ebe0cfbb0c chore: set tun default stack 2024-11-28 04:22:13 +08:00
huzibaca 59c52199e3 chore: update 2024-11-27 14:27:16 +08:00
wonfen 148def4c0f release 2.0.2 2024-11-27 12:41:51 +08:00
huzibaca 3177ea0f4d chore: update 2024-11-27 11:34:52 +08:00
huzibaca 7d65ce847a fix: when tun is closed, the full profile configuration is not restored 2024-11-27 10:35:42 +08:00
huzibaca 2ac27fcfa7 refactor: backup implementation
1. timeouts can be set for different operations
2. performance optimization
2024-11-27 07:34:34 +08:00
huzibaca 373c8ec0e5 chore: update 2024-11-27 07:09:03 +08:00
huzibaca 7df5ca1912 fix: linux build failed 2024-11-27 05:54:48 +08:00
huzibaca d8dc60ff0d fix: fixer #2158 2024-11-27 05:47:08 +08:00
huzibaca 56deafee61 chore: update version 2024-11-27 05:09:46 +08:00
huzibaca 4c29850e1c fix: build failed. #2156 2024-11-27 05:07:02 +08:00
huzibaca 90bf5e0782 chore: timeout adjusted to 15 seconds 2024-11-26 15:09:58 +08:00
huzibaca c2d07ce4c1 fix: add scroll bar and scroll to top button to the test page . fixer #2118 2024-11-26 14:44:43 +08:00
wonfen 564d81fd31 style: adjust 2024-11-26 12:11:20 +08:00
huzibaca f8b7b82ba9 chore: remove window title 2024-11-26 10:11:55 +08:00
huzibaca 71cb2a97c0 fix: kernel-caused silent mode failure to start windows 2024-11-26 09:00:01 +08:00
huzibaca 1cc6472002 chore: update 2024-11-26 05:46:34 +08:00
huzibaca 9b2c80494d refactor: Implement using third-party libraries 2024-11-26 04:51:19 +08:00
huzibaca 4ccc2a2961 chore: update 2024-11-26 03:30:15 +08:00
huzibaca 17a8dfb58a fix: file drag and drop import cannot be used 2024-11-26 03:07:25 +08:00
wonfen 184b588f20 style: Increased light color contrast to prevent blurring on some displays 2024-11-26 01:05:30 +08:00
huzibaca 87c4ebe0da chore: replace mui grid with mui grid2 2024-11-25 12:51:13 +08:00
wonfen 18a0a15e16 update change log 2024-11-25 06:46:29 +08:00
huzibaca c8fd62f388 fix: Try to fix vcruntime duplicate installation 2024-11-25 06:33:47 +08:00
huzibaca 97f03de295 fix: update failed(updater.install called before updater.download) 2024-11-25 05:27:26 +08:00
huzibaca 2c0a5666fc fix: when restoring webdav, the current username, password and url are not preserved 2024-11-25 03:41:35 +08:00
huzibaca 5c3f0d5b60 fix: password failed due to character escaping 2024-11-25 02:48:56 +08:00
huzibaca 746763ed34 fix: remove comments from svg icons to prevent front-end crashes. fixer #2093 2024-11-25 01:58:02 +08:00
wonfen e3800a575f fix: fakeip dns 2024-11-25 01:50:30 +08:00
huzibaca c78c936762 chore: replace mui grid with mui grid2 2024-11-25 01:34:18 +08:00
huzibaca ea5d6f9c46 fix: tun allocates the wrong private network segment, causing conflicts. 2024-11-25 01:06:51 +08:00
wonfen 97f8022276 release 2.0.1 2024-11-24 09:00:20 +08:00
huzibaca d718bd9141 fix: If an older version of the executable exists, delete it(2) 2024-11-24 08:25:46 +08:00
huzibaca 8ddd48eda1 feat: try to use vscode first, if not found then use system default app 2024-11-24 08:20:00 +08:00
huzibaca 3e20404959 fix: If an older version of the executable exists, delete it 2024-11-24 01:24:51 +08:00
huzibaca dde7ead751 Merge branch 'fix-migrate-tauri2-errors'
* fix-migrate-tauri2-errors: (288 commits)

# Conflicts:
#	.github/ISSUE_TEMPLATE/bug_report.yml
2024-11-24 00:14:46 +08:00
huzibaca be719680b0 fix: windows cannot open yaml file 2024-11-23 19:46:53 +08:00
wonfen d6f5a79ac9 fix: release workflow 2024-11-23 12:36:16 +08:00
wonfen a3140f48b4 release 2.0.0 2024-11-23 11:34:17 +08:00
huzibaca 335883f9f0 chore: set the request timeout to 3 seconds 2024-11-23 06:50:28 +08:00
huzibaca f2b1b88242 chore: remove notes 2024-11-23 06:47:43 +08:00
huzibaca 4163ee484c chore: update 2024-11-23 06:42:05 +08:00
huzibaca a14c3f81b0 fix: restore the window state first, then set the window size 2024-11-23 06:39:25 +08:00
huzibaca f08d2c998d chore: update 2024-11-23 06:22:46 +08:00
huzibaca 1b0cf938d9 chore: add debug codes & logs(2) 2024-11-23 06:20:13 +08:00
huzibaca 03d714f2d4 chore: add debug codes & logs 2024-11-23 05:43:13 +08:00
huzibaca 4feef53cb0 chore: set the request timeout to 5 seconds 2024-11-22 14:15:31 +08:00
huzibaca 397e6a9d57 feat: added scroll top button for agent and rule pages 2024-11-22 09:22:44 +08:00
huzibaca 9df2ceba84 chore: remove debug code 2024-11-22 09:08:25 +08:00
huzibaca 1b65dc7256 fix: macOS DNS restore failed 2024-11-22 03:09:39 +08:00
huzibaca 4ffadab4c5 chore: hide DNS cache file 2024-11-22 02:42:55 +08:00
huzibaca 7ed90ff7f8 fix: husky - install command is DEPRECATED 2024-11-21 12:06:52 +08:00
huzibaca ec79126b4a chore: remove unused code 2024-11-21 11:44:19 +08:00
huzibaca ee13bb559e chore: remove unused code 2024-11-21 11:24:19 +08:00
huzibaca ffbc0cdff4 fix: password should not be trimmed 2024-11-21 11:14:40 +08:00
huzibaca 801b79b728 chore: remove unused code 2024-11-21 11:13:11 +08:00
huzibaca cd1d719a92 fix: serde::json passing IVerge to the front end without deserialization 2024-11-21 06:01:56 +08:00
huzibaca cf3848f15d chore: update 2024-11-20 07:37:30 +08:00
huzibaca df5d43283b chore: remove notes 2024-11-20 07:37:03 +08:00
huzibaca 91f0f9f65e feat: encryption configuration properties 2024-11-20 07:27:42 +08:00
huzibaca 708e5d1941 fix: auto launch does not worki 2024-11-20 03:52:19 +08:00
huzibaca 8773117e50 chore: update 2024-11-20 01:15:03 +08:00
huzibaca 8840e63c1c fix: exit_app does not work 2024-11-20 01:04:55 +08:00
huzibaca 4e2d9d6acd fix: windows cannot save window state(2) 2024-11-20 00:27:53 +08:00
huzibaca 94259f9515 fix: windows cannot save window state 2024-11-19 23:32:32 +08:00
huzibaca 5b0ff01cbf chore: update deps 2024-11-19 13:35:57 +08:00
huzibaca 3ae1109a31 chore: format 2024-11-19 13:28:17 +08:00
wonfen f50b9655ae feat: improve set dns logic 2024-11-19 11:38:23 +08:00
huzibaca 081daee53f chore: update web:build comman, use tsc --noEmit 2024-11-19 04:42:37 +08:00
huzibaca 95123aceb5 feat: add logger highlighting, support regular and case matching 2024-11-19 04:10:10 +08:00
huzibaca 2a7b22c96f feat: add logger highlighting 2024-11-19 03:29:44 +08:00
huzibaca c1b15490c1 fix: log pause button not working 2024-11-19 02:56:58 +08:00
huzibaca f8cb84a706 feat: logger support all level filters 2024-11-19 01:40:45 +08:00
huzibaca 063f9034a1 chore: update deps 2024-11-18 23:51:43 +08:00
huzibaca 9c1b2ff89c chore: remove unused code 2024-11-18 23:48:02 +08:00
Christine. 44c05d9fd9 fix: field error, #2044 (#2045) 2024-11-18 07:34:57 -08:00
huzibaca dd3c149e98 refactor: remove useSWRSubscription and use useEffect 2024-11-18 16:41:17 +08:00
huzibaca 0623fe4dc3 chore: remove unused code 2024-11-18 08:16:31 +08:00
huzibaca 2746ff68c8 feat: Log level status is saved to local storage 2024-11-18 08:14:21 +08:00
huzibaca 62eb070c1b refactor: use zustand store, rewrite log clearing logic 2024-11-18 06:48:23 +08:00
huzibaca 3b8147b6ad chore: typo warn -> warning 2024-11-18 06:01:51 +08:00
huzibaca 221b732472 refactor: logger fetch logic 2024-11-18 05:58:06 +08:00
huzibaca 132641f269 fix: the CJS build of Vite's Node API is deprecated, part 2 2024-11-18 01:21:00 +08:00
huzibaca d8cf585fc5 fix: the CJS build of Vite's Node API is deprecated 2024-11-18 01:07:16 +08:00
Christine. 7f0d9952aa chore: Replace test URL to support iPv4&iPv6 (#2033) 2024-11-18 00:20:21 +08:00
huzibaca 415b4879f1 fix: webdav list interface compatibility issue 2024-11-17 23:57:28 +08:00
huzibaca 7e5d6ef9b6 fix: webdav refreshes data and clears the original data when an error occurs. 2024-11-17 23:50:34 +08:00
huzibaca ff297957c7 chore: update 2024-11-17 01:01:36 +08:00
huzibaca 7489f5e62d chore: update 2024-11-17 00:46:35 +08:00
huzibaca 3575c16326 fix: fixes #1968 2024-11-16 23:09:10 +08:00
wonfen 04f4f5f713 release rc.7 2024-11-16 10:15:50 +08:00
huzibaca eb78af7c77 chore: remove debug code 2024-11-16 07:35:34 +08:00
huzibaca 114d5ab541 fix: fixes #1940 2024-11-16 06:08:49 +08:00
huzibaca f13140e39e fix: When the shortcut key closes the window, the window is minimized 2024-11-16 04:46:20 +08:00
huzibaca 97717c648e fix: windows arm64 vsruntime is not installed 2024-11-16 03:12:11 +08:00
huzibaca ec6705eaf4 Merge branch 'fix-tray-menu-flash-on-windows' into fix-migrate-tauri2-errors
* fix-tray-menu-flash-on-windows:
  chore: update
2024-11-16 02:54:24 +08:00
Chenx Dust 1e11eff811 feat: support mptcp and smux display (#1995)
Corresponding pull request in mihomo: https://github.com/MetaCubeX/mihomo/pull/1646
2024-11-16 01:35:22 +08:00
huzibaca 13fb8c5d7d chore: update 2024-11-15 17:19:14 +08:00
huzibaca e87e06dd37 chore: unified code format 2024-11-14 03:21:18 +08:00
huzibaca b9bb79b4dc fix: call parameter error 2024-11-14 03:16:32 +08:00
wonfen d0f907f349 chore: update info & i18n 2024-11-14 03:01:37 +08:00
huzibaca 867c83b84c chore: update dep(tauri-bubild) 2024-11-13 21:49:53 +08:00
huzibaca 632d389411 chore: update deps 2024-11-13 21:39:55 +08:00
huzibaca b4df1b6e42 feat: add refresh button 2024-11-13 00:53:52 +08:00
huzibaca 176620e3bb fix: InputProps is deprecated 2024-11-13 00:30:30 +08:00
huzibaca c37f22dc65 fix: mui grid has been deprecated 2024-11-13 00:21:22 +08:00
huzibaca 9d9cf27460 fix: application restart, window status not saved 2024-11-12 22:49:08 +08:00
huzibaca 6e8def3ef7 fix: syntax issues caused by upgrading mui5 2024-11-12 20:05:28 +08:00
huzibaca ec0d872f11 chore: update 2024-11-12 19:44:57 +08:00
huzibaca 9e4a8708e7 fix: import error caused by dependency upgrade 2024-11-12 19:35:56 +08:00
huzibaca a681fdeee1 fix: undefined error 2024-11-12 19:34:53 +08:00
huzibaca 8763a76475 chore: update deps 2024-11-12 19:06:04 +08:00
wonfen 4306fba997 release rc.6 2024-11-12 03:49:21 +08:00
huzibaca 3759239dac feat: add webdav backup 2024-11-12 02:55:02 +08:00
huzibaca 4ec0b1d6e4 chore: update 2024-11-09 12:13:50 +08:00
huzibaca 6271726f07 Merge branch 'feat-webdav-backup' of ssh://github.com/clash-verge-rev/clash-verge-rev into feat-webdav-backup
# Conflicts:
#	src/locales/fa.json
#	src/locales/ru.json
#	src/locales/zh.json
2024-11-09 11:54:33 +08:00
wonfen 26dd097962 update: Readme 2024-11-09 09:00:53 +08:00
wonfen e5740579f4 fix: translation 2024-11-09 08:59:30 +08:00
huzibaca ca97e3a3e6 chore: update 2024-11-09 06:56:58 +08:00
huzibaca d3f6822080 chore: update 2024-11-09 05:50:51 +08:00
huzibaca 9d9a6dfddb chore: update 2024-11-08 23:42:00 +08:00
huzibaca c3e24d7b96 chore: update 2024-11-08 21:46:15 +08:00
huzibaca 20d163cf3a Merge branch 'feat-add-unified-delay' into fix-migrate-tauri2-errors 2024-11-06 13:56:36 +08:00
huzibaca cf90f3abc9 chore: update 2024-11-06 13:52:36 +08:00
huzibaca 8871660f0e chore: update 2024-11-06 10:07:02 +08:00
huzibaca 97a030c22e fix: @use rules must be written before any other rules. 2024-11-06 09:48:31 +08:00
huzibaca 8a6ce0f0db fix: Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0. 2024-11-06 09:46:17 +08:00
huzibaca 285b6b9287 chore: update 2024-11-05 21:40:02 +08:00
huzibaca 3515cc8e63 chore: update 2024-11-05 21:35:17 +08:00
huzibaca b0132c9718 chore: update 2024-11-05 18:38:50 +08:00
huzibaca ef84c31761 chore: update 2024-11-05 18:30:28 +08:00
huzibaca 4953d4b4d0 chore: add tooltip 2024-11-05 17:39:59 +08:00
huzibaca 2833718c90 feat: add unified delay 2024-11-05 16:24:58 +08:00
huzibaca ab7775e1ef fix: wrong window state save point 2024-11-04 09:53:40 +08:00
huzibaca a5c4562f59 chore: update deps 2024-11-02 10:25:38 +08:00
huzibaca 00bd5b0e1e chore: update 2024-11-02 07:27:08 +08:00
huzibaca b66c07bd96 chore: update (+1 squashed commit)
Squashed commits:
[78899ef] fix: bbr2 not working in windows 11(24H2)
2024-11-02 07:10:47 +08:00
huzibaca d8d0a59371 chore: update 2024-11-01 06:03:01 +08:00
The1111mp 953153ea1a chore: added support for compiling linux arm architecture (#1945)
* chore: added support for compiling linux arm architecture

Signed-off-by: The1111mp <The1111mp@outlook.com>

* chore: remove debug code

Signed-off-by: The1111mp <The1111mp@outlook.com>

---------

Signed-off-by: The1111mp <The1111mp@outlook.com>
2024-10-31 17:20:16 +08:00
downerand周晓亮 fc03ce1247 fix: fix TUN icon is overwritten by system proxy icon (#1961)
Co-authored-by: 周晓亮 <zhouxl@jiyitech.com>
2024-10-31 09:07:21 +08:00
huzibaca 08bf70681c feat: use tauri_plugin_window_state 2024-10-30 18:52:53 +08:00
huzibaca 00bc6a6301 revert: feat: use tauri-plugin-persisted-scope 2024-10-30 18:28:55 +08:00
huzibaca c5916cf5ec feat: use tauri-plugin-persisted-scope 2024-10-30 16:37:47 +08:00
huzibaca 4213a5fad1 chore: window hide replaces window minimize 2024-10-30 13:51:58 +08:00
wonfen 17fc9cf0eb release: rc.5 2024-10-30 13:08:37 +08:00
wonfen eb1a1b3786 chore: update bug report template 2024-10-30 12:59:19 +08:00
huzibaca dce72a16f0 chore: remove useless hooks
the window is not closed, it is minimized, so the position still exists
2024-10-30 10:06:54 +08:00
huzibaca 7550c2321c fix: linux file permissions problem
check-config configuration file moved from temp_dir to home_dir, resolved
2024-10-30 09:08:44 +08:00
huzibaca f0c4360210 chore: restore linux custom close button 2024-10-30 06:30:49 +08:00
huzibaca d10323701a fix: performance issues caused by closing windows on mac 2024-10-30 06:27:29 +08:00
huzibaca 94c437c3e3 chore: update 2024-10-29 10:15:07 +08:00
huzibaca 202fb19ab1 chore: update 2024-10-29 10:13:53 +08:00
huzibaca 2bb3498602 chore: disable websocket logging by default to reduce performance consumption. 2024-10-29 09:19:21 +08:00
huzibaca 9d476d7add fix incorrect usage of useCustomTheme 2024-10-29 09:07:08 +08:00
huzibaca 014829e69a chore: optimised the logic of dns processing 2024-10-28 13:09:55 +08:00
huzibaca a127cd6444 fix: dns not restored when exiting the app 2024-10-28 00:24:57 +08:00
Christine. 486ffbdc08 add more contribution details for Windows (#1939)
* doc: add more building details

Some friends said the build failed.

* chore: replace test url

replace default delay check URL, some user say that the delay numbers seen in CVR are very different from those in other software, and this default test result is not a valid reference.
2024-10-27 13:00:47 +08:00
huzibaca 526c5bdd91 fix: unused variable 2024-10-27 10:19:42 +08:00
huzibaca db11651f85 chore: update 2024-10-27 06:47:16 +08:00
huzibaca 4ce7d9f7a9 feat: macos service installation query, can't avoid it, add service run check, if installation not confirmed, exit automatically after 10 seconds 2024-10-27 06:32:25 +08:00
huzibaca df3c296850 Revert "chore: update resource"
This reverts commit e3c52781f3.
2024-10-27 05:10:26 +08:00
huzibaca 5ef9220175 chore: update 2024-10-27 05:06:42 +08:00
huzibaca e3c52781f3 chore: update resource 2024-10-27 04:46:24 +08:00
huzibaca 1cb1946497 chore: update deps 2024-10-27 01:24:13 +08:00
huzibaca 9e486e6836 chore: update 2024-10-26 19:23:49 +08:00
wonfen e5dc4a55b0 release rc.4 2024-10-26 12:05:09 +08:00
huzibaca 76d04805a4 fix:public DNS not set in macos, tun+fake-ip 2024-10-26 08:55:00 +08:00
huzibaca 652a20c523 chore: update 2024-10-26 01:44:06 +08:00
huzibaca 6c2af1bef5 chore: update 2024-10-25 21:25:06 +08:00
huzibaca ba2aa9d6e7 chore: update 2024-10-25 14:13:30 +08:00
huzibaca a1c72fe780 chore: update 2024-10-25 14:12:27 +08:00
huzibaca 197f9fd964 fix: the tray icon does not display the correct colours on macos. 2024-10-25 13:58:22 +08:00
wonfen fc2c10bb3f chore: add mac sun ico 2024-10-25 12:27:22 +08:00
wonfen d7dd797494 release 2.0rc3 2024-10-24 12:52:20 +08:00
huzibaca 4f954657a1 chore: update 2024-10-24 11:14:18 +08:00
huzibaca 3eddcafeb6 Revert "chore: global mode doesn't show proxy groups, use placeholder images instead"
This reverts commit 3df4718709.
2024-10-24 07:44:21 +08:00
huzibaca 3df4718709 chore: global mode doesn't show proxy groups, use placeholder images instead 2024-10-24 07:30:30 +08:00
huzibaca b5fddf8fa1 chore: update 2024-10-24 06:58:21 +08:00
huzibaca fdc8a83f25 fix: proxy view display error 2024-10-24 06:54:27 +08:00
huzibaca e79121e46a refactor: the logic of profiles activation 2024-10-24 05:02:47 +08:00
huzibaca aab71e55de fix: restart app failure 2024-10-24 02:54:57 +08:00
huzibaca caf4ee6863 fix: tun mode switch is not effective 2024-10-24 02:16:28 +08:00
huzibaca 30b0c45539 refactor: IRuntime::patch_config() 2024-10-23 10:34:14 +08:00
huzibaca 2ab75db9c9 chore: update 2024-10-23 09:26:14 +08:00
huzibaca f9009de894 chore: update 2024-10-23 05:48:01 +08:00
huzibaca da8ed1ac5a fix: failed to install service on macos 2024-10-23 04:46:47 +08:00
huzibaca afde7e4a44 feat: add windows uninstall script 2024-10-22 04:46:08 +08:00
huzibaca 3e94adb4b1 feat: add linux uninstall script 2024-10-22 03:17:04 +08:00
huzibaca 5c577e533f fix: debian executable file has no permissions 2024-10-22 02:37:36 +08:00
huzibaca c64b9d5a2b chore: update 2024-10-22 01:34:26 +08:00
huzibaca 545ea16ae8 fix: change the window close to minimize the window.
there is currently an error in the tauri library.
2024-10-21 23:16:45 +08:00
huzibaca bf245a4ee1 fix: pac url error ,fixes #1889 2024-10-21 22:52:10 +08:00
wonfen b28fc26ae6 chore: update bug_report template 2024-10-21 11:30:07 +08:00
huzibaca 750dfdb3a8 Merge branch 'fix-migrate-tauri2-errors' of ssh://github.com/clash-verge-rev/clash-verge-rev into fix-migrate-tauri2-errors
* 'fix-migrate-tauri2-errors' of ssh://github.com/clash-verge-rev/clash-verge-rev:
  chore: update to 2.0 rc2
2024-10-21 04:35:42 +08:00
huzibaca 5b93c2b907 chore: update 2024-10-21 04:35:21 +08:00
wonfen bbd72a79cb chore: update to 2.0 rc2 2024-10-21 03:15:02 +08:00
huzibaca babf5d840c chore: replace sudo with pkexec execution 2024-10-21 02:31:43 +08:00
huzibaca 90091db3b1 chore: update 2024-10-21 02:17:41 +08:00
huzibaca ef03dc12bb chore: update 2024-10-21 01:35:28 +08:00
huzibaca a35014f0b5 chore: update 2024-10-21 01:30:13 +08:00
huzibaca abb0a049a8 chore: update 2024-10-21 01:12:52 +08:00
huzibaca 3de18e2a86 chore: update 2024-10-21 00:50:43 +08:00
huzibaca 399bbd56ec chore: update deps 2024-10-21 00:45:06 +08:00
huzibaca 38999d284a chore: update 2024-10-21 00:34:28 +08:00
huzibaca b61b63d100 fix: failed to start system proxy with PAC mode 2024-10-20 23:13:23 +08:00
huzibaca 0dc92fbc99 chore: update deps 2024-10-20 22:35:16 +08:00
huzibaca 1cb7cd8859 Merge branch 'fix-linux-errors' into fix-migrate-tauri2-errors
* fix-linux-errors:
  chore: try to fix service not started on linux
2024-10-20 21:57:41 +08:00
huzibaca 199700cf09 fix: mac commad+q global hijack is not released 2024-10-20 06:06:47 +08:00
huzibaca ee78b7898b chore: try to fix service not started on linux 2024-10-20 04:48:42 +08:00
huzibaca a47ded7cf2 chore: update 2024-10-18 22:50:03 +08:00
huzibaca 770f031b8b fix: try to fix service not started on linux 2024-10-18 07:08:34 +08:00
wonfen 18139c7114 chore: adjust translation 2024-10-17 05:09:00 +08:00
huzibaca 4a64654f49 chore: update deps 2024-10-16 22:49:24 +08:00
huzibaca 377ff279b8 chore: update 2024-10-16 02:55:23 +08:00
huzibaca d5d39e1c2a fix: system proxy cannot be closed on mac 2024-10-16 02:52:48 +08:00
huzibaca f7b1da7557 chore: update 2024-10-16 01:55:22 +08:00
huzibaca 5e8c6c6b23 chore : update version 2024-10-16 01:53:33 +08:00
huzibaca 64634ef07b chore: t emporarily cancel window closure and change to minimize 2024-10-16 01:53:16 +08:00
huzibaca 7de2474611 Merge branch 'fix-migrate-tauri2-errors' of ssh://github.com/clash-verge-rev/clash-verge-rev into fix-migrate-tauri2-errors 2024-10-15 02:38:57 +08:00
huzibaca ea2b6a9ad8 fix: path escape issue 2024-10-15 02:35:50 +08:00
wonfen 6ee7be7cae chore: update changelog and texts 2024-10-15 02:32:25 +08:00
huzibaca 12690ed464 chore: remove windows esc shortcut key 2024-10-14 13:45:26 +08:00
huzibaca 90361242ac chore: update 2024-10-13 03:01:32 +08:00
huzibaca cd8daa929a chore: update deps 2024-10-13 02:55:35 +08:00
huzibaca a07c579497 chore: adjust tun default parameters 2024-10-13 02:42:37 +08:00
huzibaca e78f619be1 fox: external-controller cors error 2024-10-11 22:01:57 +08:00
huzibaca 5e7ff36e8e chore: complete field identifier 2024-10-11 14:33:11 +08:00
huzibaca e7e0caaa41 Merge branch 'main' into fix-migrate-tauri2-errors 2024-10-11 02:56:30 +08:00
Lvc Revincx 00c6e41239 fix: task bar icon misssing under linux wayland (#1816) 2024-10-11 02:52:57 +08:00
huzibaca c604ae38dd fix: installation service error 2024-10-11 00:59:34 +08:00
huzibaca 25e25c2285 chore: update 2024-10-10 23:34:55 +08:00
huzibaca b4180b5b48 chore:update 2024-10-10 18:52:20 +08:00
huzibaca d14bda7e7a chore: update 2024-10-10 18:40:39 +08:00
huzibaca 045ddc5ca5 chore: update 2024-10-10 02:21:22 +08:00
huzibaca 3f3fad0db7 feat: Modify startup logic and install services by default 2024-10-10 00:34:36 +08:00
huzibaca 30c77b891d chore: update 2024-10-09 01:14:03 +08:00
huzibaca 5e126364ed refactor: core logic 2024-10-08 02:39:17 +08:00
huzibaca 7f2f9588ac chore: update 2024-10-06 02:03:32 +08:00
huzibaca e299e246e3 chore: revert 2024-10-06 01:09:59 +08:00
huzibaca 0541a0c69f refactor: tun mode is turned on and off, does not depend on services, and does not affect other configurations 2024-10-05 22:45:59 +08:00
huzibaca 1178f7c892 refactor: init_log 2024-10-05 12:19:44 +08:00
huzibaca a3e78bd76d refactor: get_bypass func 2024-10-05 02:58:41 +08:00
huzibaca bcf33b779e chore: update 2024-10-05 00:03:11 +08:00
huzibaca d0ba506011 chore: update deps 2024-10-04 23:56:50 +08:00
huzibaca 477d1f2aee feat: add color to log 2024-10-04 22:38:06 +08:00
huzibaca aa4b1ad8e8 chore: update 2024-10-04 05:37:53 +08:00
huzibaca 6b902a7ad8 chore: update 2024-10-04 05:27:59 +08:00
huzibaca a626f2ce29 chore: update 2024-10-03 14:31:40 +08:00
huzibaca 2685a02e1a chore: update 2024-10-03 12:01:06 +08:00
huzibaca bf1f201d4b feat: windows uses sysproxy.exe for system proxy 2024-10-03 02:09:22 +08:00
huzibaca 335578dd91 refactor: simplify sysproxy logic
1. close all proxies directly when reset_proxy
2. init_sysproxy and update_sysproxy combined into one
3. optimize lock usage
4 ptimize the thread loop of guard_sysproxy,
2024-10-03 01:31:37 +08:00
huzibaca 3a3fc986c4 chore: replace sysproxy-rs repo 2024-10-02 17:43:14 +08:00
huzibaca 0e1e54aff4 feat: test item, when icon is svg, add svg format check 2024-10-01 20:49:03 +08:00
huzibaca 0b8f24a92e chore: optimize lock 2024-10-01 00:27:08 +08:00
huzibaca 3cbcd4630c chore: remove the manual release lock 2024-09-30 23:07:46 +08:00
wonfen 709ab3dd88 chore: add service mode info 2024-09-30 01:04:15 +08:00
huzibaca b3d93d0761 chore: update 2024-09-28 20:20:40 +08:00
huzibaca 79d1539149 refactor: logic optimization 2024-09-28 12:37:01 +08:00
huzibaca ba1a7e0fd6 ,fix: when updating the verge configuration, notification error 2024-09-27 22:25:23 +08:00
wonfen 30ebfd84cf chore: update bug report template 2024-09-27 14:08:50 +08:00
huzibaca aef215d810 chore: update 2024-09-27 10:05:31 +08:00
huzibaca cee872e944 chore: remove compatibility code 2024-09-27 10:04:23 +08:00
huzibaca c50e8f9de8 refactor: logic optimization 2024-09-27 00:24:05 +08:00
huzibaca 11a8e3465f chore: update 2024-09-26 19:47:25 +08:00
huzibaca e8ed7c1e3e chore: remove compatibility code 2024-09-26 19:47:00 +08:00
huzibaca 5b822d238d chore: update 2024-09-26 19:23:16 +08:00
huzibaca 053754fea8 chore: update 2024-09-26 12:20:57 +08:00
huzibaca 7a9fd118a3 chore: update 2024-09-25 21:20:36 +08:00
huzibaca 2aa3c8cfe1 chore: update 2024-09-25 21:07:01 +08:00
huzibaca ec2bf37ad4 Revert "chore: cover panic error"
This reverts commit b8cb48aead.
2024-09-25 20:22:49 +08:00
huzibaca fc5c959a55 chore: unified hotkey registration 2024-09-25 15:45:12 +08:00
huzibaca b8cb48aead chore: cover panic error 2024-09-25 15:44:05 +08:00
huzibaca 4eccedcd78 fix: the save_indow_size_position method has been triggered twice, at the same time
1. remove windowEvent::destoryed  hook
2024-09-25 13:54:05 +08:00
huzibaca 08b1160d63 chore: remove useless exit codes 2024-09-25 11:47:01 +08:00
huzibaca 3bcd8b8b2c fix: code lint 2024-09-24 20:11:33 +08:00
huzibaca 4b20b65a22 fix: code lint 2024-09-24 20:09:30 +08:00
huzibaca 4b234a3211 fix: code lint 2024-09-24 20:06:25 +08:00
huzibaca 2ca7310baf chore: add cross-env dev dependency 2024-09-24 16:35:19 +08:00
huzibaca 5d742a9037 chore: update 2024-09-23 23:57:08 +08:00
huzibaca a3465d292c refactor: unify and simplify the call of app_handle(2) 2024-09-23 23:15:51 +08:00
huzibaca 8a3a094414 chore: update 2024-09-23 17:15:28 +08:00
huzibaca 52d5f2c992 chore: update 2024-09-23 16:53:00 +08:00
huzibaca 60e0f972d0 refactor: unify and simplify the call of app_handle 2024-09-23 16:31:58 +08:00
huzibaca 0a9182519d chore: update 2024-09-22 22:15:36 +08:00
huzibaca bfded924d7 chore: rename method 2024-09-22 20:29:43 +08:00
huzibaca c653c458b9 Revert "chore: update"
This reverts commit 4b7ffa1465.
2024-09-22 16:46:24 +08:00
huzibaca 4b7ffa1465 chore: update 2024-09-22 15:59:59 +08:00
huzibaca a5c871e933 chore: update 2024-09-22 15:39:52 +08:00
huzibaca 56983c4d2a chore: update 2024-09-22 14:08:08 +08:00
huzibaca e9721ecc4d Revert "chore: remove useless exit codes and hooks"
This reverts commit 40ff3fd4bf.
2024-09-22 00:41:20 +08:00
huzibaca 84fe9c84a8 fix: remove unused variable 2024-09-22 00:32:40 +08:00
huzibaca 40ff3fd4bf chore: remove useless exit codes and hooks 2024-09-21 21:05:34 +08:00
huzibaca e92074e586 chore: try adjusting the triggering of the tray mouse button 2024-09-21 15:38:14 +08:00
huzibaca 424c1f7d8f chore : update 2024-09-20 18:02:19 +08:00
huzibaca 868707dbde fix: tray menu text was not aligned 2024-09-20 17:59:24 +08:00
huzibaca 29d54fe589 chore: update 2024-09-20 17:49:31 +08:00
huzibaca c246ae3bcf Fix: Custom tray icon not working
1. Contains potential deadlock
2024-09-20 16:26:23 +08:00
huzibaca ab887c656a fix: function parameter call error 2024-09-20 08:27:06 +08:00
Sukka eefeec7f0a chore: drop fs-extra (#1739)
nb!
2024-09-20 02:01:36 +08:00
huzibaca 74777df344 chore: update dev dependencis 2024-09-18 21:34:20 +08:00
huzibaca c21ce578f5 fix: exit hotkey conflict 2024-09-18 15:16:43 +08:00
huzibaca 7d1b7adda5 fix: whether the window starts as fullscreen or not. 2024-09-18 13:23:27 +08:00
huzibaca f64528dd87 chore: revert 2024-09-18 12:24:52 +08:00
huzibaca 3ca1d03d34 refactor: exit app 2024-09-18 02:48:55 +08:00
huzibaca 49929bd473 fix: the shortcut key to exit cannot be used 2024-09-18 01:41:46 +08:00
huzibaca 25f15e12cf fix: hotkey status not accurately processed, resulting in two triggers 2024-09-17 10:59:39 +08:00
huzibaca b6d15c27b6 chore: update 2024-09-17 08:28:13 +08:00
huzibaca 2f8c5c9b20 chore: update 2024-09-17 08:02:29 +08:00
huzibaca bee1373f92 chore: add window to allow full screen configuration 2024-09-16 15:25:01 +08:00
huzibaca 8a4535d55e chore: update 2024-09-16 07:08:22 +08:00
huzibaca 613fb60859 chore: update 2024-09-16 06:43:25 +08:00
huzibaca 18f9978fcf chore: update 2024-09-16 06:37:39 +08:00
huzibaca 9b527cb53c chore: update 2024-09-16 06:17:22 +08:00
huzibaca c06e4af79d chore: update 2024-09-16 05:50:05 +08:00
huzibaca e89c3ed65e chore: modify application description definition 2024-09-16 05:38:52 +08:00
huzibaca eb60cfb083 fox: try to restore the processing logic of windows custom scheme 2024-09-16 05:30:29 +08:00
huzibaca 65ce7429b7 chore: update dev workflow 2024-09-15 07:57:34 +08:00
huzibaca 4b4a4927c4 chore: add log 2024-09-15 07:03:04 +08:00
huzibaca f57d2df5ec chore: follow official standards and adjust the main file 2024-09-15 06:24:53 +08:00
wonfen d70e18ba8d chore: update change log and texts 2024-09-15 00:33:54 +08:00
huzibaca 962aeb1c75 fix: when importing subscriptions, force the window to open 2024-09-14 10:03:19 +08:00
huzibaca 6d7711f3ea fix: after importing a subscription, it cannot be automatically switched to the current subscription. 2024-09-13 18:17:14 +08:00
huzibaca ba487611ee chore: update action(rust-toolchain) 2024-09-13 14:21:51 +08:00
huzibaca 6a636444a7 chore: update action(rust-toolchain) 2024-09-13 14:14:27 +08:00
wonfen 25c766ed94 chore: change folder name 2024-09-13 12:03:28 +08:00
huzibaca dbc3723bdc feat: Added web notification of successful subscription import 2024-09-13 11:24:49 +08:00
huzibaca 3963c60fc1 fix: when the service is not installed, the tray hides the Tun mode 2024-09-13 04:12:25 +08:00
huzibaca b888c7729e refactor: url scheme implementation 2024-09-13 03:21:55 +08:00
huzibaca a39ece6156 chore: delete useless emebd server api 2024-09-12 19:01:08 +08:00
huzibaca 624eb5b085 fix:tray click event 2024-09-12 17:02:17 +08:00
huzibaca 9ec8c903c5 feat: Optimize kernel startup logic
1. tun mode startup logic
2. Remove invalid creation process PID logic
2024-09-12 15:35:08 +08:00
huzibaca 2396a6b35a feat: log panic 2024-09-12 07:59:51 +08:00
huzibaca 1fc54e49d9 chore: update deps 2024-09-12 06:14:23 +08:00
huzibaca 30ee7a9e85 fix: missing items in system tray 2024-09-12 05:45:15 +08:00
huzibaca b9be5e8c44 feat: migrate tauri 2.0 2024-09-11 08:15:03 +08:00
huzibaca ae1c6d4617 chore: update 2024-09-04 07:53:16 +08:00
huzibaca 7b11e157c4 chore: update capabilities 2024-09-04 05:57:37 +08:00
wonfen 5783b7b392 chore: 2.0.0 beta release 2024-09-03 00:00:12 +08:00
huzibaca 507d52bcb9 chore: update 2024-09-02 19:33:17 +08:00
wonfen d45929c604 chore: use in-house update proxy 2024-08-24 02:02:17 +08:00
wonfen 632c32addb Style: fix macos new version btn position 2024-08-22 07:33:51 +08:00
wonfen 7d2b18759e Release 1.7.7 2024-08-22 03:47:33 +08:00
wonfen aa8f6f4ad6 chore: Shorten text to prevent Windows tooltip truncation. 2024-08-22 03:41:16 +08:00
wonfen 592167ffb7 fix: profiles will not be selected after import 2024-08-22 00:57:04 +08:00
wonfen 12a6bfad00 Release 1.7.6 2024-08-18 00:49:23 +08:00
tony-sung 1ea3b956ad Update web-ui-viewer.tsx Fix yacd parameter hostname error (#1484) 2024-08-18 00:23:24 +08:00
MystiPanda 1b7dafe743 feat: auto select profile 2024-07-30 09:06:12 +08:00
MystiPanda 624eb2a2ba chore: prepend new added rules 2024-07-24 14:25:06 +08:00
MystiPanda 88d98517c8 fix: wrap the password in quotes
#1460
2024-07-24 14:22:23 +08:00
Avan 9528606906 style: AddressDisplay padding (#1457) 2024-07-23 22:17:51 +08:00
nhsmw 09b44a3af3 fix: update test url 2024-07-19 10:32:41 +08:00
MystiPanda dc861eca7d chore: remove unnecessary console log 2024-07-18 23:37:54 +08:00
MystiPanda e749fe70e2 fix: use group testUrl
#1384
2024-07-18 15:04:55 +08:00
MystiPanda d20c082deb fix: windows x86 updater url 2024-07-18 14:38:31 +08:00
MystiPanda c4abc3ade3 fix: error command status 2024-07-16 18:37:56 +08:00
MystiPanda ff28aab56e fix: MacOS service install error 2024-07-16 13:01:27 +08:00
MystiPanda 1a6f842492 fix(#1335): support cidr 2024-07-16 12:22:11 +08:00
MystiPanda af0cd4a342 fix: try to fix install service 2024-07-16 12:07:23 +08:00
MystiPanda d98b3224cf chore: disable okBtn 2024-07-15 23:38:49 +08:00
MystiPanda f2e5f0754c ci: fix winget uploader 2024-07-15 23:27:38 +08:00
MystiPanda ec1601f317 Release 1.7.5 2024-07-15 22:35:33 +08:00
MystiPanda 54116608dc ci: fix winget uploader 2024-07-15 22:33:57 +08:00
MystiPanda fea6735ea1 fix: copy necessary files 2024-07-15 22:25:04 +08:00
MystiPanda 2f77a07b3d Release 1.7.4 2024-07-15 20:56:18 +08:00
MystiPanda 4cd7ccda57 fix: unified switch style 2024-07-15 20:42:00 +08:00
MystiPanda ef337ffb69 feat: optimize the service mode interaction logic. 2024-07-15 20:02:05 +08:00
MystiPanda 9ff5d8d4e0 Revert "fix: try to fix abnormal stuck"
This reverts commit e14ddbe23c.
2024-07-14 12:49:06 +08:00
Sukka 199a5eed60 fix(#1387): avoid catastrophic backtracking (#1396) 2024-07-14 12:26:02 +08:00
MystiPanda e14ddbe23c fix: try to fix abnormal stuck
#1387
2024-07-13 21:41:04 +08:00
MystiPanda 3878e66ea4 chore: update locale 2024-07-13 19:15:07 +08:00
MystiPanda c14a33444b chore: unified icon style 2024-07-13 19:01:16 +08:00
MystiPanda 48f7c15035 feat: display network interface 2024-07-13 14:15:13 +08:00
Avan 32212a46e2 feature: copy clash env (#1391)
* feat: copy clash env

* style: use ContentCopyRounded replace CopyAll
2024-07-13 01:03:46 +08:00
Avan b4025c45da style: EditorViewer title align center (#1390) 2024-07-12 23:29:55 +08:00
MystiPanda bd589c4422 fix: service install failed on macos 2024-07-12 20:54:31 +08:00
dongchengjie f85f7758e6 chore: checkbox items' title & button icons 2024-07-12 18:24:19 +08:00
dongchengjie 49e36b6e00 chore: editor resizing debounce 2024-07-08 23:18:06 +08:00
dongchengjie df6e245e5c chore: remove dialog overflow-scroll 2024-07-08 22:07:43 +08:00
dongchengjie 4e4f9af5b7 fix: editor fails to resize after toggling on macos 2024-07-08 21:40:56 +08:00
MystiPanda aa9e63b51a fix dev.yml 2024-07-08 16:17:23 +08:00
MystiPanda 82340582b2 ci: add dev workflow 2024-07-08 13:20:36 +08:00
Akioe Yu 9d045e24db fix: dnd box z-index (#1353)
* fix: dnd box z-index

* fix: dnd boxes
2024-07-08 10:49:57 +08:00
MystiPanda 1dcc95d16a perf: optimized response speed 2024-07-08 00:29:49 +08:00
dongchengjie 78c32d51db chore: group types locale 2024-07-07 21:56:20 +08:00
❤是纱雾酱哟~ 545a2c6688 ci: Integrate "Winget Releaser" (#1326) 2024-07-07 20:42:17 +08:00
MystiPanda 359d9285fe perf: change port too slow 2024-07-07 18:22:02 +08:00
MystiPanda 1bd51be99c feat: get network interface 2024-07-07 18:13:10 +08:00
dongchengjie ee56080af0 fix: usage percent style 2024-07-07 18:08:02 +08:00
MystiPanda 18196c4a77 fix: trojan uri parser 2024-07-07 11:47:49 +08:00
MystiPanda 14cd3b99cc fix: tray icon size
#1341
2024-07-07 11:18:22 +08:00
MystiPanda ff75fe47ee chore: disable browser autocomplete 2024-07-07 11:16:59 +08:00
MystiPanda 7e2cc180c3 Release 1.7.3 2024-07-06 18:48:25 +08:00
MystiPanda bef9eea87b fix: bypass reg error
#1335
2024-07-06 18:23:23 +08:00
MystiPanda 9cd1aef1db fix: limite cipher types 2024-07-06 10:49:42 +08:00
MystiPanda ab25cf0637 fix: rule parser 2024-07-06 10:35:29 +08:00
MystiPanda 32f37720dd Revert "chore: add base64 decode step"
This reverts commit 38eb3123be.
2024-07-06 10:12:51 +08:00
MystiPanda 766cf3aeae chore: decode base64 2024-07-06 10:12:40 +08:00
MystiPanda 905353d540 fix: some error 2024-07-06 09:54:16 +08:00
MystiPanda 38eb3123be chore: add base64 decode step 2024-07-06 00:57:38 +08:00
MystiPanda 0183edd450 fix: style 2024-07-06 00:45:21 +08:00
MystiPanda 2f640a946e fix: type 2024-07-06 00:23:52 +08:00
MystiPanda ec6c2adf9b chore: Improve URI parser 2024-07-05 22:44:05 +08:00
MystiPanda 5a1edc5ffb feat: finish rpoxies editor 2024-07-05 19:49:32 +08:00
MystiPanda 149d482c7d fix: groups config type error
feat(unfinished): add proxy editor
2024-07-05 00:38:50 +08:00
MystiPanda c9f784e4fa feat: add profile name param for script 2024-07-04 23:11:54 +08:00
MystiPanda 9edafa75e7 build: remove unused resource file 2024-07-04 22:34:48 +08:00
MystiPanda 08baba545d chore: disable unnecessary ports by default 2024-07-04 18:58:34 +08:00
MystiPanda 60d0b29236 feat: support cache for groups editor 2024-07-04 18:53:39 +08:00
MystiPanda f5ee4fb5b5 chore: unified style 2024-07-04 18:50:21 +08:00
MystiPanda 5b7b3be6f9 fix: useseq error 2024-07-04 14:30:34 +08:00
MystiPanda c933560102 fix: locale typo 2024-07-04 14:18:21 +08:00
MystiPanda 12a80f35fe feat: support visual edit for proxy group 2024-07-04 14:13:19 +08:00
MystiPanda 43f901eeb9 fix: rules editor get groups error 2024-07-04 11:03:51 +08:00
MystiPanda e8440e06a1 feat: support get merged rule-set name 2024-07-03 22:13:24 +08:00
dongchengjie 19e9e9d032 fix: remove rule condition where is not required 2024-07-03 17:03:12 +08:00
245 changed files with 35735 additions and 12174 deletions
+5
View File
@@ -0,0 +1,5 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
+15 -6
View File
@@ -2,6 +2,7 @@ name: 问题反馈 / Bug report
title: "[BUG] "
description: 反馈你遇到的问题 / Report the issue you are experiencing
labels: ["bug"]
type: "Bug"
body:
- type: markdown
@@ -11,20 +12,28 @@ body:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
4. Please be sure to check out [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version update log
5. Please be sure to try the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version to ensure that the problem still exists
6. Please describe the problem in detail according to the template specification and try to update the Alpha version, otherwise the issue will be closed
- type: textarea
id: description
attributes:
label: 问题描述 / Describe the bug
description: 详细清晰地描述你遇到的问题 / A clear and concise description of what the bug is
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
validations:
required: true
- type: textarea
attributes:
label: 软件版本 / Verge Version
description: 请提供Verge的具体版本,如果是alpha版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of Verge. If it is an alpha version, please indicate the download time (accurate to hours and minutes)
validations:
required: true
- type: textarea
@@ -51,6 +60,6 @@ body:
- type: textarea
attributes:
label: 日志 / Log
description: 请提供完整或相关部分的Debug日志 / Please provide the complete or relevant part of the Debug log
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug/trace,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to trace, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
validations:
required: true
@@ -2,6 +2,7 @@ name: 功能请求 / Feature request
title: "[Feature] "
description: 提出你的功能请求 / Propose your feature request
labels: ["enhancement"]
type: "Feature"
body:
- type: markdown
@@ -33,3 +34,14 @@ body:
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
validations:
required: true
- type: checkboxes
id: os-labels
attributes:
label: 适用系统 / Target OS
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
options:
- label: windows
- label: macos
- label: linux
validations:
required: true
-4
View File
@@ -1,4 +0,0 @@
FROM rust:buster
COPY entrypoint.sh /entrypoint.sh
RUN chmod a+x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
-14
View File
@@ -1,14 +0,0 @@
name: "Build for Linux"
branding:
icon: user-check
color: gray-dark
inputs:
target:
required: true
description: "Rust Target"
runs:
using: "docker"
image: "Dockerfile"
args:
- ${{ inputs.target }}
-5
View File
@@ -1,5 +0,0 @@
pnpm install
pnpm check $INPUT_TARGET
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
pnpm build --target $INPUT_TARGET
-53
View File
@@ -1,53 +0,0 @@
#!/bin/bash
wget https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz
tar -Jxvf ./node-v20.10.0-linux-x64.tar.xz
export PATH=$(pwd)/node-v20.10.0-linux-x64/bin:$PATH
npm install pnpm -g
rustup target add "$INPUT_TARGET"
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
apt-get update
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
elif [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
dpkg --add-architecture i386
apt-get update
apt-get install -y libstdc++6:i386 libgdk-pixbuf2.0-dev:i386 libatomic1:i386 gcc-multilib g++-multilib libwebkit2gtk-4.0-dev:i386 libssl-dev:i386 libgtk-3-dev:i386 librsvg2-dev:i386 patchelf:i386 libayatana-appindicator3-dev:i386
export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
dpkg --add-architecture arm64
apt-get update
apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross libwebkit2gtk-4.0-dev:arm64 libgtk-3-dev:arm64 patchelf:arm64 librsvg2-dev:arm64 libayatana-appindicator3-dev:arm64
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1
elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
dpkg --add-architecture armhf
apt-get update
apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross libwebkit2gtk-4.0-dev:armhf libgtk-3-dev:armhf patchelf:armhf librsvg2-dev:armhf libayatana-appindicator3-dev:armhf
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
export CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc
export CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1
elif [ "$INPUT_TARGET" = "riscv64gc-unknown-linux-gnu" ]; then
dpkg --add-architecture riscv64
apt-get update
apt-get install -y libncurses6:riscv64 libtinfo6:riscv64 linux-libc-dev:riscv64 libncursesw6:riscv64 libssl3:riscv64 libcups2:riscv64
apt-get install -y --no-install-recommends g++-riscv64-linux-gnu libc6-dev-riscv64-cross libwebkit2gtk-4.0-dev:riscv64 libgtk-3-dev:riscv64 patchelf:riscv64 librsvg2-dev:riscv64 libayatana-appindicator3-dev:riscv64
export CARGO_TARGET_RISCV64_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc
export CC_riscv64_unknown_linux_gnu=riscv64-linux-gnu-gcc
export CXX_riscv64_unknown_linux_gnu=riscv64-linux-gnu-g++
export PKG_CONFIG_PATH=/usr/lib/riscv64-linux-gnu/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1
else
echo "Unknown target: $INPUT_TARGET" && exit 1
fi
bash .github/build-for-linux/build.sh
+311 -103
View File
@@ -2,9 +2,9 @@ name: Alpha Build
on:
workflow_dispatch:
push:
branches: [main]
tags-ignore: [updater, alpha]
schedule:
# UTC+8 0,6,12,18
- cron: "0 16,22,4,10 * * *"
permissions: write-all
env:
CARGO_INCREMENTAL: 0
@@ -15,21 +15,198 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
check_commit:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version changed
id: check
run: |
# For manual workflow_dispatch, always run
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
# Store current version from package.json
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "Current version: $CURRENT_VERSION"
# Get the previous commit's package.json version
git checkout HEAD~1 package.json
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
echo "Previous version: $PREVIOUS_VERSION"
# Reset back to current commit
git checkout HEAD package.json
# Check if version changed
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "Version unchanged: $CURRENT_VERSION"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
delete_old_assets:
needs: check_commit
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Delete Old Alpha Release Assets
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const releaseTag = 'alpha';
try {
// Get the release by tag name
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: releaseTag
});
console.log(`Found release with ID: ${release.id}`);
// Delete each asset
if (release.assets && release.assets.length > 0) {
console.log(`Deleting ${release.assets.length} assets`);
for (const asset of release.assets) {
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
await github.rest.repos.deleteReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: asset.id
});
}
console.log('All assets deleted successfully');
} else {
console.log('No assets found to delete');
}
} catch (error) {
if (error.status === 404) {
console.log('Release not found, nothing to delete');
} else {
console.error('Error:', error);
throw error;
}
}
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: delete_old_assets
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch Alpha update logs
id: fetch_alpha_logs
run: |
# Check if UPDATELOG.md exists
if [ -f "UPDATELOG.md" ]; then
# Extract the section starting with ## and containing -alpha until the next ## or end of file
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$ALPHA_LOGS" ]; then
echo "Found alpha update logs"
echo "ALPHA_LOGS<<EOF" >> $GITHUB_ENV
echo "$ALPHA_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No alpha sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash
- run: |
# 检查 ALPHA_LOGS 是否存在,如果不存在则使用默认消息
if [ -z "$ALPHA_LOGS" ]; then
echo "No alpha logs found, using default message"
ALPHA_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found alpha logs"
fi
# 生成 release.txt 文件
cat > release.txt << EOF
$ALPHA_LOGS
## 我应该下载哪个版本?
### MacOS
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg
### Linux
- Linux 64位: amd64.deb/amd64.rpm
- Linux arm64 architecture: arm64.deb/aarch64.rpm
- Linux armv7架构: armhf.deb/armhfp.rpm
### Windows (不再支持Win7)
#### 正常版本(推荐)
- 64位: x64-setup.exe
- arm64架构: arm64-setup.exe
#### 便携版问题很多不再提供
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- 64位: x64_fixed_webview2-setup.exe
- arm64架构: arm64_fixed_webview2-setup.exe
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
alpha:
needs: update_tag
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
@@ -37,7 +214,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@1.77.0
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -49,10 +226,16 @@ jobs:
cache-all-crates: true
cache-on-failure: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -64,13 +247,16 @@ jobs:
pnpm i
pnpm check ${{ matrix.target }}
- name: Release Alpha Version
run: pnpm release-alpha-version
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@@ -86,38 +272,112 @@ jobs:
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: pnpm portable ${{ matrix.target }} --alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
alpha-for-linux:
alpha-for-linux-arm:
needs: update_tag
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-unknown-linux-gnu
- os: ubuntu-latest
- os: ubuntu-22.04
target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
arch: arm64
- os: ubuntu-22.04
target: armv7-unknown-linux-gnueabihf
arch: armhf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Release Alpha Version
run: pnpm release-alpha-version
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
EOF
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
sudo mv /tmp/sources.list /etc/apt/sources.list
sudo dpkg --add-architecture ${{ matrix.arch }}
sudo apt update
sudo apt install -y \
libxslt1.1:${{ matrix.arch }} \
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
libayatana-appindicator3-dev:${{ matrix.arch }} \
libssl-dev:${{ matrix.arch }} \
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
uses: ./.github/build-for-linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Get Version
run: |
@@ -131,7 +391,6 @@ jobs:
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: |
@@ -139,6 +398,7 @@ jobs:
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
alpha-for-fixed-webview2:
needs: update_tag
strategy:
fail-fast: false
matrix:
@@ -146,9 +406,6 @@ jobs:
- os: windows-latest
target: x86_64-pc-windows-msvc
arch: x64
- os: windows-latest
target: i686-pc-windows-msvc
arch: x86
- os: windows-latest
target: aarch64-pc-windows-msvc
arch: arm64
@@ -170,7 +427,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -182,6 +439,9 @@ jobs:
pnpm i
pnpm check ${{ matrix.target }}
- name: Release Alpha Version
run: pnpm release-alpha-version
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
@@ -195,24 +455,37 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Rename
run: |
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
Rename-Item $file.FullName $newName
}
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
@@ -221,68 +494,3 @@ jobs:
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [alpha, alpha-for-linux, alpha-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash
- name: Update Tag
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
cat > release.txt << 'EOF'
## 我应该下载哪个版本?
### MacOS (提示文件损坏或开发者无法验证请查看下面FAQ)
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg
### Linux
- Linux 64位: amd64.deb/amd64.rpm
- Linux 32位: i386.deb/i386.rpm
- Linux arm64架构: arm64.deb/aarch64.rpm
- Linux armv7架构: armhf.deb/armhfp.rpm
### Windows (Win7 用户请查看下面FAQ中的解决方案)
#### 正常版本(推荐)
- 64位: x64-setup.exe
- 32位: x86-setup.exe
- arm64架构: arm64-setup.exe
#### 便携版(不推荐使用,无法自动更新)
- 64位: x64_portable.zip
- 32位: x86_portable.zip
- arm64架构: arm64_portable.zip
#### 内置Webview2版(体积较大,仅在企业版系统或Win7无法安装webview2时使用)
- 64位: x64_fixed_webview2-setup.exe
- 32位: x86_fixed_webview2-setup.exe
- arm64架构: arm64_fixed_webview2-setup.exe
### FAQ
- [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
+94
View File
@@ -0,0 +1,94 @@
name: Development Test
on:
workflow_dispatch:
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
dev:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
bundle: nsis
- os: macos-latest
target: aarch64-apple-darwin
bundle: dmg
- os: macos-latest
target: x86_64-apple-darwin
bundle: dmg
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
- name: Upload Artifacts
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
if-no-files-found: error
- name: Upload Artifacts
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
if-no-files-found: error
+153 -36
View File
@@ -6,6 +6,10 @@ permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
release:
@@ -15,14 +19,14 @@ jobs:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
@@ -30,7 +34,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@1.77.0
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -40,11 +44,18 @@ jobs:
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -61,8 +72,8 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@@ -76,44 +87,115 @@ jobs:
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: pnpm portable ${{ matrix.target }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-for-linux:
release-for-linux-arm:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-unknown-linux-gnu
- os: ubuntu-latest
- os: ubuntu-22.04
target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
arch: arm64
- os: ubuntu-22.04
target: armv7-unknown-linux-gnueabihf
arch: armhf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
EOF
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
sudo mv /tmp/sources.list /etc/apt/sources.list
sudo dpkg --add-architecture ${{ matrix.arch }}
sudo apt update
sudo apt install -y \
libxslt1.1:${{ matrix.arch }} \
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
libayatana-appindicator3-dev:${{ matrix.arch }} \
libssl-dev:${{ matrix.arch }} \
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
uses: ./.github/build-for-linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
uses: softprops/action-gh-release@v2
@@ -134,9 +216,6 @@ jobs:
- os: windows-latest
target: x86_64-pc-windows-msvc
arch: x64
- os: windows-latest
target: i686-pc-windows-msvc
arch: x86
- os: windows-latest
target: aarch64-pc-windows-msvc
arch: arm64
@@ -152,11 +231,13 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -181,17 +262,31 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Rename
run: |
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
Rename-Item $file.FullName $newName
}
- name: Upload Release
uses: softprops/action-gh-release@v2
@@ -209,7 +304,7 @@ jobs:
release-update:
runs-on: ubuntu-latest
needs: [release, release-for-linux]
needs: [release, release-for-linux-arm]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -217,7 +312,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -242,7 +337,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -256,3 +351,25 @@ jobs:
run: pnpm updater-fixed-webview2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget:
runs-on: ubuntu-latest
needs: [release-update]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Submit to Winget
uses: vedantmgoyal9/winget-releaser@main
with:
identifier: ClashVergeRev.ClashVergeRev
version: ${{env.VERSION}}
release-tag: v${{env.VERSION}}
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -36,7 +36,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
+2 -1
View File
@@ -7,4 +7,5 @@ dist-ssr
update.json
scripts/_env.sh
.vscode
.tool-versions
.tool-versions
.idea
+15 -3
View File
@@ -1,4 +1,16 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
#!/bin/bash
pnpm pretty-quick --staged
#pnpm pretty-quick --staged
# 运行 clippy fmt
#cargo fmt --manifest-path ./src-tauri/Cargo.toml
# if [ $? -ne 0 ]; then
# echo "rustfmt failed to format the code. Please fix the issues and try again."
# exit 1
# fi
#git add .
# 允许提交
exit 0
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# 运行 clippy
#cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
# 如果 clippy 失败,阻止 push
#if [ $? -ne 0 ]; then
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
# exit 1
#fi
# 允许 push
exit 0
+46 -7
View File
@@ -17,15 +17,28 @@ If you're a Windows user, you may need to perform some additional steps:
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
- The gnu `patch` tool should be installed
### Install Node.js Packages
After installing Rust and Node.js, install the necessary Node.js packages:
When you setup `Rust` environment, Only use toolchain with `Windows MSVC` , to change settings follow command:
```shell
pnpm i
rustup target add x86_64-pc-windows-msvc
rustup set default-host x86_64-pc-windows-msvc
```
### Download the Clash Binary
### Install Node.js Package
After installing Rust and Node.js, install the necessary Node.js and Node Package Manager:
```shell
npm install pnpm -g
```
### Install Dependencies
```shell
pnpm install
```
### Download the Mihomo Core Binary
You have two options for downloading the clash binary:
@@ -35,7 +48,7 @@ You have two options for downloading the clash binary:
# Use '--force' to force update to the latest version
# pnpm run check --force
```
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
- 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).
### Run the Development Server
@@ -49,12 +62,38 @@ pnpm dev:diff
### Build the Project
If you want to build the project, use:
To build this project:
```shell
pnpm build
```
For a faster build, use the following command
```shell
pnpm build:fast
```
This uses Rust's fast-release profile which significantly reduces compilation time by disabling optimization and LTO. The resulting binary will be larger and less performant than the standard build, but it's useful for testing changes quickly.
The `Artifacts` will display in the `log` in the Terminal.
### Build clean
To clean rust build:
```shell
pnpm clean
```
### Portable Version (Windows Only)
To package portable version after the build:
```shell
pnpm portable
```
## Contributing Your Changes
Once you have made your changes:
+15 -9
View File
@@ -25,34 +25,40 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
---
### TG Group: [@clash_verge_rev](https://t.me/clash_verge_rev)
### TG 频道: [@clash_verge_rev](https://t.me/clash_verge_re)
## Promotion
[狗狗加速 —— 技术流机场 Doggygo VPN](https://狗狗加速.com)
[狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:https://verge.狗狗加速.com/#/register?code=oaxsAGo6
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
- 海外团队,无跑路风险,高达 50% 返佣
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
- 解锁流媒体及 ChatGPT
- 官网:https://狗狗加速.com
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
## Features
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://clash-verge-rev.github.io)
- Improved UI and supports custom theme color.
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
- System proxy setting and guard.
- 基于性能强劲的 Rust 和 Tauri 2 框架
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection`。
- 配置文件管理和增强(Merge 和 Script),配置文件语法提示。
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。
- 可视化节点和规则编辑
- WebDav 配置备份和同步
### FAQ
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
### Donation
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
## Development
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
+435
View File
@@ -1,3 +1,438 @@
## v2.2.1
**发行代号:拓**
感谢 Tunglies 对 Verge 后端重构,性能优化做出的重大贡献!
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
2.2.1 相对于 2.2.0(已下架不在提供)
修复了:
1. **首页**
- 修复 Direct 模式首页无法渲染
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
- 修复 系统代理标识判断不准的问题
- 修复 系统代理地址错误的问题
- 代理模式“多余的切换动画”
2. **系统**
- 修复 MacOS 无法使用快捷键粘贴/选择/复制订阅地址。
- 修复 代理端口设置同步问题。
- 修复 Linux 无法与 Mihomo 核心 和 ClashVergeRev 服务通信
3. **界面**
- 修复 连接详情卡没有跟随主题色
4. **轻量模式**
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
新增了:
1. **首页**
- 首页文本过长自动截断
2. **轻量模式**
- 新增托盘进入轻量模式支持
- 新增进入轻量模式快捷键支持
3. **系统**
- 在 ClashVergeRev 对 Mihomo 进行操作时,总是尝试确保两者运行
- 服务器模式下启动mihomo内核的时候查找并停止其他已经存在的内核进程,防止内核假死等问题带来的通信失败
4. **托盘**
- 新增 MacOS 启用托盘速率显示时,可选隐藏托盘图标显示
---
## v2.2.0(已下架不在提供)
#### 新增功能
1. **首页**
- 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。
- 首页支持轻量模式切换。
- 流量统计数据持久保存。
- 限制首页配置文件卡片URL长度。
2. **DNS 设置与覆写**
- 新增 DNS 覆写功能。
- 默认启用 DNS 覆写。
3. **解锁测试**
- 新增解锁测试页面。
4. **轻量模式**
- 新增轻量模式及设置。
- 添加自动轻量模式定时器。
5. **系统支持**
- Mihomo(meta)内核升级 1.19.3
- macOS 支持 CMD+W 关闭窗口。
- 新增 macOS 应用菜单。
- 添加 macOS 安装服务时候的管理员权限提示。
- 新增 sidecar(用户空间启动内核) 模式。
6. **其他**
- 增强延迟测试日志和错误处理。
- 添加诊断信息导出。
- 新增代理命令。
#### 修复
1. **系统**
- 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。
- 修复 macOS 静默启动崩溃。
- 修复 macOS tray图标错位到左上角的问题。
- 修复 Windows/Linux 运行时崩溃。
- 修复 Win10 阴影和边框问题。
2. **构建**
- 修复构建失败问题。
#### 优化
1. **性能**
- 重构后端,巨幅性能优化。
- 优化首页组件性能。
- 优化流量图表资源使用。
- 提升代理组列表滚动性能。
- 加快应用退出速度。
- 加快进入轻量模式速度。
- 优化小数值速度更新。
- 增加请求超时至 60 秒。
- 修复代理节点选择同步。
- 优化修改verge配置性能。
2. **重构**
- 重构后端,巨幅性能优化。
- 优化定时器管理。
- 重构 MihomoManager 处理流量。
- 优化 WebSocket 连接。
3. **其他**
- 更新依赖。
- 默认 TUN 堆栈改为 gvisor。
---
## v2.1.2
**发行代号:臻**
代号释义: 千锤百炼臻至善,集性能跃升、功能拓展、交互焕新于一体,彰显持续打磨、全方位优化的迭代精神。
感谢 Tychristine 对社区群组管理做出的重大贡献!
##### 2.1.2相对2.1.1(已下架不在提供)更新了:
- 无法更新和签名验证失败的问题(该死的CDN缓存)
- 设置菜单区分Verge基本设置和高级设置
- 增加v2 Updater的更多功能和权限
- 退出Verge后Tun代理状态仍保留的问题
##### 2.1.1相对2.1.0(已下架不在提供)更新了:
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
- 文件类型判断不准导致脚本检测报错的问题
- 打开Win下的阴影(Win10因底层兼容性问题,可能圆角和边框显示不太完美)
- 边框去白边
- 修复Linux下编译问题
- 修复热键无法关闭面板的问题
##### 2.1.0 - 发行代号:臻
### 功能新增
- 新增窗口状态实时监控与自动保存功能
- 增强核心配置变更时的验证与错误处理机制
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
- 添加连接表列宽持久化设置与进程过滤功能
- 新增代理组首字母导航与动态滚动定位功能
- 实现连接追踪暂停/恢复功能
- 支持从托盘菜单快速切换代理配置
- 添加轻量级模式开关选项
- 允许用户自定义TUN模式增强类型和FakeIP范围
- 新增系统代理状态指示器
- 增加Alpha版本自动重命名逻辑
- 优化字母导航工具提示与防抖交互机制
### 性能优化
- 重构代理列表渲染逻辑,提升布局计算效率
- 优化代理数据更新机制,采用乐观UI策略
- 改进虚拟列表渲染性能(Virtuoso)
- 提升主窗口Clash模式切换速度(感谢Tunglies)
- 加速内核关闭流程并优化管理逻辑
- 优化节点延迟刷新速率
- 改进托盘网速显示更新逻辑
- 提升配置验证错误信息的可读性
- 重构服务架构,优化代码组织结构(感谢Tunglies)
- 优化内核启动时的配置验证流程
### 问题修复
- 修复删除节点时关联组信息残留问题
- 解决菜单切换异常与重复勾选问题
- 修正连接页流量计算错误
- 修复Windows圆角显示异常问题
- 解决控制台废弃API警告
- 修复全局热键空值导致的崩溃
- 修复Alpha版本Windows打包重命名问题
- 修复MacOS端口切换崩溃问题
- 解决Linux持续集成更新器问题
- 修复静默启动后热键失效问题
- 修正TypeScript代理组类型定义
- 修复Windows托盘图标空白问题
- 优化远程目标地址显示(替换旧版IP展示)
### 交互体验
- 统一多平台托盘图标点击行为
- 优化代理列表滚动流畅度
- 改进日志搜索功能与数据管理
- 重构热键管理逻辑,修复托盘冻结问题
- 优化托盘网速显示样式
- 增强字母导航工具提示的动态响应
### 国际化
- 新增配置检查多语言支持
- 添加轻量级模式多语言文本
- 完善多语言翻译内容
### 维护更新
- 将默认TUN协议栈改为gVisor
- 更新Node.js运行版本
- 移除自动生成更新器文件
- 清理废弃代码与未使用组件
- 禁用工作流自动Alpha标签更新
- 更新依赖库版本
- 添加MacOS格式转换函数专项测试
- 优化开发模式日志输出
### 安全增强
- 强化应用启动时的配置验证机制
- 改进脚本验证与异常处理流程
- 修复编译警告(移除无用导入)
---
## v2.0.3
### Notice
- !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了:巨量改进与性能、稳定性提升,目前Clash Verge Rev已经有了比肩cfw的健壮性;而且更强大易用!
- 由于更改了服务安装逻辑,每次更新安装需要输入系统密码卸载老版本服务和安装新版本服务,以后可以丝滑使用tun(虚拟网卡)模式
### 2.0.3相对于2.0.2改进修复了:
1. 修复VLess-URL识别网络类型错误 f400f90 #2126
2. 新增系统代理绕过文本校验 c71e18e
3. 修复脚本编辑器UI显示不正确 6197249 #2267
4. 修复Shift热键无效 589324b #2278
5. 新增nushell环境变量复制 d233a84
6. 修复全局扩展脚本无法覆写DNS d22b37c #2235
7. 切换到系统代理相对于稳定的版本 38745d4
8. 修改fake-ip-range网段 0e3b631
9. 修复窗口隐藏后WebSocket未断开连接,减小内存风险 b42d13f
10. 改进系统代理绕过设置 c5c840d
11. 修复i18n翻译文本缺失 b149084
12. 修复双击托盘图标打开面板 f839d3b #2346
13. 修复Windows10窗口白色边框 4f6ca40 #2425
14. 修复Windows窗口状态恢复 4f6ca40
15. 改进保存配置文件自动重启Mihomo内核 0669f7a
16. 改进更新托盘图标性能 d9291d4
17. 修复保存配置后代理列表未更新 542baf9 #2460
18. 新增MacOS托盘显示实时速率,可在"界面设置"中关闭 1b2f1b6
19. 新增托盘菜单显示已设置的快捷键 eeff4d4
20. 新增重载配置文件错误响应"400"时显示更多错误信息 c5989d2 #2492
21. 修复GUI代理状态与菜单显示不一致 13b63b5 #2502
22. 新增默认语言跟随系统语言(无语言支持即为英语),添加了阿拉伯语、印尼语、鞑靼语支持 9655f77 #2940
### Features
- Meta(mihomo)内核升级 1.19.1
- 增加更多语言和托盘语言跟随
- MacOS增加状态栏速率显示
- 托盘显示快捷键
- 重载配置文件错误响应"400"时显示更多错误信息
- 改进保存配置文件自动重启Mihomo内核
### Performance
- 改进更新托盘图标性能
- 窗口隐藏后WebSocket断开连接
---
## v2.0.2
### Notice
- !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了:巨量改进与性能、稳定性提升,目前Clash Verge Rev已经有了比肩cfw的健壮性;而且更强大易用!
- 由于更改了服务安装逻辑,Mac/Linux 首次安装需要输入系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
- 因 Tauri 2.0 底层 bug,关闭窗口后保留webview进程,优点是再次打开面板更快,缺点是内存使用略有增加
### 2.0.2相对于2.0.1改进了:
- MacOS 下自定义图标可以支持彩色、单色切换
- 修正了 Linux 下多个内核僵尸进程的问题
- 修正了 DNS ipv6 强制覆盖的逻辑
- 修改了 MacOS tun 模式下覆盖设置 dns 字段的问题
- 修正了 MacOS tray 图标不会随代理模式更改的问题
- 静默启动下重复运行会出现多个实例的bug
- 安装的时候自动删除历史残留启动项
- Tun模式默认是还用内核推荐的 mixed 堆栈
- 改进了默认窗口大小(启动软件窗口不会那么小了)
- 改进了 WebDAV 备份超时时间机制
- 测试菜单添加滚动条
- 改进和修正了 Tun 模式下对设置的覆盖逻辑
- 修复了打开配置出错的问题
- 修复了配置文件无法拖拽添加的问题
- 改善了浅色模式的对比度
### 2.0.1相对于2.0.0改进了:
- 无法从 2.0rc和2.0.0 升级的问题(已经安装了2.0版本的需手动下载安装)
- MacOS 系统下少有的无法安装服务,无法启动的问题,目前更健壮了
- 当系统中没有 yaml 编辑器的情况下,打开文件程序崩溃的问题
- Windows 应用内升级和覆盖安装不会删除老执行文件的问题
- 修改优化了 mac 下 fakeip 段和 dns
- 测试菜单 svg 图标格式检查
- 应用内升级重复安装 vs runtime 的问题
- 修复外部控制下密码有特殊字符认证出错的问题
- 修复恢复 Webdav 备份设置后, Webdav 设置丢失的问题
- 代理页面增加快速回到顶部的按钮
### Breaking changes
- 重大框架升级:使用 Tauri 2.0(巨量改进与性能提升)
- 出现 bug 到 issues 中提出;以后不再接受1.x版本的bug反馈。
- 强烈建议完全删除 1.x 老版本再安装此版本 !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
### Features
- Meta(mihomo)内核升级 1.18.10
- Win 下的系统代理替换为 Shadowsocks/CFW/v2rayN 等成熟的 sysproxy.exe 方案,解决拨号/VPN 环境下无法设置系统代理的问题
- 服务模式改进为启动软件时自动安装,TUN 模式可自由开启不再限制于服务模式
- Mac 下可用 URL Scheme 导入订阅
- 可使用 Ctrl(cmd)+Q 快捷键退出程序
- 成功导入订阅的提示消息
- 能自动选中新导入的订阅
- 日志加入颜色区分
- 改进多处文本表述
- 加入图标 svg 格式检测
- 增加更多 app 调试日志
- 添加 MacOS 下白色桌面的 tray 黑色配色(但会代理系统代理、tun 模式图标失效的问题)
- 增加 Webdav 备份功能
- 添加统一延迟的设置开关
- 添加 Windows 下自动检测并下载 vc runtime 的功能
- 支持显示 mux 和 mptcp 的节点标识
- 延迟测试连接更换 http 的 cp.cloudflare.com/generate_204 (关闭统一延迟的情况下延迟测试结果会有所增加)
- 重构日志记录逻辑,可以收集和筛选所有日志类型了(之前无法记录debug的日志类型)
### Performance
- 优化及重构内核启动管理逻辑
- 优化 TUN 启动逻辑
- 重构和优化 app_handle
- 重构系统代理绕过逻辑
- 移除无用的 PID 创建逻辑
- 优化系统 DNS 设置逻辑
- 后端实现窗口控制
- 重构 MacOS 下的 DNS 设置逻辑
### Bugs Fixes
- 修复已有多个订阅导入新订阅会跳选订阅的问题
- 修复多个 Linux 下的 bug, Tun 模式在 Linux 下目前工作正常
- 修复 Linux wayland 下任务栏图标缺失的问题
- 修复 Linux KDE 桌面环境无法启动的问题
- 移除多余退出变量和钩子
- 修复 MacOS 下 tray 菜单重启 app 失效的问题
- 修复某些特定配置文件载入失败的问题
- 修复 MacOS 下 tun 模式 fakeip 不生效的问题
- 修复 Linux 下 关闭 tun 模式文件报错的问题
- 修复快捷键设置的相关 bug
- 修复 Win 下点左键菜单闪现的问题(Mac 下的操作逻辑相反,默认情况下不管点左/右键均会打开菜单,闪现不属于 bug)
### Known issues
- Windows 下窗口大小无法记忆(等待上游修复)
- Webdav 备份因为安全性和兼容性问题,暂不支持跨平台配置同步
---
## v1.7.7
### Bugs Fixes
- 修复导入订阅没有自动重载(不显示节点)的问题
- 英语状态下修复 Windows 工具栏提示文本超过限制的问题
---
## v1.7.6
### Notice
- Clash Verge Rev 目前已进入稳定周期,日后更新将着重于 bug 修复与内核常规升级
### Features
- Meta(mihomo)内核升级 1.18.7
- 界面细节调整
- 优化服务模式安装逻辑
- 移除无用的 console log
- 能自动选择第一个订阅
### Bugs Fixes
- 修复服务模式安装问题
- 修复 Mac 下的代理绕过 CIDR 写法过滤
- 修复 32 位升级 URL
- 修复不同分组 URL 测试地址配置无效的问题
- 修复 Web UI 下的一处 hostname 参数
---
## v1.7.5
### Features
- 展示局域网 IP 地址信息
- 在设置页面直接复制环境变量
- 优化服务模式安装逻辑
### Performance
- 优化切换订阅速度
- 优化更改端口速度
### Bugs Fixes
- 调整 MacOS 托盘图标大小
- Trojan URI 解析错误
- 卡片拖动显示层级错误
- 代理绕过格式检查错误
- MacOS 下编辑器最大化失败
- MacOS 服务安装失败
- 更改窗口大小导致闪退的问题
---
## v1.7.3
### Features
- 支持可视化编辑订阅代理组
- 支持可视化编辑订阅节点
- 支持可视化编辑订阅规则
- 扩展脚本支持订阅名称参数 `function main(config, profileName)`
### Bugs Fixes
- 代理绕过格式检查错误
---
## v1.7.2
### Break Changes
Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 274 KiB

+72 -53
View File
@@ -1,87 +1,105 @@
{
"name": "clash-verge",
"version": "1.7.2",
"version": "2.2.1",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",
"dev:diff": "tauri dev -f verge-dev",
"build": "tauri build",
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-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",
"web:dev": "vite",
"web:build": "tsc && vite build",
"web:build": "tsc --noEmit && vite build",
"web:serve": "vite preview",
"check": "node scripts/check.mjs",
"updater": "node scripts/updater.mjs",
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"prepare": "husky install"
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
"release-alpha-version": "node scripts/release-alpha_version.mjs",
"prepare": "husky",
"clean": "cd ./src-tauri && cargo clean && cd -"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.15.21",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.15.21",
"@mui/x-data-grid": "^7.8.0",
"@tauri-apps/api": "^1.6.0",
"@mui/icons-material": "^6.4.8",
"@mui/lab": "6.0.0-beta.25",
"@mui/material": "^6.4.8",
"@mui/x-data-grid": "^7.28.0",
"@tauri-apps/api": "2.2.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
"@tauri-apps/plugin-notification": "^2.2.2",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "2.2.0",
"@tauri-apps/plugin-updater": "2.3.0",
"@types/d3-shape": "^3.1.7",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.0",
"axios": "^1.7.2",
"dayjs": "1.11.5",
"foxact": "^0.2.35",
"i18next": "^23.11.5",
"ahooks": "^3.8.4",
"axios": "^1.8.3",
"cli-color": "^2.0.4",
"d3-shape": "^3.2.0",
"dayjs": "1.11.13",
"foxact": "^0.2.44",
"glob": "^11.0.1",
"i18next": "^24.2.3",
"js-base64": "^3.7.7",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"meta-json-schema": "1.18.6",
"monaco-editor": "^0.49.0",
"monaco-yaml": "^5.2.0",
"nanoid": "^5.0.7",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.3.1",
"nanoid": "^5.1.5",
"peggy": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.52.1",
"react-error-boundary": "^4.1.2",
"react-hook-form": "^7.54.2",
"react-i18next": "^13.5.0",
"react-markdown": "^9.0.1",
"react-monaco-editor": "^0.55.0",
"react-router-dom": "^6.24.0",
"react-markdown": "^9.1.0",
"react-monaco-editor": "^0.56.2",
"react-router-dom": "^6.30.0",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.7.11",
"react-virtuoso": "^4.12.5",
"recharts": "^2.15.1",
"sockette": "^2.0.6",
"swr": "^2.2.5",
"tar": "^6.2.1",
"types-pac": "^1.0.2"
"swr": "^2.3.3",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.3"
},
"devDependencies": {
"@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.6.0",
"@types/fs-extra": "^9.0.13",
"@actions/github": "^6.0.0",
"@tauri-apps/cli": "2.2.7",
"@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-transition-group": "^4.4.10",
"@vitejs/plugin-legacy": "^5.4.1",
"@vitejs/plugin-react": "^4.3.1",
"adm-zip": "^0.5.14",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-transition-group": "^4.4.12",
"@vitejs/plugin-legacy": "^6.0.2",
"@vitejs/plugin-react": "^4.3.4",
"adm-zip": "^0.5.16",
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
"https-proxy-agent": "^5.0.1",
"husky": "^7.0.4",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"meta-json-schema": "^1.19.3",
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"sass": "^1.77.6",
"terser": "^5.31.1",
"typescript": "^5.5.3",
"vite": "^5.3.2",
"prettier": "^3.5.3",
"pretty-quick": "^4.1.1",
"sass": "^1.86.0",
"terser": "^5.39.0",
"typescript": "^5.8.2",
"vite": "^6.2.2",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
"vite-plugin-svgr": "^4.3.0"
},
"prettier": {
"tabWidth": 2,
@@ -89,5 +107,6 @@
"singleQuote": false,
"endOfLine": "lf"
},
"packageManager": "pnpm@9.1.4"
"type": "module",
"packageManager": "pnpm@9.13.2"
}
+4037 -5125
View File
File diff suppressed because it is too large Load Diff
+154 -78
View File
@@ -1,11 +1,14 @@
import fs from "fs-extra";
import fs from "fs";
import fsp from "fs/promises";
import zlib from "zlib";
import tar from "tar";
import { extract } from "tar";
import path from "path";
import AdmZip from "adm-zip";
import fetch from "node-fetch";
import proxyAgent from "https-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
import { execSync } from "child_process";
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
import { glob } from "glob";
const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
@@ -82,7 +85,7 @@ async function getLatestAlphaVersion() {
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_ALPHA_VERSION_URL, {
@@ -91,9 +94,9 @@ async function getLatestAlphaVersion() {
});
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest alpha version: ${META_ALPHA_VERSION}`);
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
} catch (error) {
console.error("Error fetching latest alpha version:", error.message);
log_error("Error fetching latest alpha version:", error.message);
process.exit(1);
}
}
@@ -129,7 +132,7 @@ async function getLatestReleaseVersion() {
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_VERSION_URL, {
@@ -138,9 +141,9 @@ async function getLatestReleaseVersion() {
});
let v = await response.text();
META_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest release version: ${META_VERSION}`);
log_info(`Latest release version: ${META_VERSION}`);
} catch (error) {
console.error("Error fetching latest release version:", error.message);
log_error("Error fetching latest release version:", error.message);
process.exit(1);
}
}
@@ -150,13 +153,13 @@ async function getLatestReleaseVersion() {
*/
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
@@ -205,44 +208,44 @@ async function resolveSidecar(binInfo) {
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
const sidecarPath = path.join(sidecarDir, targetFile);
await fs.mkdirp(sidecarDir);
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
await fsp.mkdir(sidecarDir, { recursive: true });
if (!FORCE && fs.existsSync(sidecarPath)) return;
const tempDir = path.join(TEMP_DIR, name);
const tempZip = path.join(tempDir, zipFile);
const tempExe = path.join(tempDir, exeFile);
await fs.mkdirp(tempDir);
await fsp.mkdir(tempDir, { recursive: true });
try {
if (!(await fs.pathExists(tempZip))) {
if (!fs.existsSync(tempZip)) {
await downloadFile(downloadURL, tempZip);
}
if (zipFile.endsWith(".zip")) {
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
log_debug(`"${name}" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.rename(tempExe, sidecarPath);
console.log(`[INFO]: "${name}" unzip finished`);
await fsp.rename(tempExe, sidecarPath);
log_success(`unzip finished: "${name}"`);
} else if (zipFile.endsWith(".tgz")) {
// tgz
await fs.mkdirp(tempDir);
await tar.extract({
await fsp.mkdir(tempDir, { recursive: true });
await extract({
cwd: tempDir,
file: tempZip,
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
});
const files = await fs.readdir(tempDir);
console.log(`[DEBUG]: "${name}" files in tempDir:`, files);
const files = await fsp.readdir(tempDir);
log_debug(`"${name}" files in tempDir:`, files);
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
if (extractedFile) {
const extractedFilePath = path.join(tempDir, extractedFile);
await fs.rename(extractedFilePath, sidecarPath);
console.log(`[INFO]: "${name}" file renamed to "${sidecarPath}"`);
await fsp.rename(extractedFilePath, sidecarPath);
log_success(`"${name}" file renamed to "${sidecarPath}"`);
execSync(`chmod 755 ${sidecarPath}`);
console.log(`[INFO]: "${name}" chmod binary finished`);
log_success(`chmod binary finished: "${name}"`);
} else {
throw new Error(`Expected file not found in ${tempDir}`);
}
@@ -252,16 +255,15 @@ async function resolveSidecar(binInfo) {
const writeStream = fs.createWriteStream(sidecarPath);
await new Promise((resolve, reject) => {
const onError = (error) => {
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
log_error(`"${name}" gz failed:`, error.message);
reject(error);
};
readStream
.pipe(zlib.createGunzip().on("error", onError))
.pipe(writeStream)
.on("finish", () => {
console.log(`[INFO]: "${name}" gunzip finished`);
execSync(`chmod 755 ${sidecarPath}`);
console.log(`[INFO]: "${name}" chmod binary finished`);
log_success(`chmod binary finished: "${name}"`);
resolve();
})
.on("error", onError);
@@ -269,35 +271,58 @@ async function resolveSidecar(binInfo) {
}
} catch (err) {
// 需要删除文件
await fs.remove(sidecarPath);
await fsp.rm(sidecarPath, { recursive: true, force: true });
throw err;
} finally {
// delete temp dir
await fs.remove(tempDir);
await fsp.rm(tempDir, { recursive: true, force: true });
}
}
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
localPath: path.join(cwd, "scripts/set_dns.sh"),
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
localPath: path.join(cwd, "scripts/unset_dns.sh"),
});
/**
* download the file to the resources dir
*/
async function resolveResource(binInfo) {
const { file, downloadURL } = binInfo;
const { file, downloadURL, localPath } = binInfo;
const resDir = path.join(cwd, "src-tauri/resources");
const targetPath = path.join(resDir, file);
if (!FORCE && (await fs.pathExists(targetPath))) return;
if (!FORCE && fs.existsSync(targetPath)) return;
await fs.mkdirp(resDir);
await downloadFile(downloadURL, targetPath);
if (downloadURL) {
await fsp.mkdir(resDir, { recursive: true });
await downloadFile(downloadURL, targetPath);
}
console.log(`[INFO]: ${file} finished`);
if (localPath) {
await fs.copyFile(localPath, targetPath, (err) => {
if (err) {
console.error("Error copying file:", err);
} else {
console.log("File was copied successfully");
}
});
log_debug(`copy file finished: "${localPath}"`);
}
log_success(`${file} finished`);
}
/**
* download file and save to `path`
*/
async function downloadFile(url, path) {
*/ async function downloadFile(url, path) {
const options = {};
const httpProxy =
@@ -307,7 +332,7 @@ async function downloadFile(url, path) {
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
options.agent = new HttpsProxyAgent(httpProxy);
}
const response = await fetch(url, {
@@ -316,9 +341,9 @@ async function downloadFile(url, path) {
headers: { "Content-Type": "application/octet-stream" },
});
const buffer = await response.arrayBuffer();
await fs.writeFile(path, new Uint8Array(buffer));
await fsp.writeFile(path, new Uint8Array(buffer));
console.log(`[INFO]: download finished "${url}"`);
log_success(`download finished: ${url}`);
}
// SimpleSC.dll
@@ -329,87 +354,111 @@ const resolvePlugin = async () => {
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
await fs.mkdirp(pluginDir);
await fs.mkdirp(tempDir);
if (!FORCE && (await fs.pathExists(pluginPath))) return;
await fsp.mkdir(pluginDir, { recursive: true });
await fsp.mkdir(tempDir, { recursive: true });
if (!FORCE && fs.existsSync(pluginPath)) return;
try {
if (!(await fs.pathExists(tempZip))) {
if (!fs.existsSync(tempZip)) {
await downloadFile(url, tempZip);
}
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "SimpleSC" entry name`, entry.entryName);
log_debug(`"SimpleSC" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.copyFile(tempDll, pluginPath);
console.log(`[INFO]: "SimpleSC" unzip finished`);
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
log_success(`unzip finished: "SimpleSC"`);
} finally {
await fs.remove(tempDir);
await fsp.rm(tempDir, { recursive: true, force: true });
}
};
// service chmod
const resolveServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service",
"install-service",
"uninstall-service",
"clash-verge-service*",
"install-service*",
"uninstall-service*",
];
const resDir = path.join(cwd, "src-tauri/resources");
for (let f of serviceExecutables) {
const targetPath = path.join(resDir, f);
if (await fs.pathExists(targetPath)) {
execSync(`chmod 755 ${targetPath}`);
console.log(`[INFO]: "${targetPath}" chmod finished`);
// 使用glob模块来处理通配符
const files = glob.sync(path.join(resDir, f));
for (let filePath of files) {
if (fs.existsSync(filePath)) {
execSync(`chmod 755 ${filePath}`);
log_success(`chmod finished: "${filePath}"`);
}
}
}
};
// 在 resolveResource 函数后添加新函数
async function resolveLocales() {
const srcLocalesDir = path.join(cwd, "src/locales");
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
try {
// 确保目标目录存在
await fsp.mkdir(targetLocalesDir, { recursive: true });
// 读取所有语言文件
const files = await fsp.readdir(srcLocalesDir);
// 复制每个文件
for (const file of files) {
const srcPath = path.join(srcLocalesDir, file);
const targetPath = path.join(targetLocalesDir, file);
await fsp.copyFile(srcPath, targetPath);
log_success(`Copied locale file: ${file}`);
}
log_success("All locale files copied successfully");
} catch (err) {
log_error("Error copying locale files:", err.message);
throw err;
}
}
/**
* main
*/
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "clash-verge-service" + ext,
file: "clash-verge-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "install-service" + ext,
file: "install-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/install-service${ext}`,
});
};
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "uninstall-service" + ext,
file: "uninstall-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
});
};
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/set_dns.sh`,
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/unset_dns.sh`,
});
const resolveMmdb = () =>
resolveResource({
file: "Country.mmdb",
@@ -431,6 +480,12 @@ const resolveEnableLoopback = () =>
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
});
const resolveWinSysproxy = () =>
resolveResource({
file: "sysproxy.exe",
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
});
const tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
@@ -449,8 +504,6 @@ const tasks = [
{ name: "service", func: resolveService, retry: 5 },
{ name: "install", func: resolveInstall, retry: 5 },
{ name: "uninstall", func: resolveUninstall, retry: 5 },
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
{ name: "mmdb", func: resolveMmdb, retry: 5 },
{ name: "geosite", func: resolveGeosite, retry: 5 },
{ name: "geoip", func: resolveGeoIP, retry: 5 },
@@ -463,24 +516,48 @@ const tasks = [
{
name: "service_chmod",
func: resolveServicePermission,
retry: 1,
unixOnly: true,
retry: 5,
unixOnly: platform === "linux" || platform === "darwin",
},
{
name: "windows-sysproxy",
func: resolveWinSysproxy,
retry: 5,
winOnly: true,
},
{
name: "set_dns_script",
func: resolveSetDnsScript,
retry: 5,
macosOnly: true,
},
{
name: "unset_dns_script",
func: resolveUnSetDnsScript,
retry: 5,
macosOnly: true,
},
{
name: "locales",
func: resolveLocales,
retry: 2,
},
];
async function runTask() {
const task = tasks.shift();
if (!task) return;
if (task.winOnly && platform !== "win32") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
if (task.unixOnly && platform === "win32") return runTask();
if (task.winOnly && platform !== "win32") return runTask();
if (task.macosOnly && platform !== "darwin") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
for (let i = 0; i < task.retry; i++) {
try {
await task.func();
break;
} catch (err) {
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
log_error(`task::${task.name} try ${i} ==`, err.message);
if (i === task.retry - 1) throw err;
}
}
@@ -488,4 +565,3 @@ async function runTask() {
}
runTask();
runTask();
+56
View File
@@ -0,0 +1,56 @@
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
/**
* 为Alpha版本重命名版本号
*/
const execPromise = promisify(exec);
/**
* 标准输出HEAD hash
*/
async function getLatestCommitHash() {
try {
const { stdout } = await execPromise("git rev-parse HEAD");
const commitHash = stdout.trim();
// 格式化,只截取前7位字符
const formathash = commitHash.substring(0, 7);
console.log(`Found the latest commit hash code: ${commitHash}`);
return formathash;
} catch (error) {
console.error("pnpm run fix-alpha-version ERROR", error);
}
}
/**
* @param string 传入格式化后的hash
* 将新的版本号写入文件 package.json
*/
async function updatePackageVersion(newVersion) {
// 获取内容根目录
const _dirname = process.cwd();
const packageJsonPath = path.join(_dirname, "package.json");
try {
// 读取文件
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
// 获取键值替换
let result = packageJson.version.replace("alpha", newVersion);
console.log("[INFO]: Current version is: ", result);
packageJson.version = result;
// 写入版本号
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(`[INFO]: Alpha version update to: ${newVersion}`);
} catch (error) {
console.error("pnpm run fix-alpha-version ERROR", error);
}
}
const newVersion = await getLatestCommitHash();
updatePackageVersion(newVersion).catch(console.error);
+9 -6
View File
@@ -1,4 +1,5 @@
import fs from "fs-extra";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
@@ -30,12 +31,14 @@ async function resolvePortable() {
const configDir = path.join(releaseDir, ".config");
if (!(await fs.pathExists(releaseDir))) {
if (!fs.existsSync(releaseDir)) {
throw new Error("could not found the release dir");
}
await fs.mkdir(configDir);
await fs.createFile(path.join(configDir, "PORTABLE"));
await fsp.mkdir(configDir, { recursive: true });
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
}
const zip = new AdmZip();
@@ -46,9 +49,9 @@ async function resolvePortable() {
zip.addLocalFolder(
path.join(
releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
),
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
);
zip.addLocalFolder(configDir, ".config");
+8 -48
View File
@@ -1,21 +1,17 @@
import fs from "fs-extra";
import fs from "fs";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
import fsp from "fs/promises";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
"i686-pc-windows-msvc": "x86",
"aarch64-pc-windows-msvc": "arm64",
};
const PROCESS_MAP = {
x64: "x64",
ia32: "x86",
arm64: "arm64",
};
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
@@ -29,16 +25,17 @@ async function resolvePortable() {
: `./src-tauri/target/release`;
const configDir = path.join(releaseDir, ".config");
if (!(await fs.pathExists(releaseDir))) {
if (!fs.existsSync(releaseDir)) {
throw new Error("could not found the release dir");
}
await fs.mkdir(configDir);
await fs.createFile(path.join(configDir, "PORTABLE"));
await fsp.mkdir(configDir, { recursive: true });
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
}
const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
@@ -47,46 +44,9 @@ async function resolvePortable() {
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
const { version } = packageJson;
const zipFile = `Clash.Verge_${version}_${arch}_portable.zip`;
zip.writeZip(zipFile);
console.log("[INFO]: create portable zip successfully");
// push release assets
if (process.env.GITHUB_TOKEN === undefined) {
throw new Error("GITHUB_TOKEN is required");
}
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
console.log("[INFO]: upload to ", tag);
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag,
});
let assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
let id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,
});
}
console.log(release.name);
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: release.id,
name: zipFile,
data: zip.toBuffer(),
});
}
resolvePortable().catch(console.error);
+96
View File
@@ -0,0 +1,96 @@
import fs from "fs/promises";
import path from "path";
/**
* 更新 package.json 文件中的版本号
*/
async function updatePackageVersion() {
const _dirname = process.cwd();
const packageJsonPath = path.join(_dirname, "package.json");
try {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
let result = packageJson.version;
if (!result.includes("alpha")) {
result = `${result}-alpha`;
}
console.log("[INFO]: Current package.json version is: ", result);
packageJson.version = result;
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(`[INFO]: package.json version updated to: ${result}`);
} catch (error) {
console.error("Error updating package.json version:", error);
}
}
/**
* 更新 Cargo.toml 文件中的版本号
*/
async function updateCargoVersion() {
const _dirname = process.cwd();
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
try {
const data = await fs.readFile(cargoTomlPath, "utf8");
const lines = data.split("\n");
const updatedLines = lines.map((line) => {
if (line.startsWith("version =")) {
const versionMatch = line.match(/version\s*=\s*"([^"]+)"/);
if (versionMatch && !versionMatch[1].includes("alpha")) {
const newVersion = `${versionMatch[1]}-alpha`;
return line.replace(versionMatch[1], newVersion);
}
}
return line;
});
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
} catch (error) {
console.error("Error updating Cargo.toml version:", error);
}
}
/**
* 更新 tauri.conf.json 文件中的版本号
*/
async function updateTauriConfigVersion() {
const _dirname = process.cwd();
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
try {
const data = await fs.readFile(tauriConfigPath, "utf8");
const tauriConfig = JSON.parse(data);
let version = tauriConfig.version;
if (!version.includes("alpha")) {
version = `${version}-alpha`;
}
console.log("[INFO]: Current tauri.conf.json version is: ", version);
tauriConfig.version = version;
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(`[INFO]: tauri.conf.json version updated to: ${version}`);
} catch (error) {
console.error("Error updating tauri.conf.json version:", error);
}
}
/**
* 主函数,依次更新所有文件的版本号
*/
async function main() {
await updatePackageVersion();
await updateCargoVersion();
await updateTauriConfigVersion();
}
main().catch(console.error);
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# 验证IPv4地址格式
function is_valid_ipv4() {
local ip=$1
local IFS='.'
local -a octets
[[ ! $ip =~ ^([0-9]+\.){3}[0-9]+$ ]] && return 1
read -r -a octets <<<"$ip"
[ "${#octets[@]}" -ne 4 ] && return 1
for octet in "${octets[@]}"; do
if ! [[ "$octet" =~ ^[0-9]+$ ]] || ((octet < 0 || octet > 255)); then
return 1
fi
done
return 0
}
# 验证IPv6地址格式
function is_valid_ipv6() {
local ip=$1
if [[ ! $ip =~ ^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$ ]] &&
[[ ! $ip =~ ^(([0-9a-fA-F]{0,4}:){0,7}:|(:[0-9a-fA-F]{0,4}:){0,6}:[0-9a-fA-F]{0,4})$ ]]; then
return 1
fi
return 0
}
# 验证IP地址是否为有效的IPv4或IPv6
function is_valid_ip() {
is_valid_ipv4 "$1" || is_valid_ipv6 "$1"
}
# 检查参数
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
# 获取网络接口和硬件端口
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
/Hardware Port:/{port=$0; gsub("Hardware Port: ", "", port)}
/Device: /{if ($2 == dev) {print port; exit}}
')
# 获取当前DNS设置
original_dns=$(networksetup -getdnsservers "$hardware_port")
# 检查当前DNS设置是否有效
is_valid_dns=false
for ip in $original_dns; do
ip=$(echo "$ip" | tr -d '[:space:]')
if [ -n "$ip" ] && (is_valid_ipv4 "$ip" || is_valid_ipv6 "$ip"); then
is_valid_dns=true
break
fi
done
# 更新DNS设置
if [ "$is_valid_dns" = false ]; then
echo "empty" >.original_dns.txt
else
echo "$original_dns" >.original_dns.txt
fi
networksetup -setdnsservers "$hardware_port" "$1"
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
/Hardware Port:/{
port=$0; gsub("Hardware Port: ", "", port)
}
/Device: /{
if ($2 == dev) {
print port;
exit
}
}
')
if [ -f .original_dns.txt ]; then
original_dns=$(cat .original_dns.txt)
networksetup -setdnsservers "$hardware_port" $original_dns
rm -rf .original_dns.txt
fi
+43 -3
View File
@@ -1,4 +1,5 @@
import fs from "fs-extra";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
const UPDATE_LOG = "UPDATELOG.md";
@@ -12,11 +13,11 @@ export async function resolveUpdateLog(tag) {
const file = path.join(cwd, UPDATE_LOG);
if (!(await fs.pathExists(file))) {
if (!fs.existsSync(file)) {
throw new Error("could not found UPDATELOG.md");
}
const data = await fs.readFile(file).then((d) => d.toString("utf8"));
const data = await fsp.readFile(file, "utf-8");
const map = {};
let p = "";
@@ -42,3 +43,42 @@ export async function resolveUpdateLog(tag) {
return map[tag].join("\n").trim();
}
export async function resolveUpdateLogDefault() {
const cwd = process.cwd();
const file = path.join(cwd, UPDATE_LOG);
if (!fs.existsSync(file)) {
throw new Error("could not found UPDATELOG.md");
}
const data = await fsp.readFile(file, "utf-8");
const reTitle = /^## v[\d\.]+/;
const reEnd = /^---/;
let isCapturing = false;
let content = [];
let firstTag = "";
for (const line of data.split("\n")) {
if (reTitle.test(line) && !isCapturing) {
isCapturing = true;
firstTag = line.slice(3).trim();
continue;
}
if (isCapturing) {
if (reEnd.test(line)) {
break;
}
content.push(line);
}
}
if (!firstTag) {
throw new Error("could not found any version tag in UPDATELOG.md");
}
return content.join("\n").trim();
}
+1 -1
View File
@@ -100,7 +100,7 @@ async function resolveUpdater() {
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) {
updateDataNew.platforms[key].url =
"https://mirror.ghproxy.com/" + value.url;
"https://download.clashverge.dev/" + value.url;
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
}
+155 -50
View File
@@ -1,10 +1,15 @@
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog } from "./updatelog.mjs";
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
// Add stable update JSON filenames
const UPDATE_TAG_NAME = "updater";
const UPDATE_JSON_FILE = "update.json";
const UPDATE_JSON_PROXY = "update-proxy.json";
// Add alpha update JSON filenames
const ALPHA_TAG_NAME = "updater-alpha";
const ALPHA_UPDATE_JSON_FILE = "update-alpha.json";
const ALPHA_UPDATE_JSON_PROXY = "update-alpha-proxy.json";
/// generate update.json
/// upload to update tag's release asset
@@ -16,26 +21,73 @@ async function resolveUpdater() {
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 10,
page: 1,
});
// Fetch all tags using pagination
let allTags = [];
let page = 1;
const perPage = 100;
// get the latest publish tag
const tag = tags.find((t) => t.name.startsWith("v"));
while (true) {
const { data: pageTags } = await github.rest.repos.listTags({
...options,
per_page: perPage,
page: page,
});
console.log(tag);
allTags = allTags.concat(pageTags);
// Break if we received fewer tags than requested (last page)
if (pageTags.length < perPage) {
break;
}
page++;
}
const tags = allTags;
console.log(`Retrieved ${tags.length} tags in total`);
// More flexible tag detection with regex patterns
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
// Get the latest stable tag and pre-release tag
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
console.log("All tags:", tags.map((t) => t.name).join(", "));
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
console.log(
"Pre-release tag:",
preReleaseTag ? preReleaseTag.name : "None found",
);
console.log();
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
// Process stable release
if (stableTag) {
await processRelease(github, options, stableTag, false);
}
// Process pre-release if found
if (preReleaseTag) {
await processRelease(github, options, preReleaseTag, true);
}
}
// Process a release (stable or alpha) and generate update files
async function processRelease(github, options, tag, isAlpha) {
if (!tag) return;
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
});
const updateData = {
name: tag.name,
notes: await resolveUpdateLog(tag.name), // use updatelog.md
notes: await resolveUpdateLog(tag.name).catch(() =>
resolveUpdateLogDefault().catch(() => "No changelog available"),
),
pub_date: new Date().toISOString(),
platforms: {
win64: { signature: "", url: "" }, // compatible with older formats
@@ -56,39 +108,40 @@ async function resolveUpdater() {
},
};
const promises = latestRelease.assets.map(async (asset) => {
const promises = release.assets.map(async (asset) => {
const { name, browser_download_url } = asset;
// Process all the platform URL and signature data
// win64 url
if (name.endsWith("x64-setup.nsis.zip")) {
if (name.endsWith("x64-setup.exe")) {
updateData.platforms.win64.url = browser_download_url;
updateData.platforms["windows-x86_64"].url = browser_download_url;
}
// win64 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) {
if (name.endsWith("x64-setup.exe.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms.win64.signature = sig;
updateData.platforms["windows-x86_64"].signature = sig;
}
// win32 url
if (name.endsWith("x64-setup.nsis.zip")) {
if (name.endsWith("x86-setup.exe")) {
updateData.platforms["windows-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) {
if (name.endsWith("x86-setup.exe.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig;
}
// win arm url
if (name.endsWith("arm64-setup.nsis.zip")) {
if (name.endsWith("arm64-setup.exe")) {
updateData.platforms["windows-aarch64"].url = browser_download_url;
}
// win arm signature
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
if (name.endsWith("arm64-setup.exe.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-aarch64"].signature = sig;
}
@@ -143,55 +196,107 @@ async function resolveUpdater() {
}
});
// 生成一个代理github的更新文件
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
// Generate a proxy update file for accelerated GitHub resources
const updateDataNew = JSON.parse(JSON.stringify(updateData));
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) {
updateDataNew.platforms[key].url =
"https://mirror.ghproxy.com/" + value.url;
"https://download.clashverge.dev/" + value.url;
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
}
});
// update the update.json
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: UPDATE_TAG_NAME,
});
// Get the appropriate updater release based on isAlpha flag
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
console.log(
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
releaseTag,
);
// delete the old assets
for (let asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
try {
let updateRelease;
try {
// Try to get the existing release
const response = await github.rest.repos.getReleaseByTag({
...options,
asset_id: asset.id,
tag: releaseTag,
});
updateRelease = response.data;
console.log(
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
);
} catch (error) {
// If release doesn't exist, create it
if (error.status === 404) {
console.log(
`Release with tag ${releaseTag} not found, creating new release...`,
);
const createResponse = await github.rest.repos.createRelease({
...options,
tag_name: releaseTag,
name: isAlpha
? "Auto-update Alpha Channel"
: "Auto-update Stable Channel",
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
prerelease: isAlpha,
});
updateRelease = createResponse.data;
console.log(
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
);
} else {
// If it's another error, throw it
throw error;
}
}
if (asset.name === UPDATE_JSON_PROXY) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error); // do not break the pipeline
// File names based on release type
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
// Delete existing assets with these names
for (let asset of updateRelease.assets) {
if (asset.name === jsonFile) {
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: asset.id,
});
}
if (asset.name === proxyFile) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error); // do not break the pipeline
}
}
// Upload new assets
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: jsonFile,
data: JSON.stringify(updateData, null, 2),
});
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: proxyFile,
data: JSON.stringify(updateDataNew, null, 2),
});
console.log(
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
);
} catch (error) {
console.error(
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
error.message,
);
}
// upload new assets
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_FILE,
data: JSON.stringify(updateData, null, 2),
});
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_PROXY,
data: JSON.stringify(updateDataNew, null, 2),
});
}
// get the signature file content
+11
View File
@@ -0,0 +1,11 @@
import clc from "cli-color";
export const log_success = (msg, ...optionalParams) =>
console.log(clc.green(msg), ...optionalParams);
export const log_error = (msg, ...optionalParams) =>
console.log(clc.red(msg), ...optionalParams);
export const log_info = (msg, ...optionalParams) =>
console.log(clc.bgBlue(msg), ...optionalParams);
var debugMsg = clc.xterm(245);
export const log_debug = (msg, ...optionalParams) =>
console.log(debugMsg(msg), ...optionalParams);
+1
View File
@@ -0,0 +1 @@
avoid-breaking-exported-api = true
+2
View File
@@ -1,6 +1,8 @@
# Generated by Cargo
# will have compiled files and executables
/target/
gen/
WixTools
resources
sidecar
+4302 -1741
View File
File diff suppressed because it is too large Load Diff
Regular → Executable
+94 -16
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "1.7.2"
version = "2.2.1"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -9,42 +9,86 @@ default-run = "clash-verge"
edition = "2021"
build = "build.rs"
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version="1", features = [] }
tauri-build = { version = "2.0.6", features = [] }
[dependencies]
warp = "0.3"
anyhow = "1.0"
dirs = "5.0"
anyhow = "1.0.97"
dirs = "6.0"
open = "5.1"
log = "0.4"
dunce = "1.0"
log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.30"
boa_engine = "0.18"
chrono = "0.4.40"
sysinfo = "0.33.1"
boa_engine = "0.20.0"
serde_json = "1.0"
serde_yaml = "0.9"
once_cell = "1.19"
once_cell = "1.20.3"
port_scanner = "0.1.5"
delay_timer = "0.11"
delay_timer = "0.11.6"
parking_lot = "0.12"
auto-launch = "0.5.0"
percent-encoding = "2.3.1"
window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] }
fs2 = "0.4.3"
window-shadows = { version = "0.2.2" }
tokio = { version = "1.43", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
tauri = { version="1", features = [ "fs-read-file", "fs-exists", "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "icon-ico", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
regex = "1.10.5"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
image = "0.25.5"
imageproc = "0.25.0"
rusttype = "0.9"
tauri = { version = "2.3.1", features = [
"protocol-asset",
"devtools",
"tray-icon",
"image-ico",
"image-png",
] }
network-interface = { version = "2.0.0", features = ["serde"] }
tauri-plugin-shell = "2.2.0"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-notification = "2.2.1"
tauri-plugin-process = "2.2.0"
tauri-plugin-clipboard-manager = "2.2.1"
tauri-plugin-deep-link = "2.2.0"
tauri-plugin-devtools = "2.0.0-rc"
url = "2.5.4"
zip = "2.2.3"
reqwest_dav = "0.1.14"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.1"
tokio-tungstenite = "0.26.2"
futures = "0.3"
sys-locale = "0.3.1"
async-trait = "0.1.87"
mihomo_api = { path = "src_crates/crate_mihomo_api" }
ab_glyph = "0.2.29"
tungstenite = "0.26.2"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
deelevate = "0.2.0"
winreg = "0.52.0"
winreg = "0.55.0"
url = "2.5.4"
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.2.0"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-updater = "2.5.1"
tauri-plugin-window-state = "2.2.1"
#openssl
[features]
@@ -57,3 +101,37 @@ panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"
strip = true
[profile.dev]
incremental = true
[profile.fast-release]
inherits = "release" # 继承 release 的配置
panic = "abort" # 与 release 相同
codegen-units = 256 # 增加编译单元,提升编译速度
lto = false # 禁用 LTO,提升编译速度
opt-level = 0 # 禁用优化,大幅提升编译速度
debug = true # 保留调试信息
strip = false # 不剥离符号,保留调试信息
[profile.fast-dev]
inherits = "dev" # 继承 dev 的配置
codegen-units = 256 # 增加编译单元,提升编译速度
opt-level = 0 # 禁用优化,进一步提升编译速度
incremental = true # 启用增量编译
debug = true # 保留调试信息
strip = false # 不剥离符号,保留调试信息
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies]
env_logger = "0.11.0"
mockito = "1.7.0"
tempfile = "3.17.1"
[workspace]
members = [
"src_crates/crate_mihomo_api",]
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Clash Verge</string>
<key>CFBundleURLSchemes</key>
<array>
<string>clash</string>
</array>
</dict>
</array>
</dict>
</plist>
BIN
View File
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
{
"identifier": "desktop-capability",
"platforms": ["macOS", "windows", "linux"],
"webviews": ["main"],
"windows": ["main"],
"permissions": [
"global-shortcut:default",
"updater:default",
"dialog:default",
"dialog:allow-ask",
"dialog:allow-message",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"process:allow-restart",
"deep-link:default",
"window-state:default",
"window-state:default",
"autostart:default"
]
}
+85
View File
@@ -0,0 +1,85 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-file",
"fs:allow-exists",
{
"identifier": "fs:scope",
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
},
"fs:allow-write-file",
{
"identifier": "fs:scope",
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
},
"fs:allow-app-read",
"fs:allow-app-read-recursive",
"fs:allow-appcache-read",
"fs:allow-appcache-read-recursive",
"fs:allow-appconfig-read",
"fs:allow-appconfig-read-recursive",
"core:window:allow-create",
"core:window:allow-center",
"core:window:allow-request-user-attention",
"core:window:allow-set-resizable",
"core:window:allow-set-maximizable",
"core:window:allow-set-minimizable",
"core:window:allow-set-closable",
"core:window:allow-set-title",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-close",
"core:window:allow-set-decorations",
"core:window:allow-set-always-on-top",
"core:window:allow-set-content-protected",
"core:window:allow-set-size",
"core:window:allow-set-min-size",
"core:window:allow-set-max-size",
"core:window:allow-set-position",
"core:window:allow-set-fullscreen",
"core:window:allow-set-focus",
"core:window:allow-set-icon",
"core:window:allow-set-skip-taskbar",
"core:window:allow-set-cursor-grab",
"core:window:allow-set-cursor-visible",
"core:window:allow-set-cursor-icon",
"core:window:allow-set-cursor-position",
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-start-dragging",
"core:window:allow-maximize",
"core:window:allow-toggle-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-set-maximizable",
"core:window:allow-set-minimizable",
"core:webview:allow-print",
"shell:allow-execute",
"shell:allow-open",
"shell:allow-kill",
"shell:allow-spawn",
"shell:allow-stdin-write",
"dialog:allow-open",
"notification:default",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-register-all",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
"process:allow-restart",
"process:allow-exit",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"shell:default",
"dialog:default",
"notification:default"
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -2,8 +2,9 @@
Categories={{{categories}}}
Comment={{{comment}}}
Exec={{{exec}}} %u
StartupWMClass={{{exec}}}
Icon={{{icon}}}
Name={{{name}}}
Terminal=false
Type=Application
MimeType=x-scheme-handler/clash;
MimeType=x-scheme-handler/clash;
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-service
chmod +x /usr/bin/clash-verge-service
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
/usr/bin/uninstall-service
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.application-groups</key>
<array>
<string>io.github.clash-verge-rev.clash-verge-rev</string>
</array>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>
@@ -1,5 +1,6 @@
; This file is copied from https://github.com/tauri-apps/tauri/blob/tauri-v1.5/tooling/bundler/src/bundle/windows/templates/installer.nsi
; and edit to fit the needs of the project. the latest tauri 2.x has a different base nsi script.
RequestExecutionLevel admin
Unicode true
; Set the compression algorithm. Default is LZMA.
@@ -16,6 +17,8 @@ Unicode true
!include "StrFunc.nsh"
!include "Win\COM.nsh"
!include "Win\Propkey.nsh"
!include "WinVer.nsh"
!include "LogicLib.nsh"
!addplugindir "$%AppData%\Local\NSIS\"
${StrCase}
${StrLoc}
@@ -688,11 +691,114 @@ SectionEnd
app_check_done:
!macroend
Var VC_REDIST_URL
Var VC_REDIST_EXE
Section CheckAndInstallVSRuntime
; 检查是否已安装 Visual C++ Redistributable
${If} ${IsNativeARM64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${ElseIf} ${RunningX64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${Else}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${EndIf}
checkInstall:
; 检查注册表
${If} ${RunningX64}
SetRegView 64
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${ARCH}" "Installed"
${If} $R0 == "1"
Goto Done
${EndIf}
${Else}
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86" "Installed"
${If} $R0 == "1"
Goto Done
${EndIf}
${EndIf}
; 如果没有安装,则下载并安装
DetailPrint "正在下载 Visual C++ Redistributable..."
nsisdl::download "$VC_REDIST_URL" "$TEMP\$VC_REDIST_EXE"
Pop $0
${If} $0 == "success"
DetailPrint "正在安装 Visual C++ Redistributable..."
ExecWait '"$TEMP\$VC_REDIST_EXE" /quiet /norestart' $0
${If} $0 == 0
DetailPrint "Visual C++ Redistributable 安装成功"
${Else}
DetailPrint "Visual C++ Redistributable 安装失败"
${EndIf}
Delete "$TEMP\$VC_REDIST_EXE"
${Else}
DetailPrint "Visual C++ Redistributable 下载失败"
${EndIf}
Done:
SectionEnd
Section Install
SetOutPath $INSTDIR
nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete old files before installation
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Copy main executable
File "${MAINBINARYSRCPATH}"
@@ -813,6 +919,35 @@ Section Uninstall
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete the app directory and its content from disk
; Copy main executable
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
@@ -827,6 +962,10 @@ Section Uninstall
Delete "$INSTDIR\\{{this}}"
{{/each}}
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"
+205
View File
@@ -0,0 +1,205 @@
use super::CmdResult;
use crate::{feat, utils::dirs, wrap_err};
use tauri::Manager;
/// 打开应用程序所在目录
#[tauri::command]
pub fn open_app_dir() -> CmdResult<()> {
let app_dir = wrap_err!(dirs::app_home_dir())?;
wrap_err!(open::that(app_dir))
}
/// 打开核心所在目录
#[tauri::command]
pub fn open_core_dir() -> CmdResult<()> {
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
wrap_err!(open::that(core_dir))
}
/// 打开日志目录
#[tauri::command]
pub fn open_logs_dir() -> CmdResult<()> {
let log_dir = wrap_err!(dirs::app_logs_dir())?;
wrap_err!(open::that(log_dir))
}
/// 打开网页链接
#[tauri::command]
pub fn open_web_url(url: String) -> CmdResult<()> {
wrap_err!(open::that(url))
}
/// 打开/关闭开发者工具
#[tauri::command]
pub fn open_devtools(app_handle: tauri::AppHandle) {
if let Some(window) = app_handle.get_webview_window("main") {
if !window.is_devtools_open() {
window.open_devtools();
} else {
window.close_devtools();
}
}
}
/// 退出应用
#[tauri::command]
pub fn exit_app() {
feat::quit(Some(0));
}
/// 重启应用
#[tauri::command]
pub async fn restart_app() -> CmdResult<()> {
feat::restart_app();
Ok(())
}
/// 获取便携版标识
#[tauri::command]
pub fn get_portable_flag() -> CmdResult<bool> {
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
}
/// 获取应用目录
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = wrap_err!(dirs::app_home_dir())?
.to_string_lossy()
.to_string();
Ok(app_home_dir)
}
/// 获取当前自启动状态
#[tauri::command]
pub fn get_auto_launch_status() -> CmdResult<bool> {
use crate::core::sysopt::Sysopt;
wrap_err!(Sysopt::global().get_launch_status())
}
/// 下载图标缓存
#[tauri::command]
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
let icon_path = icon_cache_dir.join(&name);
// 如果文件已存在,直接返回路径
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().to_string());
}
// 确保缓存目录存在
if !icon_cache_dir.exists() {
let _ = std::fs::create_dir_all(&icon_cache_dir);
}
// 使用临时文件名来下载
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
// 下载文件到临时位置
let response = wrap_err!(reqwest::get(&url).await)?;
// 检查内容类型是否为图片
let content_type = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let is_image = content_type.starts_with("image/");
// 获取响应内容
let content = wrap_err!(response.bytes().await)?;
// 检查内容是否为HTML (针对CDN错误页面)
let is_html = content.len() > 15
&& (content.starts_with(b"<!DOCTYPE html")
|| content.starts_with(b"<html")
|| content.starts_with(b"<?xml"));
// 只有当内容确实是图片时才保存
if is_image && !is_html {
{
let mut file = match std::fs::File::create(&temp_path) {
Ok(file) => file,
Err(_) => {
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().to_string());
} else {
return Err("Failed to create temporary file".into());
}
}
};
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
}
// 再次检查目标文件是否已存在,避免重命名覆盖其他线程已完成的文件
if !icon_path.exists() {
match std::fs::rename(&temp_path, &icon_path) {
Ok(_) => {}
Err(_) => {
let _ = std::fs::remove_file(&temp_path);
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().to_string());
}
}
}
} else {
let _ = std::fs::remove_file(&temp_path);
}
Ok(icon_path.to_string_lossy().to_string())
} else {
let _ = std::fs::remove_file(&temp_path);
Err(format!("下载的内容不是有效图片: {}", url))
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct IconInfo {
name: String,
previous_t: String,
current_t: String,
}
/// 复制图标文件
#[tauri::command]
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
use std::{fs, path::Path};
let file_path = Path::new(&path);
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
if !icon_dir.exists() {
let _ = fs::create_dir_all(&icon_dir);
}
let ext = match file_path.extension() {
Some(e) => e.to_string_lossy().to_string(),
None => "ico".to_string(),
};
let dest_path = icon_dir.join(format!(
"{0}-{1}.{ext}",
icon_info.name, icon_info.current_t
));
if file_path.exists() {
if icon_info.previous_t.trim() != "" {
fs::remove_file(
icon_dir.join(format!("{0}-{1}.png", icon_info.name, icon_info.previous_t)),
)
.unwrap_or_default();
fs::remove_file(
icon_dir.join(format!("{0}-{1}.ico", icon_info.name, icon_info.previous_t)),
)
.unwrap_or_default();
}
match fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()),
}
} else {
Err("file not found".to_string())
}
}
+223
View File
@@ -0,0 +1,223 @@
use super::CmdResult;
use crate::{config::*, core::*, feat, module::mihomo::MihomoManager, wrap_err};
use serde_yaml::Mapping;
/// 复制Clash环境变量
#[tauri::command]
pub fn copy_clash_env() -> CmdResult {
feat::copy_clash_env();
Ok(())
}
/// 获取Clash信息
#[tauri::command]
pub fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().latest().get_client_info())
}
/// 修改Clash配置
#[tauri::command]
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
wrap_err!(feat::patch_clash(payload).await)
}
/// 修改Clash模式
#[tauri::command]
pub async fn patch_clash_mode(payload: String) -> CmdResult {
feat::change_clash_mode(payload);
Ok(())
}
/// 切换Clash核心
#[tauri::command]
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
log::info!(target: "app", "changing core to {clash_core}");
match CoreManager::global()
.change_core(Some(clash_core.clone()))
.await
{
Ok(_) => {
log::info!(target: "app", "core changed to {clash_core}");
handle::Handle::notice_message("config_core::change_success", &clash_core);
handle::Handle::refresh_clash();
Ok(None)
}
Err(err) => {
let error_msg = err.to_string();
log::error!(target: "app", "failed to change core: {error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
}
}
/// 重启核心
#[tauri::command]
pub async fn restart_core() -> CmdResult {
wrap_err!(CoreManager::global().restart_core().await)
}
/// 获取代理延迟
#[tauri::command]
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
timeout: i32,
) -> CmdResult<serde_json::Value> {
MihomoManager::global()
.test_proxy_delay(&name, url, timeout)
.await
}
/// 测试URL延迟
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
}
/// 保存DNS配置到单独文件
#[tauri::command]
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use crate::utils::dirs;
use serde_yaml;
use std::fs;
// 获取DNS配置文件路径
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
// 保存DNS配置到文件
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
Ok(())
}
/// 应用或撤销DNS配置
#[tauri::command]
pub fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{handle, CoreManager},
utils::dirs,
};
use tauri::async_runtime;
// 使用spawn来处理异步操作
async_runtime::spawn(async move {
if apply {
// 读取DNS配置文件
let dns_path = match dirs::app_home_dir() {
Ok(path) => path.join("dns_config.yaml"),
Err(e) => {
log::error!(target: "app", "Failed to get home dir: {}", e);
return;
}
};
if !dns_path.exists() {
log::warn!(target: "app", "DNS config file not found");
return;
}
let dns_yaml = match std::fs::read_to_string(&dns_path) {
Ok(content) => content,
Err(e) => {
log::error!(target: "app", "Failed to read DNS config: {}", e);
return;
}
};
// 解析DNS配置并创建patch
let patch_config = match serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
Ok(config) => {
let mut patch = serde_yaml::Mapping::new();
patch.insert("dns".into(), config.into());
patch
}
Err(e) => {
log::error!(target: "app", "Failed to parse DNS config: {}", e);
return;
}
};
log::info!(target: "app", "Applying DNS config from file");
// 重新生成配置,确保DNS配置被正确应用
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
Config::runtime()
.latest()
.patch_config(patch_config.clone());
// 首先重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
return;
}
// 然后应用新配置
if let Err(err) = CoreManager::global().update_config().await {
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
} else {
log::info!(target: "app", "DNS config successfully applied");
handle::Handle::refresh_clash();
}
} else {
// 当关闭DNS设置时,不需要对配置进行任何修改
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
log::info!(target: "app", "DNS settings disabled, regenerating config");
// 重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config: {}", err);
return;
}
// 应用新配置
match CoreManager::global().update_config().await {
Ok(_) => {
log::info!(target: "app", "Config regenerated successfully");
handle::Handle::refresh_clash();
}
Err(err) => {
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
}
}
}
});
Ok(())
}
/// 检查DNS配置文件是否存在
#[tauri::command]
pub fn check_dns_config_exists() -> CmdResult<bool> {
use crate::utils::dirs;
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
Ok(dns_path.exists())
}
/// 获取DNS配置文件内容
#[tauri::command]
pub async fn get_dns_config_content() -> CmdResult<String> {
use crate::utils::dirs;
use std::fs;
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
if !dns_path.exists() {
return Err("DNS config file not found".into());
}
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
Ok(content)
}
+8
View File
@@ -0,0 +1,8 @@
use super::CmdResult;
use crate::{core::CoreManager, wrap_err};
/// 修复系统服务
#[tauri::command]
pub async fn repair_service() -> CmdResult {
wrap_err!(CoreManager::global().repair_service().await)
}
+9
View File
@@ -0,0 +1,9 @@
use crate::module::lightweight;
use super::CmdResult;
#[tauri::command]
pub async fn entry_lightweight_mode() -> CmdResult {
lightweight::entry_lightweight_mode();
Ok(())
}
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
use anyhow::Result;
// Common result type used by command functions
pub type CmdResult<T = ()> = Result<T, String>;
// Command modules
pub mod app;
pub mod clash;
pub mod core;
pub mod media_unlock_checker;
pub mod network;
pub mod profile;
pub mod proxy;
pub mod runtime;
pub mod save_profile;
pub mod system;
pub mod uwp;
pub mod validate;
pub mod verge;
pub mod webdav;
pub mod lighteweight;
// Re-export all command functions for backwards compatibility
pub use app::*;
pub use clash::*;
pub use core::*;
pub use media_unlock_checker::*;
pub use network::*;
pub use profile::*;
pub use proxy::*;
pub use runtime::*;
pub use save_profile::*;
pub use system::*;
pub use uwp::*;
pub use validate::*;
pub use verge::*;
pub use webdav::*;
pub use lighteweight::*;
+63
View File
@@ -0,0 +1,63 @@
use super::CmdResult;
use crate::wrap_err;
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
use sysproxy::{Autoproxy, Sysproxy};
/// get the system proxy
#[tauri::command]
pub fn get_sys_proxy() -> CmdResult<Mapping> {
let current = wrap_err!(Sysproxy::get_system_proxy())?;
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
map.insert(
"server".into(),
format!("{}:{}", current.host, current.port).into(),
);
map.insert("bypass".into(), current.bypass.into());
Ok(map)
}
/// get the system proxy
#[tauri::command]
pub fn get_auto_proxy() -> CmdResult<Mapping> {
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
map.insert("url".into(), current.url.into());
Ok(map)
}
/// 获取网络接口列表
#[tauri::command]
pub fn get_network_interfaces() -> Vec<String> {
use sysinfo::Networks;
let mut result = Vec::new();
let networks = Networks::new_with_refreshed_list();
for (interface_name, _) in &networks {
result.push(interface_name.clone());
}
result
}
/// 获取网络接口详细信息
#[tauri::command]
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
let names = get_network_interfaces();
let interfaces = wrap_err!(NetworkInterface::show())?;
let mut result = Vec::new();
for interface in interfaces {
if names.contains(&interface.name) {
result.push(interface);
}
}
Ok(result)
}
+177
View File
@@ -0,0 +1,177 @@
use super::CmdResult;
use crate::{
config::*,
core::*,
feat, log_err, ret_err,
utils::{dirs, help},
wrap_err,
};
/// 获取配置文件列表
#[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> {
let _ = tray::Tray::global().update_menu();
Ok(Config::profiles().data().clone())
}
/// 增强配置文件
#[tauri::command]
pub async fn enhance_profiles() -> CmdResult {
match CoreManager::global().update_config().await {
Ok((true, _)) => {
println!("[enhance_profiles] 配置更新成功");
log_err!(tray::Tray::global().update_tooltip());
handle::Handle::refresh_clash();
Ok(())
}
Ok((false, error_msg)) => {
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
handle::Handle::notice_message("config_validate::error", &error_msg);
Ok(())
}
Err(e) => {
println!("[enhance_profiles] 更新过程发生错误: {}", e);
handle::Handle::notice_message("config_validate::process_terminated", e.to_string());
Ok(())
}
}
}
/// 导入配置文件
#[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
wrap_err!(Config::profiles().data().append_item(item))
}
/// 重新排序配置文件
#[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
}
/// 创建配置文件
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
wrap_err!(Config::profiles().data().append_item(item))
}
/// 更新配置文件
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
wrap_err!(feat::update_profile(index, option).await)
}
/// 删除配置文件
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
if should_update {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
}
Ok(())
}
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
println!("[cmd配置patch] 开始修改配置文件");
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().latest().current.clone();
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
// 更新profiles配置
println!("[cmd配置patch] 正在更新配置草稿");
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
// 更新配置并进行验证
match CoreManager::global().update_config().await {
Ok((true, _)) => {
println!("[cmd配置patch] 配置更新成功");
handle::Handle::refresh_clash();
let _ = tray::Tray::global().update_tooltip();
Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?;
Ok(true)
}
Ok((false, error_msg)) => {
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
Config::profiles().discard();
// 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile {
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
};
// 静默恢复,不触发验证
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?;
println!("[cmd配置patch] 成功恢复到之前的配置");
}
// 发送验证错误通知
handle::Handle::notice_message("config_validate::error", &error_msg);
Ok(false)
}
Err(e) => {
println!("[cmd配置patch] 更新过程发生错误: {}", e);
Config::profiles().discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
Ok(false)
}
}
}
/// 根据profile name修改profiles
#[tauri::command]
pub async fn patch_profiles_config_by_profile_index(
_app_handle: tauri::AppHandle,
profile_index: String,
) -> CmdResult<bool> {
let profiles = IProfiles {
current: Some(profile_index),
items: None,
};
patch_profiles_config(profiles).await
}
/// 修改某个profile item的
#[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
Ok(())
}
/// 查看配置文件
#[tauri::command]
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
let file = {
wrap_err!(Config::profiles().latest().get_item(&index))?
.file
.clone()
.ok_or("the file field is null")
}?;
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
if !path.exists() {
ret_err!("the file not found");
}
wrap_err!(help::open_file(app_handle, path))
}
/// 读取配置文件内容
#[tauri::command]
pub fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = wrap_err!(profiles.get_item(&index))?;
let data = wrap_err!(item.read_file())?;
Ok(data)
}
+26
View File
@@ -0,0 +1,26 @@
use super::CmdResult;
use crate::{core::CoreManager, module::mihomo::MihomoManager};
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
CoreManager::global().ensure_running_core().await;
let mannager = MihomoManager::global();
let proxies = mannager
.refresh_proxies()
.await
.map(|_| mannager.get_proxies())
.or_else(|_| Ok(mannager.get_proxies()));
proxies
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
CoreManager::global().ensure_running_core().await;
let mannager = MihomoManager::global();
let providers = mannager
.refresh_providers_proxies()
.await
.map(|_| mannager.get_providers_proxies())
.or_else(|_| Ok(mannager.get_providers_proxies()));
providers
}
+36
View File
@@ -0,0 +1,36 @@
use super::CmdResult;
use crate::{config::*, wrap_err};
use anyhow::Context;
use serde_yaml::Mapping;
use std::collections::HashMap;
/// 获取运行时配置
#[tauri::command]
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().latest().config.clone())
}
/// 获取运行时YAML配置
#[tauri::command]
pub fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime();
let runtime = runtime.latest();
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::to_string(config).context("failed to convert config to yaml")
))
}
/// 获取运行时存在的键
#[tauri::command]
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().latest().exists_keys.clone())
}
/// 获取运行时日志
#[tauri::command]
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().latest().chain_logs.clone())
}
+116
View File
@@ -0,0 +1,116 @@
use super::CmdResult;
use crate::{config::*, core::*, utils::dirs, wrap_err};
use std::fs;
/// 保存profiles的配置
#[tauri::command]
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
if file_data.is_none() {
return Ok(());
}
// 在异步操作前完成所有文件操作
let (file_path, original_content, is_merge_file) = {
let profiles = Config::profiles();
let profiles_guard = profiles.latest();
let item = wrap_err!(profiles_guard.get_item(&index))?;
// 确定是否为merge类型文件
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let content = wrap_err!(item.read_file())?;
let path = item.file.clone().ok_or("file field is null")?;
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
(profiles_dir.join(path), content, is_merge)
};
// 保存新的配置文件
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_path_str = file_path.to_string_lossy().to_string();
println!(
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
file_path_str, is_merge_file
);
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
if is_merge_file {
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
match CoreManager::global()
.validate_config_file(&file_path_str, Some(true))
.await
{
Ok((true, _)) => {
println!("[cmd配置save] merge文件语法验证通过");
// 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await {
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
}
return Ok(());
}
Ok((false, error_msg)) => {
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
return Ok(());
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
return Err(e.to_string());
}
}
}
// 非merge文件使用完整验证流程
match CoreManager::global()
.validate_config_file(&file_path_str, None)
.await
{
Ok((true, _)) => {
println!("[cmd配置save] 验证成功");
Ok(())
}
Ok((false, error_msg)) => {
println!("[cmd配置save] 验证失败: {}", error_msg);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
// 智能判断错误类型
let is_script_error = file_path_str.ends_with(".js")
|| error_msg.contains("Script syntax error")
|| error_msg.contains("Script must contain a main function")
|| error_msg.contains("Failed to read script file");
if error_msg.contains("YAML syntax error")
|| error_msg.contains("Failed to read file:")
|| (!file_path_str.ends_with(".js") && !is_script_error)
{
// 普通YAML错误使用YAML通知处理
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理
println!("[cmd配置save] 脚本文件验证失败,发送通知");
let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg);
}
Ok(())
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
Err(e.to_string())
}
}
}
+71
View File
@@ -0,0 +1,71 @@
use super::CmdResult;
use crate::{
core::{self, handle, service, CoreManager},
module::sysinfo::PlatformSpecification,
wrap_err,
};
use once_cell::sync::Lazy;
use std::{
sync::atomic::{AtomicI64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use tauri_plugin_clipboard_manager::ClipboardExt;
// 存储应用启动时间的全局变量
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
// 获取当前系统时间,转换为毫秒级时间戳
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
AtomicI64::new(now)
});
#[tauri::command]
pub async fn export_diagnostic_info() -> CmdResult<()> {
let sysinfo = PlatformSpecification::new();
let info = format!("{:?}", sysinfo);
let app_handle = handle::Handle::global().app_handle().unwrap();
let cliboard = app_handle.clipboard();
if cliboard.write_text(info).is_err() {
log::error!(target: "app", "Failed to write to clipboard");
}
Ok(())
}
#[tauri::command]
pub async fn get_system_info() -> CmdResult<String> {
let sysinfo = PlatformSpecification::new();
let info = format!("{:?}", sysinfo);
Ok(info)
}
/// 获取当前内核运行模式
#[tauri::command]
pub async fn get_running_mode() -> Result<String, String> {
match CoreManager::global().get_running_mode().await {
core::RunningMode::Service => Ok("service".to_string()),
core::RunningMode::Sidecar => Ok("sidecar".to_string()),
core::RunningMode::NotRunning => Ok("not_running".to_string()),
}
}
/// 安装/重装系统服务
#[tauri::command]
pub async fn install_service() -> CmdResult {
wrap_err!(service::reinstall_service().await)
}
/// 获取应用的运行时间(毫秒)
#[tauri::command]
pub fn get_app_uptime() -> CmdResult<i64> {
let start_time = APP_START_TIME.load(Ordering::Relaxed);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
Ok(now - start_time)
}
+28
View File
@@ -0,0 +1,28 @@
use super::CmdResult;
/// Platform-specific implementation for UWP functionality
#[cfg(windows)]
mod platform {
use super::CmdResult;
use crate::{core::win_uwp, wrap_err};
pub async fn invoke_uwp_tool() -> CmdResult {
wrap_err!(win_uwp::invoke_uwptools().await)
}
}
/// Stub implementation for non-Windows platforms
#[cfg(not(windows))]
mod platform {
use super::CmdResult;
pub async fn invoke_uwp_tool() -> CmdResult {
Ok(())
}
}
/// Command exposed to Tauri
#[tauri::command]
pub async fn invoke_uwp_tool() -> CmdResult {
platform::invoke_uwp_tool().await
}
+104
View File
@@ -0,0 +1,104 @@
use super::CmdResult;
use crate::core::*;
/// 发送脚本验证通知消息
#[tauri::command]
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
handle::Handle::notice_message(&status, &msg);
Ok(())
}
/// 处理脚本验证相关的所有消息通知
/// 统一通知接口,保持消息类型一致性
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
if !result.0 {
let error_msg = &result.1;
// 根据错误消息内容判断错误类型
let status = if error_msg.starts_with("File not found:") {
"config_validate::file_not_found"
} else if error_msg.starts_with("Failed to read script file:") {
"config_validate::script_error"
} else if error_msg.starts_with("Script syntax error:") {
"config_validate::script_syntax_error"
} else if error_msg == "Script must contain a main function" {
"config_validate::script_missing_main"
} else {
// 如果是其他类型错误,作为一般脚本错误处理
"config_validate::script_error"
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
handle::Handle::notice_message(status, error_msg);
}
}
/// 验证指定脚本文件
#[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
log::info!(target: "app", "验证脚本文件: {}", file_path);
match CoreManager::global()
.validate_config_file(&file_path, None)
.await
{
Ok(result) => {
handle_script_validation_notice(&result, "脚本文件");
Ok(result.0) // 返回验证结果布尔值
}
Err(e) => {
let error_msg = e.to_string();
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
Ok(false)
}
}
}
/// 处理YAML验证相关的所有消息通知
/// 统一通知接口,保持消息类型一致性
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
if !result.0 {
let error_msg = &result.1;
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
// 检查是否为merge文件
let is_merge_file = file_type.contains("合并");
// 根据错误消息内容判断错误类型
let status = if error_msg.starts_with("File not found:") {
"config_validate::file_not_found"
} else if error_msg.starts_with("Failed to read file:") {
"config_validate::yaml_read_error"
} else if error_msg.starts_with("YAML syntax error:") {
if is_merge_file {
"config_validate::merge_syntax_error"
} else {
"config_validate::yaml_syntax_error"
}
} else if error_msg.contains("mapping values are not allowed") {
if is_merge_file {
"config_validate::merge_mapping_error"
} else {
"config_validate::yaml_mapping_error"
}
} else if error_msg.contains("did not find expected key") {
if is_merge_file {
"config_validate::merge_key_error"
} else {
"config_validate::yaml_key_error"
}
} else {
// 如果是其他类型错误,根据文件类型作为一般错误处理
if is_merge_file {
"config_validate::merge_error"
} else {
"config_validate::yaml_error"
}
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
handle::Handle::notice_message(status, error_msg);
}
}
+16
View File
@@ -0,0 +1,16 @@
use super::CmdResult;
use crate::{config::*, feat, wrap_err};
/// 获取Verge配置
#[tauri::command]
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge();
let verge_data = verge.data().clone();
Ok(IVergeResponse::from(verge_data))
}
/// 修改Verge配置
#[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
wrap_err!(feat::patch_verge(payload, false).await)
}
+46
View File
@@ -0,0 +1,46 @@
use super::CmdResult;
use crate::{config::*, core, feat, wrap_err};
use reqwest_dav::list_cmd::ListFile;
/// 保存 WebDAV 配置
#[tauri::command]
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
webdav_password: Some(password),
..IVerge::default()
};
Config::verge().draft().patch_config(patch.clone());
Config::verge().apply();
Config::verge()
.data()
.save_file()
.map_err(|err| err.to_string())?;
core::backup::WebDavClient::global().reset();
Ok(())
}
/// 创建 WebDAV 备份并上传
#[tauri::command]
pub async fn create_webdav_backup() -> CmdResult<()> {
wrap_err!(feat::create_backup_and_upload_webdav().await)
}
/// 列出 WebDAV 上的备份文件
#[tauri::command]
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
wrap_err!(feat::list_wevdav_backup().await)
}
/// 删除 WebDAV 上的备份文件
#[tauri::command]
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
wrap_err!(feat::delete_webdav_backup(filename).await)
}
/// 从 WebDAV 恢复备份文件
#[tauri::command]
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
wrap_err!(feat::restore_webdav_backup(filename).await)
}
-373
View File
@@ -1,373 +0,0 @@
use crate::{
config::*,
core::*,
feat,
utils::{dirs, help, resolve},
};
use crate::{ret_err, wrap_err};
use anyhow::{Context, Result};
use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque};
use sysproxy::{Autoproxy, Sysproxy};
use tauri::{api, Manager};
type CmdResult<T = ()> = Result<T, String>;
#[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> {
Ok(Config::profiles().data().clone())
}
#[tauri::command]
pub async fn enhance_profiles() -> CmdResult {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
Ok(())
}
#[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
wrap_err!(Config::profiles().data().append_item(item))
}
#[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
}
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
wrap_err!(Config::profiles().data().append_item(item))
}
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
wrap_err!(feat::update_profile(index, option).await)
}
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
if should_update {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
}
Ok(())
}
/// 修改profiles的
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
let _ = handle::Handle::update_systray_part();
Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?;
Ok(())
}
Err(err) => {
Config::profiles().discard();
log::error!(target: "app", "{err}");
Err(format!("{err}"))
}
}
}
/// 修改某个profile item的
#[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
wrap_err!(timer::Timer::global().refresh())
}
#[tauri::command]
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
let file = {
wrap_err!(Config::profiles().latest().get_item(&index))?
.file
.clone()
.ok_or("the file field is null")
}?;
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
if !path.exists() {
ret_err!("the file not found");
}
wrap_err!(help::open_file(app_handle, path))
}
#[tauri::command]
pub fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = wrap_err!(profiles.get_item(&index))?;
let data = wrap_err!(item.read_file())?;
Ok(data)
}
#[tauri::command]
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
if file_data.is_none() {
return Ok(());
}
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = wrap_err!(profiles.get_item(&index))?;
wrap_err!(item.save_file(file_data.unwrap()))
}
#[tauri::command]
pub fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().latest().get_client_info())
}
#[tauri::command]
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().latest().config.clone())
}
#[tauri::command]
pub fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime();
let runtime = runtime.latest();
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::to_string(config).context("failed to convert config to yaml")
))
}
#[tauri::command]
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().latest().exists_keys.clone())
}
#[tauri::command]
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().latest().chain_logs.clone())
}
#[tauri::command]
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
wrap_err!(feat::patch_clash(payload).await)
}
#[tauri::command]
pub fn get_verge_config() -> CmdResult<IVerge> {
Ok(Config::verge().data().clone())
}
#[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
wrap_err!(feat::patch_verge(payload).await)
}
#[tauri::command]
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
wrap_err!(CoreManager::global().change_core(clash_core).await)
}
/// restart the sidecar
#[tauri::command]
pub async fn restart_sidecar() -> CmdResult {
wrap_err!(CoreManager::global().run_core().await)
}
/// get the system proxy
#[tauri::command]
pub fn get_sys_proxy() -> CmdResult<Mapping> {
let current = wrap_err!(Sysproxy::get_system_proxy())?;
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
map.insert(
"server".into(),
format!("{}:{}", current.host, current.port).into(),
);
map.insert("bypass".into(), current.bypass.into());
Ok(map)
}
/// get the system proxy
#[tauri::command]
pub fn get_auto_proxy() -> CmdResult<Mapping> {
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
map.insert("url".into(), current.url.into());
Ok(map)
}
#[tauri::command]
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
Ok(logger::Logger::global().get_log())
}
#[tauri::command]
pub fn open_app_dir() -> CmdResult<()> {
let app_dir = wrap_err!(dirs::app_home_dir())?;
wrap_err!(open::that(app_dir))
}
#[tauri::command]
pub fn open_core_dir() -> CmdResult<()> {
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
wrap_err!(open::that(core_dir))
}
#[tauri::command]
pub fn open_logs_dir() -> CmdResult<()> {
let log_dir = wrap_err!(dirs::app_logs_dir())?;
wrap_err!(open::that(log_dir))
}
#[tauri::command]
pub fn open_web_url(url: String) -> CmdResult<()> {
wrap_err!(open::that(url))
}
#[cfg(windows)]
pub mod uwp {
use super::*;
use crate::core::win_uwp;
#[tauri::command]
pub async fn invoke_uwp_tool() -> CmdResult {
wrap_err!(win_uwp::invoke_uwptools().await)
}
}
#[tauri::command]
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
timeout: i32,
) -> CmdResult<clash_api::DelayRes> {
match clash_api::get_proxy_delay(name, url, timeout).await {
Ok(res) => Ok(res),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub fn get_portable_flag() -> CmdResult<bool> {
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
}
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
}
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = wrap_err!(dirs::app_home_dir())?
.to_string_lossy()
.to_string();
Ok(app_home_dir)
}
#[tauri::command]
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
let icon_path = icon_cache_dir.join(name);
if !icon_cache_dir.exists() {
let _ = std::fs::create_dir_all(&icon_cache_dir);
}
if !icon_path.exists() {
let response = wrap_err!(reqwest::get(url).await)?;
let mut file = wrap_err!(std::fs::File::create(&icon_path))?;
let content = wrap_err!(response.bytes().await)?;
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
}
Ok(icon_path.to_string_lossy().to_string())
}
#[tauri::command]
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
let file_path = std::path::Path::new(&path);
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
if !icon_dir.exists() {
let _ = std::fs::create_dir_all(&icon_dir);
}
let ext = match file_path.extension() {
Some(e) => e.to_string_lossy().to_string(),
None => "ico".to_string(),
};
let png_dest_path = icon_dir.join(format!("{name}.png"));
let ico_dest_path = icon_dir.join(format!("{name}.ico"));
let dest_path = icon_dir.join(format!("{name}.{ext}"));
if file_path.exists() {
std::fs::remove_file(png_dest_path).unwrap_or_default();
std::fs::remove_file(ico_dest_path).unwrap_or_default();
match std::fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()),
}
} else {
Err("file not found".to_string())
}
}
#[tauri::command]
pub fn open_devtools(app_handle: tauri::AppHandle) {
if let Some(window) = app_handle.get_window("main") {
if !window.is_devtools_open() {
window.open_devtools();
} else {
window.close_devtools();
}
}
}
#[tauri::command]
pub fn exit_app(app_handle: tauri::AppHandle) {
let _ = resolve::save_window_size_position(&app_handle, true);
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
std::process::exit(0);
}
pub mod service {
use super::*;
use crate::core::service;
#[tauri::command]
pub async fn check_service() -> CmdResult<service::JsonResponse> {
wrap_err!(service::check_service().await)
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
wrap_err!(service::install_service().await)
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
wrap_err!(service::uninstall_service().await)
}
}
#[cfg(not(windows))]
pub mod uwp {
use super::*;
#[tauri::command]
pub async fn invoke_uwp_tool() -> CmdResult {
Ok(())
}
}
+12 -5
View File
@@ -32,13 +32,13 @@ impl IClashTemp {
pub fn template() -> Self {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
tun.insert("stack".into(), "gvisor".into());
tun.insert("device".into(), "Mihomo".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].into());
tun.insert("mtu".into(), 1500.into());
#[cfg(not(target_os = "windows"))]
map.insert("redir-port".into(), 7895.into());
#[cfg(target_os = "linux")]
@@ -50,9 +50,13 @@ impl IClashTemp {
map.insert("allow-lan".into(), false.into());
map.insert("mode".into(), "rule".into());
map.insert("external-controller".into(), "127.0.0.1:9097".into());
let mut cors_map = Mapping::new();
cors_map.insert("allow-private-network".into(), true.into());
cors_map.insert("allow-origins".into(), vec!["*"].into());
map.insert("secret".into(), "".into());
map.insert("tun".into(), tun.into());
map.insert("external-controller-cors".into(), cors_map.into());
map.insert("unified-delay".into(), true.into());
Self(map)
}
@@ -153,17 +157,20 @@ impl IClashTemp {
}
pub fn guard_mixed_port(config: &Mapping) -> u16 {
let mut port = config
.get("mixed-port")
let raw_value = config.get("mixed-port");
let mut port = raw_value
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7897);
if port == 0 {
port = 7897;
}
port
}
+52 -13
View File
@@ -1,12 +1,14 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
config::PrfItem,
core::{handle, CoreManager},
enhance,
utils::{dirs, help},
};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::{env::temp_dir, path::PathBuf};
use std::path::PathBuf;
use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@@ -64,20 +66,57 @@ impl Config {
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles().data().append_item(script_item.clone())?;
}
crate::log_err!(Self::generate().await);
if let Err(err) = Self::generate_file(ConfigType::Run) {
log::error!(target: "app", "{err}");
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
// 如果不存在就将默认的clash文件拿过来
if !runtime_path.exists() {
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Clash Verge Runtime"),
)?;
// 生成运行时配置
crate::log_err!(Self::generate().await);
// 生成运行时配置文件并验证
let config_result = Self::generate_file(ConfigType::Run);
let validation_result = if config_result.is_ok() {
// 验证配置文件
println!("[首次启动] 开始验证配置");
match CoreManager::global().validate_config().await {
Ok((is_valid, error_msg)) => {
if !is_valid {
println!(
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
error_msg
);
CoreManager::global()
.use_default_config("config_validate::boot_error", &error_msg)
.await?;
Some(("config_validate::boot_error", error_msg))
} else {
println!("[首次启动] 配置验证成功");
Some(("config_validate::success", String::new()))
}
}
Err(err) => {
println!("[首次启动] 验证进程执行失败: {}", err);
CoreManager::global()
.use_default_config("config_validate::process_terminated", "")
.await?;
Some(("config_validate::process_terminated", String::new()))
}
}
} else {
println!("[首次启动] 生成配置文件失败,使用默认配置");
CoreManager::global()
.use_default_config("config_validate::error", "")
.await?;
Some(("config_validate::error", String::new()))
};
// 在单独的任务中发送通知
if let Some((msg_type, msg_content)) = validation_result {
tauri::async_runtime::spawn(async move {
sleep(Duration::from_secs(2)).await;
handle::Handle::notice_message(msg_type, &msg_content);
});
}
Ok(())
}
@@ -85,7 +124,7 @@ impl Config {
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
let path = match typ {
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
ConfigType::Check => temp_dir().join(CHECK_CONFIG),
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
};
let runtime = Config::runtime();
+95
View File
@@ -0,0 +1,95 @@
use crate::utils::dirs::get_encryption_key;
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const NONCE_LENGTH: usize = 12;
/// Encrypt data
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
let encryption_key = get_encryption_key()?;
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
let cipher = Aes256Gcm::new(key);
// Generate random nonce
let mut nonce = vec![0u8; NONCE_LENGTH];
getrandom::fill(&mut nonce)?;
// Encrypt data
let ciphertext = cipher
.encrypt(nonce.as_slice().into(), data.as_bytes())
.map_err(|e| format!("Encryption failed: {}", e))?;
// Concatenate nonce and ciphertext and encode them in base64
let mut combined = nonce;
combined.extend(ciphertext);
Ok(STANDARD.encode(combined))
}
/// Decrypt data
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
let encryption_key = get_encryption_key()?;
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
let cipher = Aes256Gcm::new(key);
// Decode from base64
let data = STANDARD.decode(encrypted)?;
if data.len() < NONCE_LENGTH {
return Err("Invalid encrypted data".into());
}
// Separate nonce and ciphertext
let (nonce, ciphertext) = data.split_at(NONCE_LENGTH);
// Decrypt data
let plaintext = cipher
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {}", e))?;
String::from_utf8(plaintext).map_err(|e| e.into())
}
/// Serialize encrypted function
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
S: Serializer,
{
// 如果序列化失败,返回 None
let json = match serde_json::to_string(value) {
Ok(j) => j,
Err(_) => return serializer.serialize_none(),
};
// 如果加密失败,返回 None
match encrypt_data(&json) {
Ok(encrypted) => serializer.serialize_str(&encrypted),
Err(_) => serializer.serialize_none(),
}
}
/// Deserialize decrypted function
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: for<'de> Deserialize<'de> + Default,
D: Deserializer<'a>,
{
// 如果反序列化字符串失败,返回默认值
let encrypted = match String::deserialize(deserializer) {
Ok(s) => s,
Err(_) => return Ok(T::default()),
};
// 如果解密失败,返回默认值
let decrypted_string = match decrypt_data(&encrypted) {
Ok(data) => data,
Err(_) => return Ok(T::default()),
};
// 如果 JSON 解析失败,返回默认值
match serde_json::from_str(&decrypted_string) {
Ok(value) => Ok(value),
Err(_) => Ok(T::default()),
}
}
+4 -7
View File
@@ -2,18 +2,15 @@ mod clash;
#[allow(clippy::module_inception)]
mod config;
mod draft;
mod encrypt;
mod prfitem;
mod profiles;
mod runtime;
mod verge;
pub use self::clash::*;
pub use self::config::*;
pub use self::draft::*;
pub use self::prfitem::*;
pub use self::profiles::*;
pub use self::runtime::*;
pub use self::verge::*;
pub use self::{
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
};
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
+3 -3
View File
@@ -234,10 +234,10 @@ impl PrfItem {
option: Option<PrfOption>,
) -> Result<PrfItem> {
let opt_ref = option.as_ref();
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
let accept_invalid_certs =
opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval);
let mut merge = opt_ref.and_then(|o| o.merge.clone());
+38 -5
View File
@@ -71,7 +71,6 @@ impl IProfiles {
if let Some(current) = patch.current {
let items = self.items.as_ref().unwrap();
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
@@ -111,6 +110,7 @@ impl IProfiles {
if item.uid.is_none() {
bail!("the uid should not be null");
}
let uid = item.uid.clone();
// save the file data
// move the field value after save
@@ -128,6 +128,12 @@ impl IProfiles {
.with_context(|| format!("failed to write to file \"{}\"", file))?;
}
if self.current.is_none()
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
{
self.current = uid;
}
if self.items.is_none() {
self.items = Some(vec![]);
}
@@ -135,6 +141,7 @@ impl IProfiles {
if let Some(items) = self.items.as_mut() {
items.push(item)
}
self.save_file()
}
@@ -355,10 +362,15 @@ impl IProfiles {
}
// delete the original uid
if current == uid {
self.current = match !items.is_empty() {
true => items[0].uid.clone(),
false => None,
};
self.current = None;
for item in items.iter() {
if item.itype == Some("remote".to_string())
|| item.itype == Some("local".to_string())
{
self.current = item.uid.clone();
break;
}
}
}
self.items = Some(items);
@@ -452,4 +464,25 @@ impl IProfiles {
_ => None,
}
}
/// 判断profile是否是current指向的
pub fn is_current_profile_index(&self, index: String) -> bool {
self.current == Some(index)
}
/// 获取所有的profiles(uid,名称)
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
self.items.as_ref().map(|items| {
items
.iter()
.filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
Some((uid, name))
} else {
None
}
})
.collect()
})
}
}
+18 -14
View File
@@ -19,27 +19,31 @@ impl IRuntime {
// 这里只更改 allow-lan | ipv6 | log-level | tun
pub fn patch_config(&mut self, patch: Mapping) {
if let Some(config) = self.config.as_mut() {
["allow-lan", "ipv6", "log-level"]
["allow-lan", "ipv6", "log-level", "unified-delay"]
.into_iter()
.for_each(|key| {
if let Some(value) = patch.get(key).to_owned() {
config.insert(key.into(), value.clone());
}
});
let tun = config.get("tun");
let mut tun = tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = patch.get("tun");
let patch_tun = patch_tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
use_keys(&patch_tun).into_iter().for_each(|key| {
if let Some(value) = patch_tun.get(&key).to_owned() {
tun.insert(key.into(), value.clone());
}
});
config.insert("tun".into(), Value::from(tun));
if patch_tun.is_some() {
let tun = config.get("tun");
let mut tun = tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = patch_tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
use_keys(&patch_tun).into_iter().for_each(|key| {
if let Some(value) = patch_tun.get(&key).to_owned() {
tun.insert(key.into(), value.clone());
}
});
config.insert("tun".into(), Value::from(tun));
}
}
}
}
+237 -32
View File
@@ -1,5 +1,7 @@
use crate::config::DEFAULT_PAC;
use crate::utils::{dirs, help};
use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
utils::{dirs, help, i18n},
};
use anyhow::Result;
use log::LevelFilter;
use serde::{Deserialize, Serialize};
@@ -7,9 +9,6 @@ use serde::{Deserialize, Serialize};
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVerge {
/// app listening port for app singleton
pub app_singleton_port: Option<u16>,
/// app log level
/// silent | error | warn | info | debug | trace
pub app_log_level: Option<String>,
@@ -59,10 +58,6 @@ pub struct IVerge {
/// clash tun mode
pub enable_tun_mode: Option<bool>,
/// windows service mode
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_service_mode: Option<bool>,
/// can the app auto startup
pub enable_auto_launch: Option<bool>,
@@ -75,6 +70,9 @@ pub struct IVerge {
/// enable proxy guard
pub enable_proxy_guard: Option<bool>,
/// enable dns settings - this controls whether dns_config.yaml is applied
pub enable_dns_settings: Option<bool>,
/// always use default bypass
pub use_default_bypass: Option<bool>,
@@ -104,6 +102,13 @@ pub struct IVerge {
/// format: {func},{key}
pub hotkeys: Option<Vec<String>>,
/// enable global hotkey
pub enable_global_hotkey: Option<bool>,
/// 首页卡片设置
/// 控制首页各个卡片的显示和隐藏
pub home_cards: Option<serde_json::Value>,
/// 切换代理时自动关闭连接
pub auto_close_connection: Option<bool>,
@@ -122,21 +127,13 @@ pub struct IVerge {
/// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>,
/// 测试网站列表
/// 测试站列表
pub test_list: Option<Vec<IVergeTestItem>>,
/// 日志清理
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
pub auto_log_clean: Option<i32>,
/// window size and position
#[serde(skip_serializing_if = "Option::is_none")]
pub window_size_position: Option<Vec<f64>>,
/// window size and position
#[serde(skip_serializing_if = "Option::is_none")]
pub window_is_maximized: Option<bool>,
/// 是否启用随机端口
pub enable_random_port: Option<bool>,
@@ -162,6 +159,46 @@ pub struct IVerge {
pub verge_port: Option<u16>,
pub verge_http_enabled: Option<bool>,
/// WebDAV 配置 (加密存储)
#[serde(
serialize_with = "serialize_encrypted",
deserialize_with = "deserialize_encrypted",
skip_serializing_if = "Option::is_none",
default
)]
pub webdav_url: Option<String>,
/// WebDAV 用户名 (加密存储)
#[serde(
serialize_with = "serialize_encrypted",
deserialize_with = "deserialize_encrypted",
skip_serializing_if = "Option::is_none",
default
)]
pub webdav_username: Option<String>,
/// WebDAV 密码 (加密存储)
#[serde(
serialize_with = "serialize_encrypted",
deserialize_with = "deserialize_encrypted",
skip_serializing_if = "Option::is_none",
default
)]
pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
pub enable_tray_icon: Option<bool>,
/// 自动进入轻量模式
pub enable_auto_light_weight_mode: Option<bool>,
/// 自动进入轻量模式的延迟(分钟)
pub auto_light_weight_minutes: Option<u64>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -189,6 +226,21 @@ pub struct IVergeTheme {
}
impl IVerge {
fn get_system_language() -> String {
let sys_lang = sys_locale::get_locale()
.unwrap_or_else(|| String::from("en"))
.to_lowercase();
let lang_code = sys_lang.split(['_', '-']).next().unwrap_or("en");
let supported_languages = i18n::get_supported_languages();
if supported_languages.contains(&lang_code.to_string()) {
lang_code.to_string()
} else {
String::from("en")
}
}
pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config,
@@ -202,13 +254,13 @@ impl IVerge {
pub fn template() -> Self {
Self {
clash_core: Some("verge-mihomo".into()),
language: Some("zh".into()),
language: Some(Self::get_system_language()),
theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))]
env_type: Some("bash".into()),
#[cfg(target_os = "windows")]
env_type: Some("powershell".into()),
start_page: Some("/".into()),
start_page: Some("/home".into()),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_group_icon: Some(true),
@@ -227,16 +279,16 @@ impl IVerge {
#[cfg(not(target_os = "windows"))]
verge_redir_port: Some(7895),
#[cfg(not(target_os = "windows"))]
verge_redir_enabled: Some(true),
verge_redir_enabled: Some(false),
#[cfg(target_os = "linux")]
verge_tproxy_port: Some(7896),
#[cfg(target_os = "linux")]
verge_tproxy_enabled: Some(true),
verge_tproxy_enabled: Some(false),
verge_mixed_port: Some(7897),
verge_socks_port: Some(7898),
verge_socks_enabled: Some(true),
verge_socks_enabled: Some(false),
verge_port: Some(7899),
verge_http_enabled: Some(true),
verge_http_enabled: Some(false),
enable_proxy_guard: Some(false),
use_default_bypass: Some(true),
proxy_guard_duration: Some(30),
@@ -244,6 +296,17 @@ impl IVerge {
auto_check_update: Some(true),
enable_builtin_enhanced: Some(true),
auto_log_clean: Some(3),
webdav_url: None,
webdav_username: None,
webdav_password: None,
enable_tray_speed: Some(true),
enable_tray_icon: Some(true),
enable_global_hotkey: Some(true),
enable_auto_light_weight_mode: Some(false),
auto_light_weight_minutes: Some(10),
enable_dns_settings: Some(true),
home_cards: None,
service_state: None,
..Self::default()
}
}
@@ -282,7 +345,6 @@ impl IVerge {
patch!(tun_tray_icon);
patch!(enable_tun_mode);
patch!(enable_service_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_random_port);
@@ -311,6 +373,7 @@ impl IVerge {
patch!(web_ui_list);
patch!(clash_core);
patch!(hotkeys);
patch!(enable_global_hotkey);
patch!(auto_close_connection);
patch!(auto_check_update);
@@ -320,8 +383,17 @@ impl IVerge {
patch!(proxy_layout_column);
patch!(test_list);
patch!(auto_log_clean);
patch!(window_size_position);
patch!(window_is_maximized);
patch!(webdav_url);
patch!(webdav_username);
patch!(webdav_password);
patch!(enable_tray_speed);
patch!(enable_tray_icon);
patch!(enable_auto_light_weight_mode);
patch!(auto_light_weight_minutes);
patch!(enable_dns_settings);
patch!(home_cards);
patch!(service_state);
}
/// 在初始化前尝试拿到单例端口的值
@@ -330,11 +402,7 @@ impl IVerge {
const SERVER_PORT: u16 = 33331;
#[cfg(feature = "verge-dev")]
const SERVER_PORT: u16 = 11233;
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT),
Err(_) => SERVER_PORT, // 这里就不log错误了
}
SERVER_PORT
}
/// 获取日志等级
@@ -354,3 +422,140 @@ impl IVerge {
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct IVergeResponse {
pub app_log_level: Option<String>,
pub language: Option<String>,
pub theme_mode: Option<String>,
pub tray_event: Option<String>,
pub env_type: Option<String>,
pub start_page: Option<String>,
pub startup_script: Option<String>,
pub traffic_graph: Option<bool>,
pub enable_memory_usage: Option<bool>,
pub enable_group_icon: Option<bool>,
pub common_tray_icon: Option<bool>,
#[cfg(target_os = "macos")]
pub tray_icon: Option<String>,
pub menu_icon: Option<String>,
pub sysproxy_tray_icon: Option<bool>,
pub tun_tray_icon: Option<bool>,
pub enable_tun_mode: Option<bool>,
pub enable_auto_launch: Option<bool>,
pub enable_silent_start: Option<bool>,
pub enable_system_proxy: Option<bool>,
pub enable_proxy_guard: Option<bool>,
pub enable_global_hotkey: Option<bool>,
pub use_default_bypass: Option<bool>,
pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>,
pub proxy_auto_config: Option<bool>,
pub pac_file_content: Option<String>,
pub theme_setting: Option<IVergeTheme>,
pub web_ui_list: Option<Vec<String>>,
pub clash_core: Option<String>,
pub hotkeys: Option<Vec<String>>,
pub auto_close_connection: Option<bool>,
pub auto_check_update: Option<bool>,
pub default_latency_test: Option<String>,
pub default_latency_timeout: Option<i32>,
pub enable_builtin_enhanced: Option<bool>,
pub proxy_layout_column: Option<i32>,
pub test_list: Option<Vec<IVergeTestItem>>,
pub auto_log_clean: Option<i32>,
pub enable_random_port: Option<bool>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_enabled: Option<bool>,
#[cfg(target_os = "linux")]
pub verge_tproxy_port: Option<u16>,
#[cfg(target_os = "linux")]
pub verge_tproxy_enabled: Option<bool>,
pub verge_mixed_port: Option<u16>,
pub verge_socks_port: Option<u16>,
pub verge_socks_enabled: Option<bool>,
pub verge_port: Option<u16>,
pub verge_http_enabled: Option<bool>,
pub webdav_url: Option<String>,
pub webdav_username: Option<String>,
pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
pub enable_tray_icon: Option<bool>,
pub enable_auto_light_weight_mode: Option<bool>,
pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>,
pub service_state: Option<crate::core::service::ServiceState>,
}
impl From<IVerge> for IVergeResponse {
fn from(verge: IVerge) -> Self {
Self {
app_log_level: verge.app_log_level,
language: verge.language,
theme_mode: verge.theme_mode,
tray_event: verge.tray_event,
env_type: verge.env_type,
start_page: verge.start_page,
startup_script: verge.startup_script,
traffic_graph: verge.traffic_graph,
enable_memory_usage: verge.enable_memory_usage,
enable_group_icon: verge.enable_group_icon,
common_tray_icon: verge.common_tray_icon,
#[cfg(target_os = "macos")]
tray_icon: verge.tray_icon,
menu_icon: verge.menu_icon,
sysproxy_tray_icon: verge.sysproxy_tray_icon,
tun_tray_icon: verge.tun_tray_icon,
enable_tun_mode: verge.enable_tun_mode,
enable_auto_launch: verge.enable_auto_launch,
enable_silent_start: verge.enable_silent_start,
enable_system_proxy: verge.enable_system_proxy,
enable_proxy_guard: verge.enable_proxy_guard,
enable_global_hotkey: verge.enable_global_hotkey,
use_default_bypass: verge.use_default_bypass,
system_proxy_bypass: verge.system_proxy_bypass,
proxy_guard_duration: verge.proxy_guard_duration,
proxy_auto_config: verge.proxy_auto_config,
pac_file_content: verge.pac_file_content,
theme_setting: verge.theme_setting,
web_ui_list: verge.web_ui_list,
clash_core: verge.clash_core,
hotkeys: verge.hotkeys,
auto_close_connection: verge.auto_close_connection,
auto_check_update: verge.auto_check_update,
default_latency_test: verge.default_latency_test,
default_latency_timeout: verge.default_latency_timeout,
enable_builtin_enhanced: verge.enable_builtin_enhanced,
proxy_layout_column: verge.proxy_layout_column,
test_list: verge.test_list,
auto_log_clean: verge.auto_log_clean,
enable_random_port: verge.enable_random_port,
#[cfg(not(target_os = "windows"))]
verge_redir_port: verge.verge_redir_port,
#[cfg(not(target_os = "windows"))]
verge_redir_enabled: verge.verge_redir_enabled,
#[cfg(target_os = "linux")]
verge_tproxy_port: verge.verge_tproxy_port,
#[cfg(target_os = "linux")]
verge_tproxy_enabled: verge.verge_tproxy_enabled,
verge_mixed_port: verge.verge_mixed_port,
verge_socks_port: verge.verge_socks_port,
verge_socks_enabled: verge.verge_socks_enabled,
verge_port: verge.verge_port,
verge_http_enabled: verge.verge_http_enabled,
webdav_url: verge.webdav_url,
webdav_username: verge.webdav_username,
webdav_password: verge.webdav_password,
enable_tray_speed: verge.enable_tray_speed,
enable_tray_icon: verge.enable_tray_icon,
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards,
service_state: verge.service_state,
}
}
}
+227
View File
@@ -0,0 +1,227 @@
use crate::{config::Config, utils::dirs};
use anyhow::Error;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use std::{
collections::HashMap,
env::{consts::OS, temp_dir},
fs,
io::Write,
path::PathBuf,
sync::Arc,
time::Duration,
};
use tokio::time::timeout;
use zip::write::SimpleFileOptions;
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
const TIMEOUT_DELETE: u64 = 3; // 删除超时 30 秒
#[derive(Clone)]
struct WebDavConfig {
url: String,
username: String,
password: String,
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
enum Operation {
Upload,
Download,
List,
Delete,
}
impl Operation {
fn timeout(&self) -> u64 {
match self {
Operation::Upload => TIMEOUT_UPLOAD,
Operation::Download => TIMEOUT_DOWNLOAD,
Operation::List => TIMEOUT_LIST,
Operation::Delete => TIMEOUT_DELETE,
}
}
}
pub struct WebDavClient {
config: Arc<Mutex<Option<WebDavConfig>>>,
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
config: Arc::new(Mutex::new(None)),
clients: Arc::new(Mutex::new(HashMap::new())),
})
}
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
// 先尝试从缓存获取
{
let clients = self.clients.lock();
if let Some(client) = clients.get(&op) {
return Ok(client.clone());
}
}
// 获取或创建配置
let config = {
let mut lock = self.config.lock();
if let Some(cfg) = lock.as_ref() {
cfg.clone()
} else {
let verge = Config::verge().latest().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
{
let msg = "Unable to create web dav client, please make sure the webdav config is correct".to_string();
return Err(anyhow::Error::msg(msg));
}
let config = WebDavConfig {
url: verge
.webdav_url
.unwrap_or_default()
.trim_end_matches('/')
.to_string(),
username: verge.webdav_username.unwrap_or_default(),
password: verge.webdav_password.unwrap_or_default(),
};
*lock = Some(config.clone());
config
}
};
// 创建新的客户端
let client = reqwest_dav::ClientBuilder::new()
.set_agent(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout()))
.build()
.unwrap(),
)
.set_host(config.url)
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
.build()?;
// 确保备份目录存在
let list_result = client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
.await;
if list_result.is_err() {
client.mkcol(dirs::BACKUP_DIR).await?;
}
// 缓存客户端
{
let mut clients = self.clients.lock();
clients.insert(op, client.clone());
}
Ok(client)
}
pub fn reset(&self) {
*self.config.lock() = None;
self.clients.lock().clear();
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
let client = self.get_client(Operation::Upload).await?;
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
let fut = client.put(webdav_path.as_ref(), fs::read(file_path)?);
timeout(Duration::from_secs(TIMEOUT_UPLOAD), fut).await??;
Ok(())
}
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
let client = self.get_client(Operation::Download).await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, filename);
let fut = async {
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
Ok::<(), Error>(())
};
timeout(Duration::from_secs(TIMEOUT_DOWNLOAD), fut).await??;
Ok(())
}
pub async fn list(&self) -> Result<Vec<ListFile>, Error> {
let client = self.get_client(Operation::List).await?;
let path = format!("{}/", dirs::BACKUP_DIR);
let fut = async {
let files = client
.list(path.as_str(), reqwest_dav::Depth::Number(1))
.await?;
let mut final_files = Vec::new();
for file in files {
if let ListEntity::File(file) = file {
final_files.push(file);
}
}
Ok::<Vec<ListFile>, Error>(final_files)
};
timeout(Duration::from_secs(TIMEOUT_LIST), fut).await?
}
pub async fn delete(&self, file_name: String) -> Result<(), Error> {
let client = self.get_client(Operation::Delete).await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, file_name);
let fut = client.delete(&path);
timeout(Duration::from_secs(TIMEOUT_DELETE), fut).await??;
Ok(())
}
}
pub fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
let zip_path = temp_dir().join(&zip_file_name);
let file = fs::File::create(&zip_path)?;
let mut zip = zip::ZipWriter::new(file);
zip.add_directory("profiles/", SimpleFileOptions::default())?;
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
for entry in entries {
let entry = entry.unwrap();
let path = entry.path();
if path.is_file() {
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
zip.start_file(backup_path, options)?;
zip.write_all(fs::read(path).unwrap().as_slice())?;
}
}
}
zip.start_file(dirs::CLASH_CONFIG, options)?;
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
let mut verge_config: serde_json::Value =
serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
if let Some(obj) = verge_config.as_object_mut() {
obj.remove("webdav_username");
obj.remove("webdav_password");
obj.remove("webdav_url");
}
zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?;
zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
zip.finish()?;
Ok((zip_file_name, zip_path))
}
-146
View File
@@ -1,146 +0,0 @@
use crate::config::Config;
use anyhow::{bail, Result};
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::collections::HashMap;
/// PUT /configs
/// path 是绝对路径
pub async fn put_configs(path: &str) -> Result<()> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/configs");
let mut data = HashMap::new();
data.insert("path", path);
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client.put(&url).headers(headers).json(&data);
let response = builder.send().await?;
match response.status().as_u16() {
204 => Ok(()),
status => {
bail!("failed to put configs with status \"{status}\"")
}
}
}
/// PATCH /configs
pub async fn patch_configs(config: &Mapping) -> Result<()> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/configs");
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client.patch(&url).headers(headers.clone()).json(config);
builder.send().await?;
Ok(())
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct DelayRes {
delay: u64,
}
/// GET /proxies/{name}/delay
/// 获取代理延迟
pub async fn get_proxy_delay(
name: String,
test_url: Option<String>,
timeout: i32,
) -> Result<DelayRes> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/proxies/{name}/delay");
let default_url = "http://1.1.1.1";
let test_url = test_url
.map(|s| if s.is_empty() { default_url.into() } else { s })
.unwrap_or(default_url.into());
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client
.get(&url)
.headers(headers)
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
let response = builder.send().await?;
Ok(response.json::<DelayRes>().await?)
}
/// 根据clash info获取clash服务地址和请求头
fn clash_client_info() -> Result<(String, HeaderMap)> {
let client = { Config::clash().data().get_client_info() };
let server = format!("http://{}", client.server);
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse()?);
if let Some(secret) = client.secret {
let secret = format!("Bearer {}", secret).parse()?;
headers.insert("Authorization", secret);
}
Ok((server, headers))
}
/// 缩短clash的日志
#[allow(dead_code)]
pub fn parse_log(log: String) -> String {
if log.starts_with("time=") && log.len() > 33 {
return (log[33..]).to_owned();
}
if log.len() > 9 {
return (log[9..]).to_owned();
}
log
}
/// 缩短clash -t的错误输出
/// 仅适配 clash p核 8-26、clash meta 1.13.1
pub fn parse_check_output(log: String) -> String {
let t = log.find("time=");
let m = log.find("msg=");
let mr = log.rfind('"');
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
let e = match log.find("level=error msg=") {
Some(e) => e + 17,
None => m + 5,
};
if mr > m {
return (log[e..mr]).to_owned();
}
}
let l = log.find("error=");
let r = log.find("path=").or(Some(log.len()));
if let (Some(l), Some(r)) = (l, r) {
return (log[(l + 6)..(r - 1)]).to_owned();
}
log
}
#[test]
fn test_parse_check_output() {
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
let str3 = r#"
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
configuration file xxx\n
"#;
let res1 = parse_check_output(str1.into());
let res2 = parse_check_output(str2.into());
let res3 = parse_check_output(str3.into());
println!("res1: {res1}");
println!("res2: {res2}");
println!("res3: {res3}");
assert_eq!(res1, res3);
}
+943 -255
View File
File diff suppressed because it is too large Load Diff
+63 -28
View File
@@ -1,14 +1,17 @@
use super::tray::Tray;
use crate::log_err;
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use parking_lot::RwLock;
use std::sync::Arc;
use tauri::{AppHandle, Manager, Window};
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
use tauri_plugin_shell::process::CommandChild;
use std::fs::File;
#[derive(Debug, Default, Clone)]
pub struct Handle {
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
pub is_exiting: Arc<RwLock<bool>>,
pub core_process: Arc<RwLock<Option<CommandChild>>>,
pub core_lock: Arc<RwLock<Option<File>>>,
}
impl Handle {
@@ -16,19 +19,29 @@ impl Handle {
static HANDLE: OnceCell<Handle> = OnceCell::new();
HANDLE.get_or_init(|| Handle {
app_handle: Arc::new(Mutex::new(None)),
app_handle: Arc::new(RwLock::new(None)),
is_exiting: Arc::new(RwLock::new(false)),
core_process: Arc::new(RwLock::new(None)),
core_lock: Arc::new(RwLock::new(None)),
})
}
pub fn init(&self, app_handle: AppHandle) {
*self.app_handle.lock() = Some(app_handle);
pub fn init(&self, app_handle: &AppHandle) {
let mut handle = self.app_handle.write();
*handle = Some(app_handle.clone());
}
pub fn get_window(&self) -> Option<Window> {
self.app_handle
.lock()
.as_ref()
.and_then(|a| a.get_window("main"))
pub fn app_handle(&self) -> Option<AppHandle> {
self.app_handle.read().clone()
}
pub fn get_window(&self) -> Option<WebviewWindow> {
let app_handle = self.app_handle().unwrap();
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
if window.is_none() {
log::debug!(target:"app", "main window not found");
}
window
}
pub fn refresh_clash() {
@@ -56,22 +69,44 @@ impl Handle {
}
}
pub fn update_systray() -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("update_systray unhandled error");
}
Tray::update_systray(app_handle.as_ref().unwrap())?;
Ok(())
pub fn set_is_exiting(&self) {
let mut is_exiting = self.is_exiting.write();
*is_exiting = true;
}
/// update the system tray state
pub fn update_systray_part() -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("update_systray unhandled error");
}
Tray::update_part(app_handle.as_ref().unwrap())?;
Ok(())
pub fn set_core_process(&self, process: CommandChild) {
let mut core_process = self.core_process.write();
*core_process = Some(process);
}
pub fn take_core_process(&self) -> Option<CommandChild> {
let mut core_process = self.core_process.write();
core_process.take()
}
/// 检查是否有运行中的核心进程
pub fn has_core_process(&self) -> bool {
self.core_process.read().is_some()
}
pub fn is_exiting(&self) -> bool {
*self.is_exiting.read()
}
/// 设置核心文件锁
pub fn set_core_lock(&self, file: File) {
let mut core_lock = self.core_lock.write();
*core_lock = Some(file);
}
/// 释放核心文件锁
pub fn release_core_lock(&self) -> Option<File> {
let mut core_lock = self.core_lock.write();
core_lock.take()
}
/// 检查是否持有核心文件锁
pub fn has_core_lock(&self) -> bool {
self.core_lock.read().is_some()
}
}
Regular → Executable
+162 -31
View File
@@ -1,14 +1,16 @@
use crate::{config::Config, feat, log_err};
use crate::{
config::Config, core::handle, feat, log_err, module::lightweight::entry_lightweight_mode,
utils::resolve,
};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use tauri::{AppHandle, GlobalShortcutManager};
use tauri::{async_runtime, Manager};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
app_handle: Arc<Mutex<Option<AppHandle>>>,
}
impl Hotkey {
@@ -17,16 +19,30 @@ impl Hotkey {
HOTKEY.get_or_init(|| Hotkey {
current: Arc::new(Mutex::new(Vec::new())),
app_handle: Arc::new(Mutex::new(None)),
})
}
pub fn init(&self, app_handle: AppHandle) -> Result<()> {
*self.app_handle.lock() = Some(app_handle);
pub fn init(&self) -> Result<()> {
let verge = Config::verge();
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
println!(
"Initializing hotkeys, global hotkey enabled: {}",
enable_global_hotkey
);
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
// 如果全局热键被禁用,则不注册热键
if !enable_global_hotkey {
println!("Global hotkey is disabled, skipping registration");
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
return Ok(());
}
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
println!("Found {} hotkeys to register", hotkeys.len());
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
for hotkey in hotkeys.iter() {
let mut iter = hotkey.split(',');
let func = iter.next();
@@ -34,55 +50,169 @@ impl Hotkey {
match (key, func) {
(Some(key), Some(func)) => {
log_err!(self.register(key, func));
println!("Registering hotkey: {} -> {}", key, func);
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
if let Err(e) = self.register(key, func) {
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
} else {
println!("Successfully registered hotkey {} -> {}", key, func);
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
}
}
_ => {
let key = key.unwrap_or("None");
let func = func.unwrap_or("None");
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
println!("Invalid hotkey configuration: `{key}`:`{func}`");
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
}
}
}
self.current.lock().clone_from(hotkeys);
} else {
println!("No hotkeys configured");
log::info!(target: "app", "No hotkeys configured");
}
Ok(())
}
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
let app_handle = self.app_handle.lock();
if app_handle.is_none() {
bail!("failed to get the hotkey manager");
}
Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
pub fn reset(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
manager.unregister_all()?;
Ok(())
}
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let mut manager = self.get_manager()?;
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
if manager.is_registered(hotkey)? {
println!(
"Attempting to register hotkey: {} for function: {}",
hotkey, func
);
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
if manager.is_registered(hotkey) {
println!(
"Hotkey {} was already registered, unregistering first",
hotkey
);
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
manager.unregister(hotkey)?;
}
let f = match func.trim() {
"open_or_close_dashboard" => feat::open_or_close_dashboard,
"open_or_close_dashboard" => {
println!("Registering open_or_close_dashboard function");
log::info!(target: "app", "Registering open_or_close_dashboard function");
|| {
println!("=== Hotkey Dashboard Window Operation Start ===");
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
// 使用 spawn_blocking 来确保在正确的线程上执行
async_runtime::spawn_blocking(|| {
println!("Toggle dashboard window visibility");
log::info!(target: "app", "Toggle dashboard window visibility");
// 检查窗口是否存在
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏它
if window.is_visible().unwrap_or(false) {
println!("Window is visible, hiding it");
log::info!(target: "app", "Window is visible, hiding it");
let _ = window.hide();
} else {
// 如果窗口不可见,则显示它
println!("Window is hidden, showing it");
log::info!(target: "app", "Window is hidden, showing it");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
} else {
// 如果窗口不存在,创建一个新窗口
println!("Window does not exist, creating a new one");
log::info!(target: "app", "Window does not exist, creating a new one");
resolve::create_window();
}
});
println!("=== Hotkey Dashboard Window Operation End ===");
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
}
}
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"toggle_system_proxy" => feat::toggle_system_proxy,
"toggle_tun_mode" => feat::toggle_tun_mode,
"toggle_system_proxy" => || feat::toggle_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
"entry_lightweight_mode" => || entry_lightweight_mode(),
"quit" => || feat::quit(Some(0)),
#[cfg(target_os = "macos")]
"hide" => || feat::hide(),
_ => bail!("invalid function \"{func}\""),
_ => {
println!("Invalid function: {}", func);
log::error!(target: "app", "Invalid function: {}", func);
bail!("invalid function \"{func}\"");
}
};
manager.register(hotkey, f)?;
log::info!(target: "app", "register hotkey {hotkey} {func}");
let is_quit = func.trim() == "quit";
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
if event.state == ShortcutState::Pressed {
println!("Hotkey pressed: {:?}", hotkey);
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
if hotkey.key == Code::KeyQ && is_quit {
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
println!("Executing quit function");
log::info!(target: "app", "Executing quit function");
f();
}
}
} else {
// 直接执行函数,不做任何状态检查
println!("Executing function directly");
log::info!(target: "app", "Executing function directly");
// 获取全局热键状态
let is_enable_global_hotkey = Config::verge()
.latest()
.enable_global_hotkey
.unwrap_or(true);
if is_enable_global_hotkey {
f();
} else if let Some(window) = app_handle.get_webview_window("main") {
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
let is_visible = window.is_visible().unwrap_or(false);
let is_focused = window.is_focused().unwrap_or(false);
if is_focused && is_visible {
f();
}
}
}
}
});
println!("Successfully registered hotkey {} for {}", hotkey, func);
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
Ok(())
}
fn unregister(&self, hotkey: &str) -> Result<()> {
self.get_manager()?.unregister(hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}");
pub fn unregister(&self, hotkey: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?;
log::debug!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
@@ -105,7 +235,7 @@ impl Hotkey {
Ok(())
}
fn get_map_from_vec(hotkeys: &Vec<String>) -> HashMap<&str, &str> {
fn get_map_from_vec(hotkeys: &[String]) -> HashMap<&str, &str> {
let mut map = HashMap::new();
hotkeys.iter().for_each(|hotkey| {
@@ -153,8 +283,9 @@ impl Hotkey {
impl Drop for Hotkey {
fn drop(&mut self) {
if let Ok(mut manager) = self.get_manager() {
let _ = manager.unregister_all();
let app_handle = handle::Handle::global().app_handle().unwrap();
if let Err(e) = app_handle.global_shortcut().unregister_all() {
log::error!(target:"app", "Error unregistering all hotkeys: {:?}", e);
}
}
}
-36
View File
@@ -1,36 +0,0 @@
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::VecDeque, sync::Arc};
const LOGS_QUEUE_LEN: usize = 100;
pub struct Logger {
log_data: Arc<Mutex<VecDeque<String>>>,
}
impl Logger {
pub fn global() -> &'static Logger {
static LOGGER: OnceCell<Logger> = OnceCell::new();
LOGGER.get_or_init(|| Logger {
log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
})
}
pub fn get_log(&self) -> VecDeque<String> {
self.log_data.lock().clone()
}
pub fn set_log(&self, text: String) {
let mut logs = self.log_data.lock();
if logs.len() > LOGS_QUEUE_LEN {
logs.pop_front();
}
logs.push_back(text);
}
pub fn clear_log(&self) {
let mut logs = self.log_data.lock();
logs.clear();
}
}
+1 -2
View File
@@ -1,9 +1,8 @@
pub mod clash_api;
pub mod backup;
#[allow(clippy::module_inception)]
mod core;
pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod service;
pub mod sysopt;
pub mod timer;
+380 -183
View File
@@ -1,17 +1,82 @@
use crate::config::{Config, IVerge};
use crate::core::handle;
use crate::utils::dirs;
use crate::{config::Config, utils::dirs};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use std::{env::current_exe, process::Command as StdCommand};
use tokio::time::sleep;
use std::{collections::HashMap, env::current_exe, path::PathBuf, process::Command as StdCommand, time::{SystemTime, UNIX_EPOCH}};
use tokio::time::Duration;
// Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211";
const REQUIRED_SERVICE_VERSION: &str = "1.0.5"; // 定义所需的服务版本号
// 限制重装时间和次数的常量
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次
const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct ServiceState {
pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒)
pub install_count: u32, // 24小时内安装次数
pub last_check_time: u64, // 上次检查时间
pub last_error: Option<String>, // 上次错误信息
}
impl ServiceState {
// 获取当前的服务状态
pub fn get() -> Self {
if let Some(state) = Config::verge().latest().service_state.clone() {
return state;
}
Self::default()
}
// 保存服务状态
pub fn save(&self) -> Result<()> {
let config = Config::verge();
let mut latest = config.latest().clone();
latest.service_state = Some(self.clone());
*config.draft() = latest;
config.apply();
Config::verge().latest().save_file()
}
// 更新安装信息
pub fn record_install(&mut self) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// 检查是否需要重置计数器(24小时已过)
if now - self.last_install_time > ONE_DAY_SECS {
self.install_count = 0;
}
self.last_install_time = now;
self.install_count += 1;
}
// 检查是否可以重新安装
pub fn can_reinstall(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// 如果在冷却期内,不允许重装
if now - self.last_install_time < REINSTALL_COOLDOWN_SECS {
return false;
}
// 如果24小时内安装次数过多,也不允许
if now - self.last_install_time < ONE_DAY_SECS && self.install_count >= MAX_REINSTALLS_PER_DAY {
return false;
}
true
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponseBody {
@@ -21,6 +86,12 @@ pub struct ResponseBody {
pub log_file: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionResponse {
pub service: String,
pub version: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JsonResponse {
pub code: u64,
@@ -28,24 +99,50 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>,
}
/// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
///
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionJsonResponse {
pub code: u64,
pub msg: String,
pub data: Option<VersionResponse>,
}
#[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> {
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
// 获取当前服务状态
let mut service_state = ServiceState::get();
// 检查是否允许重装
if !service_state.can_reinstall() {
log::warn!(target:"app", "service reinstall rejected: cooldown period or max attempts reached");
bail!("Service reinstallation is rate limited. Please try again later.");
}
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service.exe");
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
if !install_path.exists() {
bail!("installer exe not found");
bail!(format!("installer not found: {install_path:?}"));
}
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let _ = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
.status()?,
};
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
@@ -55,123 +152,60 @@ pub async fn install_service() -> Result<()> {
};
if !status.success() {
bail!(
let error = format!(
"failed to install service with status {}",
status.code().unwrap()
);
service_state.last_error = Some(error.clone());
service_state.save()?;
bail!(error);
}
// 记录安装信息并保存
service_state.record_install();
service_state.last_error = None;
service_state.save()?;
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> {
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
let install_path = tauri::utils::platform::current_exe()?.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("uninstall-service");
if !install_path.exists() {
bail!(format!("installer not found: {install_path:?}"));
}
let elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(installer_path).status()?,
_ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(installer_path)
.status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
if !uninstall_path.exists() {
bail!("uninstaller exe not found");
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ ");
let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\ ");
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
.status()?,
};
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
);
}
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> {
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let elevator = crate::utils::unix_helper::linux_elevator();
let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(uninstaller_path).status()?,
_ => StdCommand::new(elevator)
0 => StdCommand::new(uninstall_shell).status()?,
_ => StdCommand::new(elevator.clone())
.arg("sh")
.arg("-c")
.arg(uninstaller_path)
.arg(uninstall_shell)
.status()?,
};
log::info!(target:"app", "status code:{}", status.code().unwrap());
let status = match get_effective_uid() {
0 => StdCommand::new(install_shell).status()?,
_ => StdCommand::new(elevator.clone())
.arg("sh")
.arg("-c")
.arg(install_shell)
.status()?,
};
@@ -186,16 +220,43 @@ pub async fn uninstall_service() -> Result<()> {
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service");
let uninstall_path = binary_path.with_file_name("uninstall-service");
if !install_path.exists() {
bail!(format!("installer not found: {install_path:?}"));
}
let shell = uninstaller_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let install_shell: String = install_path.to_string_lossy().into_owned();
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
// 获取提示文本,如果 i18n 失败则使用硬编码默认值
let prompt = crate::utils::i18n::t("Service Administrator Prompt");
let prompt = if prompt == "Service Administrator Prompt" {
if Config::verge().latest().language.as_deref() == Some("zh")
|| Config::verge().latest().language.is_none()
{
"Clash Verge 需要使用管理员权限来重新安装系统服务"
} else {
"Clash Verge needs administrator privileges to reinstall the system service"
}
} else {
&prompt
};
let command = format!(
r#"do shell script "sudo '{uninstall_shell}' && sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
);
log::debug!(target: "app", "command: {}", command);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
@@ -207,7 +268,6 @@ pub async fn uninstall_service() -> Result<()> {
status.code().unwrap()
);
}
Ok(())
}
@@ -216,6 +276,7 @@ pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.timeout(Duration::from_secs(3))
.build()?
.get(url)
.send()
@@ -228,31 +289,80 @@ pub async fn check_service() -> Result<JsonResponse> {
Ok(response)
}
/// start the clash by service
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let status = check_service().await?;
/// check the service version
pub async fn check_service_version() -> Result<String> {
let url = format!("{SERVICE_URL}/version");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.timeout(Duration::from_secs(3))
.build()?
.get(url)
.send()
.await
.context("failed to connect to the Clash Verge Service")?
.json::<VersionJsonResponse>()
.await
.context("failed to parse the Clash Verge Service version response")?;
if status.code == 0 {
stop_core_by_service().await?;
sleep(Duration::from_secs(1)).await;
match response.data {
Some(data) => Ok(data.version),
None => bail!("service version not found in response"),
}
}
/// check if service needs to be reinstalled
pub async fn check_service_needs_reinstall() -> bool {
// 获取当前服务状态
let service_state = ServiceState::get();
// 首先检查是否在冷却期或超过重装次数限制
if !service_state.can_reinstall() {
log::info!(target: "app", "service reinstall check: in cooldown period or max attempts reached");
return false;
}
let clash_core = { Config::verge().latest().clash_core.clone() };
let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
// compatibility
if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
// 然后才检查版本和可用性
match check_service_version().await {
Ok(version) => {
// 打印更详细的日志,方便排查问题
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
if needs_reinstall {
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
version, REQUIRED_SERVICE_VERSION);
// 打印版本字符串的原始字节,确认没有隐藏字符
log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
} else {
log::info!(target: "app", "服务版本匹配,无需重装");
}
needs_reinstall
},
Err(err) => {
// 检查服务是否可用,如果可用但版本检查失败,可能只是版本API有问题
match is_service_running().await {
Ok(true) => {
log::info!(target: "app", "service is running but version check failed: {}", err);
false // 服务在运行,不需要重装
}
_ => {
log::info!(target: "app", "service is not running or unavailable");
true // 服务不可用,需要重装
}
}
}
}
}
/// 尝试使用现有服务启动核心,不进行重装
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
log::info!(target:"app", "attempting to start core with existing service");
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");
@@ -261,6 +371,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let config_dir = dirs::app_home_dir()?;
let config_dir = dirs::path_to_str(&config_dir)?;
#[cfg(target_os = "linux")]
let config_dir = &(config_dir.replace("/verge-mihomo", "") + "/resources");
let log_path = dirs::service_log_file()?;
let log_path = dirs::path_to_str(&log_path)?;
@@ -274,81 +386,166 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
map.insert("config_file", config_file);
map.insert("log_file", log_path);
log::info!(target:"app", "start service: {:?}", map.clone());
let url = format!("{SERVICE_URL}/start_clash");
let res = reqwest::ClientBuilder::new()
let _ = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.json(&map)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// start the clash by service
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
log::info!(target: "app", "正在尝试通过服务启动核心");
// 先检查服务版本,不受冷却期限制
let version_check = match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
version, REQUIRED_SERVICE_VERSION);
// 通过字节比较确保完全匹配
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
log::warn!(target: "app", "服务版本不匹配,需要重装");
false // 版本不匹配
} else {
log::info!(target: "app", "服务版本匹配");
true // 版本匹配
}
},
Err(err) => {
log::warn!(target: "app", "无法获取服务版本: {}", err);
false // 无法获取版本
}
};
// 先尝试直接启动服务,如果服务可用且版本匹配
if version_check {
if let Ok(true) = is_service_running().await {
// 服务正在运行且版本匹配,直接使用
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
return start_with_existing_service(config_file).await;
}
}
// 强制执行版本检查,如果版本不匹配则重装
if !version_check {
log::info!(target: "app", "服务版本不匹配,尝试重装");
// 获取服务状态,检查是否可以重装
let service_state = ServiceState::get();
if !service_state.can_reinstall() {
log::warn!(target: "app", "由于限制无法重装服务");
// 尝试直接启动,即使版本不匹配
if let Ok(()) = start_with_existing_service(config_file).await {
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
return Ok(());
} else {
bail!("服务版本不匹配且无法重装,启动失败");
}
}
// 尝试重装
log::info!(target: "app", "开始重装服务");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
// 尝试使用现有服务
log::info!(target: "app", "尝试使用现有服务");
return start_with_existing_service(config_file).await;
}
// 重装成功,尝试启动
log::info!(target: "app", "服务重装成功,尝试启动");
return start_with_existing_service(config_file).await;
}
// 检查服务状态
match check_service().await {
Ok(_) => {
// 服务可访问但可能没有运行核心,尝试直接启动
log::info!(target: "app", "服务可用但未运行核心,尝试启动");
if let Ok(()) = start_with_existing_service(config_file).await {
return Ok(());
}
},
Err(err) => {
log::warn!(target: "app", "服务检查失败: {}", err);
}
}
// 服务不可用或启动失败,检查是否需要重装
if check_service_needs_reinstall().await {
log::info!(target: "app", "服务需要重装");
// 尝试重装
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
bail!("Failed to reinstall service: {}", err);
}
// 重装后再次尝试启动
log::info!(target: "app", "服务重装完成,尝试启动核心");
start_with_existing_service(config_file).await
} else {
// 不需要或不能重装,返回错误
log::warn!(target: "app", "服务不可用且无法重装");
bail!("Service is not available and cannot be reinstalled at this time")
}
}
/// stop the clash by service
pub(super) async fn stop_core_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/stop_clash");
let res = reqwest::ClientBuilder::new()
let _ = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// set dns by service
pub async fn set_dns_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/set_dns");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
/// 检查服务是否正在运行
pub async fn is_service_running() -> Result<bool> {
let resp = check_service().await?;
if res.code != 0 {
bail!(res.msg);
// 检查服务状态码和消息
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
Ok(true)
} else {
Ok(false)
}
Ok(())
}
/// unset dns by service
pub async fn unset_dns_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/unset_dns");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
/// 强制重装服务(用于UI中的修复服务按钮)
pub async fn force_reinstall_service() -> Result<()> {
log::info!(target: "app", "用户请求强制重装服务");
if res.code != 0 {
bail!(res.msg);
// 创建默认服务状态(重置所有限制)
let service_state = ServiceState::default();
service_state.save()?;
log::info!(target: "app", "已重置服务状态,开始执行重装");
// 执行重装
match reinstall_service().await {
Ok(()) => {
log::info!(target: "app", "服务重装成功");
Ok(())
},
Err(err) => {
log::error!(target: "app", "强制重装服务失败: {}", err);
bail!("强制重装服务失败: {}", err)
}
}
Ok(())
}
+255 -293
View File
@@ -1,48 +1,36 @@
use crate::{
config::{Config, IVerge},
core::handle::Handle,
log_err,
};
use anyhow::{anyhow, Result};
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use anyhow::Result;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::env::current_exe;
use std::sync::Arc;
use sysproxy::{Autoproxy, Sysproxy};
use tauri::async_runtime::Mutex as TokioMutex;
use tauri_plugin_autostart::ManagerExt;
use tokio::time::{sleep, Duration};
pub struct Sysopt {
/// current system proxy setting
cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
/// record the original system proxy
/// recover it when exit
old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
/// current auto proxy setting
cur_autoproxy: Arc<Mutex<Option<Autoproxy>>>,
/// record the original auto proxy
/// recover it when exit
old_autoproxy: Arc<Mutex<Option<Autoproxy>>>,
update_sysproxy: Arc<TokioMutex<bool>>,
reset_sysproxy: Arc<TokioMutex<bool>>,
/// helps to auto launch the app
auto_launch: Arc<Mutex<Option<AutoLaunch>>>,
auto_launch: Arc<Mutex<bool>>,
/// record whether the guard async is running or not
guard_state: Arc<TokioMutex<bool>>,
guard_state: Arc<Mutex<bool>>,
}
#[cfg(target_os = "windows")]
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
#[cfg(target_os = "linux")]
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1";
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,localhost,*.local,*.crashlytics.com,<local>";
"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>";
fn get_bypass() -> String {
// let bypass = DEFAULT_BYPASS.to_string();
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
let res = {
let verge = Config::verge();
@@ -53,125 +41,35 @@ fn get_bypass() -> String {
Some(bypass) => bypass,
None => "".to_string(),
};
#[cfg(target_os = "windows")]
let bypass = if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else {
if use_default {
format!("{};{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
#[cfg(not(target_os = "windows"))]
let bypass = if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else {
if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
bypass
if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
}
impl Sysopt {
pub fn global() -> &'static Sysopt {
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
SYSOPT.get_or_init(|| Sysopt {
cur_sysproxy: Arc::new(Mutex::new(None)),
old_sysproxy: Arc::new(Mutex::new(None)),
cur_autoproxy: Arc::new(Mutex::new(None)),
old_autoproxy: Arc::new(Mutex::new(None)),
auto_launch: Arc::new(Mutex::new(None)),
guard_state: Arc::new(TokioMutex::new(false)),
update_sysproxy: Arc::new(TokioMutex::new(false)),
reset_sysproxy: Arc::new(TokioMutex::new(false)),
auto_launch: Arc::new(Mutex::new(false)),
guard_state: Arc::new(false.into()),
})
}
/// init the sysproxy
pub fn init_sysproxy(&self) -> Result<()> {
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let (enable, pac) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
)
};
let mut sys = Sysproxy {
enable,
host: String::from("127.0.0.1"),
port,
bypass: get_bypass(),
};
let mut auto = Autoproxy {
enable,
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
};
if pac {
sys.enable = false;
let old = Sysproxy::get_system_proxy().ok();
sys.set_system_proxy()?;
*self.old_sysproxy.lock() = old;
*self.cur_sysproxy.lock() = Some(sys);
let old = Autoproxy::get_auto_proxy().ok();
auto.set_auto_proxy()?;
*self.old_autoproxy.lock() = old;
*self.cur_autoproxy.lock() = Some(auto);
} else {
auto.enable = false;
let old = Autoproxy::get_auto_proxy().ok();
auto.set_auto_proxy()?;
*self.old_autoproxy.lock() = old;
*self.cur_autoproxy.lock() = Some(auto);
let old = Sysproxy::get_system_proxy().ok();
sys.set_system_proxy()?;
*self.old_sysproxy.lock() = old;
*self.cur_sysproxy.lock() = Some(sys);
}
// run the system proxy guard
pub fn init_guard_sysproxy(&self) -> Result<()> {
self.guard_proxy();
Ok(())
}
/// update the system proxy
pub fn update_sysproxy(&self) -> Result<()> {
let mut cur_sysproxy = self.cur_sysproxy.lock();
let old_sysproxy = self.old_sysproxy.lock();
let mut cur_autoproxy = self.cur_autoproxy.lock();
let old_autoproxy = self.old_autoproxy.lock();
let (enable, pac) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
)
};
if pac && (cur_autoproxy.is_none() || old_autoproxy.is_none()) {
drop(cur_autoproxy);
drop(old_autoproxy);
return self.init_sysproxy();
}
if !pac && (cur_sysproxy.is_none() || old_sysproxy.is_none()) {
drop(cur_sysproxy);
drop(old_sysproxy);
return self.init_sysproxy();
}
/// init the sysproxy
pub async fn update_sysproxy(&self) -> Result<()> {
let _lock = self.update_sysproxy.lock().await;
let port = Config::verge()
.latest()
@@ -179,185 +77,197 @@ impl Sysopt {
.unwrap_or(Config::clash().data().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let mut sysproxy = cur_sysproxy.take().unwrap();
sysproxy.bypass = get_bypass();
sysproxy.port = port;
let (sys_enable, pac_enable) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
)
};
let mut autoproxy = cur_autoproxy.take().unwrap();
autoproxy.url = format!("http://127.0.0.1:{pac_port}/commands/pac");
#[cfg(not(target_os = "windows"))]
{
let mut sys = Sysproxy {
enable: false,
host: String::from("127.0.0.1"),
port,
bypass: get_bypass(),
};
let mut auto = Autoproxy {
enable: false,
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
};
if pac {
sysproxy.enable = false;
sysproxy.set_system_proxy()?;
*cur_sysproxy = Some(sysproxy);
autoproxy.enable = enable;
autoproxy.set_auto_proxy()?;
*cur_autoproxy = Some(autoproxy);
} else {
autoproxy.enable = false;
autoproxy.set_auto_proxy()?;
*cur_autoproxy = Some(autoproxy);
sysproxy.enable = enable;
sysproxy.set_system_proxy()?;
*cur_sysproxy = Some(sysproxy);
if !sys_enable {
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
return Ok(());
}
if pac_enable {
sys.enable = false;
auto.enable = true;
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
return Ok(());
}
if sys_enable {
auto.enable = false;
sys.enable = true;
auto.set_auto_proxy()?;
sys.set_system_proxy()?;
return Ok(());
}
}
#[cfg(target_os = "windows")]
{
if !sys_enable {
return self.reset_sysproxy().await;
}
use crate::{core::handle::Handle, utils::dirs};
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
let shell = app_handle.shell();
let output = if pac_enable {
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["pac", address.as_str()])
.output()
.await
.unwrap();
output
} else {
let address = format!("{}:{}", "127.0.0.1", port);
let bypass = get_bypass();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap();
output
};
if !output.status.success() {
bail!("sysproxy exe run failed");
}
}
Ok(())
}
/// reset the sysproxy
pub fn reset_sysproxy(&self) -> Result<()> {
let mut cur_sysproxy = self.cur_sysproxy.lock();
let mut old_sysproxy = self.old_sysproxy.lock();
let mut cur_autoproxy = self.cur_autoproxy.lock();
let mut old_autoproxy = self.old_autoproxy.lock();
let cur_sysproxy = cur_sysproxy.take();
let cur_autoproxy = cur_autoproxy.take();
if let Some(mut old) = old_sysproxy.take() {
// 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port);
if old.enable && port_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
}
old.set_system_proxy()?;
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
// 没有原代理,就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_system_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
pub async fn reset_sysproxy(&self) -> Result<()> {
let _lock = self.reset_sysproxy.lock().await;
//直接关闭所有代理
#[cfg(not(target_os = "windows"))]
{
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
let mut autoproxy = Autoproxy::get_auto_proxy()?;
sysproxy.enable = false;
autoproxy.enable = false;
autoproxy.set_auto_proxy()?;
sysproxy.set_system_proxy()?;
}
if let Some(mut old) = old_autoproxy.take() {
// 如果原代理和当前代理 URL一致,就disable关闭,否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let url_same = cur_autoproxy.map_or(true, |cur| old.url == cur.url);
if old.enable && url_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
}
old.set_auto_proxy()?;
} else if let Some(mut cur @ Autoproxy { enable: true, .. }) = cur_autoproxy {
// 没有原代理,就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_auto_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
}
Ok(())
}
/// init the auto launch
pub fn init_launch(&self) -> Result<()> {
let app_exe = current_exe()?;
// let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())
.ok_or(anyhow!("failed to get file stem"))?;
let app_path = app_exe
.as_os_str()
.to_str()
.ok_or(anyhow!("failed to get app_path"))?
.to_string();
// fix issue #26
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
{
use crate::{core::handle::Handle, utils::dirs};
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
// use the /Applications/Clash Verge.app path
#[cfg(target_os = "macos")]
let app_path = (|| -> Option<String> {
let path = std::path::PathBuf::from(&app_path);
let path = path.parent()?.parent()?.parent()?;
let extension = path.extension()?.to_str()?;
match extension == "app" {
true => Some(path.as_os_str().to_str()?.to_string()),
false => None,
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
})()
.unwrap_or(app_path);
// fix #403
#[cfg(target_os = "linux")]
let app_path = {
use crate::core::handle::Handle;
use tauri::Manager;
let shell = app_handle.shell();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["set", "1"])
.output()
.await
.unwrap();
let handle = Handle::global();
match handle.app_handle.lock().as_ref() {
Some(app_handle) => {
let appimage = app_handle.env().appimage;
appimage
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or(app_path)
}
None => app_path,
if !output.status.success() {
bail!("sysproxy exe run failed");
}
};
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(&app_path)
.build()?;
*self.auto_launch.lock() = Some(auto);
}
Ok(())
}
/// update the startup
pub fn update_launch(&self) -> Result<()> {
let auto_launch = self.auto_launch.lock();
if auto_launch.is_none() {
drop(auto_launch);
return self.init_launch();
}
let _lock = self.auto_launch.lock();
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
let auto_launch = auto_launch.as_ref().unwrap();
let app_handle = Handle::global().app_handle().unwrap();
let autostart_manager = app_handle.autolaunch();
log::info!(target: "app", "Setting auto launch to: {}", enable);
match enable {
true => auto_launch.enable()?,
false => log_err!(auto_launch.disable()), // 忽略关闭的错误
true => {
let result = autostart_manager.enable();
if let Err(ref e) = result {
log::error!(target: "app", "Failed to enable auto launch: {}", e);
} else {
log::info!(target: "app", "Auto launch enabled successfully");
}
log_err!(result)
},
false => {
let result = autostart_manager.disable();
if let Err(ref e) = result {
log::error!(target: "app", "Failed to disable auto launch: {}", e);
} else {
log::info!(target: "app", "Auto launch disabled successfully");
}
log_err!(result)
},
};
Ok(())
}
/// launch a system proxy guard
/// read config from file directly
pub fn guard_proxy(&self) {
use tokio::time::{sleep, Duration};
/// 获取当前自启动的实际状态
pub fn get_launch_status(&self) -> Result<bool> {
let app_handle = Handle::global().app_handle().unwrap();
let autostart_manager = app_handle.autolaunch();
match autostart_manager.is_enabled() {
Ok(status) => {
log::info!(target: "app", "Auto launch status: {}", status);
Ok(status)
},
Err(e) => {
log::error!(target: "app", "Failed to get auto launch status: {}", e);
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
}
}
}
let guard_state = self.guard_state.clone();
fn guard_proxy(&self) {
let _lock = self.guard_state.lock();
tauri::async_runtime::spawn(async move {
// if it is running, exit
let mut state = guard_state.lock().await;
if *state {
return;
}
*state = true;
drop(state);
// default duration is 10s
let mut wait_secs = 10u64;
@@ -377,7 +287,7 @@ impl Sysopt {
// stop loop
if !enable || !guard {
break;
continue;
}
// update duration
@@ -385,6 +295,20 @@ impl Sysopt {
log::debug!(target: "app", "try to guard the system proxy");
let sysproxy = Sysproxy::get_system_proxy();
let autoproxy = Autoproxy::get_auto_proxy();
if sysproxy.is_err() || autoproxy.is_err() {
log::error!(target: "app", "failed to get the system proxy");
continue;
}
let sysproxy_enable = sysproxy.ok().map(|s| s.enable).unwrap_or(false);
let autoproxy_enable = autoproxy.ok().map(|s| s.enable).unwrap_or(false);
if sysproxy_enable || autoproxy_enable {
continue;
}
let port = {
Config::verge()
.latest()
@@ -392,27 +316,65 @@ impl Sysopt {
.unwrap_or(Config::clash().data().get_mixed_port())
};
let pac_port = IVerge::get_singleton_port();
if pac {
let autoproxy = Autoproxy {
enable: true,
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
};
log_err!(autoproxy.set_auto_proxy());
} else {
let sysproxy = Sysproxy {
enable: true,
host: "127.0.0.1".into(),
port,
bypass: get_bypass(),
};
#[cfg(not(target_os = "windows"))]
{
if pac {
let autoproxy = Autoproxy {
enable: true,
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
};
log_err!(autoproxy.set_auto_proxy());
} else {
let sysproxy = Sysproxy {
enable: true,
host: "127.0.0.1".into(),
port,
bypass: get_bypass(),
};
log_err!(sysproxy.set_system_proxy());
log_err!(sysproxy.set_system_proxy());
}
}
}
let mut state = guard_state.lock().await;
*state = false;
drop(state);
#[cfg(target_os = "windows")]
{
use crate::{core::handle::Handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = dirs::service_path().unwrap();
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
break;
}
let shell = app_handle.shell();
let output = if pac {
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["pac", address.as_str()])
.output()
.await
.unwrap()
} else {
let address = format!("{}:{}", "127.0.0.1", port);
let bypass = get_bypass();
shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap()
};
if !output.status.success() {
break;
}
};
}
});
}
}
+205 -73
View File
@@ -1,23 +1,32 @@
use crate::config::Config;
use crate::feat;
use crate::{config::Config, core::CoreManager, feat};
use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, sync::Arc};
type TaskID = u64;
#[derive(Debug, Clone)]
pub struct TimerTask {
pub task_id: TaskID,
pub interval_minutes: u64,
#[allow(unused)]
pub last_run: i64, // Timestamp of last execution
}
pub struct Timer {
/// cron manager
delay_timer: Arc<Mutex<DelayTimer>>,
pub delay_timer: Arc<RwLock<DelayTimer>>,
/// save the current state
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
/// save the current state - using RwLock for better read concurrency
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
/// increment id
timer_count: Arc<Mutex<TaskID>>,
/// increment id - kept as mutex since it's just a counter
pub timer_count: Arc<Mutex<TaskID>>,
/// Flag to mark if timer is initialized - atomic for better performance
pub initialized: Arc<std::sync::atomic::AtomicBool>,
}
impl Timer {
@@ -25,68 +34,145 @@ impl Timer {
static TIMER: OnceCell<Timer> = OnceCell::new();
TIMER.get_or_init(|| Timer {
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(Mutex::new(HashMap::new())),
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(RwLock::new(HashMap::new())),
timer_count: Arc::new(Mutex::new(1)),
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
})
}
/// restore timer
/// Initialize timer with better error handling and atomic operations
pub fn init(&self) -> Result<()> {
self.refresh()?;
// Use compare_exchange for thread-safe initialization check
if self
.initialized
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_err()
{
log::debug!(target: "app", "Timer already initialized, skipping...");
return Ok(());
}
log::info!(target: "app", "Initializing timer...");
// Initialize timer tasks
if let Err(e) = self.refresh() {
// Reset initialization flag on error
self.initialized
.store(false, std::sync::atomic::Ordering::SeqCst);
log::error!(target: "app", "Failed to initialize timer: {}", e);
return Err(e);
}
let cur_timestamp = chrono::Local::now().timestamp();
let timer_map = self.timer_map.lock();
let delay_timer = self.delay_timer.lock();
if let Some(items) = Config::profiles().latest().get_items() {
// Collect profiles that need immediate update
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
items
.iter()
.filter_map(|item| {
// mins to seconds
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
let interval = item.option.as_ref()?.update_interval? as i64;
let updated = item.updated? as i64;
let uid = item.uid.as_ref()?;
if interval > 0 && cur_timestamp - updated >= interval {
Some(item)
if interval > 0 && cur_timestamp - updated >= interval * 60 {
Some(uid.clone())
} else {
None
}
})
.for_each(|item| {
if let Some(uid) = item.uid.as_ref() {
if let Some((task_id, _)) = timer_map.get(uid) {
crate::log_err!(delay_timer.advance_task(*task_id));
}
.collect::<Vec<String>>()
} else {
Vec::new()
};
// Advance tasks outside of locks to minimize lock contention
if !profiles_to_update.is_empty() {
let timer_map = self.timer_map.read();
let delay_timer = self.delay_timer.write();
for uid in profiles_to_update {
if let Some(task) = timer_map.get(&uid) {
log::info!(target: "app", "Advancing task for uid: {}", uid);
if let Err(e) = delay_timer.advance_task(task.task_id) {
log::warn!(target: "app", "Failed to advance task {}: {}", uid, e);
}
})
}
}
}
log::info!(target: "app", "Timer initialization completed");
Ok(())
}
/// Correctly update all cron tasks
/// Refresh timer tasks with better error handling
pub fn refresh(&self) -> Result<()> {
// Generate diff outside of lock to minimize lock contention
let diff_map = self.gen_diff();
let mut timer_map = self.timer_map.lock();
let mut delay_timer = self.delay_timer.lock();
if diff_map.is_empty() {
log::debug!(target: "app", "No timer changes needed");
return Ok(());
}
for (uid, diff) in diff_map.into_iter() {
log::info!(target: "app", "Refreshing {} timer tasks", diff_map.len());
// Apply changes while holding locks
let mut timer_map = self.timer_map.write();
let mut delay_timer = self.delay_timer.write();
for (uid, diff) in diff_map {
match diff {
DiffFlag::Del(tid) => {
let _ = timer_map.remove(&uid);
crate::log_err!(delay_timer.remove_task(tid));
timer_map.remove(&uid);
if let Err(e) = delay_timer.remove_task(tid) {
log::warn!(target: "app", "Failed to remove task {} for uid {}: {}", tid, uid, e);
} else {
log::debug!(target: "app", "Removed task {} for uid {}", tid, uid);
}
}
DiffFlag::Add(tid, val) => {
let _ = timer_map.insert(uid.clone(), (tid, val));
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
DiffFlag::Add(tid, interval) => {
let task = TimerTask {
task_id: tid,
interval_minutes: interval,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task);
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
log::error!(target: "app", "Failed to add task for uid {}: {}", uid, e);
timer_map.remove(&uid); // Rollback on failure
} else {
log::debug!(target: "app", "Added task {} for uid {}", tid, uid);
}
}
DiffFlag::Mod(tid, val) => {
let _ = timer_map.insert(uid.clone(), (tid, val));
crate::log_err!(delay_timer.remove_task(tid));
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
DiffFlag::Mod(tid, interval) => {
// Remove old task first
if let Err(e) = delay_timer.remove_task(tid) {
log::warn!(target: "app", "Failed to remove old task {} for uid {}: {}", tid, uid, e);
}
// Then add the new one
let task = TimerTask {
task_id: tid,
interval_minutes: interval,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task);
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
log::error!(target: "app", "Failed to update task for uid {}: {}", uid, e);
timer_map.remove(&uid); // Rollback on failure
} else {
log::debug!(target: "app", "Updated task {} for uid {}", tid, uid);
}
}
}
}
@@ -94,18 +180,17 @@ impl Timer {
Ok(())
}
/// generate a uid -> update_interval map
/// Generate map of profile UIDs to update intervals
fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().latest().get_items() {
for item in items.iter() {
if item.option.is_some() {
let option = item.option.as_ref().unwrap();
let interval = option.update_interval.unwrap_or(0);
if interval > 0 {
new_map.insert(item.uid.clone().unwrap(), interval);
if let Some(option) = item.option.as_ref() {
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
if interval > 0 {
new_map.insert(uid.clone(), interval);
}
}
}
}
@@ -114,39 +199,50 @@ impl Timer {
new_map
}
/// generate the diff map for refresh
/// Generate differences between current and new timer configuration
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
let mut diff_map = HashMap::new();
let timer_map = self.timer_map.lock();
let new_map = self.gen_map();
let cur_map = &timer_map;
cur_map.iter().for_each(|(uid, (tid, val))| {
let new_val = new_map.get(uid).unwrap_or(&0);
// Read lock for comparing current state
let timer_map = self.timer_map.read();
if *new_val == 0 {
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
} else if new_val != val {
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
// Find tasks to modify or delete
for (uid, task) in timer_map.iter() {
match new_map.get(uid) {
Some(&interval) if interval != task.interval_minutes => {
// Task exists but interval changed
diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval));
}
None => {
// Task no longer needed
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
}
_ => {
// Task exists with same interval, no change needed
}
}
});
}
let mut count = self.timer_count.lock();
// Find new tasks to add
let mut next_id = *self.timer_count.lock();
new_map.iter().for_each(|(uid, val)| {
if cur_map.get(uid).is_none() {
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
*count += 1;
for (uid, &interval) in new_map.iter() {
if !timer_map.contains_key(uid) {
diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval));
next_id += 1;
}
});
}
// Update counter only if we added new tasks
if next_id > *self.timer_count.lock() {
*self.timer_count.lock() = next_id;
}
diff_map
}
/// add a cron task
/// Add a timer task with better error handling
fn add_task(
&self,
delay_timer: &mut DelayTimer,
@@ -154,12 +250,19 @@ impl Timer {
tid: TaskID,
minutes: u64,
) -> Result<()> {
log::info!(target: "app", "Adding task: uid={}, id={}, interval={}min", uid, tid, minutes);
// Create a task with reasonable retries and backoff
let task = TaskBuilder::default()
.set_task_id(tid)
.set_maximum_parallel_runnable_num(1)
.set_frequency_repeated_by_minutes(minutes)
// .set_frequency_repeated_by_seconds(minutes) // for test
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
.spawn_async_routine(move || {
let uid = uid.clone();
async move {
Self::async_task(uid).await;
}
})
.context("failed to create timer task")?;
delay_timer
@@ -169,10 +272,39 @@ impl Timer {
Ok(())
}
/// the task runner
/// Async task with better error handling and logging
async fn async_task(uid: String) {
log::info!(target: "app", "running timer task `{uid}`");
crate::log_err!(feat::update_profile(uid, None).await);
let task_start = std::time::Instant::now();
log::info!(target: "app", "Running timer task for profile: {}", uid);
// Update profile
let profile_result = feat::update_profile(uid.clone(), None).await;
match profile_result {
Ok(_) => {
// Update configuration
match CoreManager::global().update_config().await {
Ok(_) => {
let duration = task_start.elapsed().as_millis();
log::info!(
target: "app",
"Timer task completed successfully for uid: {} (took {}ms)",
uid, duration
);
}
Err(e) => {
log::error!(
target: "app",
"Failed to refresh config after profile update for uid {}: {}",
uid, e
);
}
}
}
Err(e) => {
log::error!(target: "app", "Failed to update profile uid {}: {}", uid, e);
}
}
}
}
-349
View File
@@ -1,349 +0,0 @@
use crate::{
cmds,
config::Config,
feat,
utils::{dirs, resolve},
};
use anyhow::Result;
use tauri::{
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
SystemTraySubmenu,
};
pub struct Tray {}
impl Tray {
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
let zh = { Config::verge().latest().language == Some("zh".into()) };
let version = app_handle.package_info().version.to_string();
macro_rules! t {
($en: expr, $zh: expr) => {
if zh {
$zh
} else {
$en
}
};
}
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"open_window",
t!("Dashboard", "打开面板"),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new(
"rule_mode",
t!("Rule Mode", "规则模式"),
))
.add_item(CustomMenuItem::new(
"global_mode",
t!("Global Mode", "全局模式"),
))
.add_item(CustomMenuItem::new(
"direct_mode",
t!("Direct Mode", "直连模式"),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new(
"system_proxy",
t!("System Proxy", "系统代理"),
))
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
.add_item(CustomMenuItem::new(
"copy_env",
t!("Copy Env", "复制环境变量"),
))
.add_submenu(SystemTraySubmenu::new(
t!("Open Dir", "打开目录"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"open_app_dir",
t!("App Dir", "应用目录"),
))
.add_item(CustomMenuItem::new(
"open_core_dir",
t!("Core Dir", "内核目录"),
))
.add_item(CustomMenuItem::new(
"open_logs_dir",
t!("Logs Dir", "日志目录"),
)),
))
.add_submenu(SystemTraySubmenu::new(
t!("More", "更多"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"restart_clash",
t!("Restart Clash", "重启 Clash"),
))
.add_item(CustomMenuItem::new(
"restart_app",
t!("Restart App", "重启应用"),
))
.add_item(
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")))
}
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
app_handle
.tray_handle()
.set_menu(Tray::tray_menu(app_handle))?;
Tray::update_part(app_handle)?;
Ok(())
}
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
let zh = { Config::verge().latest().language == Some("zh".into()) };
let version = app_handle.package_info().version.to_string();
macro_rules! t {
($en: expr, $zh: expr) => {
if zh {
$zh
} else {
$en
}
};
}
let mode = {
Config::clash()
.latest()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let tray = app_handle.tray_handle();
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
let _ = tray.get_item("global_mode").set_selected(mode == "global");
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
#[cfg(target_os = "linux")]
match mode.as_str() {
"rule" => {
let _ = tray
.get_item("rule_mode")
.set_title(t!("Rule Mode ✔", "规则模式 ✔"));
let _ = tray
.get_item("global_mode")
.set_title(t!("Global Mode", "全局模式"));
let _ = tray
.get_item("direct_mode")
.set_title(t!("Direct Mode", "直连模式"));
}
"global" => {
let _ = tray
.get_item("rule_mode")
.set_title(t!("Rule Mode", "规则模式"));
let _ = tray
.get_item("global_mode")
.set_title(t!("Global Mode ✔", "全局模式 ✔"));
let _ = tray
.get_item("direct_mode")
.set_title(t!("Direct Mode", "直连模式"));
}
"direct" => {
let _ = tray
.get_item("rule_mode")
.set_title(t!("Rule Mode", "规则模式"));
let _ = tray
.get_item("global_mode")
.set_title(t!("Global Mode", "全局模式"));
let _ = tray
.get_item("direct_mode")
.set_title(t!("Direct Mode ✔", "直连模式 ✔"));
}
_ => {}
}
let verge = Config::verge();
let verge = verge.latest();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
#[cfg(target_os = "macos")]
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
#[cfg(target_os = "macos")]
match tray_icon.as_str() {
"monochrome" => {
let _ = tray.set_icon_as_template(true);
}
"colorful" => {
let _ = tray.set_icon_as_template(false);
}
_ => {}
}
let mut indication_icon = if *system_proxy {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"monochrome" => include_bytes!("../../icons/tray-icon-sys-mono.ico").to_vec(),
"colorful" => include_bytes!("../../icons/tray-icon-sys.ico").to_vec(),
_ => include_bytes!("../../icons/tray-icon-sys-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-sys.ico").to_vec();
if *sysproxy_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
let ico_path = icon_dir_path.join("sysproxy.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"monochrome" => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
"colorful" => include_bytes!("../../icons/tray-icon.ico").to_vec(),
_ => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon.ico").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
};
if *tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"monochrome" => include_bytes!("../../icons/tray-icon-tun-mono.ico").to_vec(),
"colorful" => include_bytes!("../../icons/tray-icon-tun.ico").to_vec(),
_ => include_bytes!("../../icons/tray-icon-tun-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-tun.ico").to_vec();
if *tun_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("tun.png");
let ico_path = icon_dir_path.join("tun.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
indication_icon = icon
}
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
#[cfg(target_os = "linux")]
{
if *system_proxy {
let _ = tray
.get_item("system_proxy")
.set_title(t!("System Proxy ✔", "系统代理 ✔"));
} else {
let _ = tray
.get_item("system_proxy")
.set_title(t!("System Proxy", "系统代理"));
}
if *tun_mode {
let _ = tray
.get_item("tun_mode")
.set_title(t!("TUN Mode ✔", "Tun 模式 ✔"));
} else {
let _ = tray
.get_item("tun_mode")
.set_title(t!("TUN Mode", "Tun 模式"));
}
}
let switch_map = {
let mut map = std::collections::HashMap::new();
map.insert(true, "on");
map.insert(false, "off");
map
};
let mut current_profile_name = "None".to_string();
let profiles = Config::profiles();
let profiles = profiles.latest();
if let Some(current_profile_uid) = profiles.get_current() {
let current_profile = profiles.get_item(&current_profile_uid);
current_profile_name = match &current_profile.unwrap().name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
};
let _ = tray.set_tooltip(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"),
switch_map[system_proxy],
t!("TUN Mode", "Tun 模式"),
switch_map[tun_mode],
t!("Curent Profile", "当前订阅"),
current_profile_name
));
Ok(())
}
pub fn on_click(app_handle: &AppHandle) {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event = tray_event.unwrap_or("main_window".into());
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"main_window" => resolve::create_window(app_handle),
_ => {}
}
}
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event {
#[cfg(not(target_os = "macos"))]
SystemTrayEvent::LeftClick { .. } => Tray::on_click(app_handle),
#[cfg(target_os = "macos")]
SystemTrayEvent::RightClick { .. } => Tray::on_click(app_handle),
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"copy_env" => feat::copy_clash_env(app_handle),
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => api::process::restart(&app_handle.env()),
"quit" => cmds::exit_app(app_handle.clone()),
_ => {}
},
_ => {}
}
}
}
+677
View File
@@ -0,0 +1,677 @@
use once_cell::sync::OnceCell;
use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::{
cmd,
config::Config,
feat,
module::{lightweight::entry_lightweight_mode, mihomo::Rate},
resolve,
utils::{dirs, i18n::t, resolve::VERSION},
};
use anyhow::Result;
#[cfg(target_os = "macos")]
use futures::StreamExt;
#[cfg(target_os = "macos")]
use parking_lot::Mutex;
#[cfg(target_os = "macos")]
use parking_lot::RwLock;
#[cfg(target_os = "macos")]
pub use speed_rate::{SpeedRate, Traffic};
#[cfg(target_os = "macos")]
use std::collections::hash_map::DefaultHasher;
#[cfg(target_os = "macos")]
use std::hash::{Hash, Hasher};
#[cfg(target_os = "macos")]
use std::sync::Arc;
use tauri::{
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconEvent},
App, AppHandle, Wry,
};
#[cfg(target_os = "macos")]
use tokio::sync::broadcast;
use super::handle;
#[cfg(target_os = "macos")]
pub struct Tray {
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
is_subscribed: Arc<RwLock<bool>>,
pub icon_hash: Arc<Mutex<Option<u64>>>,
pub icon_cache: Arc<Mutex<Option<Vec<u8>>>>,
pub rate_cache: Arc<Mutex<Option<Rate>>>,
}
#[cfg(not(target_os = "macos"))]
pub struct Tray {}
impl Tray {
pub fn global() -> &'static Tray {
static TRAY: OnceCell<Tray> = OnceCell::new();
#[cfg(target_os = "macos")]
return TRAY.get_or_init(|| Tray {
speed_rate: Arc::new(Mutex::new(None)),
shutdown_tx: Arc::new(RwLock::new(None)),
is_subscribed: Arc::new(RwLock::new(false)),
icon_hash: Arc::new(Mutex::new(None)),
icon_cache: Arc::new(Mutex::new(None)),
rate_cache: Arc::new(Mutex::new(None)),
});
#[cfg(not(target_os = "macos"))]
return TRAY.get_or_init(|| Tray {});
}
pub fn init(&self) -> Result<()> {
#[cfg(target_os = "macos")]
{
let mut speed_rate = self.speed_rate.lock();
*speed_rate = Some(SpeedRate::new());
}
Ok(())
}
pub fn create_systray(&self, app: &App) -> Result<()> {
let mut builder = TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.icon_as_template(false);
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
if tray_event.as_str() != "tray_menu" {
builder = builder.show_menu_on_left_click(false);
}
}
let tray = builder.build(app)?;
tray.on_tray_icon_event(|_, event| {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
log::debug!(target: "app","tray event: {:?}", tray_event);
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => resolve::create_window(),
_ => {}
}
}
});
tray.on_menu_event(on_menu_event);
Ok(())
}
/// 更新托盘点击行为
pub fn update_click_behavior(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
let tray = app_handle.tray_by_id("main").unwrap();
match tray_event.as_str() {
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
_ => tray.set_show_menu_on_left_click(false)?,
}
Ok(())
}
/// 更新托盘菜单
pub fn update_menu(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let mode = {
Config::clash()
.latest()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let profile_uid_and_name = Config::profiles()
.data()
.all_profile_uid_and_name()
.unwrap_or_default();
let tray = app_handle.tray_by_id("main").unwrap();
let _ = tray.set_menu(Some(create_tray_menu(
&app_handle,
Some(mode.as_str()),
*system_proxy,
*tun_mode,
profile_uid_and_name,
)?));
Ok(())
}
/// 更新托盘图标
#[allow(unused_variables)]
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let tray = app_handle.tray_by_id("main").unwrap();
#[cfg(target_os = "macos")]
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let icon_bytes = if *system_proxy && !*tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon-sys.ico").to_vec();
if *sysproxy_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
let ico_path = icon_dir_path.join("sysproxy.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else if *tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon-tun.ico").to_vec();
if *tun_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("tun.png");
let ico_path = icon_dir_path.join("tun.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon.ico").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
};
#[cfg(target_os = "macos")]
{
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
let is_colorful = tray_icon == "colorful";
let icon_hash = {
let mut hasher = DefaultHasher::new();
icon_bytes.clone().hash(&mut hasher);
hasher.finish()
};
let mut icon_hash_guard = self.icon_hash.lock();
let mut icon_bytes_guard = self.icon_cache.lock();
if *icon_hash_guard != Some(icon_hash) {
*icon_hash_guard = Some(icon_hash);
*icon_bytes_guard = Some(icon_bytes.clone());
}
if !enable_tray_speed || (!enable_tray_speed && !enable_tray_icon) {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(
&(*icon_bytes_guard).clone().unwrap(),
)?));
let _ = tray.set_icon_as_template(!is_colorful);
return Ok(());
}
let rate = if let Some(rate) = rate {
Some(rate)
} else {
let guard = self.speed_rate.lock();
if let Some(rate) = guard.as_ref().unwrap().get_curent_rate() {
Some(rate)
} else {
Some(Rate::default())
}
};
let mut rate_guard = self.rate_cache.lock();
if *rate_guard != rate {
*rate_guard = rate;
let bytes = if enable_tray_icon {
Some(icon_bytes_guard.as_ref().unwrap())
} else {
None
};
let rate = rate_guard.as_ref();
let rate_bytes = SpeedRate::add_speed_text(bytes, rate).unwrap();
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
let _ = tray.set_icon_as_template(!is_colorful);
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
{
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
Ok(())
}
}
/// 更新托盘提示
pub fn update_tooltip(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let version = VERSION.get().unwrap();
let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let switch_map = {
let mut map = std::collections::HashMap::new();
map.insert(true, "on");
map.insert(false, "off");
map
};
let mut current_profile_name = "None".to_string();
let profiles = Config::profiles();
let profiles = profiles.latest();
if let Some(current_profile_uid) = profiles.get_current() {
let current_profile = profiles.get_item(&current_profile_uid);
current_profile_name = match &current_profile.unwrap().name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
};
let tray = app_handle.tray_by_id("main").unwrap();
let _ = tray.set_tooltip(Some(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t("SysProxy"),
switch_map[system_proxy],
t("TUN"),
switch_map[tun_mode],
t("Profile"),
current_profile_name
)));
Ok(())
}
pub fn update_part(&self) -> Result<()> {
self.update_menu()?;
self.update_icon(None)?;
self.update_tooltip()?;
Ok(())
}
/// 订阅流量数据
#[cfg(target_os = "macos")]
pub async fn subscribe_traffic(&self) -> Result<()> {
log::info!(target: "app", "subscribe traffic");
// 如果已经订阅,先取消订阅
if *self.is_subscribed.read() {
self.unsubscribe_traffic();
}
let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
*self.shutdown_tx.write() = Some(shutdown_tx);
*self.is_subscribed.write() = true;
let speed_rate = Arc::clone(&self.speed_rate);
let is_subscribed = Arc::clone(&self.is_subscribed);
tauri::async_runtime::spawn(async move {
let mut shutdown = shutdown_rx;
'outer: loop {
match Traffic::get_traffic_stream().await {
Ok(mut stream) => loop {
tokio::select! {
Some(traffic) = stream.next() => {
if let Ok(traffic) = traffic {
let guard = speed_rate.lock();
let enable_tray_speed: bool = Config::verge().latest().enable_tray_speed.unwrap_or(true);
if !enable_tray_speed {
continue;
}
if let Some(sr) = guard.as_ref() {
if let Some(rate) = sr.update_and_check_changed(traffic.up, traffic.down) {
let _ = Tray::global().update_icon(Some(rate));
}
}
}
}
_ = shutdown.recv() => break 'outer,
}
},
Err(e) => {
log::error!(target: "app", "Failed to get traffic stream: {}", e);
// 如果获取流失败,等待一段时间后重试
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
// 检查是否应该继续重试
if !*is_subscribed.read() {
break;
}
}
}
}
});
Ok(())
}
/// 取消订阅 traffic 数据
#[cfg(target_os = "macos")]
pub fn unsubscribe_traffic(&self) {
log::info!(target: "app", "unsubscribe traffic");
*self.is_subscribed.write() = false;
if let Some(tx) = self.shutdown_tx.write().take() {
drop(tx);
}
}
}
fn create_tray_menu(
app_handle: &AppHandle,
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
profile_uid_and_name: Vec<(String, String)>,
) -> Result<tauri::menu::Menu<Wry>> {
let mode = mode.unwrap_or("");
let version = VERSION.get().unwrap();
let hotkeys = Config::verge()
.latest()
.hotkeys
.as_ref()
.map(|h| {
h.iter()
.filter_map(|item| {
let mut parts = item.split(',');
match (parts.next(), parts.next()) {
(Some(func), Some(key)) => Some((func.to_string(), key.to_string())),
_ => None,
}
})
.collect::<std::collections::HashMap<String, String>>()
})
.unwrap_or_default();
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
.iter()
.map(|(profile_uid, profile_name)| {
let is_current_profile = Config::profiles()
.data()
.is_current_profile_index(profile_uid.to_string());
CheckMenuItem::with_id(
app_handle,
format!("profiles_{}", profile_uid),
t(profile_name),
true,
is_current_profile,
None::<&str>,
)
.unwrap()
})
.collect();
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
.iter()
.map(|item| item as &dyn IsMenuItem<Wry>)
.collect();
let open_window = &MenuItem::with_id(
app_handle,
"open_window",
t("Dashboard"),
true,
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
)
.unwrap();
let rule_mode = &CheckMenuItem::with_id(
app_handle,
"rule_mode",
t("Rule Mode"),
true,
mode == "rule",
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
)
.unwrap();
let global_mode = &CheckMenuItem::with_id(
app_handle,
"global_mode",
t("Global Mode"),
true,
mode == "global",
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
)
.unwrap();
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
t("Direct Mode"),
true,
mode == "direct",
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
)
.unwrap();
let profiles = &Submenu::with_id_and_items(
app_handle,
"profiles",
t("Profiles"),
true,
&profile_menu_items,
)
.unwrap();
let system_proxy = &CheckMenuItem::with_id(
app_handle,
"system_proxy",
t("System Proxy"),
true,
system_proxy_enabled,
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
)
.unwrap();
let tun_mode = &CheckMenuItem::with_id(
app_handle,
"tun_mode",
t("TUN Mode"),
true,
tun_mode_enabled,
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
)
.unwrap();
let lighteweight_mode = &MenuItem::with_id(
app_handle,
"entry_lightweight_mode",
t("LightWeight Mode"),
true,
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
)
.unwrap();
let copy_env =
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
t("Conf Dir"),
true,
None::<&str>,
)
.unwrap();
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
t("Core Dir"),
true,
None::<&str>,
)
.unwrap();
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
t("Logs Dir"),
true,
None::<&str>,
)
.unwrap();
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
t("Open Dir"),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
let restart_clash = &MenuItem::with_id(
app_handle,
"restart_clash",
t("Restart Clash Core"),
true,
None::<&str>,
)
.unwrap();
let restart_app = &MenuItem::with_id(
app_handle,
"restart_app",
t("Restart App"),
true,
None::<&str>,
)
.unwrap();
let app_version = &MenuItem::with_id(
app_handle,
"app_version",
format!("{} {version}", t("Verge Version")),
true,
None::<&str>,
)
.unwrap();
let more = &Submenu::with_id_and_items(
app_handle,
"more",
t("More"),
true,
&[restart_clash, restart_app, app_version],
)
.unwrap();
let quit =
&MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q")).unwrap();
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
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,
])
.build()
.unwrap();
Ok(menu)
}
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
match event.id.as_ref() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];
println!("change mode to: {}", mode);
feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => crate::log_err!(cmd::open_app_dir()),
"open_core_dir" => crate::log_err!(cmd::open_core_dir()),
"open_logs_dir" => crate::log_err!(cmd::open_logs_dir()),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(),
"entry_lightweight_mode" => entry_lightweight_mode(),
"quit" => {
println!("quit");
feat::quit(Some(0));
}
id if id.starts_with("profiles_") => {
let profile_index = &id["profiles_".len()..];
feat::toggle_proxy_profile(profile_index.into());
}
_ => {}
}
}
+267
View File
@@ -0,0 +1,267 @@
use crate::{
module::mihomo::{MihomoManager, Rate},
utils::help::format_bytes_speed,
};
use ab_glyph::FontArc;
use anyhow::Result;
use futures::Stream;
use image::{GenericImageView, Rgba, RgbaImage};
use imageproc::drawing::draw_text_mut;
use parking_lot::Mutex;
use std::{io::Cursor, sync::Arc};
use tokio_tungstenite::tungstenite::{http, Message};
use tungstenite::client::IntoClientRequest;
#[derive(Debug, Clone)]
pub struct SpeedRate {
rate: Arc<Mutex<(Rate, Rate)>>,
last_update: Arc<Mutex<std::time::Instant>>,
}
impl SpeedRate {
pub fn new() -> Self {
Self {
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
}
}
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
let mut rates = self.rate.lock();
let mut last_update = self.last_update.lock();
let now = std::time::Instant::now();
// 限制更新频率为每秒最多2次(500ms)
if now.duration_since(*last_update).as_millis() < 500 {
return None;
}
let (current, previous) = &mut *rates;
// Avoid unnecessary float conversions for small value checks
let should_update = if current.up < 1000 && down < 1000 {
// For small values, always update to ensure accuracy
current.up != up || current.down != down
} else {
// For larger values, use integer math to check for >5% change
// Multiply by 20 instead of dividing by 0.05 to avoid floating point
let up_threshold = current.up / 20;
let down_threshold = current.down / 20;
(up > current.up && up - current.up > up_threshold)
|| (up < current.up && current.up - up > up_threshold)
|| (down > current.down && down - current.down > down_threshold)
|| (down < current.down && current.down - down > down_threshold)
};
if !should_update {
return None;
}
*previous = current.clone();
current.up = up;
current.down = down;
*last_update = now;
if previous != current {
Some(current.clone())
} else {
None
}
}
pub fn get_curent_rate(&self) -> Option<Rate> {
let rates = self.rate.lock();
let (current, _) = &*rates;
Some(current.clone())
}
// 分离图标加载和速率渲染
pub fn add_speed_text<'a>(
icon_bytes: Option<&'a Vec<u8>>,
rate: Option<&'a Rate>,
) -> Result<Vec<u8>> {
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
let (mut icon_width, mut icon_height) = (0, 256);
let icon_image = if let Some(bytes) = icon_bytes {
let icon_image = image::load_from_memory(bytes)?;
icon_width = icon_image.width();
icon_height = icon_image.height();
icon_image
} else {
// 返回一个空的 RGBA 图像
image::DynamicImage::new_rgba8(0, 0)
};
// 判断是否为彩色图标
let is_colorful = if let Some(bytes) = icon_bytes {
!crate::utils::help::is_monochrome_image_from_bytes(bytes).unwrap_or(false)
} else {
false
};
// 增加文本宽度和间距
let total_width = if icon_bytes.is_some() {
if icon_width < 580 {
icon_width + 580
} else {
icon_width
}
} else {
580
};
// 创建新的透明画布
let mut combined_image = RgbaImage::new(total_width, icon_height);
// 将原始图标绘制到新画布的左侧
if icon_bytes.is_some() {
for y in 0..icon_height {
for x in 0..icon_width {
let pixel = icon_image.get_pixel(x, y);
combined_image.put_pixel(x, y, pixel);
}
}
}
// 选择文本颜色
let (text_color, shadow_color) = if is_colorful {
// 彩色图标使用黑色文本和轻微白色阴影
(
Rgba([255u8, 255u8, 255u8, 255u8]),
Rgba([0u8, 0u8, 0u8, 160u8]),
)
} else {
// 单色图标使用白色文本和轻微黑色阴影
(
Rgba([255u8, 255u8, 255u8, 255u8]),
Rgba([0u8, 0u8, 0u8, 120u8]),
)
};
// 减小字体大小以适应文本区域
let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf");
let font = FontArc::try_from_vec(font_data.to_vec()).unwrap();
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
let scale = ab_glyph::PxScale::from(font_size);
// 使用更简洁的速率格式
let up_text = format_bytes_speed(rate.up);
let down_text = format_bytes_speed(rate.down);
// 计算文本位置,确保垂直间距合适
// 修改文本位置为居右显示
let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
// 计算右对齐的文本位置
let up_text_x = total_width - up_text_width;
let down_text_x = total_width - down_text_width;
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
let text_height = font_size as i32;
let total_text_height = text_height * 2;
let up_y = (icon_height as i32 - total_text_height) / 2;
let down_y = up_y + text_height;
// 绘制速率文本(先阴影后文字)
let shadow_offset = 1;
// 绘制上行速率
draw_text_mut(
&mut combined_image,
shadow_color,
up_text_x as i32 + shadow_offset,
up_y + shadow_offset,
scale,
&font,
&up_text,
);
draw_text_mut(
&mut combined_image,
text_color,
up_text_x as i32,
up_y,
scale,
&font,
&up_text,
);
// 绘制下行速率
draw_text_mut(
&mut combined_image,
shadow_color,
down_text_x as i32 + shadow_offset,
down_y + shadow_offset,
scale,
&font,
&down_text,
);
draw_text_mut(
&mut combined_image,
text_color,
down_text_x as i32,
down_y,
scale,
&font,
&down_text,
);
// 将结果转换为 PNG 数据
let mut bytes = Vec::new();
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
Ok(bytes)
}
}
#[derive(Debug, Clone)]
pub struct Traffic {
pub up: u64,
pub down: u64,
}
impl Traffic {
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
{
use futures::stream::{self, StreamExt};
use std::time::Duration;
let stream = Box::pin(
stream::unfold((), |_| async {
loop {
let (url, token) = MihomoManager::get_traffic_ws_url();
let mut request = url.into_client_request().unwrap();
request
.headers_mut()
.insert(http::header::AUTHORIZATION, token);
match tokio_tungstenite::connect_async(request).await {
Ok((ws_stream, _)) => {
log::info!(target: "app", "traffic ws connection established");
return Some((
ws_stream.map(|msg| {
msg.map_err(anyhow::Error::from).and_then(|msg: Message| {
let data = msg.into_text()?;
let json: serde_json::Value = serde_json::from_str(&data)?;
Ok(Traffic {
up: json["up"].as_u64().unwrap_or(0),
down: json["down"].as_u64().unwrap_or(0),
})
})
}),
(),
));
}
Err(e) => {
log::error!(target: "app", "traffic ws connection failed: {e}");
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
}
}
}
})
.flatten(),
);
Ok(stream)
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
function main(config) {
function main(config, _name) {
if (config.mode === "script") {
config.mode = "rule";
}
@@ -1,4 +1,4 @@
function main(config) {
function main(config, _name) {
if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => {
if (p.type === "hysteria" && typeof p.alpn === "string") {
+2 -1
View File
@@ -1,7 +1,7 @@
use serde_yaml::{Mapping, Value};
use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 11] = [
pub const HANDLE_FIELDS: [&str; 12] = [
"mode",
"redir-port",
"tproxy-port",
@@ -13,6 +13,7 @@ pub const HANDLE_FIELDS: [&str; 11] = [
"ipv6",
"external-controller",
"secret",
"unified-delay",
];
pub const DEFAULT_FIELDS: [&str; 5] = [
+1 -3
View File
@@ -54,9 +54,7 @@ fn test_merge() -> anyhow::Result<()> {
let merge = serde_yaml::from_str::<Mapping>(merge)?;
let config = serde_yaml::from_str::<Mapping>(config)?;
let result = serde_yaml::to_string(&use_merge(merge, config))?;
println!("{result}");
let _ = serde_yaml::to_string(&use_merge(merge, config))?;
Ok(())
}
+41 -18
View File
@@ -5,17 +5,10 @@ mod script;
pub mod seq;
mod tun;
use self::chain::*;
use self::field::*;
use self::merge::*;
use self::script::*;
use self::seq::*;
use self::tun::*;
use crate::config::Config;
use crate::utils::tmpl;
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
use crate::{config::Config, utils::tmpl};
use serde_yaml::Mapping;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
type ResultLog = Vec<(String, String)>;
@@ -25,28 +18,29 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
let clash_config = { Config::clash().latest().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled) = {
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.clash_core.clone(),
verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(true),
verge.verge_http_enabled.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(false),
verge.verge_http_enabled.unwrap_or(false),
verge.enable_dns_settings.unwrap_or(false),
)
};
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge();
let verge = verge.latest();
verge.verge_redir_enabled.unwrap_or(true)
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge();
let verge = verge.latest();
verge.verge_tproxy_enabled.unwrap_or(true)
verge.verge_tproxy_enabled.unwrap_or(false)
};
// 从profiles里拿东西
@@ -59,6 +53,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
groups_item,
global_merge,
global_script,
profile_name,
) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
@@ -123,6 +118,12 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let name = profiles
.get_item(&profiles.get_current().unwrap_or_default())
.ok()
.and_then(|item| item.name.clone())
.unwrap_or_default();
(
current,
merge,
@@ -132,6 +133,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
groups,
global_merge,
global_script,
name,
)
};
@@ -147,7 +149,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Script(script) = global_script.data {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
match use_script(script, config.to_owned(), profile_name.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
@@ -180,7 +182,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Script(script) = script_item.data {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
match use_script(script, config.to_owned(), profile_name.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
@@ -239,7 +241,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned()) {
match use_script(script, config.to_owned(), "".to_string()) {
Ok((res_config, _)) => {
config = res_config;
}
@@ -254,6 +256,27 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
config = use_tun(config, enable_tun).await;
config = use_sort(config);
// 应用独立的DNS配置(如果启用)
if enable_dns_settings {
use crate::utils::dirs;
use std::fs;
// 尝试读取dns_config.yaml
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::from_str::<serde_yaml::Mapping>(&dns_yaml) {
// 将DNS配置合并到最终配置中
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
}
}
}
}
}
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys);
exists_keys = exists_set.into_iter().collect();
+8 -6
View File
@@ -2,7 +2,11 @@ use super::use_lowercase;
use anyhow::{Error, Result};
use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
pub fn use_script(
script: String,
config: Mapping,
name: String,
) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use std::sync::{Arc, Mutex};
let mut context = Context::default();
@@ -42,7 +46,7 @@ pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(Stri
let code = format!(
r#"try{{
{script};
JSON.stringify(main({config_str})||'')
JSON.stringify(main({config_str},'{name}')||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
@@ -97,11 +101,9 @@ fn test_script() {
"#;
let config = serde_yaml::from_str(config).unwrap();
let (config, results) = use_script(script.into(), config).unwrap();
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let config_str = serde_yaml::to_string(&config).unwrap();
println!("{config_str}");
let _ = serde_yaml::to_string(&config).unwrap();
dbg!(results);
}
+142 -40
View File
@@ -1,53 +1,155 @@
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
prepend: Sequence,
append: Sequence,
delete: Sequence,
pub prepend: Sequence,
pub append: Sequence,
pub delete: Vec<String>,
}
pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
let mut prepend = seq_map.prepend;
let append = seq_map.append;
let delete = seq_map.delete;
pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
let SeqMap {
prepend,
append,
delete,
} = seq;
let origin_seq = config.get(&name).map_or(Sequence::default(), |val| {
val.as_sequence().unwrap_or(&Sequence::default()).clone()
});
let mut seq = origin_seq.clone();
prepend.reverse();
for item in prepend {
seq.insert(0, item);
let mut new_seq = Sequence::new();
new_seq.extend(prepend);
if let Some(Value::Sequence(origin)) = config.get(field) {
// Filter out deleted items
let filtered: Sequence = origin
.iter()
.filter(|item| {
if let Value::String(s) = item {
!delete.contains(s)
} else if let Value::Mapping(m) = item {
if let Some(Value::String(name)) = m.get("name") {
!delete.contains(name)
} else {
true
}
} else {
true
}
})
.cloned()
.collect();
new_seq.extend(filtered);
}
for item in append {
seq.push(item);
}
let mut delete_names = Vec::new();
for item in delete {
let item = item.clone();
if let Some(name) = if item.is_string() {
Some(item)
} else {
item.get("name").map(|y| y.clone())
} {
delete_names.push(name.clone());
new_seq.extend(append);
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());
}
}
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
}
}
seq.retain(|x| {
if let Some(x_name) = if x.is_string() {
Some(x)
} else {
x.get("name")
} {
!delete_names.contains(&x_name)
} else {
true
}
});
let mut config = config.clone();
config.insert(Value::from(name), Value::from(seq));
return config;
config
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused_imports)]
use serde_yaml::Value;
#[test]
fn test_delete_proxy_and_references() {
let config_str = r#"
proxies:
- name: "proxy1"
type: "ss"
- name: "proxy2"
type: "vmess"
proxy-groups:
- name: "group1"
type: "select"
proxies:
- "proxy1"
- "proxy2"
- name: "group2"
type: "select"
proxies:
- "proxy1"
"#;
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".to_string()],
};
config = use_seq(seq, config, "proxies");
// Check if proxy1 is removed from proxies
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
assert_eq!(proxies.len(), 1);
assert_eq!(
proxies[0]
.as_mapping()
.unwrap()
.get("name")
.unwrap()
.as_str()
.unwrap(),
"proxy2"
);
// Check if proxy1 is removed from all groups
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
let group1_proxies = groups[0]
.as_mapping()
.unwrap()
.get("proxies")
.unwrap()
.as_sequence()
.unwrap();
let group2_proxies = groups[1]
.as_mapping()
.unwrap()
.get("proxies")
.unwrap()
.as_sequence()
.unwrap();
assert_eq!(group1_proxies.len(), 1);
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
assert_eq!(group2_proxies.len(), 0);
}
}
+49 -47
View File
@@ -1,4 +1,3 @@
use crate::{core::service, log_err};
use serde_yaml::{Mapping, Value};
macro_rules! revise {
@@ -9,6 +8,7 @@ macro_rules! revise {
}
// if key not exists then append value
#[allow(unused_macros)]
macro_rules! append {
($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into());
@@ -21,58 +21,60 @@ macro_rules! append {
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);
if !enable && tun_val.is_none() {
return config;
}
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
revise!(tun_val, "enable", enable);
if enable {
// 读取DNS配置
let dns_key = Value::from("dns");
let dns_val = config.get(&dns_key);
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let ipv6_key = Value::from("ipv6");
let ipv6_val = config
.get(&ipv6_key)
.and_then(|v| v.as_bool())
.unwrap_or(false);
// 检查现有的 enhanced-mode 设置
let current_mode = dns_val
.get(Value::from("enhanced-mode"))
.and_then(|v| v.as_str())
.unwrap_or("fake-ip");
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) {
revise!(dns_val, "enable", true);
revise!(dns_val, "ipv6", ipv6_val);
if !dns_val.contains_key(Value::from("enhanced-mode")) {
revise!(dns_val, "enhanced-mode", "fake-ip");
}
if !dns_val.contains_key(Value::from("fake-ip-range")) {
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
}
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
}
}
// 当TUN启用时,将修改后的DNS配置写回
revise!(config, "dns", dns_val);
} else {
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
#[cfg(target_os = "macos")]
crate::utils::resolve::restore_public_dns().await;
}
// 更新TUN配置
revise!(tun_val, "enable", enable);
revise!(config, "tun", tun_val);
if enable {
log_err!(service::set_dns_by_service().await);
use_dns_for_tun(config)
} else {
log_err!(service::unset_dns_by_service().await);
config
}
}
fn use_dns_for_tun(mut config: Mapping) -> Mapping {
let dns_key = Value::from("dns");
let dns_val = config.get(&dns_key);
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
// 开启tun将同时开启dns
revise!(dns_val, "enable", true);
append!(dns_val, "enhanced-mode", "fake-ip");
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
append!(
dns_val,
"nameserver",
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
);
append!(dns_val, "fallback", vec![] as Vec<&str>);
#[cfg(target_os = "windows")]
append!(
dns_val,
"fake-ip-filter",
vec![
"dns.msftncsi.com",
"www.msftncsi.com",
"www.msftconnecttest.com"
]
);
revise!(config, "dns", dns_val);
config
}
-404
View File
@@ -1,404 +0,0 @@
//!
//! feat mod 里的函数主要用于
//! - hotkey 快捷键
//! - timer 定时器
//! - cmds 页面调用
//!
use crate::config::*;
use crate::core::*;
use crate::log_err;
use crate::utils::resolve;
use anyhow::{bail, Result};
use serde_yaml::{Mapping, Value};
use tauri::{AppHandle, ClipboardManager, Manager};
// 打开面板
pub fn open_or_close_dashboard() {
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
if let Some(window) = app_handle.get_window("main") {
if let Ok(true) = window.is_focused() {
let _ = window.close();
return;
}
}
resolve::create_window(app_handle);
}
}
// 重启clash
pub fn restart_clash_core() {
tauri::async_runtime::spawn(async {
match CoreManager::global().run_core().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
}
}
});
}
// 切换模式 rule/global/direct/script mode
pub fn change_clash_mode(mode: String) {
let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), mode.clone().into());
tauri::async_runtime::spawn(async move {
log::debug!(target: "app", "change clash mode to {mode}");
match clash_api::patch_configs(&mapping).await {
Ok(_) => {
// 更新订阅
Config::clash().data().patch_config(mapping);
if Config::clash().data().save_config().is_ok() {
handle::Handle::refresh_clash();
log_err!(handle::Handle::update_systray_part());
}
}
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
// 切换系统代理
pub fn toggle_system_proxy() {
let enable = Config::verge().draft().enable_system_proxy;
let enable = enable.unwrap_or(false);
tauri::async_runtime::spawn(async move {
match patch_verge(IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
})
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
// 切换tun模式
pub fn toggle_tun_mode() {
let enable = Config::verge().data().enable_tun_mode;
let enable = enable.unwrap_or(false);
tauri::async_runtime::spawn(async move {
match patch_verge(IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
})
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
/// 修改clash的订阅
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
let res = {
let redir_port = patch.get("redir-port");
let tproxy_port = patch.get("tproxy-port");
let mixed_port = patch.get("mixed-port");
let socks_port = patch.get("socks-port");
let port = patch.get("port");
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
if mixed_port.is_some() && !enable_random_port {
let changed = mixed_port.unwrap()
!= Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
// 检查端口占用
if changed {
if let Some(port) = mixed_port.unwrap().as_u64() {
if !port_scanner::local_port_available(port as u16) {
Config::clash().discard();
bail!("port already in use");
}
}
}
};
// 激活订阅
if redir_port.is_some()
|| tproxy_port.is_some()
|| mixed_port.is_some()
|| socks_port.is_some()
|| port.is_some()
|| patch.get("secret").is_some()
|| patch.get("external-controller").is_some()
{
Config::generate().await?;
CoreManager::global().run_core().await?;
handle::Handle::refresh_clash();
}
// 更新系统代理
if mixed_port.is_some() {
log_err!(sysopt::Sysopt::global().init_sysproxy());
}
if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part());
}
Config::runtime().latest().patch_config(patch);
<Result<()>>::Ok(())
};
match res {
Ok(()) => {
Config::clash().apply();
Config::clash().data().save_config()?;
Ok(())
}
Err(err) => {
Config::clash().discard();
Err(err)
}
}
}
/// 修改verge的订阅
/// 一般都是一个个的修改
pub async fn patch_verge(patch: IVerge) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy;
let pac = patch.proxy_auto_config;
let pac_content = patch.pac_file_content;
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let port = patch.verge_mixed_port;
#[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon;
let common_tray_icon = patch.common_tray_icon;
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
let tun_tray_icon = patch.tun_tray_icon;
#[cfg(not(target_os = "windows"))]
let redir_enabled = patch.verge_redir_enabled;
#[cfg(target_os = "linux")]
let tproxy_enabled = patch.verge_tproxy_enabled;
let socks_enabled = patch.verge_socks_enabled;
let http_enabled = patch.verge_http_enabled;
let res = {
let service_mode = patch.enable_service_mode;
if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
Config::generate().await?;
CoreManager::global().run_core().await?;
} else if tun_mode.is_some() {
update_core_config().await?;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
}
if socks_enabled.is_some() || http_enabled.is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
}
if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?;
}
if system_proxy.is_some()
|| proxy_bypass.is_some()
|| port.is_some()
|| pac.is_some()
|| pac_content.is_some()
{
sysopt::Sysopt::global().update_sysproxy()?;
sysopt::Sysopt::global().guard_proxy();
}
if let Some(true) = patch.enable_proxy_guard {
sysopt::Sysopt::global().guard_proxy();
}
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys)?;
}
if language.is_some() {
handle::Handle::update_systray()?;
} else if system_proxy.is_some()
|| tun_mode.is_some()
|| common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
{
handle::Handle::update_systray_part()?;
}
#[cfg(target_os = "macos")]
if tray_icon.is_some() {
handle::Handle::update_systray_part()?;
}
<Result<()>>::Ok(())
};
match res {
Ok(()) => {
Config::verge().apply();
Config::verge().data().save_file()?;
Ok(())
}
Err(err) => {
Config::verge().discard();
Err(err)
}
}
}
/// 更新某个profile
/// 如果更新当前订阅就激活订阅
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
let url_opt = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
if !is_remote {
None // 直接更新
} else if item.url.is_none() {
bail!("failed to get the profile item url");
} else {
Some((item.url.clone().unwrap(), item.option.clone()))
}
};
let should_update = match url_opt {
Some((url, opt)) => {
let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
Some(uid) == profiles.get_current()
}
None => true,
};
if should_update {
update_core_config().await?;
}
Ok(())
}
/// 更新订阅
async fn update_core_config() -> Result<()> {
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
Ok(())
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
Err(err)
}
}
}
/// copy env variable
pub fn copy_clash_env(app_handle: &AppHandle) {
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
let http_proxy = format!("http://127.0.0.1:{}", port);
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
let sh =
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let mut cliboard = app_handle.clipboard_manager();
let env_type = { Config::verge().latest().env_type.clone() };
let env_type = match env_type {
Some(env_type) => env_type,
None => {
#[cfg(not(target_os = "windows"))]
let default = "bash";
#[cfg(target_os = "windows")]
let default = "powershell";
default.to_string()
}
};
match env_type.as_str() {
"bash" => cliboard.write_text(sh).unwrap_or_default(),
"cmd" => cliboard.write_text(cmd).unwrap_or_default(),
"powershell" => cliboard.write_text(ps).unwrap_or_default(),
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
};
}
pub async fn test_delay(url: String) -> Result<u32> {
use tokio::time::{Duration, Instant};
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
let proxy_scheme = format!("http://127.0.0.1:{port}");
if !tun_mode {
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
let request = builder
.timeout(Duration::from_millis(10000))
.build()?
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
let start = Instant::now();
let response = request.send().await;
match response {
Ok(response) => {
log::trace!(target: "app", "test_delay response: {:#?}", response);
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
Ok(10000u32)
}
}
Err(err) => {
log::trace!(target: "app", "test_delay error: {:#?}", err);
Err(err.into())
}
}
}
+88
View File
@@ -0,0 +1,88 @@
use crate::{
config::{Config, IVerge},
core::backup,
log_err,
utils::dirs::app_home_dir,
};
use anyhow::Result;
use reqwest_dav::list_cmd::ListFile;
use std::fs;
/// Create a backup and upload to WebDAV
pub async fn create_backup_and_upload_webdav() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
log::error!(target: "app", "Failed to create backup: {:#?}", err);
err
})?;
if let Err(err) = backup::WebDavClient::global()
.upload(temp_file_path.clone(), file_name)
.await
{
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
return Err(err);
}
if let Err(err) = std::fs::remove_file(&temp_file_path) {
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
}
Ok(())
}
/// List WebDAV backups
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
backup::WebDavClient::global().list().await.map_err(|err| {
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
err
})
}
/// Delete WebDAV backup
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
backup::WebDavClient::global()
.delete(filename)
.await
.map_err(|err| {
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
err
})
}
/// Restore WebDAV backup
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge();
let verge_data = verge.data().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let backup_storage_path = app_home_dir().unwrap().join(&filename);
backup::WebDavClient::global()
.download(filename, backup_storage_path.clone())
.await
.map_err(|err| {
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
err
})?;
// extract zip file
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
zip.extract(app_home_dir()?)?;
log_err!(
super::patch_verge(
IVerge {
webdav_url,
webdav_username,
webdav_password,
..IVerge::default()
},
false
)
.await
);
// 最后删除临时文件
fs::remove_file(backup_storage_path)?;
Ok(())
}
+115
View File
@@ -0,0 +1,115 @@
use crate::{
config::Config,
core::{handle, tray, CoreManager},
log_err,
module::mihomo::MihomoManager,
utils::resolve,
};
use serde_yaml::{Mapping, Value};
use tauri::Manager;
/// Restart the Clash core
pub fn restart_clash_core() {
tauri::async_runtime::spawn(async {
match CoreManager::global().restart_core().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
}
}
});
}
/// Restart the application
pub fn restart_app() {
tauri::async_runtime::spawn_blocking(|| {
tauri::async_runtime::block_on(async {
log_err!(CoreManager::global().stop_core().await);
});
resolve::resolve_reset();
let app_handle = handle::Handle::global().app_handle().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
tauri::process::restart(&app_handle.env());
});
}
/// Change Clash mode (rule/global/direct/script)
pub fn change_clash_mode(mode: String) {
let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), mode.clone().into());
// Convert YAML mapping to JSON Value
let json_value = serde_json::json!({
"mode": mode
});
tauri::async_runtime::spawn(async move {
log::debug!(target: "app", "change clash mode to {mode}");
CoreManager::global().ensure_running_core().await;
match MihomoManager::global().patch_configs(json_value).await {
Ok(_) => {
// 更新订阅
Config::clash().data().patch_config(mapping);
if Config::clash().data().save_config().is_ok() {
handle::Handle::refresh_clash();
log_err!(tray::Tray::global().update_menu());
log_err!(tray::Tray::global().update_icon(None));
}
}
Err(err) => println!("{err}"),
}
});
}
/// Test connection delay to a URL
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
CoreManager::global().ensure_running_core().await;
use tokio::time::{Duration, Instant};
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
let proxy_scheme = format!("http://127.0.0.1:{port}");
if !tun_mode {
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
let request = builder
.timeout(Duration::from_millis(10000))
.build()?
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
let start = Instant::now();
let response = request.send().await;
match response {
Ok(response) => {
log::trace!(target: "app", "test_delay response: {:#?}", response);
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
Ok(10000u32)
}
}
Err(err) => {
log::trace!(target: "app", "test_delay error: {:#?}", err);
Err(err.into())
}
}
}
+225
View File
@@ -0,0 +1,225 @@
use crate::{
config::{Config, IVerge},
core::{handle, hotkey, sysopt, tray, CoreManager},
log_err,
module::lightweight,
};
use anyhow::Result;
use serde_yaml::Mapping;
/// Patch Clash configuration
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
let res = {
// 激活订阅
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
Config::generate().await?;
CoreManager::global().restart_core().await?;
} else {
if patch.get("mode").is_some() {
log_err!(tray::Tray::global().update_menu());
log_err!(tray::Tray::global().update_icon(None));
}
Config::runtime().latest().patch_config(patch);
CoreManager::global().update_config().await?;
}
handle::Handle::refresh_clash();
<Result<()>>::Ok(())
};
match res {
Ok(()) => {
Config::clash().apply();
Config::clash().data().save_config()?;
Ok(())
}
Err(err) => {
Config::clash().discard();
Err(err)
}
}
}
// Define update flags as bitflags for better performance
#[derive(Clone, Copy)]
enum UpdateFlags {
None = 0,
RestartCore = 1 << 0,
ClashConfig = 1 << 1,
VergeConfig = 1 << 2,
Launch = 1 << 3,
SysProxy = 1 << 4,
SystrayIcon = 1 << 5,
Hotkey = 1 << 6,
SystrayMenu = 1 << 7,
SystrayTooltip = 1 << 8,
SystrayClickBehavior = 1 << 9,
LighteWeight = 1 << 10,
}
/// Patch Verge configuration
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy;
let pac = patch.proxy_auto_config;
let pac_content = patch.pac_file_content;
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let mixed_port = patch.verge_mixed_port;
#[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon;
#[cfg(not(target_os = "macos"))]
let tray_icon: Option<String> = None;
let common_tray_icon = patch.common_tray_icon;
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
let tun_tray_icon = patch.tun_tray_icon;
#[cfg(not(target_os = "windows"))]
let redir_enabled = patch.verge_redir_enabled;
#[cfg(not(target_os = "windows"))]
let redir_port = patch.verge_redir_port;
#[cfg(target_os = "linux")]
let tproxy_enabled = patch.verge_tproxy_enabled;
#[cfg(target_os = "linux")]
let tproxy_port = patch.verge_tproxy_port;
let socks_enabled = patch.verge_socks_enabled;
let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
let enable_tray_speed = patch.enable_tray_speed;
let enable_tray_icon = patch.enable_tray_icon;
let enable_global_hotkey = patch.enable_global_hotkey;
let tray_event = patch.tray_event;
let home_cards = patch.home_cards.clone();
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
let res: std::result::Result<(), anyhow::Error> = {
// Initialize with no flags set
let mut update_flags: i32 = UpdateFlags::None as i32;
if tun_mode.is_some() {
update_flags |= UpdateFlags::ClashConfig as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
update_flags |= UpdateFlags::SystrayTooltip as i32;
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if enable_global_hotkey.is_some() || home_cards.is_some() {
update_flags |= UpdateFlags::VergeConfig as i32;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() || redir_port.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() || tproxy_port.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
if socks_enabled.is_some()
|| http_enabled.is_some()
|| socks_port.is_some()
|| http_port.is_some()
|| mixed_port.is_some()
{
update_flags |= UpdateFlags::RestartCore as i32;
}
if auto_launch.is_some() {
update_flags |= UpdateFlags::Launch as i32;
}
if system_proxy.is_some() {
update_flags |= UpdateFlags::SysProxy as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
update_flags |= UpdateFlags::SystrayTooltip as i32;
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
update_flags |= UpdateFlags::SysProxy as i32;
}
if language.is_some() {
update_flags |= UpdateFlags::SystrayMenu as i32;
}
if common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
|| tray_icon.is_some()
|| enable_tray_speed.is_some()
|| enable_tray_icon.is_some()
{
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if patch.hotkeys.is_some() {
update_flags |= UpdateFlags::Hotkey as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
}
if tray_event.is_some() {
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
}
if enable_auto_light_weight.is_some() {
update_flags |= UpdateFlags::LighteWeight as i32;
}
// Process updates based on flags
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
Config::generate().await?;
CoreManager::global().restart_core().await?;
}
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
CoreManager::global().update_config().await?;
handle::Handle::refresh_clash();
}
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
handle::Handle::refresh_verge();
}
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
sysopt::Sysopt::global().update_launch()?;
}
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
}
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
tray::Tray::global().update_menu()?;
}
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
tray::Tray::global().update_icon(None)?;
}
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
tray::Tray::global().update_tooltip()?;
}
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
tray::Tray::global().update_click_behavior()?;
}
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
if enable_auto_light_weight.unwrap() {
lightweight::enable_auto_light_weight_mode();
} else {
lightweight::disable_auto_light_weight_mode();
}
}
<Result<()>>::Ok(())
};
match res {
Ok(()) => {
Config::verge().apply();
if !not_save_file {
Config::verge().data().save_file()?;
}
Ok(())
}
Err(err) => {
Config::verge().discard();
Err(err)
}
}
}
+14
View File
@@ -0,0 +1,14 @@
mod backup;
mod clash;
mod config;
mod profile;
mod proxy;
mod window;
// Re-export all functions from modules
pub use backup::*;
pub use clash::*;
pub use config::*;
pub use profile::*;
pub use proxy::*;
pub use window::*;
+84
View File
@@ -0,0 +1,84 @@
use crate::{
cmd,
config::{Config, PrfItem, PrfOption},
core::{handle, CoreManager, *},
};
use anyhow::{bail, Result};
/// Toggle proxy profile
pub fn toggle_proxy_profile(profile_index: String) {
tauri::async_runtime::spawn(async move {
let app_handle = handle::Handle::global().app_handle().unwrap();
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
Ok(_) => {
let _ = tray::Tray::global().update_menu();
}
Err(err) => {
log::error!(target: "app", "{err}");
}
}
});
}
/// Update a profile
/// If updating current profile, activate it
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
println!("[订阅更新] 开始更新订阅 {}", uid);
let url_opt = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
None // 非远程订阅直接更新
} else if item.url.is_none() {
println!("[订阅更新] {} 缺少URL,无法更新", uid);
bail!("failed to get the profile item url");
} else {
println!(
"[订阅更新] {} 是远程订阅,URL: {}",
uid,
item.url.clone().unwrap()
);
Some((item.url.clone().unwrap(), item.option.clone()))
}
};
let should_update = match url_opt {
Some((url, opt)) => {
println!("[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
println!("[订阅更新] 更新订阅配置");
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current
}
None => true,
};
if should_update {
println!("[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await {
Ok(_) => {
println!("[订阅更新] 更新成功");
handle::Handle::refresh_clash();
}
Err(err) => {
println!("[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target: "app", "{err}");
}
}
}
Ok(())
}

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