Compare commits

...
178 Commits
Author SHA1 Message Date
wonfen ad623da86d chore: update i18n (#4128) 2025-07-22 08:41:02 +08:00
Tunglies 3ec7772e5d fix: update sysinfo and zip dependencies to specific versions in Cargo.toml 2025-07-22 08:05:19 +08:00
Tunglies e6b57561e3 chore: update cargo.lock 2025-07-22 08:02:07 +08:00
Tunglies 3b1c2c95ec Revert "chore(deps): update rust crate zip to 4.3.0 (#4073)"
This reverts commit fed64bcd08.
2025-07-22 07:59:26 +08:00
Tunglies 364abafffe fix: update prerequisites link in CONTRIBUTING.md and update dependencies in Cargo.lock
- Updated the link for installing Rust and Node.js in CONTRIBUTING.md.
- Added new dependencies: wayland-backend, wayland-client, wayland-protocols.
- Updated versions for several dependencies including async-channel, async-io, async-process, and others.
- Removed deprecated dispatch2 version and added new dlib dependency.
- Updated various dependencies to their latest versions for improved stability and features.
2025-07-20 04:19:25 +08:00
Copilot 389281b96c Revert sysinfo dependency from 0.36.0 to 0.35.2 (#4086) 2025-07-16 09:34:03 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> fed64bcd08 chore(deps): update rust crate zip to 4.3.0 (#4073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 22:25:25 +08:00
Tunglies 17a5c4d094 chore: update sysinfo dependency to version 0.36.0 (#4082)
* chore: update sysinfo dependency to version 0.36.0

* fix: update sysinfo dependency to enable specific features
2025-07-15 22:23:24 +08:00
Dyna f2317c7816 fix: log levels will not display their respective logs correctly (#4074)
* fix-log levels will not display their respective logs correctly

* fix-logs

* Update UPDATELOG.md

* Update UPDATELOG.md
2025-07-15 18:55:53 +08:00
Tunglies 7fce7ca62f Revert "chore(deps): update cargo dependencies (#4023)" (#4072)
This reverts commit 2c9aa4bca7.
2025-07-14 23:55:39 +08:00
Tunglies 167bcb222d fix: add platform-specific input options for workflow dispatch in dev.yml 2025-07-14 20:14:35 +08:00
Tunglies 108a599666 fix: add platform input options for workflow dispatch in dev.yml 2025-07-14 20:09:46 +08:00
Tunglies 57476741cd fix: improve logging format for old shortcut removal failure 2025-07-14 19:31:19 +08:00
Tunglies 077f3e79f8 fix: unify startup shortcut name to "Clash Verge" on Windows 2025-07-14 04:22:06 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2c9aa4bca7 chore(deps): update cargo dependencies (#4023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:34:47 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 9cf0f1e0a7 chore(deps): update npm dependencies (#4014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:34:38 +08:00
Dyna 660c03a564 optimization: (fix: the crash when inputting special characters in the search box (#4026)) (#4029)
* add a test rule

* add translation support
2025-07-10 21:09:02 +08:00
Dyna 1d49d79af2 fix: the crash when inputting special characters in the search box (#4026)
* add special character escapes

* update logs
2025-07-10 13:34:37 +08:00
Dyna 4ccb17dde6 fix: the problem that the DNS override nameserver-policy field cannot correctly recognize multiple writing methods (#4011)
* fix: the problem that the DNS override nameserver-policy field cannot correctly recognize multiple writing methods

* update logs

* fix-dns-viewer.tsx
2025-07-08 17:05:17 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8bc433711d chore(deps): update rust crate tokio to 1.46.1 (#3989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 18:04:40 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2e82bd8624 chore(deps): update npm dependencies (#3986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 18:04:32 +08:00
❤是纱雾酱哟~ 4b57d513a2 Chore (Renovate): Disable Dependency Dashboard (#3999)
* Chore (Renovate): Disable Dependency Dashboard

- Close #3658

* Fix (Renovate): Prettify JSON format according to "prettier"
2025-07-07 18:02:29 +08:00
Dyna d36c3a83a9 fix:DNS override server support is left blank by default (#3997)
* fix:DNS server support being left blank by default

* update logs
2025-07-06 11:01:05 +08:00
Tunglies ac3afe4dee fix: manage setup Mutex crash (#3995)
* Revert "Revert "refactor: Replace std::sync::Mutex with parking_lot::Mutex for improved performance and consistency in multiple files" (#3990)"

This reverts commit 667844aa12.

* refactor: Manage lightweight state in the app setup and clean up unused proxy client code

* refactor: Move macOS-specific Manager import under conditional compilation
2025-07-06 02:14:48 +08:00
Dyna 667844aa12 Revert "refactor: Replace std::sync::Mutex with parking_lot::Mutex for improved performance and consistency in multiple files" (#3990) 2025-07-05 12:17:02 +08:00
Tunglies 6d192233d1 refactor: Replace std::sync::Mutex with parking_lot::Mutex for improved performance and consistency in multiple files 2025-07-05 00:33:53 +08:00
Tunglies 764ef48fd1 refactor(Draft): Replace latest() with latest_ref() and data() with data_mut() in multiple files for improved mutability handling and consistency across the codebase (#3987)
* feat: add benchmarking for draft operations and new draft management structure

* Refactor Config Access: Replace `latest()` with `latest_ref()` and `data()` with `data_mut()` in multiple files for improved mutability handling and consistency across the codebase.

* refactor: remove DraftNew implementation and related benchmarks for cleaner codebase
2025-07-04 22:43:23 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 3f95c81243 chore(deps): update rust crate tokio to 1.46.0 (#3943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:51:25 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7c385d329d chore(deps): update npm dependencies (#3944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:51:18 +08:00
Dyna 553438d219 fix windows installer (#3979)
* add loop detection Clash Verge Lnk

* add delete the window-state.json file .window-state.json file

* add cycle through the registry and delete useless registry entries

* add delete the specified registry path

* update logs
2025-07-04 12:28:15 +08:00
rebecca554owen 941921904c fix: remove unused redir-port and tproxy-port on Windows (#3969)
Directly remove redir-port and tproxy-port configurations on Windows platform
2025-07-04 12:27:59 +08:00
wonfen 7d808ed798 fix: prevent freeze caused by enabling Proxy Guard 2025-07-04 12:22:22 +08:00
❤是纱雾酱哟~ 7704072a65 Chore (ISSUE_TEMPLATE): 进一步优化问题反馈模板 (#3975)
- 当前模板强依赖于用户把日志贴到正确的位置上
- 参考 WinGet 的问题模板,放弃日志折叠功能,强制用户以代码块形式粘贴日志
2025-07-03 18:41:46 +08:00
❤是纱雾酱哟~ 9076baf1c8 Chore (ISSUE_TEMPLATE): 修复错误的 YAML 语法 (#3951)
- 修改「OS」分类的类型:"dropdown" -> "checkbox"
  - 一个问题可能不只影响一个特定的操作系统
- 修改「软件版本」分类,增加 "render" 属性
  - 这能让 GFM 直接把这部分内容渲染为 render 指定语法的代码片段
2025-07-03 14:30:46 +08:00
954ff53d9b refactor: use React in its intended way (#3963)
* refactor: replace `useEffect` w/ `useLocalStorage`

* refactor: replace `useEffect` w/ `useSWR`

* refactor: replace `useEffect` and `useSWR`. clean up `useRef`

* refactor: use `requestIdleCallback`

* refactor: replace `useEffect` w/ `useMemo`

* fix: clean up `useEffect`

* refactor: replace `useEffect` w/ `useSWR`

* refactor: remove unused `useCallback`

* refactor: enhance performance and memory management in frontend processes

* refactor: improve pre-push script structure and readability

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-07-02 23:34:13 +08:00
Tunglies 37d268bb16 chore(deps): remove deprecated dependencies (#3960)
* chore(deps):  remove deprecated dependencies

* refactor: simplify string formatting in make_key function
2025-07-02 03:07:50 +08:00
Tunglies 5befa90f81 chore(deps): update dashmap to version 7.0.0-rc2 2025-07-01 02:02:14 +08:00
Tunglies 6e3313f00d chore: remove redundant bug fix section from UPDATELOG.md 2025-06-30 21:11:01 +08:00
7b5afb7afe Add cors (#3909)
* add external `cors` control panel

* optimize format

* fix-script.rs

* fix-service.rs

* fix-rs

async_proxy_query.rs

event_driven_proxy.rs

service_ipc.rs

service.rs

sysopt.rs

* lower the prettier version number to 3.5.3

* Revert "lower the prettier version number to 3.5.3"

This reverts commit 0f1c3dfa8a.

* fix: prttier erros

* add developer environment detection and controlled the display of development environment URL

* submit required

* fix-external-controller-cors

* use the custom component ToggleButton to ensure a uniform button style

* fix-tsx

hotkey-viewer.tsx

external-controller-cors.tsx

* fix-bug_report.yml

* remove the annoying title

* fix-write overload problem

* Individual button settings

* fix-setting-clash.tsx

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-06-30 20:48:20 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> fe13dad06f chore(deps): update rust crate ab_glyph to 0.2.30 (#3932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 20:22:43 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> b41bad0ae2 chore(deps): update npm dependencies (#3918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 20:22:37 +08:00
Tunglies 18ef7f0272 feat: enhance proxy management with caching and refresh logic 2025-06-30 20:14:04 +08:00
❤是纱雾酱哟~ 4435a5aee4 Chore (ISSUE_TEMPLATE): 修复错误的 YAML 缩进 (bug_report.yml) (#3933) 2025-06-29 19:27:13 +08:00
Tunglies d9e3a47894 fix: adjust formatting of log content section in bug report template 2025-06-29 10:41:18 +08:00
❤是纱雾酱哟~andTunglies c96be18187 chore (ISSUE_TEMPLATE): 修改问题反馈模板 (#3927)
* chore (ISSUE_TEMPLATE): 修改问题反馈模板

- 修改 bug_report.yml
- 增加一段预置的格式模板,指引用户将日志粘贴在规范的 Code Block 中以保持日志原有的格式排版
- 使用 <details /> 块以压缩日志片段,对于非 Contributors / Collaborators 用户友好,规避日志霸屏效果,简化爬楼。

* 接受来自审查的建议

- 增加英语支持,以方便非中文母语用户

Co-authored-by: Tunglies <tunglies.dev@outlook.com>

---------

Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-06-28 21:23:46 +08:00
Tunglies 47416dd3f8 chore: update UPDATELOG 2025-06-28 20:03:30 +08:00
Tunglies 4486f734bb fix: correct traffic percentage calculation to handle zero total gracefully #3920 2025-06-28 20:00:49 +08:00
Tunglies bea6a2c8f7 chore: update Prettier configuration and dependencies; refactor code formatting for consistency (#3926) 2025-06-28 18:37:53 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 82af9ed78e chore(deps): update cargo dependencies (#3908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 23:48:42 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2b9e38d259 chore(deps): update npm dependencies (#3910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 23:48:18 +08:00
Tunglies 7c4222aed2 chore: update Node.js setup steps in formatting workflow to use corepack for package management (#3914) 2025-06-27 23:46:51 +08:00
Tunglies a574ced428 Refactor logging statements to use the new formatting syntax for improved readability and consistency across the codebase. This includes updating error, warning, and info logs in various modules such as system commands, configuration, core functionalities, and utilities. Additionally, minor adjustments were made to string formatting in backup and proxy features to enhance clarity. 2025-06-27 23:30:59 +08:00
wonfen cf437e6d94 chore: reduce default auto clean log to 7 days 2025-06-27 09:29:55 +08:00
Tunglies e1bb8aa125 chore(pre-push): update comments for clarity and refine format check condition 2025-06-27 00:36:00 +08:00
Tunglies c11bdd81e9 chore(i18n): add "Saved Successfully" message to English and Chinese locales 2025-06-27 00:32:37 +08:00
Tunglies f1192c95a8 feat: add notification system with hotkey events and permission handling (#3867)
* feat: add notification system with hotkey events and permission handling

* Add macOS-specific handling for AppHidden notification

Introduces conditional support for the AppHidden notification event,
enabling macOS-specific behavior. Updates the enum and notification
logic to include this platform-specific feature.

Improves macOS user experience by accommodating system-level
application hiding events.

* Implement feature X to enhance user experience and fix bug Y in module Z

* refactor(notification): update notification keys for consistency and clarity

* chore(deps): update dependencies to latest versions
2025-06-26 23:09:07 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
ae187cc21a chore(deps): update npm dependencies (#3871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-06-26 22:33:58 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a7875718f7 chore(deps): update rust crate tauri to 2.6.1 (#3906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 22:31:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5db1f7cda7 chore(deps): update cargo dependencies (#3896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 22:29:13 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> cb98b17052 chore(deps): update npm dependencies to v7 (#3886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 01:09:50 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7aee1c6d6e chore(deps): update cargo dependencies (#3872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 01:09:36 +08:00
TRH f22199b7d9 chore(logging): 去除无用日志 (#3882)
* chore(logging): 去除无用日志

* chore: 删除相关计时代码
2025-06-24 16:49:06 +08:00
Tunglies 0a8d6e5147 fix: add validation for profile URL format during import 2025-06-23 23:30:33 +08:00
希亚的西红柿andTunglies 6d519dac1e fix: auto light-weight mode doesn't take effect in silent-start mode (#3875)
* fix: auto light-weight mode does not take effect when silent-start mode is enabled

* refactor: streamline window state retrieval and hiding logic

* fix: add checks for remote name and existence before format check in pre-push hook

* fix: simplify remote checks in pre-push hook to enhance clarity and maintainability

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-06-23 23:13:31 +08:00
wonfen d5a174c71b perf: improve proxy status indicator and toggle responsiveness 2025-06-23 12:59:24 +08:00
wonfen 628de70e89 chore: remove unused imports 2025-06-23 00:09:17 +08:00
wonfen fee08f3826 fix: correct address display error caused by async system proxy retrieval 2025-06-22 23:19:11 +08:00
wonfen bdfc383a18 refactor: enhance Windows proxy retrieval by using WinAPI for registry access 2025-06-22 21:05:50 +08:00
wonfen f6b5524e0e refactor: replace shell command check with WinAPI call 2025-06-22 18:45:38 +08:00
Tunglies e7461fccab refactor: remove unused macOS tray speed display and improve tray icon handling (#3862)
* refactor: remove unused macOS tray speed display and improve tray icon handling

* refactor: disable macOS specific logging during core initialization

* feat: add linux elevator function to determine privilege escalation command
2025-06-22 16:28:06 +08:00
wonfen a92872c831 fix: resolve race condition freeze on rapid tray icon clicks in lightweight mode 2025-06-22 15:44:09 +08:00
Tunglies 094feb74ec feat: add new monochrome tray icons for system and tunnel 2025-06-22 15:32:33 +08:00
Tunglies 9b1c660306 feat: add new monochrome tray icons for system and tunnel 2025-06-22 15:31:59 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4b860ba897 chore(deps): update npm dependencies (#3831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 13:24:25 +08:00
Tunglies 3d8b2cf35f fix: improve error handling for application handle retrieval (#3860)
* fix: improve error handling for application handle retrieval

* fix: correct argument passing for command execution and improve URL stripping logic
2025-06-22 01:16:57 +08:00
wonfen 41fc13cfe2 fix: format & update 2025-06-21 22:39:12 +08:00
wonfen 5fde5dcc7c feat: implement async proxy lookup and optimize system/auto proxy retrieval logic 2025-06-21 21:56:15 +08:00
wonfen 034885d810 feat: introduce event-driven proxy manager and optimize proxy config updates 2025-06-21 21:56:15 +08:00
Tunglies 3f7a7b8cd2 chore: add cargo deny config 2025-06-21 21:22:20 +08:00
Tunglies 98dc50a9ed refactor: remove redundant steps for cleaning old release assets 2025-06-21 20:42:27 +08:00
Tunglies 5dd820d12e feat: add job to clean old release assets before uploading new ones 2025-06-21 20:36:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> bc30db2875 chore(deps): update rust crate libc to 0.2.174 (#3794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-21 18:41:06 +08:00
Dyna abe914d446 fix the issue that CSS cannot customize background (#3854)
* fixed the issue that css background cannot be used

* update logs

* fix logs
2025-06-21 16:53:10 +08:00
wonfen cc65ce6812 chore: update to 2.3.2 2025-06-21 10:09:22 +08:00
wonfen 1a6454ee79 perf: optimize profile switching logic with interrupt support to prevent freeze 2025-06-21 10:04:12 +08:00
Just want to protect you b72f397369 fix the issue of system proxy port being out of sync (#3841)
* repair system proxy port real-time response

* update logs

* fix-logs
2025-06-21 10:03:06 +08:00
wonfen e698fe8d18 feat: add cleanup for redundant mihomo processes 2025-06-21 08:13:12 +08:00
wonfen c8cad1c295 chore: add funding button 2025-06-20 20:52:40 +08:00
wonfen c423fe90e2 Release 2.3.1 2025-06-18 20:42:13 +08:00
623461b649 about tray and silent startup + automatic startup light repair and optimization (#3814)
* fixed the issue that the tray does not display in light mode

* add show main window after exiting light mode

* fix: the issue where other selections could not respond in real time

* Refactored the original silent + self-starting lightweight #3708

* update logs

* fix formatting issues

* fix

* fix nesting

* repair previous formatting issues

* fix: resolve issue with tray not restoring from lightweight mode

* remove the tray display that should not exist

* fix translation field issue

---------

Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-18 20:00:33 +08:00
Tunglies 1f9ba4de5c feat: add translation for 'Already Using Latest Core' in English and Chinese locales 2025-06-18 19:10:08 +08:00
Tunglies 10f792b8bc fix: streamline pnpm installation steps in autobuild workflow (#3816)
* fix: streamline pnpm installation steps in autobuild workflow

* fix: format code for better readability in useCustomTheme and _layout components
2025-06-18 18:57:05 +08:00
Tunglies 357b64e9a0 feat: enhance pre-push hook to include remote URL check and improve format check messaging 2025-06-18 18:56:03 +08:00
Tunglies f3ad6cee41 feat: add code format check in pre-push hook 2025-06-18 18:55:53 +08:00
972310ea4e fix:Linux displays white boxes, white dots and white bars (#3785)
Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-18 08:28:54 +08:00
时雨andTunglies f5315fd737 fix: 修正Linux系统窗口下页面白边和顶部圆角白底 (#3778)
Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-18 08:28:40 +08:00
Tunglies c40c1960ed feat: add window-state management and recovery functionality (#3792)
* feat: add window-state management and recovery functionality

* fix: correct file name reference for window-state.json in uninstall section

* fix: remove unused tauri_plugin_window_state import
2025-06-18 02:21:00 +08:00
Tunglies c8ae37811c chore: add pnpm cache configuration to Node setup steps 2025-06-18 01:42:06 +08:00
Tunglies 15f05748a5 fix: update tauri-plugin-autostart to version 2.4.0 and change source to crates.io 2025-06-18 01:34:40 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 0a0a25913b chore(deps): update npm dependencies (#3718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 01:31:39 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 945999889c chore(deps): update cargo dependencies (#3731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 01:31:24 +08:00
Tunglies ce7e1d6456 chore: remove pnpm cache configuration from workflow 2025-06-18 01:30:38 +08:00
Tunglies 72c4b9de8a doc: clarify macOS Intel X86 architecture build error message 2025-06-18 01:30:05 +08:00
Tunglies a8257e8cb2 fix: unexpected behavior while pulling resources (#3789)
fix: unexpected behavior while pulling resources

在 commit 25cfd162f6 中引入新的 pnpm prepare 执行脚本指令,而 prepare 关键字与包管理器的生命周期脚本冲突。导致在运行 Workflow 的时候被执行两次资源拉取,并且被当做生命周期脚本的时候没有携带预期的 \`${{ matrix.target }}\`。这一行为进一步影响 macOS Intel X86 平台上的构建。

所影响的 issues: #3753, #3771
2025-06-18 01:11:33 +08:00
Tunglies 15d3c765ac feat: add debug logging for matrix target and update prepare script invocation 2025-06-17 23:35:23 +08:00
wonfen e7d3d7e0ae chore: update changelog 2025-06-17 22:05:03 +08:00
wonfen c0f3d35e13 fix: resolve namespace issue 2025-06-17 19:47:24 +08:00
wonfen fc30fab9cd feat: unify window manager and optimize window show/hide logic 2025-06-17 13:02:47 +08:00
wonfen a67e8388a9 fix: restore window from tray when status bar icon is present 2025-06-17 12:59:05 +08:00
wonfen ac7307b1f7 feat: enhance profile management and proxy refresh with improved event listening and state updates 2025-06-17 11:38:53 +08:00
Tunglies 4068e5ec9c fix: clippy error in https://github.com/clash-verge-rev/clash-verge-rev/commit/26acce94a40e9203774107187809b0309918d08a#diff-3a1c371e65c9809233eadec0aed9dfc09813f06b4d622ffd5a613ae6020048c8R298 2025-06-17 08:24:10 +08:00
Just want to protect youandAhao ea71181692 fix : js script escape error issue introduced #3751 (#3759)
* refactoring fix js script is too strict

* fix:format

---------

Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
2025-06-16 21:04:40 +08:00
wonfen 9b46c1a991 refactor: unify log output format 2025-06-16 21:03:45 +08:00
wonfen 26acce94a4 feat: add clash_core config validation & auto-fix to default; fixed "No such file or directory (os error 2)" 2025-06-16 20:27:02 +08:00
wonfen 2a0e1e206e Release 2.3.0 2025-06-16 12:01:14 +08:00
Tunglies f83d1d1582 fix: unexpected workflow changes #3684 2025-06-16 00:57:53 +08:00
wonfen 77a090d5eb i18n: update en 2025-06-15 20:23:32 +08:00
陈生杂物房 a9561e0ded feat: 更新简体中文语言文件 (#3684)
* feat: 更新简体中文语言文件

* 报错照样上传文件
2025-06-15 19:38:55 +08:00
wonfen dd505e4d58 perf: optimize layout initialization and enhance backend notification mechanism 2025-06-15 19:35:12 +08:00
wonfen dd0e9d4e1b fix: enhance hotkey handling in lightweight mode and improve window management 2025-06-15 17:40:59 +08:00
希亚的西红柿andTunglies 8bc451ff08 fix: proxy mode description displays incorrectly (#3719)
* fix: proxy mode description display incorrectly

* fix: update language json

---------

Co-authored-by: Tunglies <selenvow+github@gmail.com>
2025-06-15 05:19:30 +08:00
Tunglies 032e5bf32e fix: add macOS specific activation policy handling for silent start (#3740)
* fix: add macOS specific activation policy handling for silent start

* fix: resolve abnormal Dock icon launch during macOS silent startup
2025-06-15 05:10:15 +08:00
Tunglies 41ba8f6c7a fix: streamline pre-push hook to run clippy only for src-tauri changes 2025-06-14 02:47:15 +08:00
Tunglies 799783d3ef fix: ensure app exits gracefully when an instance is already running 2025-06-14 02:43:58 +08:00
Just want to protect youandAhao 77fb40506f fix the JS script error when escaping special characters to prevent special airport names from causing the script to fail to work (#3717)
* fix the JS script error

* update logs

* remove unused references

* update logs

* fix special escape test

* optimize:unescape of special characters in JS strings

* fix unaligned annotations

---------

Co-authored-by: Ahao <108321411+xuanyuan0408@users.noreply.github.com>
2025-06-13 22:59:48 +08:00
wonfen 5858f05c13 perf: async app startup loading to prevent UI freeze 2025-06-13 22:59:16 +08:00
Tunglies 0432cad112 fix: disable node and pnpm auto update to prevent breaks build 2025-06-13 14:22:21 +08:00
rebecca554owen 47ec8a348c Revert "fix: update packageManager to pnpm@10.12.1" (#3728)
This reverts commit 9bd5950f60.
2025-06-13 14:21:07 +08:00
Tunglies eef348b8dc fix: add conditional compilation for localhost URL in dev mode 2025-06-13 03:11:35 +08:00
Tunglies e78a02d0ba fix: add function to retrieve short git commit hash and update timestamp format 2025-06-13 00:17:31 +08:00
希亚的西红柿 4e54b61380 fix: silent start does not take effect (#3708)
* fix: light mode error

* feat: optimize the logic

* fix: clippy issues
2025-06-12 23:39:03 +08:00
Tunglies cc39b2734e fix: enhance system proxy toggle logic to account for autoproxy and sysproxy states 2025-06-12 22:42:33 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 16eaccb10e chore(deps): update npm dependencies (#3714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 21:55:54 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> dbc6f54a77 chore(deps): update rust crate reqwest to 0.12.20 (#3715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 21:32:03 +08:00
Tunglies 9bd5950f60 fix: update packageManager to pnpm@10.12.1 2025-06-12 21:27:23 +08:00
Tunglies 2c7c416f60 fix: simplify mutable reference handling in with_lightweight_status function 2025-06-12 21:26:22 +08:00
Tunglies 3eb0ab4197 fix: enable node/pnpm version updates in Renovate configuration 2025-06-12 21:23:31 +08:00
TianHua Liu b7cb511032 perf(Notice): replace useState by useSyncExternalStore (#3682)
* fix: the first function call creates multiple axios instances

* perf(Notice): replace useState by useSyncExternalStore

* chore: prettier --check .

* fix: prettier format
2025-06-12 20:49:39 +08:00
wonfen 18850bb51a Update Crowdin configuration file 2025-06-12 10:57:33 +08:00
wonfen 4d718e0cea Update Crowdin configuration file 2025-06-12 10:51:21 +08:00
wonfen 3a2f567055 Update Crowdin configuration file 2025-06-12 10:48:58 +08:00
rebecca554owen 4f474e1098 feat: add proxy auto scroll toggle setting (#3674)
* feat: add proxy auto scroll toggle setting

* refactor: rename enable_proxy_auto_scroll to enable_hover_jump_navigator

* 参考 Unified Delay 调整式样
2025-06-11 13:21:24 +08:00
Tunglies 22eac1a832 refactor: comment out Rust cache step in Clippy workflow 2025-06-11 07:53:59 +08:00
Tunglies 07738dd82d feat: implement lightweight mode state management and initialization #3344, d05952e945 2025-06-11 04:20:50 +08:00
Tunglies f07d508018 refactor: standardize casing for "HTTP Port" to "Http Port" in localization files 2025-06-11 03:40:16 +08:00
Tunglies b591a90100 feat: add Linux-specific tray icon builder configuration 2025-06-11 01:04:56 +08:00
Tunglies db3cfdf66f refactor: update icon variable naming for clarity and adjust macOS logging import 2025-06-11 00:51:34 +08:00
Tunglies 5cf3e1a817 refactor: streamline macOS-specific code by consolidating conditional imports and logic in sysopt, tray, lightweight, and mihomo modules 2025-06-11 00:19:06 +08:00
Tunglies 25cfd162f6 refactor: replace 'pnpm check' with 'pnpm prepare' in workflow files and update CONTRIBUTING.md 2025-06-10 23:43:04 +08:00
Tunglies 7001ef2030 feat: add Clippy linting workflow for pull requests
feat: add check step to Clippy workflow

feat: add build step for web assets in Clippy workflow

fix: set NODE_OPTIONS for web asset build to prevent memory issues
2025-06-10 23:37:10 +08:00
Tunglies cbcab72a7c refactor: simplify version check logic in run_core_by_service and update tray menu function call 2025-06-10 23:37:09 +08:00
wonfen d05b1c3130 feat: clean up redundant profiles files 2025-06-09 13:48:38 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> fbdcda9a7f chore(deps): update npm dependencies (#3660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 20:37:49 +08:00
Tunglies 0ebc482698 refactor: update UPDATELOG.md for clarity and organization, enhance security recommendations, and improve known issues section 2025-06-07 10:42:43 +08:00
Tunglies 09969d95de feat: add rustfmt configuration and CI workflow for code formatting
refactor: streamline formatting workflow by removing unused taplo steps and clarifying directory change

refactor: remove unnecessary directory change step in formatting workflow
2025-06-06 22:13:11 +08:00
Tunglies 689042df60 refactor: use Box to store large config objects and add memory usage tests
- Refactored config-related structs to use Box for storing large objects (e.g., IRuntime, IProfiles, PrfItem) to reduce stack memory usage and improve performance.
- Updated related methods and assignments to handle Boxed types correctly.
- Added and improved unit tests to compare memory usage between Boxed and non-Boxed config objects, demonstrating the memory efficiency of Box.
- Test output now shows the size difference between stack-allocated and heap-allocated (Box) config objects.
2025-06-06 14:54:48 +08:00
Tunglies 564fe15df2 refactor(tests): update format_bytes_speed test cases for consistent output 2025-06-06 14:13:48 +08:00
Tunglies 30015dfd67 refactor(state/proxy): use Box<Value> for providers_proxies to reduce struct size
- Changed providers_proxies from Value to Box<Value> in CmdProxyState
- Reduces struct size from 32 bytes to 8 bytes, optimizing memory usage
2025-06-06 12:46:58 +08:00
Tunglies 698c47da69 Refactor workflow triggers in alpha.yml to clarify usage of git tags and remove commented-out sections 2025-06-06 11:58:58 +08:00
Tunglies 46b7a520cd Add I18N issue template for translation and internationalization requests
Fix I18N issue template description and update labels and type
2025-06-06 11:58:58 +08:00
Tunglies cadd6c3497 Add Node.js and pnpm installation steps to cross-check workflow 2025-06-06 11:58:58 +08:00
wonfen c4682ab6e9 revert translation to 8aa7b34197 2025-06-06 11:27:15 +08:00
Tunglies 59594855b8 chore: add cross-platform cargo check workflow and remove debug features 2025-06-06 11:27:15 +08:00
Tunglies 44b7af0c6e chore: publish tag alpha support
chore: update alpha and release workflows to include version consistency checks
2025-06-06 11:27:13 +08:00
Tunglies 80f550d67e refactor(proxy): enhance proxy state management and refresh logic 2025-06-05 20:10:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 44f085604a chore(config): migrate config renovate.json (#3659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 1144f8017a chore(deps): update cargo dependencies (#3653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d8fa17a3e7 chore(deps): update npm dependencies (#3654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
Tunglies bf868e0ae2 fix: update serde_yaml to deprecated version and adjust range strategy in renovate configuration 2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> e1dac63f69 chore(deps): update rust crate zip to v4 (#3652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2657234761 chore(deps): update npm dependencies (#3651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 19:34:27 +08:00
Tunglies 4a345fdd41 feat: add initial renovate configuration for dependency management
chore: update renovate configuration for improved dependency management

Revert "chore: update renovate configuration for improved dependency management"

This reverts commit dcfd5f95a5.

feat: add support for grouping cargo and npm dependencies into single PRs
2025-06-05 19:34:26 +08:00
Tunglies fee6a586d7 feat: implement CmdProxyState for managing proxy refresh state and add state management module
refactor: remove unnecessary macOS conditional compilation directive
2025-06-05 19:34:24 +08:00
Tunglies 7c738483b7 feat: add desktop-windows capabilities configuration and permissions
feat: add identifier, description, and windows properties to desktop-windows capabilities

fix: correct desktop-windows capability name in configuration
2025-06-05 19:34:23 +08:00
Tunglies c3d8ed28a2 fix: update fallback language to English in i18n configuration 2025-06-05 19:34:22 +08:00
Tunglies f47b4b961b fix: update alpha and release workflows to trigger builds via git tags only
feat: update release workflow to trigger builds via git tags and add publish version script
2025-06-05 19:34:20 +08:00
Tunglies 7763abf6c2 Revert TCP concurrency
Revert "Added controller translation for TCP concurrency"

This reverts commit c9589fc75d.

Revert "add:tcp concurrency"

This reverts commit 6440f856a2.
2025-06-02 21:44:43 +08:00
Tunglies 9d9d078346 feat: enhance workflow configurations for alpha and autobuild processes 2025-06-02 18:51:58 +08:00
202 changed files with 15414 additions and 10346 deletions
+1
View File
@@ -0,0 +1 @@
github: clash-verge-rev
+12 -4
View File
@@ -9,13 +9,16 @@ body:
attributes:
value: |
## 在提交问题之前,请确认以下事项:
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. 请 **务必** 尝试 [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
@@ -34,6 +37,7 @@ body:
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)
render: text
validations:
required: true
- type: textarea
@@ -42,13 +46,13 @@ body:
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
validations:
required: true
- type: dropdown
- type: checkboxes
attributes:
label: 操作系统 / OS
options:
- Windows
- Linux
- MacOS
- label: Windows
- label: Linux
- label: MacOS
validations:
required: true
- type: input
@@ -61,5 +65,9 @@ body:
attributes:
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启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 debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
placeholder: |
日志目录一般位于 Clash Verge Rev 安装目录的 "logs/" 子目录中,请将日志内容粘贴到此处。
Log directory is usually located in the "logs/" subdirectory of the Clash Verge Rev installation directory, please paste the log content here.
render: log
validations:
required: true
+58
View File
@@ -0,0 +1,58 @@
name: I18N / 多语言相关
title: "[I18N] "
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
labels: ["I18n"]
type: "Task"
body:
- type: markdown
attributes:
value: |
## I18N 相关问题/建议
请用此模板提交翻译错误、缺失、建议或新增语言请求。
Please use this template for translation errors, missing translations, suggestions, or new language requests.
- type: textarea
id: description
attributes:
label: 问题描述 / Description
description: 详细描述你的 I18N 问题或建议 / Please describe your I18N issue or suggestion in detail
validations:
required: true
- type: input
id: language
attributes:
label: 相关语言 / Language
description: 例如 zh, en, jp, ru, ... / e.g. zh, en, jp, ru, ...
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: 建议或修正内容 / Suggestion or Correction
description: 如果是翻译修正或建议,请填写建议的内容 / If this is a translation correction or suggestion, please provide the suggested content
validations:
required: false
- type: checkboxes
id: i18n-type
attributes:
label: 问题类型 / Issue Type
description: 请选择适用类型(可多选) / Please select the applicable type(s)
options:
- label: 翻译错误 / Translation error
- label: 翻译缺失 / Missing translation
- label: 建议优化 / Suggestion
- label: 新增语言 / New language
validations:
required: true
- type: input
id: verge-version
attributes:
label: 软件版本 / Verge Version
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
validations:
required: true
+178 -141
View File
@@ -1,123 +1,176 @@
name: Alpha Build
on:
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
# 所以不再使用 workflow_dispatch 触发
# 应当通过 git tag 来触发构建
# TODO 手动控制版本号
workflow_dispatch:
# schedule:
# UTC+8 0,6,12,18
# - cron: "0 16,22,4,10 * * *"
# inputs:
# tag_name:
# description: "Alpha tag name (e.g. v1.2.3-alpha.1)"
# required: true
# type: string
# push:
# # 应当限制在 dev 分支上触发发布。
# branches:
# - dev
# # 应当限制 v*.*.*-alpha* 的 tag 来触发发布。
# tags:
# - "v*.*.*-alpha*"
permissions: write-all
env:
TAG_NAME: alpha
TAG_CHANNEL: Alpha
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:
check_commit:
check_alpha_tag:
name: Check Alpha Tag package.json Version Consistency
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 or src changed
id: check
- name: Check tag and package.json version
id: check_tag
run: |
# For manual workflow_dispatch, always run
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
TAG_REF="${GITHUB_REF##*/}"
echo "Current tag: $TAG_REF"
if [[ ! "$TAG_REF" =~ -alpha ]]; then
echo "Current tag is not an alpha tag."
exit 1
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
exit 0
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$PKG_VERSION" != *alpha* ]]; then
echo "package.json version is not an alpha version."
exit 1
fi
# Check if src or src-tauri directories changed
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
echo "Current src hash: $CURRENT_SRC_HASH"
echo "Previous src hash: $PREVIOUS_SRC_HASH"
echo "Current tauri hash: $CURRENT_TAURI_HASH"
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
echo "Source directories changed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "Version and source directories unchanged"
echo "should_run=false" >> $GITHUB_OUTPUT
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
exit 1
fi
echo "Alpha tag and package.json version are consistent."
delete_old_assets:
needs: check_commit
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
name: Delete Old Alpha Release Assets and Tags
needs: check_alpha_tag
runs-on: ubuntu-latest
steps:
- name: Delete Old Alpha Release Assets
- name: Delete Old Alpha Tags Except Latest
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const releaseTag = 'alpha';
const tagPattern = /-alpha.*/; // 匹配带有 -alpha 的 tag
const owner = context.repo.owner;
const repo = context.repo.repo;
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
// 获取所有 tag
const { data: tags } = await github.rest.repos.listTags({
owner,
repo,
per_page: 100 // 调整 per_page 以获取更多 tag
});
console.log(`Found release with ID: ${release.id}`);
// 过滤出包含 -alpha 的 tag
const alphaTags = (await Promise.all(
tags
.filter(tag => tagPattern.test(tag.name))
.map(async tag => {
// 获取每个 tag 的 commit 信息以获得日期
const { data: commit } = await github.rest.repos.getCommit({
owner,
repo,
ref: tag.commit.sha
});
return {
...tag,
commitDate: commit.committer && commit.committer.date ? commit.committer.date : commit.commit.author.date
};
})
)).sort((a, b) => {
// 按 commit 日期降序排序(最新的在前面)
return new Date(b.commitDate) - new Date(a.commitDate);
});
// Delete each asset
if (release.assets && release.assets.length > 0) {
console.log(`Deleting ${release.assets.length} assets`);
console.log(`Found ${alphaTags.length} alpha tags`);
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
if (alphaTags.length === 0) {
console.log('No alpha tags found');
return;
}
// 保留最新的 tag
const latestTag = alphaTags[0];
console.log(`Keeping latest alpha tag: ${latestTag.name}`);
// 处理其他旧的 alpha tag
for (const tag of alphaTags.slice(1)) {
console.log(`Processing tag: ${tag.name}`);
// 获取与 tag 关联的 release
try {
const { data: release } = await github.rest.repos.getReleaseByTag({
owner,
repo,
tag: tag.name
});
}
console.log('All assets deleted successfully');
} else {
console.log('No assets found to delete');
// 删除 release 下的所有资产
if (release.assets && release.assets.length > 0) {
console.log(`Deleting ${release.assets.length} assets for release ${tag.name}`);
for (const asset of release.assets) {
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
await github.rest.repos.deleteReleaseAsset({
owner,
repo,
asset_id: asset.id
});
}
}
// 删除 release
console.log(`Deleting release for tag: ${tag.name}`);
await github.rest.repos.deleteRelease({
owner,
repo,
release_id: release.id
});
// 删除 tag
console.log(`Deleting tag: ${tag.name}`);
await github.rest.git.deleteRef({
owner,
repo,
ref: `tags/${tag.name}`
});
} catch (error) {
if (error.status === 404) {
console.log(`No release found for tag ${tag.name}, deleting tag directly`);
await github.rest.git.deleteRef({
owner,
repo,
ref: `tags/${tag.name}`
});
} else {
console.error(`Error processing tag ${tag.name}:`, error);
throw error;
}
}
}
console.log('Old alpha tags and releases deleted successfully');
} catch (error) {
if (error.status === 404) {
console.log('Release not found, nothing to delete');
} else {
console.error('Error:', error);
throw error;
}
console.error('Error:', error);
throw error;
}
update_tag:
@@ -128,22 +181,18 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch Alpha update logs
id: fetch_alpha_logs
- name: Fetch UPDATE logs
id: fetch_update_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
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No alpha sections found in UPDATELOG.md"
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
@@ -156,17 +205,15 @@ jobs:
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."
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found alpha logs"
echo "Using found update logs"
fi
# 生成 release.txt 文件
cat > release.txt << EOF
$ALPHA_LOGS
$UPDATE_LOGS
## 我应该下载哪个版本?
@@ -189,7 +236,6 @@ jobs:
- arm64架构: arm64_fixed_webview2-setup.exe
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
@@ -201,14 +247,15 @@ jobs:
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
alpha:
alpha-x86-windows-macos-linux:
name: Alpha x86 Windows, MacOS and Linux
needs: update_tag
strategy:
fail-fast: false
@@ -224,7 +271,6 @@ jobs:
target: x86_64-apple-darwin
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
@@ -240,8 +286,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -262,13 +307,10 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release Alpha Version
run: pnpm release-alpha-version
- name: Patch Alpha Hash
run: pnpm fix-alpha-version
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
@@ -284,15 +326,16 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: alpha
releaseName: "Clash Verge Rev Alpha"
tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
alpha-for-linux-arm:
alpha-arm-linux:
name: Alpha ARM Linux
needs: update_tag
strategy:
fail-fast: false
@@ -319,7 +362,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -334,16 +377,13 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release Alpha Version
run: pnpm release-alpha-version
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
- name: Patch Alpha Hash
run: pnpm fix-alpha-version
- name: "Setup for linux"
run: |-
- name: Setup for linux
run: |
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
@@ -377,14 +417,14 @@ jobs:
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: "Install aarch64 tools"
- 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"
- name: Install armv7 tools
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
@@ -417,15 +457,16 @@ jobs:
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
alpha-for-fixed-webview2:
alpha-x86-arm-windows_webview2:
name: Alpha x86 and ARM Windows with WebView2
needs: update_tag
strategy:
fail-fast: false
@@ -449,8 +490,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -465,13 +505,10 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release Alpha Version
run: pnpm release-alpha-version
- name: Patch Alpha Hash
run: pnpm fix-alpha-version
# - name: Release ${{ env.TAG_CHANNEL }} Version
# run: pnpm release-version ${{ env.TAG_NAME }}
- name: Download WebView2 Runtime
run: |
@@ -515,13 +552,13 @@ jobs:
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+58 -31
View File
@@ -8,6 +8,7 @@ on:
permissions: write-all
env:
TAG_NAME: autobuild
TAG_CHANNEL: AutoBuild
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
@@ -143,12 +144,34 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev AutoBuild"
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
clean_old_assets:
name: Clean Old Release Assets
runs-on: ubuntu-latest
needs: update_tag
if: ${{ needs.update_tag.result == 'success' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove old assets from release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ env.TAG_NAME }}
run: |
VERSION=$(cat package.json | jq -r '.version')
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
for asset in $assets; do
if [[ "$asset" != *"$VERSION"* ]]; then
echo "Deleting old asset: $asset"
gh release delete-asset "$TAG_NAME" "$asset" -y
fi
done
autobuild-x86-windows-macos-linux:
name: Autobuild x86 Windows, MacOS and Linux
needs: update_tag
@@ -182,7 +205,7 @@ jobs:
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -190,23 +213,24 @@ jobs:
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: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release Autobuild Version
run: pnpm release-version autobuild
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
@@ -223,7 +247,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev AutoBuild"
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
@@ -259,24 +283,26 @@ jobs:
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release Autobuild Version
run: pnpm release-version autobuild
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
- name: Setup for linux
run: |
@@ -354,7 +380,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev AutoBuild"
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: |
@@ -387,25 +413,26 @@ jobs:
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
cache: "pnpm"
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Release Autobuild Version
run: pnpm release-version autobuild
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
- name: Download WebView2 Runtime
run: |
@@ -450,12 +477,12 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev AutoBuild"
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --autobuild
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+63
View File
@@ -0,0 +1,63 @@
name: Clippy Lint
on:
pull_request:
jobs:
clippy:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
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
save-if: false
- 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: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Build Web Assets
run: pnpm run web:build
env:
NODE_OPTIONS: "--max_old_space_size=4096"
- name: Run Clippy
run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings
+64
View File
@@ -0,0 +1,64 @@
name: Cross Platform Cargo Check
on:
workflow_dispatch:
# pull_request:
# push:
# branches: [main, dev]
permissions:
contents: read
jobs:
cargo-check:
# Treat all Rust compiler warnings as errors
env:
RUSTFLAGS: "-D warnings"
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- 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 run prebuild ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
- name: Cargo Check (deny warnings)
working-directory: src-tauri
run: |
cargo check --target ${{ matrix.target }} --workspace --all-features
+35 -6
View File
@@ -2,12 +2,28 @@ name: Development Test
on:
workflow_dispatch:
inputs:
run_windows:
description: "运行 Windows"
required: false
type: boolean
default: true
run_macos_aarch64:
description: "运行 macOS aarch64"
required: false
type: boolean
default: true
run_macos_x86_64:
description: "运行 macOS x86_64"
required: false
type: boolean
default: true
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' }}
@@ -20,47 +36,60 @@ jobs:
- os: windows-latest
target: x86_64-pc-windows-msvc
bundle: nsis
id: windows
input: run_windows
- os: macos-latest
target: aarch64-apple-darwin
bundle: dmg
id: macos-aarch64
input: run_macos_aarch64
- os: macos-latest
target: x86_64-apple-darwin
bundle: dmg
id: macos-x86_64
input: run_macos_x86_64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
if: github.event.inputs[matrix.input] == 'true'
uses: actions/checkout@v4
- name: Install Rust Stable
if: github.event.inputs[matrix.input] == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
if: github.event.inputs[matrix.input] == 'true'
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
if: github.event.inputs[matrix.input] == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install Node
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v4
name: Install pnpm
if: github.event.inputs[matrix.input] == 'true'
with:
run_install: false
- name: Pnpm install and check
if: github.event.inputs[matrix.input] == 'true'
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Tauri build
if: github.event.inputs[matrix.input] == 'true'
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
@@ -78,7 +107,7 @@ jobs:
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
- name: Upload Artifacts
if: matrix.os == 'macos-latest'
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
@@ -86,7 +115,7 @@ jobs:
if-no-files-found: error
- name: Upload Artifacts
if: matrix.os == 'windows-latest'
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
+50
View File
@@ -0,0 +1,50 @@
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
name: Check Formatting
on:
pull_request:
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: install Rust stable and rustfmt
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: run cargo fmt
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm format:check
# taplo:
# name: taplo (.toml files)
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: install Rust stable
# uses: dtolnay/rust-toolchain@stable
# - name: install taplo-cli
# uses: taiki-e/install-action@v2
# with:
# tool: taplo-cli
# - run: taplo fmt --check --diff
+41 -9
View File
@@ -1,7 +1,16 @@
name: Release Build
on:
workflow_dispatch:
# ! 为了避免重复发布版本,应当通过独特 git tag 触发。
# ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch:
push:
# 应当限制在 main 分支上触发发布。
branches:
- main
# 应当限制 v*.*.* 的 tag 触发发布。
tags:
- "v*.*.*"
permissions: write-all
env:
CARGO_INCREMENTAL: 0
@@ -12,7 +21,28 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
check_tag_version:
name: Check Release Tag and package.json Version Consistency
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check tag and package.json version
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Current tag: $TAG_REF"
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
exit 1
fi
echo "Tag and package.json version are consistent."
release:
name: Release Build
needs: check_tag_version
strategy:
fail-fast: false
matrix:
@@ -43,8 +73,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -65,7 +94,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
@@ -88,6 +117,7 @@ jobs:
args: --target ${{ matrix.target }}
release-for-linux-arm:
name: Release Build for Linux ARM
strategy:
fail-fast: false
matrix:
@@ -113,7 +143,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -128,7 +158,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: "Setup for linux"
run: |-
@@ -209,6 +239,7 @@ jobs:
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
release-for-fixed-webview2:
name: Release Build for Fixed WebView2
strategy:
fail-fast: false
matrix:
@@ -231,8 +262,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
save-if: false
- name: Install Node
uses: actions/setup-node@v4
@@ -247,7 +277,7 @@ jobs:
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
pnpm run prebuild ${{ matrix.target }}
- name: Download WebView2 Runtime
run: |
@@ -303,6 +333,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update:
name: Release Update
runs-on: ubuntu-latest
needs: [release, release-for-linux-arm]
steps:
@@ -353,6 +384,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget:
name: Submit to Winget
runs-on: ubuntu-latest
needs: [release-update]
steps:
+17 -9
View File
@@ -2,17 +2,25 @@
#pnpm pretty-quick --staged
# 运行 clippy fmt
cd src-tauri
cargo fmt
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
if git diff --cached --name-only | grep -q '^src/'; then
pnpm format:check
if [ $? -ne 0 ]; then
echo "Code format check failed in src/. Please fix formatting issues."
exit 1
fi
fi
cd ..
git add .
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cd src-tauri
cargo fmt
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
fi
cd ..
fi
#git add .
# 允许提交
exit 0
+29 -8
View File
@@ -1,13 +1,34 @@
#!/bin/bash
# 运行 clippy
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
# $1: remote name (e.g., origin)
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
# 如果 clippy 失败,阻止 push
# if [ $? -ne 0 ]; then
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
# exit 1
# fi
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cargo clippy --manifest-path ./src-tauri/Cargo.toml
if [ $? -ne 0 ]; then
echo "Clippy found issues in src-tauri. Please fix them before pushing."
exit 1
fi
fi
# Only run format check if the remote exists and is the main repo
remote_name="$1"
if git remote get-url "$remote_name" >/dev/null 2>&1; then
remote_url=$(git remote get-url "$remote_name")
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
echo "[pre-push] Running pnpm format:check..."
pnpm format:check
if [ $? -ne 0 ]; then
echo "❌ Code format check failed. Please fix formatting before pushing."
exit 1
fi
else
echo "[pre-push] Not pushing to target repo. Skipping format check."
fi
else
echo "[pre-push] Remote $remote_name does not exist. Skipping format check."
fi
# 允许 push
exit 0
+8
View File
@@ -0,0 +1,8 @@
# README.md
# UPDATELOG.md
# CONTRIBUTING.md
pnpm-lock.yaml
src-tauri/target/
src-tauri/gen/
+16
View File
@@ -0,0 +1,16 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto"
}
+19 -11
View File
@@ -8,7 +8,7 @@ Before you start contributing to the project, you need to set up your developmen
### Prerequisites
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/start/prerequisites/) to install them on your system.
### Setup for Windows Users
@@ -33,12 +33,15 @@ npm install pnpm -g
```
### Install Dependencies
Install node packages
```shell
pnpm install
```
Install apt packages ONLY for Ubuntu
```shell
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
```
@@ -49,9 +52,9 @@ You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run check
pnpm run prebuild
# Use '--force' to force update to the latest version
# pnpm run check --force
# pnpm run prebuild --force
```
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
@@ -105,20 +108,25 @@ pnpm portable
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
1. Code style formatting
1. Code quailty checks
```bash
# For rust backend
$ clash-verge-rev: pnpm clippy
# For frontend (not yet).
```
2. Code style formatting
```bash
# For rust backend
$ clash-verge-rev: cd src-tauri
$ clash-verge-rev/src-tauri: cargo fmt
# For frontend
$ clash-verge-rev: pnpm format:check
$ clash-verge-rev: pnpm format
```
2. Code quailty checks
```bash
$ clash-verge-rev: pnpm clippy
```
Once you have made your changes:
1. Fork the repository.
+12 -3
View File
@@ -18,10 +18,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
## Install
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
Go to the [release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
#### 我应当怎样选择发行版
| 版本 | 特征 | 链接 |
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
---
@@ -41,11 +49,12 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
🧩 YXVM 独立服务器优势:
- 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源,非VPS可比,性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
+223 -129
View File
@@ -1,143 +1,227 @@
## v2.3.2
### 🐞 修复问题
- 修复系统代理端口不同步问题
- 修复自定义 `css` 背景图无法生效问题
- 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
- 修复导入订阅时非 http 协议链接被错误尝试导入
- 修复切换节点后页面长时间 loading 及缓存过期导致的数据不同步问题
- 修复将快捷键名称更名为 `Clash Verge`之后无法删除图标和无法删除注册表
- 修复`DNS`覆写 `fallback` `proxy server` `nameserver` `direct Nameserver` 字段支持留空
- 修复`DNS`覆写 `nameserver-policy` 字段无法正确识别 `geo` 库
- 修复搜索框输入特殊字符崩溃
- 修复 Windows 下 Start UP 名称与 exe 名称不统一
- 修复显示 Mihomo 内核日志等级应该大于设置等级
### ✨ 新增功能
- `sidecar` 模式下清理多余的内核进程,防止运行出现异常
- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测)
- 快捷键事件通过系统通知
- 添加外部 `cors` 控制面板
### 🚀 优化改进
- 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死
- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死
- 改进主页订阅卡流量已使用比例计算精度
- 优化后端缓存刷新机制,支持毫秒级 TTL(默认 3000ms),减少重复请求并提升性能,切换节点时强制刷新后端数据,前端 UI 实时更新,操作更流畅
- 解耦前端数据拉取与后端缓存刷新,提升节点切换速度和一致性
### 🗑️ 移除内容
- 移除了 macOS tray 图标显示网络速率
### 🌐 国际化更新
- 修复部分翻译缺失和不一致问题
## v2.3.1
### 🐞 修复问题
- 增加配置文件校验,修复从古老版本升级上来的"No such file or directory (os error 2)"错误
- 修复扩展脚本转义错误
- 修复 macOS Intel X86 架构构建错误导致无法运行
- 修复 Linux 下界面边框白边问题
- 修复 托盘 无响应问题
- 修复 托盘 无法从轻量模式退出并恢复窗口
- 修复 快速切换订阅可能导致的卡死问题
### ✨ 新增功能
- 新增 window-state 窗口状态管理和恢复
### 🚀 优化改进
- 优化 托盘 统一响应
- 优化 静默启动+自启动轻量模式 运行方式
- 降低前端潜在内存泄漏风险,提升运行时性能
- 优化 React 状态、副作用、数据获取、清理等流程。
## v2.3.0
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
**发行代号:御**
代号释义: 「御」,象征掌控与守护,寓意本次版本对系统稳定性、安全性与用户体验的全面驾驭与提升。
#### ⚠️ 已知问题
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。
- MacOS 下卸载服务后需手动重启软件才能与内核通信。
尽管 `external-controller` 密钥现已自动补全默认值且不允许为空,**仍建议手动修改密钥以提高安全性**。
### 2.3.0 相对于 2.2.3
#### 🐞 修复问题
- 首页"代理模式"快速切换导致的卡死问题
- 解锁测试报错信息
- Macos 快捷键关闭窗口无法启用自动轻量模式
- 静默启动异常窗口创建和关闭流程
- Windows 错误的全局快捷键 `Ctrl+Q` 注册
- Vless URL 解码时网络类型错误
- 切换自定义代理地址导致系统代理状态异常
- Macos TUN 默认无效网卡名称
- 托盘更改订阅后 UI 不同步的问题
- 修复提权漏洞,改用带认证的 IPC 通信
- 编辑器中连字符问题
- 安装服务模式后无法立即开启 TUN 模式
- 同步更新多语言翻译
- 修复 .window-state.json 无法删除的问题
- 无法修改配置更新 HTTP 请求超时
- 修复 getDelayFix 钩子问题
- 使用外部扩展脚本覆写代理组时首页无法显示代理组
- 导出诊断 Verge 版本与设置页面不同步
### ⚠️ 已知问题
#### ✨ 新增功能
- Mihomo(Meta)内核升级至 1.19.10
- 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好
- 关闭系统代理时关闭已建立的网络连接
- 托盘显示当前轻量模式状态
- Webdav 请求加入 UA
- Webdav 支持目录重定向
- Webdav 备份目录检查和文件上传重试机制
- 系统代理守卫可检查意外设置变更并恢复
- 定时自动订阅更新也能自动回退使用代理
- 订阅请求超时机制,防止订阅更新的时候卡死
- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示
- 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死
- 更新依赖,替换弃用元素
- 首页当前节点增加排序功能
- DNS 覆写下增加 Hosts 设置功能
- 修复服务模式安装后无法立即开启 TUN 模式的问题
- 支持手动卸载服务模式,回退到 Sidecar 模式
- 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持
- 卸载服务的按钮
- 添加了Zashboard的一键跳转URL
- 使用操作系统默认的窗口管理器
- 切换、升级、重启内核的状态管理
- 更精细化控制自动日志清理,新增1天选项
- Winodws 快捷键名称改为 `Clash Verge`
- 配置加载阶段自动补全 external-controller secret 字段。
- 添加 TCP Concurrency (TCP并发)
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
- macOS:
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
- 彩色 Tray 图标颜色偏淡;
#### 🚀 优化改进
- 系统代理 Bypass 设置
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
- 切换到规则页面时自动刷新规则数据
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
- 编辑非激活订阅的时候不在触发当前订阅配置重载
- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程
- 优化系统代理设置更新逻辑
- 重构前端通知系统分离通知线程防止前端卡死
- 优化网络请求和错误处理
- 重构通知系统
- 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题
- MacOS 下默认关闭托盘速率显示
- 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验
- 优化了其他语言的翻译问题
- Mihomo 内核默认日志等级为 warn
- Clash Verge Rev 应用默认日志等级为 warn
- 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题
- 对轮询检测机制进行了优化,引入洗牌算法来增强随机性
- 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息
- 优化窗口状态初始化逻辑和添加缺失的权限设置
- 异步化配置:优化端口查找和配置保存逻辑
- 重构事件通知机制到独立线程,避免前端卡死
- 优化端口设置,每个端口可随机设置端口号
- 优化了保存机制,使用平滑函数,防止客户端卡死
- 优化端口设置退出和保存机制
- 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖
- 修改 端口检测范围 (1111-65536)
- 配置文件缺失 secret 字段时自动填充默认值 set-your-secret
- 优化异步处理,防止部分组件 UI 阻塞
- 关闭 DNS 启用
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204
- 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
#### 🗑️ 移除内容
- 窗口状态管理器
- Webdav 跨平台备份恢复限制
### 🐞 修复问题
- 修复首页“代理模式”快速切换导致的卡死问题
- 修复 MacOS 快捷键关闭窗口无法启用自动轻量模式
- 修复静默启动异常窗口的创建与关闭流程
- 修复 Windows 下错误注册的全局快捷键 `Ctrl+Q`
- 修复解锁测试报错信息与 VLESS URL 解码时的网络类型错误
- 修复切换自定义代理地址后系统代理状态异常
- 修复 macOS TUN 默认无效网卡名称
- 修复更改订阅后托盘 UI 不同步的问题
- 修复服务模式安装后无法立即开启 TUN 模式
- 修复无法删除 `.window-state.json`
- 修复无法修改配置更新 HTTP 请求超时问题
- 修复 `getDelayFix` 钩子异常
- 修复外部扩展脚本覆写代理组时首页无法显示代理组
- 修复 Verge 导出诊断版本与设置页面不同步
- 修复切换语言时设置页面可能加载失败
- 修复编辑器中连字符处理问题
- 修复提权漏洞,改用带认证的 IPC 通信机制
- 修复静默启动无法使用自动轻量模式
- 修复 JS 脚本转义特殊字符报错
- 修复 macOS 静默启动时异常启动 Dock 栏图标
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.10**
- 支持设置代理地址为非 `127.0.0.1`,提升 WSL 兼容性
- 系统代理守卫:可检测意外变更并自动恢复
- 托盘新增当前轻量模式状态显示
- 关闭系统代理时同时断开已建立的连接
- 新增 WebDAV 功能:
- 加入 UA 请求头
- 支持目录重定向
- 备份目录检查与上传重试机制
- 自动订阅更新机制:
- 加入请求超时机制防止卡死
- 支持在代理状态下自动重试订阅更新
- 支持订阅卡片点击切换下次自动更新时间,并显示更新结果提示
- DNS 设置新增 Hosts 配置功能
- 首页代理节点支持排序
- 支持服务模式手动卸载,回退至 Sidecar 模式
- 核心状态管理支持切换、升级、重启
- 配置加载阶段自动补全 `external-controller secret`
- 新增日志自动清理周期选项(含1天)
- 新增 Zashboard 一键跳转入口
- 使用系统默认窗口管理器
### 🚀 优化改进
- **系统相关:**
- 系统代理 Bypass 设置优化
- 优化代理设置更新逻辑与守卫机制
- Windows 启动方式调整为 Startup 文件夹,解决管理员模式下自启问题
- **性能与稳定性:**
- 全面异步化处理配置加载、UI 启动、事件通知等关键流程,解决卡顿问题
- 优化 MihomoManager 实现与窗口创建流程
- 改进内核日志等级为 `warn`,减少噪音输出
- 重构主进程与通知系统,提升响应性与分离度
- 优化网络请求与错误处理机制
- 添加网络管理器防止资源竞争引发 UI 卡死
- 优化配置文件加载内存使用
- 优化缓存 Mihomo proxy 和 providers 信息内存使用
- **前端与界面体验:**
- 切换规则页自动刷新数据
- 非激活订阅编辑时不再触发配置重载
- 优化托盘速率显示,macOS 下默认关闭
- Windows 快捷键名称更名为 `Clash Verge`
- 更新失败可回退至使用代理重试
- 支持异步端口查找与保存,端口支持随机生成
- 修改端口检测范围至 `1111-65536`
- 优化保存机制,使用平滑函数防止卡顿
- **配置增强与安全性:**
- 配置缺失 `secret` 字段时自动补全为 `set-your-secret`
- 强制为 Mihomo 配置补全 `external-controller-cors` 字段(默认不允许跨域,限制本地访问)计划后续支持自定义 cors
- 优化窗口权限设置与状态初始化逻辑
- 网络延迟测试替换为 HTTPS 协议:`https://cp.cloudflare.com/generate_204`
- 优化 IP 信息获取流程,添加去重机制与轮询检测算法
- 同步修复翻译错误与不一致项,优化整体语言体验
- 加强语言切换后的页面稳定性,避免加载异常
### 🗑️ 移除内容
- 窗口状态管理器(上游存在缺陷)
- WebDAV 跨平台备份恢复限制
---
## v2.2.3
#### 已知问题
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
### 2.2.3 相对于 2.2.2
#### 修复了:
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开机自启”和“DNS覆写”开关跳动问题
- 自定义托盘图标未能应用更改
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- MacOS 托盘速率显示不全
- Linux 在系统服务模式下无法拉起 Mihomo 内核
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
- 相同节点名称可能导致的页面渲染出错
- URL Schemes被截断的问题
- 首页流量统计卡更好的时间戳范围
- 静默启动无法触发自动轻量化计时器
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开机自启”和“DNS覆写”开关跳动问题
- 自定义托盘图标未能应用更改
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- MacOS 托盘速率显示不全
- Linux 在系统服务模式下无法拉起 Mihomo 内核
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
- 相同节点名称可能导致的页面渲染出错
- URL Schemes被截断的问题
- 首页流量统计卡更好的时间戳范围
- 静默启动无法触发自动轻量化计时器
#### 新增了:
- Mihomo(Meta)内核升级至 1.19.4
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
- 代理组显示节点数量
- 统一运行模式检测,支持管理员模式下开启TUN模式
- 托盘切换代理模式会根据设置自动断开之前连接
- 如订阅获取失败回退使用Clash内核代理再次尝试
- Mihomo(Meta)内核升级至 1.19.4
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
- 代理组显示节点数量
- 统一运行模式检测,支持管理员模式下开启TUN模式
- 托盘切换代理模式会根据设置自动断开之前连接
- 如订阅获取失败回退使用Clash内核代理再次尝试
#### 移除了:
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
#### 优化了:
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
- 优化首页流量图表代码,调整图表文字边距
- MacOS 托盘速率更好的显示样式和更新逻辑
- 首页仅在有流量图表时显示流量图表区域
- 更新DNS默认覆写配置
- 移除测试目录,简化资源初始化逻辑
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
- 优化首页流量图表代码,调整图表文字边距
- MacOS 托盘速率更好的显示样式和更新逻辑
- 首页仅在有流量图表时显示流量图表区域
- 更新DNS默认覆写配置
- 移除测试目录,简化资源初始化逻辑
## v2.2.2
@@ -148,23 +232,29 @@
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
#### 已知问题
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
### 2.2.2 相对于 2.2.1(已下架不再提供)
#### 修复了:
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后隐藏Dock图标
- 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后隐藏Dock图标
- 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
#### 新增了:
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制
- 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制
- 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
### 2.2.1 相对于 2.2.0(已下架不再提供)
#### 修复了:
1. **首页**
- 修复 Direct 模式首页无法渲染
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
@@ -181,6 +271,7 @@
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
#### 新增了:
1. **首页**
- 首页文本过长自动截断
2. **轻量模式**
@@ -197,6 +288,7 @@
## 2.2.0(已下架不再提供)
#### 新增功能
1. **首页**
- 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。
@@ -228,6 +320,7 @@
- 新增代理命令。
#### 修复
1. **系统**
- 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。
@@ -241,6 +334,7 @@
- 修复构建失败问题。
#### 优化
1. **性能**
- 重构后端,巨幅性能优化。
- 优化首页组件性能。
+4
View File
@@ -0,0 +1,4 @@
files:
- source: /src/locales/en.json
translation: /src/locales
multilingual: 1
+49 -59
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.3.0",
"version": "2.3.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -11,105 +11,95 @@
"web:dev": "vite",
"web:build": "tsc --noEmit && vite build",
"web:serve": "vite preview",
"check": "node scripts/check.mjs",
"prebuild": "node scripts/prebuild.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",
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
"release-version": "node scripts/release-version.mjs",
"release-alpha-version": "node scripts/release-alpha_version.mjs",
"prepare": "husky",
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.0.2",
"@mui/lab": "7.0.0-beta.12",
"@mui/material": "^7.0.2",
"@mui/x-data-grid": "^8.2.0",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
"@tauri-apps/plugin-notification": "^2.2.2",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "2.2.1",
"@tauri-apps/plugin-updater": "2.7.1",
"@tauri-apps/plugin-window-state": "^2.2.2",
"@types/d3-shape": "^3.1.7",
"@mui/icons-material": "^7.2.0",
"@mui/lab": "7.0.0-beta.14",
"@mui/material": "^7.2.0",
"@mui/x-data-grid": "^8.8.0",
"@tauri-apps/api": "2.6.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.3.0",
"@tauri-apps/plugin-fs": "^2.4.0",
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
"@tauri-apps/plugin-notification": "^2.3.0",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.0",
"@tauri-apps/plugin-updater": "2.9.0",
"@tauri-apps/plugin-window-state": "^2.3.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.4",
"axios": "^1.8.3",
"chart.js": "^4.4.9",
"ahooks": "^3.9.0",
"axios": "^1.10.0",
"chart.js": "^4.5.0",
"cli-color": "^2.0.4",
"d3-shape": "^3.2.0",
"dayjs": "1.11.13",
"foxact": "^0.2.44",
"glob": "^11.0.1",
"i18next": "^25.0.2",
"js-base64": "^3.7.7",
"foxact": "^0.2.49",
"glob": "^11.0.3",
"i18next": "^25.3.2",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.3.1",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5",
"peggy": "^5.0.0",
"react": "19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.54.2",
"react-i18next": "15.5.1",
"react-hook-form": "^7.60.0",
"react-i18next": "15.6.0",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.0",
"react-virtuoso": "^4.12.7",
"react-router-dom": "7.6.3",
"react-virtuoso": "^4.13.0",
"sockette": "^2.0.6",
"swr": "^2.3.3",
"swr": "^2.3.4",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.3"
"zustand": "^5.0.6"
},
"devDependencies": {
"@actions/github": "^6.0.0",
"@tauri-apps/cli": "2.2.7",
"@types/js-cookie": "^3.0.6",
"@actions/github": "^6.0.1",
"@tauri-apps/cli": "2.6.2",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
"@vitejs/plugin-legacy": "^6.0.2",
"@vitejs/plugin-react": "4.4.1",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@vitejs/plugin-legacy": "^7.0.0",
"@vitejs/plugin-react": "4.6.0",
"adm-zip": "^0.5.16",
"commander": "^14.0.0",
"cross-env": "^7.0.3",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"meta-json-schema": "^1.19.3",
"meta-json-schema": "^1.19.11",
"node-fetch": "^3.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",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.1.0",
"sass": "^1.89.2",
"terser": "^5.43.1",
"typescript": "^5.8.3",
"vite": "^7.0.4",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0"
},
"prettier": {
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"endOfLine": "lf"
},
"type": "module",
"packageManager": "pnpm@9.13.2"
}
}
+802 -936
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"extends": ["config:recommended", ":disableDependencyDashboard"],
"baseBranches": ["dev"],
"enabledManagers": ["cargo", "npm"],
"labels": ["dependencies"],
"ignorePaths": [
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**",
"**/crate/**",
"shared/**"
],
"rangeStrategy": "bump",
"packageRules": [
{
"semanticCommitType": "chore",
"matchPackageNames": ["*"]
},
{
"description": "Disable node/pnpm version updates",
"matchPackageNames": ["node", "pnpm"],
"matchDepTypes": ["engines", "packageManager"],
"enabled": false
},
{
"description": "Group all cargo dependencies into a single PR",
"matchManagers": ["cargo"],
"groupName": "cargo dependencies"
},
{
"description": "Group all npm dependencies into a single PR",
"matchManagers": ["npm"],
"groupName": "npm dependencies"
}
],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["serde_yaml"]
}
+31 -27
View File
@@ -1,21 +1,21 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, '../src/locales');
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const SRC_DIRS = [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../src-tauri')
path.resolve(__dirname, "../src"),
path.resolve(__dirname, "../src-tauri"),
];
const exts = ['.js', '.ts', '.tsx', '.jsx', '.vue', '.rs'];
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
// 递归获取所有文件
function getAllFiles(dir, exts) {
let files = [];
fs.readdirSync(dir).forEach(file => {
fs.readdirSync(dir).forEach((file) => {
const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts));
@@ -28,21 +28,21 @@ function getAllFiles(dir, exts) {
// 读取所有源码内容为一个大字符串
function getAllSourceContent() {
const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts));
return files.map(f => fs.readFileSync(f, 'utf8')).join('\n');
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
}
// 白名单 key,不检查这些 key 是否被使用
const WHITELIST_KEYS = [
'theme.light',
'theme.dark',
'theme.system',
"Already Using Latest Core Version"
"theme.light",
"theme.dark",
"theme.system",
"Already Using Latest Core Version",
];
// 主流程
function processI18nFile(i18nPath, lang, allSource) {
const i18n = JSON.parse(fs.readFileSync(i18nPath, 'utf8'));
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
const keys = Object.keys(i18n);
const used = {};
@@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) {
let checked = 0;
const total = keys.length;
keys.forEach(key => {
keys.forEach((key) => {
if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key];
} else {
@@ -65,8 +65,10 @@ function processI18nFile(i18nPath, lang, allSource) {
checked++;
if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`);
if (checked === total) process.stdout.write('\n');
process.stdout.write(
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
);
if (checked === total) process.stdout.write("\n");
}
});
@@ -74,25 +76,27 @@ function processI18nFile(i18nPath, lang, allSource) {
console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件
const oldPath = i18nPath + '.old';
const oldPath = i18nPath + ".old";
fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8');
console.log(`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`);
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
console.log(
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
}
function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs.readdirSync(LOCALES_DIR).filter(f =>
/^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old')
);
const files = fs
.readdirSync(LOCALES_DIR)
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const allSource = getAllSourceContent();
files.forEach(file => {
const lang = path.basename(file, '.json');
files.forEach((file) => {
const lang = path.basename(file, ".json");
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
});
}
main();
main();
+66
View File
@@ -0,0 +1,66 @@
// scripts/publish-version.mjs
import { spawn } from "child_process";
import { existsSync } from "fs";
import path from "path";
const rootDir = process.cwd();
const scriptPath = path.join(rootDir, "scripts", "release-version.mjs");
if (!existsSync(scriptPath)) {
console.error("release-version.mjs not found!");
process.exit(1);
}
const versionArg = process.argv[2];
if (!versionArg) {
console.error("Usage: pnpm publish-version <version>");
process.exit(1);
}
// 1. 调用 release-version.mjs
const runRelease = () =>
new Promise((resolve, reject) => {
const child = spawn("node", [scriptPath, versionArg], { stdio: "inherit" });
child.on("exit", (code) => {
if (code === 0) resolve();
else reject(new Error("release-version failed"));
});
});
// 2. 判断是否需要打 tag
function isSemver(version) {
return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version);
}
async function run() {
await runRelease();
let tag = null;
if (versionArg === "alpha") {
// 读取 package.json 里的主版本
const pkg = await import(path.join(rootDir, "package.json"), {
assert: { type: "json" },
});
tag = `v${pkg.default.version}-alpha`;
} else if (isSemver(versionArg)) {
// 1.2.3 或 v1.2.3
tag = versionArg.startsWith("v") ? versionArg : `v${versionArg}`;
}
if (tag) {
// 打 tag 并推送
const { execSync } = await import("child_process");
try {
execSync(`git tag ${tag}`, { stdio: "inherit" });
execSync(`git push origin ${tag}`, { stdio: "inherit" });
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
} catch (e) {
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
process.exit(1);
}
} else {
console.log("[INFO]: No git tag created for this version.");
}
}
run();
+63 -23
View File
@@ -1,5 +1,3 @@
/**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
*
@@ -30,19 +28,35 @@
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
import { execSync } from "child_process";
/**
* 生成短时间戳(格式:YYMMDDHHMM)
* 获取当前 git 短 commit hash
* @returns {string}
*/
function generateShortTimestamp() {
function getGitShortCommit() {
try {
return execSync("git rev-parse --short HEAD").toString().trim();
} catch (e) {
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
return "nogit";
}
}
/**
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
* @param {boolean} withCommit 是否带 commit
* @returns {string}
*/
function generateShortTimestamp(withCommit = false) {
const now = new Date();
const year = String(now.getFullYear()).slice(-2);
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
return `${year}${month}${day}${hours}${minutes}`;
if (withCommit) {
const gitShort = getGitShortCommit();
return `${month}${day}.${gitShort}`;
}
return `${month}${day}`;
}
/**
@@ -51,7 +65,9 @@ function generateShortTimestamp() {
* @returns {boolean}
*/
function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(version);
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
version,
);
}
/**
@@ -69,8 +85,8 @@ function normalizeVersion(version) {
* @returns {string}
*/
function getBaseVersion(version) {
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, '');
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, '');
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
return base;
}
@@ -85,10 +101,21 @@ async function updatePackageVersion(newVersion) {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
console.log("[INFO]: Current package.json version is: ", packageJson.version);
packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8");
console.log(`[INFO]: package.json version updated to: ${packageJson.version}`);
console.log(
"[INFO]: Current package.json version is: ",
packageJson.version,
);
packageJson.version = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(
`[INFO]: package.json version updated to: ${packageJson.version}`,
);
} catch (error) {
console.error("Error updating package.json version:", error);
throw error;
@@ -105,12 +132,17 @@ async function updateCargoVersion(newVersion) {
try {
const data = await fs.readFile(cargoTomlPath, "utf8");
const lines = data.split("\n");
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) {
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`);
return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${baseVersion}"`,
);
}
return line;
});
@@ -133,12 +165,21 @@ async function updateTauriConfigVersion(newVersion) {
try {
const data = await fs.readFile(tauriConfigPath, "utf8");
const tauriConfig = JSON.parse(data);
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
console.log("[INFO]: Current tauri.conf.json version is: ", tauriConfig.version);
console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
tauriConfig.version = baseVersion;
await fs.writeFile(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8");
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
} catch (error) {
console.error("Error updating tauri.conf.json version:", error);
@@ -180,8 +221,8 @@ async function main(versionArg) {
const baseVersion = getBaseVersion(currentVersion);
if (versionArg.toLowerCase() === "autobuild") {
const timestamp = generateShortTimestamp();
newVersion = `${baseVersion}+autobuild.${timestamp}`;
// 格式: 2.3.0+autobuild.250613.cc39b27
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
} else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
}
@@ -210,4 +251,3 @@ program
.argument("<version>", "version tag or full version")
.action(main)
.parse(process.argv);
+1488 -1234
View File
File diff suppressed because it is too large Load Diff
+38 -35
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.3.0"
version = "2.3.2"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -13,19 +13,19 @@ build = "build.rs"
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.2.0", features = [] }
tauri-build = { version = "2.3.0", features = [] }
[dependencies]
warp = "0.3"
warp = "0.3.7"
anyhow = "1.0.98"
dirs = "6.0"
open = "5.3"
log = "0.4"
dunce = "1.0"
log4rs = "1"
open = "5.3.2"
log = "0.4.27"
dunce = "1.0.5"
log4rs = "1.3.0"
nanoid = "0.4"
chrono = "0.4.41"
sysinfo = "0.35.0"
sysinfo = "=0.35.2"
boa_engine = "0.20.0"
serde_json = "1.0.140"
serde_yaml = "0.9.34"
@@ -33,21 +33,20 @@ once_cell = "1.21.3"
lazy_static = "1.5.0"
port_scanner = "0.1.5"
delay_timer = "0.11.6"
parking_lot = "0.12.3"
parking_lot = "0.12.4"
percent-encoding = "2.3.1"
tokio = { version = "1.44.2", features = [
tokio = { version = "1.46.1", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.15", features = ["json", "rustls-tls", "cookies"] }
reqwest = { version = "0.12.22", features = ["json", "rustls-tls", "cookies"] }
regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6"
imageproc = "0.25.0"
tauri = { version = "2.5.1", features = [
tauri = { version = "2.6.2", features = [
"protocol-asset",
"devtools",
"tray-icon",
@@ -55,37 +54,37 @@ tauri = { version = "2.5.1", features = [
"image-png",
] }
network-interface = { version = "2.0.1", features = ["serde"] }
tauri-plugin-shell = "2.2.1"
tauri-plugin-dialog = "2.2.1"
tauri-plugin-fs = "2.2.1"
tauri-plugin-process = "2.2.1"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-deep-link = "2.2.1"
tauri-plugin-shell = "2.3.0"
tauri-plugin-dialog = "2.3.0"
tauri-plugin-fs = "2.4.0"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.0"
tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.2.2"
zip = "2.6.1"
tauri-plugin-window-state = "2.3.0"
zip = "=4.2.0"
reqwest_dav = "0.2.1"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.2"
tokio-tungstenite = "0.26.2"
getrandom = "0.3.3"
futures = "0.3.31"
sys-locale = "0.3.2"
async-trait = "0.1.88"
mihomo_api = { path = "src_crates/crate_mihomo_api" }
ab_glyph = "0.2.29"
tungstenite = "0.26.2"
libc = "0.2.172"
libc = "0.2.174"
gethostname = "1.0.2"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.0"
dashmap = "7.0.0-rc2"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
deelevate = "0.2.0"
winreg = "0.55.0"
winapi = { version = "0.3", features = [
winapi = { version = "0.3.9", features = [
"winbase",
"fileapi",
"winnt",
@@ -93,15 +92,19 @@ winapi = { version = "0.3", features = [
"errhandlingapi",
"minwindef",
"winerror",
"tlhelp32",
"processthreadsapi",
"winhttp",
"winreg",
] }
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-updater = "2.7.1"
tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.3.0"
tauri-plugin-updater = "2.9.0"
[features]
default = ["custom-protocol"]
@@ -136,12 +139,12 @@ name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies]
tempfile = "3.19.1"
criterion = "0.6.0"
tempfile = "3.20.0"
[workspace]
members = ["src_crates/crate_mihomo_api"]
# [patch.crates-io]
# bitflags = { git = "https://github.com/bitflags/bitflags", rev = "2.9.0" }
# zerocopy = { git = "https://github.com/google/zerocopy", rev = "v0.8.24" }
# tungstenite = { git = "https://github.com/snapview/tungstenite-rs", rev = "v0.26.2" }
[[bench]]
name = "draft_benchmark"
harness = false
+91
View File
@@ -0,0 +1,91 @@
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
// 业务模型 & Draft
use app_lib::config::Draft as DraftNew;
use app_lib::config::IVerge;
// fn bench_apply_old(c: &mut Criterion) {
// c.bench_function("apply_draft_old", |b| {
// b.iter(|| {
// let verge = Box::new(IVerge {
// enable_auto_launch: Some(true),
// enable_tun_mode: Some(false),
// ..Default::default()
// });
// let draft = DraftOld::from(black_box(verge));
// {
// let mut d = draft.draft_mut();
// d.enable_auto_launch = Some(false);
// }
// let _ = draft.apply();
// });
// });
// }
// fn bench_discard_old(c: &mut Criterion) {
// c.bench_function("discard_draft_old", |b| {
// b.iter(|| {
// let verge = Box::new(IVerge::default());
// let draft = DraftOld::from(black_box(verge));
// {
// let mut d = draft.draft_mut();
// d.enable_auto_launch = Some(false);
// }
// let _ = draft.discard();
// });
// });
// }
/// 基准:修改草稿并 apply()
fn bench_apply_new(c: &mut Criterion) {
c.bench_function("apply_draft_new", |b| {
b.iter(|| {
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..Default::default()
});
let draft = DraftNew::from(black_box(verge));
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.apply();
});
});
}
/// 基准:修改草稿并 discard()
fn bench_discard_new(c: &mut Criterion) {
c.bench_function("discard_draft_new", |b| {
b.iter(|| {
let verge = Box::new(IVerge::default());
let draft = DraftNew::from(black_box(verge));
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.discard();
});
});
}
criterion_group!(
benches,
// bench_apply_old,
// bench_discard_old,
bench_apply_new,
bench_discard_new
);
criterion_main!(benches);
@@ -0,0 +1,9 @@
{
"identifier": "desktop-windows-capability",
"description": "permissions for desktop windows applications",
"windows": ["main"],
"permissions": [
"core:webview:allow-create-webview",
"core:webview:allow-create-webview-window"
]
}
+2 -1
View File
@@ -17,6 +17,7 @@
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"core:window:allow-set-theme"
"core:window:allow-set-theme",
"notification:default"
]
}
+236
View File
@@ -0,0 +1,236 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
"RUSTSEC-2024-0415",
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
#"MIT",
#"Apache-2.0",
#"Apache-2.0 WITH LLVM-exception",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.85
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The package spec the clarification applies to
#crate = "ring"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = []
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
bitbucket = []
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+160 -40
View File
@@ -691,8 +691,6 @@ SectionEnd
app_check_done:
!macroend
Var VC_REDIST_URL
Var VC_REDIST_EXE
@@ -701,25 +699,25 @@ Section CheckAndInstallVSRuntime
${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\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${EndIf}
@@ -754,30 +752,34 @@ Section CheckAndInstallVSRuntime
${Else}
DetailPrint "Visual C++ Redistributable 下载失败"
${EndIf}
Done:
SectionEnd
Section Install
SetOutPath $INSTDIR
nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; 删除 window-state.json 文件 .window-state.json 文件
DetailPrint "开始删除删除 window-state.json or .window-state.json"
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
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"
@@ -788,7 +790,7 @@ Section Install
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
@@ -798,7 +800,7 @@ Section Install
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Copy main executable
File "${MAINBINARYSRCPATH}"
@@ -916,26 +918,29 @@ FunctionEnd
!macroend
Section Uninstall
;删除 .window-state.json 文件
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; 删除 window-state.json 文件 .window-state.json 文件
DetailPrint "开始删除删除 window-state.json or .window-state.json"
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
; 清理自启动注册表项
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"
@@ -946,7 +951,7 @@ Section Uninstall
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
@@ -978,24 +983,144 @@ Section Uninstall
{{/each}}
RMDir "$INSTDIR"
; 删除固定栏
!insertmacro DeleteAppUserModelId
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
; 删除所有用户的桌面快捷方式
DetailPrint "开始删除所有用户桌面的 Clash Verge 快捷方式..."
; 删除公共桌面快捷方式
Delete "C:\Users\Public\Desktop\Clash Verge.lnk"
Delete "C:\Users\Public\Desktop\clash-verge.lnk"
; 枚举所有用户配置文件目录
SetRegView 64
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" ""
; 初始化循环
StrCpy $R1 0
Loop:
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $R1
${If} $R2 == ""
Goto Done
${EndIf}
; 读取用户配置文件路径
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$R2" "ProfileImagePath"
${If} $R3 != ""
; 构建用户桌面路径
StrCpy $R4 "$R3\Desktop"
; 删除该用户桌面的快捷方式
Delete "$R4\Clash Verge.lnk"
Delete "$R4\clash-verge.lnk"
DetailPrint "尝试删除用户 '$R3' 桌面的 Clash Verge 快捷方式"
${EndIf}
; 递增循环计数器
IntOp $R1 $R1 + 1
Goto Loop
Done:
DetailPrint "所有用户桌面快捷方式删除完成"
; 删除用户开始菜单文件夹
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
RMDir /r /REBOOTOK "$SMPROGRAMS\$AppStartMenuFolder"
; 删除系统级开始菜单中的 Clash Verge
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\Clash Verge.lnk"
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\clash-verge.lnk"
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge"
; 删除所有带 Clash Verge 或 clash-verge 的注册表项
DetailPrint "开始清理所有 Clash Verge 相关的注册表项..."
; 设置注册表查看模式 (64位)
SetRegView 64
; 清理 CurrentVersion\Run 中的自启动项
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
DeleteRegValue HKCU "$R1" "Clash Verge"
DeleteRegValue HKCU "$R1" "clash-verge"
DeleteRegValue HKLM "$R1" "Clash Verge"
DeleteRegValue HKLM "$R1" "clash-verge"
; 清理 App Paths
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Clash Verge.exe"
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\clash-verge.exe"
; 删除指定的注册表路径
DeleteRegKey HKLM "Software\Clash Verge Rev"
DeleteRegKey HKCU "Software\Clash Verge Rev"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\ClashVerge"
DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Clash Verge"
; 清理 Uninstall 信息
StrCpy $R1 0
EnumUninstallLoop:
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $R1
${If} $R2 == ""
Goto EnumUninstallDone
${EndIf}
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2" "DisplayName"
${If} $R3 != ""
StrCmp $R3 "Clash Verge" 0 +3
StrCmp $R3 "clash-verge" 0 +2
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2"
${EndIf}
IntOp $R1 $R1 + 1
Goto EnumUninstallLoop
EnumUninstallDone:
; 清理用户特定的注册表项
StrCpy $R1 0
EnumHKCULoop:
EnumRegKey $R2 HKCU "SOFTWARE" $R1
${If} $R2 == ""
Goto EnumHKCUDone
${EndIf}
ReadRegStr $R3 HKCU "SOFTWARE\$R2" ""
${If} $R3 != ""
StrCmp $R3 "Clash Verge" 0 +3
StrCmp $R3 "clash-verge" 0 +2
DeleteRegKey HKCU "SOFTWARE\$R2"
${EndIf}
IntOp $R1 $R1 + 1
Goto EnumHKCULoop
EnumHKCUDone:
; 清理系统范围的注册表项
StrCpy $R1 0
EnumHKLMLoop:
EnumRegKey $R2 HKLM "SOFTWARE" $R1
${If} $R2 == ""
Goto EnumHKLMDone
${EndIf}
ReadRegStr $R3 HKLM "SOFTWARE\$R2" ""
${If} $R3 != ""
StrCmp $R3 "Clash Verge" 0 +3
StrCmp $R3 "clash-verge" 0 +2
DeleteRegKey HKLM "SOFTWARE\$R2"
${EndIf}
IntOp $R1 $R1 + 1
Goto EnumHKLMLoop
EnumHKLMDone:
DetailPrint "注册表清理完成"
; Remove desktop shortcuts
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
Delete "$DESKTOP\clash-verge.lnk"
; Remove registry information for add/remove programs
!if "${INSTALLMODE}" == "both"
@@ -1015,10 +1140,6 @@ Section Uninstall
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
${EndIf}
;删除 .window-state.json 文件
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0
SetAutoClose true
@@ -1051,7 +1172,6 @@ FunctionEnd
System::Free $4
System::Free $5
${IPropertyStore::Commit} $2 ""
${IUnknown::Release} $2 ""
${IPersistFile::Save} $1 '("${shortcut}",1)'
${EndIf}
${IUnknown::Release} $1 ""
-1
View File
@@ -11,4 +11,3 @@ merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
imports_granularity = "Crate"
+4 -4
View File
@@ -147,7 +147,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
Ok(icon_path.to_string_lossy().to_string())
} else {
let _ = std::fs::remove_file(&temp_path);
Err(format!("下载的内容不是有效图片: {}", url))
Err(format!("下载的内容不是有效图片: {url}"))
}
}
@@ -217,7 +217,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
/// UI加载阶段
#[tauri::command]
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
log::info!(target: "app", "UI加载阶段更新: {}", stage);
log::info!(target: "app", "UI加载阶段更新: {stage}");
use crate::utils::resolve::UiReadyStage;
@@ -228,8 +228,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready,
_ => {
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
return Err(format!("未知的UI加载阶段: {}", stage));
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
return Err(format!("未知的UI加载阶段: {stage}"));
}
};
+12 -12
View File
@@ -14,7 +14,7 @@ pub fn copy_clash_env() -> CmdResult {
/// 获取Clash信息
#[tauri::command]
pub fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().latest().get_client_info())
Ok(Config::clash().latest_ref().get_client_info())
}
/// 修改Clash配置
@@ -49,8 +49,8 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
Ok(None)
}
Err(err) => {
let error_msg = format!("Core changed but failed to restart: {}", err);
log::error!(target: "app", "{}", error_msg);
let error_msg = format!("Core changed but failed to restart: {err}");
log::error!(target: "app", "{error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
@@ -116,7 +116,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
// 保存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);
log::info!(target: "app", "DNS config saved to {dns_path:?}");
Ok(())
}
@@ -137,7 +137,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
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);
log::error!(target: "app", "Failed to get home dir: {e}");
return;
}
};
@@ -150,7 +150,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
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);
log::error!(target: "app", "Failed to read DNS config: {e}");
return;
}
};
@@ -163,7 +163,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
patch
}
Err(e) => {
log::error!(target: "app", "Failed to parse DNS config: {}", e);
log::error!(target: "app", "Failed to parse DNS config: {e}");
return;
}
};
@@ -173,18 +173,18 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
// 重新生成配置,确保DNS配置被正确应用
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
Config::runtime()
.latest()
.draft_mut()
.patch_config(patch_config.clone());
// 首先重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
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);
log::error!(target: "app", "Failed to apply config with DNS: {err}");
} else {
log::info!(target: "app", "DNS config successfully applied");
handle::Handle::refresh_clash();
@@ -196,7 +196,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
// 重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config: {}", err);
log::error!(target: "app", "Failed to regenerate config: {err}");
return;
}
@@ -207,7 +207,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
handle::Handle::refresh_clash();
}
Err(err) => {
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
log::error!(target: "app", "Failed to apply regenerated config: {err}");
}
}
}
+24 -26
View File
@@ -37,7 +37,7 @@ fn country_code_to_emoji(country_code: &str) -> String {
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
char::from_u32(c1)
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{}{}", c1, c2)))
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
.unwrap_or_default()
}
@@ -163,7 +163,7 @@ async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
map.get("loc").map(|loc| {
let emoji = country_code_to_emoji(loc);
format!("{}{}", emoji, loc)
format!("{emoji}{loc}")
})
} else {
None
@@ -255,7 +255,7 @@ async fn check_gemini(client: &Client) -> UnlockItem {
caps.get(1).map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{}{}", emoji, country_code)
format!("{emoji}{country_code}")
})
});
@@ -308,7 +308,7 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
caps.get(1).map(|m| {
let country_code = m.as_str().trim();
let emoji = country_code_to_emoji(country_code);
format!("{}{}", emoji, country_code)
format!("{emoji}{country_code}")
})
});
@@ -384,10 +384,8 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
}
// 第二步:使用设备ID检查访问权限 (使用相同的Cookie)
let url = format!(
"https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={}",
device_id
);
let url =
format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
let token_result = match client_with_cookies.get(&url).send().await {
Ok(response) => {
@@ -431,7 +429,7 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
.map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{}{}", emoji, country_code)
format!("{emoji}{country_code}")
})
}
Err(_) => None,
@@ -470,7 +468,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
// 检查连接失败情况
if let Err(e) = &result1 {
eprintln!("Netflix请求错误: {}", e);
eprintln!("Netflix请求错误: {e}");
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
@@ -487,7 +485,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
.await;
if let Err(e) = &result2 {
eprintln!("Netflix请求错误: {}", e);
eprintln!("Netflix请求错误: {e}");
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
@@ -541,7 +539,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region_code)),
region: Some(format!("{emoji}{region_code}")),
check_time: Some(get_local_date_string()),
};
}
@@ -557,7 +555,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
}
}
Err(e) => {
eprintln!("获取Netflix区域信息失败: {}", e);
eprintln!("获取Netflix区域信息失败: {e}");
UnlockItem {
name: "Netflix".to_string(),
status: "Yes (但无法获取区域)".to_string(),
@@ -570,7 +568,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
// 其他未知错误状态
UnlockItem {
name: "Netflix".to_string(),
status: format!("Failed (状态码: {}_{}", status1, status2),
status: format!("Failed (状态码: {status1}_{status2}"),
region: None,
check_time: Some(get_local_date_string()),
}
@@ -614,7 +612,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, country)),
region: Some(format!("{emoji}{country}")),
check_time: Some(get_local_date_string()),
};
}
@@ -631,7 +629,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
}
}
Err(e) => {
eprintln!("解析Fast.com API响应失败: {}", e);
eprintln!("解析Fast.com API响应失败: {e}");
UnlockItem {
name: "Netflix".to_string(),
status: "Failed (解析错误)".to_string(),
@@ -642,7 +640,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
}
}
Err(e) => {
eprintln!("Fast.com API请求失败: {}", e);
eprintln!("Fast.com API请求失败: {e}");
UnlockItem {
name: "Netflix".to_string(),
status: "Failed (CDN API)".to_string(),
@@ -884,7 +882,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{} (from main page)", emoji, region)),
region: Some(format!("{emoji}{region} (from main page)")),
check_time: Some(get_local_date_string()),
};
}
@@ -947,7 +945,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{} (from main page)", emoji, region)),
region: Some(format!("{emoji}{region} (from main page)")),
check_time: Some(get_local_date_string()),
};
}
@@ -968,7 +966,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)),
region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()),
};
}
@@ -990,7 +988,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
UnlockItem {
name: "Disney+".to_string(),
status: "Soon".to_string(),
region: Some(format!("{}{}(即将上线)", emoji, region)),
region: Some(format!("{emoji}{region}(即将上线)")),
check_time: Some(get_local_date_string()),
}
}
@@ -999,13 +997,13 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)),
region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()),
}
}
None => UnlockItem {
name: "Disney+".to_string(),
status: format!("Failed (Error: Unknown region status for {})", region),
status: format!("Failed (Error: Unknown region status for {region})"),
region: None,
check_time: Some(get_local_date_string()),
},
@@ -1056,7 +1054,7 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Prime Video".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)),
region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()),
};
}
@@ -1170,7 +1168,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
.connection_verbose(true) // 详细连接信息
.build() {
Ok(client) => client,
Err(e) => return Err(format!("创建HTTP客户端失败: {}", e)),
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
};
// 创建共享的结果集
@@ -1284,7 +1282,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 等待所有任务完成
while let Some(res) = tasks.join_next().await {
if let Err(e) = res {
eprintln!("任务执行失败: {}", e);
eprintln!("任务执行失败: {e}");
}
}
+18 -13
View File
@@ -1,17 +1,15 @@
use super::CmdResult;
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
use crate::wrap_err;
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
use sysproxy::{Autoproxy, Sysproxy};
use tokio::task::spawn_blocking;
/// get the system proxy
#[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
let current = spawn_blocking(Sysproxy::get_system_proxy)
.await
.map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))?
.map_err(|e| format!("Failed to get system proxy: {}", e))?;
log::debug!(target: "app", "异步获取系统代理配置");
let current = AsyncProxyQuery::get_system_proxy().await;
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
@@ -21,21 +19,28 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
);
map.insert("bypass".into(), current.bypass.into());
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
Ok(map)
}
/// get the system proxy
/// 获取自动代理配置
#[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
let current = spawn_blocking(Autoproxy::get_auto_proxy)
.await
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))?
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
let proxy_manager = EventDrivenProxyManager::global();
let current = proxy_manager.get_auto_proxy_cached();
// 异步请求更新,立即返回缓存数据
tokio::spawn(async move {
let _ = proxy_manager.get_auto_proxy_async().await;
});
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
map.insert("url".into(), current.url.into());
map.insert("url".into(), current.url.clone().into());
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
Ok(map)
}
@@ -49,7 +54,7 @@ pub fn get_system_hostname() -> CmdResult<String> {
Ok(name) => name,
Err(os_string) => {
// 对于包含非UTF-8的主机名,使用调试格式化
let fallback = format!("{:?}", os_string);
let fallback = format!("{os_string:?}");
// 去掉可能存在的引号
fallback.trim_matches('"').to_string()
}
+302 -60
View File
@@ -6,51 +6,114 @@ use crate::{
utils::{dirs, help, logging::Type},
wrap_err,
};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::sync::{Mutex, RwLock};
// 添加全局互斥锁防止并发配置更新
// 全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
/// 获取配置文件列表
// 全局请求序列号跟踪,用于避免队列化执行
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None);
/// 清理配置处理状态
async fn cleanup_processing_state(sequence: u64, reason: &str) {
*CURRENT_PROCESSING_PROFILE.write().await = None;
logging!(
info,
Type::Cmd,
true,
"{},清理状态,序列号: {}",
reason,
sequence
);
}
/// 获取配置文件避免锁竞争
#[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> {
let profiles_result = tokio::time::timeout(
Duration::from_secs(3), // 3秒超时
tokio::task::spawn_blocking(move || Config::profiles().data().clone()),
// 策略1: 尝试快速获取latest数据
let latest_result = tokio::time::timeout(
Duration::from_millis(500),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let latest = profiles.latest_ref();
IProfiles {
current: latest.current.clone(),
items: latest.items.clone(),
}
}),
)
.await;
match profiles_result {
Ok(Ok(profiles)) => Ok(profiles),
match latest_result {
Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
}
Err(_) => {
// 超时情况
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
}
}
// 策略2: 如果快速获取失败,尝试获取data()
let data_result = tokio::time::timeout(
Duration::from_secs(2),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let data = profiles.latest_ref();
IProfiles {
current: data.current.clone(),
items: data.items.clone(),
}
}),
)
.await;
match data_result {
Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
logging!(
error,
Type::Cmd,
true,
"获取配置列表超时(3秒),可能存在锁竞争"
"获取draft配置任务失败: {}",
join_err
);
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
Ok(profiles)
}
Err(_) => {
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置");
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
}
}
}
Err(_) => {
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
}
}
// 策略3: fallback,尝试重新创建配置
logging!(
warn,
Type::Cmd,
true,
"所有获取配置策略都失败,尝试fallback"
);
match tokio::task::spawn_blocking(IProfiles::new).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用fallback配置成功");
Ok(profiles)
}
Err(err) => {
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
// 返回空配置避免崩溃
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
}
}
}
@@ -67,20 +130,20 @@ pub async fn enhance_profiles() -> CmdResult {
#[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))
wrap_err!(Config::profiles().data_mut().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))
wrap_err!(Config::profiles().data_mut().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))
wrap_err!(Config::profiles().data_mut().append_item(item))
}
/// 更新配置文件
@@ -92,7 +155,11 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
let should_update = wrap_err!({ Config::profiles().data_mut().delete_item(index) })?;
// 删除后自动清理冗余文件
let _ = Config::profiles().latest_ref().auto_cleanup();
if should_update {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
@@ -103,13 +170,63 @@ pub async fn delete_profile(index: String) -> CmdResult {
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 获取互斥锁,防止并发执行
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
// 为当前请求分配序列号
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
let target_profile = profiles.current.clone();
logging!(info, Type::Cmd, true, "开始修改配置文件");
logging!(
info,
Type::Cmd,
true,
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence,
target_profile
);
let mutex_result =
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
let _guard = match mutex_result {
Ok(guard) => guard,
Err(_) => {
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
logging!(
info,
Type::Cmd,
true,
"强制获取锁以处理最新请求: {}",
current_sequence
);
PROFILE_UPDATE_MUTEX.lock().await
}
};
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().latest().current.clone();
let current_profile = Config::profiles().latest_ref().current.clone();
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
@@ -120,7 +237,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles();
let profiles_data = profiles_config.latest();
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
@@ -173,7 +290,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {}", err);
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
@@ -188,7 +305,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {}", join_err);
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
@@ -199,7 +316,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {}", err);
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
@@ -221,14 +338,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
}
// 检查请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
if let Some(ref profile) = target_profile {
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
logging!(
info,
Type::Cmd,
true,
"设置当前处理profile: {}, 序列号: {}",
profile,
current_sequence
);
}
// 更新profiles配置
logging!(info, Type::Cmd, true, "正在更新配置草稿");
logging!(
info,
Type::Cmd,
true,
"正在更新配置草稿,序列号: {}",
current_sequence
);
let current_value = profiles.current.clone();
let _ = Config::profiles().draft().patch_config(profiles);
let _ = Config::profiles().draft_mut().patch_config(profiles);
// 在调用内核前再次验证请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
Config::profiles().discard();
return Ok(false);
}
// 为配置更新添加超时保护
logging!(
info,
Type::Cmd,
true,
"开始内核配置更新,序列号: {}",
current_sequence
);
let update_result = tokio::time::timeout(
Duration::from_secs(30), // 30秒超时
CoreManager::global().update_config(),
@@ -238,31 +409,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 更新配置并进行验证
match update_result {
Ok(Ok((true, _))) => {
logging!(info, Type::Cmd, true, "配置更新成功");
// 内核操作完成后再次检查请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
);
Config::profiles().discard();
return Ok(false);
}
logging!(
info,
Type::Cmd,
true,
"配置更新成功,序列号: {}",
current_sequence
);
Config::profiles().apply();
handle::Handle::refresh_clash();
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = super::proxy::force_refresh_proxies().await {
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
}
});
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Tray::global().update_tooltip() {
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
}
if let Err(e) = Tray::global().update_menu() {
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
}
// 保存配置文件
if let Err(e) = Config::profiles().data().save_file() {
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
if let Err(e) = Config::profiles().data_mut().save_file() {
log::warn!(target: "app", "异步保存配置文件失败: {e}");
}
});
// 立即通知前端配置变更
if let Some(current) = &current_value {
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
logging!(
info,
Type::Cmd,
true,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
handle::Handle::notify_profile_changed(current.clone());
}
cleanup_processing_state(current_sequence, "配置切换完成").await;
Ok(true)
}
Ok(Ok((false, error_msg))) => {
@@ -282,12 +490,16 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
items: None,
};
// 静默恢复,不触发验证
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
wrap_err!({
Config::profiles()
.draft_mut()
.patch_config(restore_profiles)
})?;
Config::profiles().apply();
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Config::profiles().data().save_file() {
log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e);
if let Err(e) = Config::profiles().data_mut().save_file() {
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
}
});
@@ -296,18 +508,38 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 发送验证错误通知
handle::Handle::notice_message("config_validate::error", &error_msg);
cleanup_processing_state(current_sequence, "配置验证失败").await;
Ok(false)
}
Ok(Err(e)) => {
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
logging!(
warn,
Type::Cmd,
true,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
Config::profiles().discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
cleanup_processing_state(current_sequence, "更新过程错误").await;
Ok(false)
}
Err(_) => {
// 超时处理
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(error, Type::Cmd, true, "{}", timeout_msg);
logging!(
error,
Type::Cmd,
true,
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
Config::profiles().discard();
if let Some(prev_profile) = current_profile {
@@ -315,18 +547,26 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"超时后尝试恢复到之前的配置: {}",
prev_profile
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
prev_profile,
current_sequence
);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
};
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
wrap_err!({
Config::profiles()
.draft_mut()
.patch_config(restore_profiles)
})?;
Config::profiles().apply();
}
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
cleanup_processing_state(current_sequence, "配置更新超时").await;
Ok(false)
}
}
@@ -352,7 +592,7 @@ pub async fn patch_profiles_config_by_profile_index(
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval
let update_interval_changed =
if let Ok(old_profile) = Config::profiles().latest().get_item(&index) {
if let Ok(old_profile) = Config::profiles().latest_ref().get_item(&index) {
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
old_interval != new_interval
@@ -361,7 +601,9 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
};
// 保存修改
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
wrap_err!(Config::profiles()
.data_mut()
.patch_item(index.clone(), profile))?;
// 如果更新间隔变更,异步刷新定时器
if update_interval_changed {
@@ -384,7 +626,7 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command]
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
let file = {
wrap_err!(Config::profiles().latest().get_item(&index))?
wrap_err!(Config::profiles().latest_ref().get_item(&index))?
.file
.clone()
.ok_or("the file field is null")
@@ -402,7 +644,7 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
#[tauri::command]
pub fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles();
let profiles = profiles.latest();
let profiles = profiles.latest_ref();
let item = wrap_err!(profiles.get_item(&index))?;
let data = wrap_err!(item.read_file())?;
Ok(data)
+28 -56
View File
@@ -1,71 +1,43 @@
use super::CmdResult;
use crate::module::mihomo::MihomoManager;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::{
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
use std::time::Duration;
static LAST_REFRESH_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
static IS_REFRESHING: AtomicBool = AtomicBool::new(false);
const REFRESH_INTERVAL: Duration = Duration::from_secs(5);
use crate::state::proxy::ProxyRequestCache;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
manager.get_refresh_proxies().await.expect("fetch failed")
})
.await;
Ok((*value).clone())
}
manager
.refresh_proxies()
.await
.map(|_| manager.get_proxies())
.or_else(|_| Ok(manager.get_proxies()))
/// 强制刷新代理缓存用于profile切换
#[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
cache.map.remove(&key);
get_proxies().await
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let cached_data = manager.get_providers_proxies();
let safe_data = if cached_data.is_null() {
serde_json::json!({
"providers": {}
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("providers", "default");
let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
manager.get_providers_proxies().await.expect("fetch failed")
})
} else {
cached_data
};
// 检查是否需要刷新
let should_refresh = {
let last_refresh = LAST_REFRESH_TIME.lock();
match *last_refresh {
Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL,
None => true,
}
};
if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) {
IS_REFRESHING.store(true, Ordering::Release);
crate::process::AsyncHandler::spawn(|| async move {
let manager = MihomoManager::global();
match manager.refresh_providers_proxies().await {
Ok(_) => {
log::debug!(target: "app", "providers_proxies静默后台刷新成功");
}
Err(e) => {
log::warn!(target: "app", "providers_proxies后台刷新失败: {}", e);
}
}
{
let mut last_refresh = LAST_REFRESH_TIME.lock();
*last_refresh = Some(Instant::now());
}
IS_REFRESHING.store(false, Ordering::Release);
});
}
Ok(safe_data)
.await;
Ok((*value).clone())
}
+4 -4
View File
@@ -7,14 +7,14 @@ use std::collections::HashMap;
/// 获取运行时配置
#[tauri::command]
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().latest().config.clone())
Ok(Config::runtime().latest_ref().config.clone())
}
/// 获取运行时YAML配置
#[tauri::command]
pub fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime();
let runtime = runtime.latest();
let runtime = runtime.latest_ref();
let config = runtime.config.as_ref();
wrap_err!(config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
@@ -26,11 +26,11 @@ pub fn get_runtime_yaml() -> CmdResult<String> {
/// 获取运行时存在的键
#[tauri::command]
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().latest().exists_keys.clone())
Ok(Config::runtime().latest_ref().exists_keys.clone())
}
/// 获取运行时日志
#[tauri::command]
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().latest().chain_logs.clone())
Ok(Config::runtime().latest_ref().chain_logs.clone())
}
+65 -16
View File
@@ -1,5 +1,11 @@
use super::CmdResult;
use crate::{config::*, core::*, utils::dirs, wrap_err};
use crate::{
config::*,
core::*,
logging,
utils::{dirs, logging::Type},
wrap_err,
};
use std::fs;
/// 保存profiles的配置
@@ -12,7 +18,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 在异步操作前完成所有文件操作
let (file_path, original_content, is_merge_file) = {
let profiles = Config::profiles();
let profiles_guard = profiles.latest();
let profiles_guard = profiles.latest_ref();
let item = wrap_err!(profiles_guard.get_item(&index))?;
// 确定是否为merge类型文件
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
@@ -26,29 +32,54 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_path_str = file_path.to_string_lossy().to_string();
println!(
logging!(
info,
Type::Config,
true,
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
file_path_str, is_merge_file
file_path_str,
is_merge_file
);
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
if is_merge_file {
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
logging!(
info,
Type::Config,
true,
"[cmd配置save] 检测到merge文件,只进行语法验证"
);
match CoreManager::global()
.validate_config_file(&file_path_str, Some(true))
.await
{
Ok((true, _)) => {
println!("[cmd配置save] merge文件语法验证通过");
logging!(
info,
Type::Config,
true,
"[cmd配置save] merge文件语法验证通过"
);
// 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await {
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
}
return Ok(());
}
Ok((false, error_msg)) => {
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
logging!(
warn,
Type::Config,
true,
"[cmd配置save] merge文件语法验证失败: {}",
error_msg
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知
@@ -57,7 +88,13 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
return Ok(());
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
return Err(e.to_string());
@@ -71,11 +108,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
.await
{
Ok((true, _)) => {
println!("[cmd配置save] 验证成功");
logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
Ok(())
}
Ok((false, error_msg)) => {
println!("[cmd配置save] 验证失败: {}", error_msg);
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 验证失败: {}",
error_msg
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
@@ -90,24 +133,30 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|| (!file_path_str.ends_with(".js") && !is_script_error)
{
// 普通YAML错误使用YAML通知处理
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
log::info!(target: "app", "[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] 脚本文件验证失败,发送通知");
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg);
}
Ok(())
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
Err(e.to_string())
+2 -2
View File
@@ -24,7 +24,7 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
#[tauri::command]
pub async fn export_diagnostic_info() -> CmdResult<()> {
let sysinfo = PlatformSpecification::new_async().await;
let info = format!("{:?}", sysinfo);
let info = format!("{sysinfo:?}");
let app_handle = handle::Handle::global().app_handle().unwrap();
let cliboard = app_handle.clipboard();
@@ -37,7 +37,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
#[tauri::command]
pub async fn get_system_info() -> CmdResult<String> {
let sysinfo = PlatformSpecification::new_async().await;
let info = format!("{:?}", sysinfo);
let info = format!("{sysinfo:?}");
Ok(info)
}
+41 -7
View File
@@ -1,5 +1,5 @@
use super::CmdResult;
use crate::core::*;
use crate::{core::*, logging, utils::logging::Type};
/// 发送脚本验证通知消息
#[tauri::command]
@@ -28,7 +28,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
"config_validate::script_error"
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
logging!(
warn,
Type::Config,
true,
"{} 验证失败: {}",
file_type,
error_msg
);
handle::Handle::notice_message(status, error_msg);
}
}
@@ -36,7 +43,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
/// 验证指定脚本文件
#[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
log::info!(target: "app", "验证脚本文件: {}", file_path);
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
match CoreManager::global()
.validate_config_file(&file_path, None)
@@ -48,7 +55,13 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
}
Err(e) => {
let error_msg = e.to_string();
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
logging!(
error,
Type::Config,
true,
"验证脚本文件过程发生错误: {}",
error_msg
);
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
Ok(false)
}
@@ -60,7 +73,14 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
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);
logging!(
info,
Type::Config,
true,
"[通知] 处理{}验证错误: {}",
file_type,
error_msg
);
// 检查是否为merge文件
let is_merge_file = file_type.contains("合并");
@@ -97,8 +117,22 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
}
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
logging!(
warn,
Type::Config,
true,
"{} 验证失败: {}",
file_type,
error_msg
);
logging!(
info,
Type::Config,
true,
"[通知] 发送通知: status={}, msg={}",
status,
error_msg
);
handle::Handle::notice_message(status, error_msg);
}
}
+2 -2
View File
@@ -5,8 +5,8 @@ use crate::{config::*, feat, wrap_err};
#[tauri::command]
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge();
let verge_data = verge.data().clone();
Ok(IVergeResponse::from(verge_data))
let verge_data = verge.latest_ref().clone();
Ok(IVergeResponse::from(*verge_data))
}
/// 修改Verge配置
+2 -2
View File
@@ -11,10 +11,10 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
webdav_password: Some(password),
..IVerge::default()
};
Config::verge().draft().patch_config(patch.clone());
Config::verge().draft_mut().patch_config(patch.clone());
Config::verge().apply();
Config::verge()
.data()
.latest_ref()
.save_file()
.map_err(|err| err.to_string())?;
core::backup::WebDavClient::global().reset();
+2 -17
View File
@@ -63,6 +63,8 @@ impl IClashTemp {
vec![
"tauri://localhost",
"http://tauri.localhost",
// Only enable this in dev mode
#[cfg(feature = "verge-dev")]
"http://localhost:3000",
"https://yacd.metacubex.one",
"https://metacubex.github.io",
@@ -95,23 +97,6 @@ impl IClashTemp {
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.into());
// 强制覆盖 external-controller-cors 字段,允许本地和 tauri 前端
let mut cors_map = Mapping::new();
cors_map.insert("allow-private-network".into(), true.into());
cors_map.insert(
"allow-origins".into(),
vec![
"tauri://localhost",
"http://tauri.localhost",
"http://localhost:3000",
"https://yacd.metacubex.one",
"https://metacubex.github.io",
"https://board.zash.run.place",
]
.into(),
);
config.insert("external-controller-cors".into(), cors_map.into());
config
}
+62 -19
View File
@@ -15,10 +15,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub struct Config {
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
runtime_config: Draft<IRuntime>,
clash_config: Draft<Box<IClashTemp>>,
verge_config: Draft<Box<IVerge>>,
profiles_config: Draft<Box<IProfiles>>,
runtime_config: Draft<Box<IRuntime>>,
}
impl Config {
@@ -26,46 +26,50 @@ impl Config {
static CONFIG: OnceCell<Config> = OnceCell::new();
CONFIG.get_or_init(|| Config {
clash_config: Draft::from(IClashTemp::new()),
verge_config: Draft::from(IVerge::new()),
profiles_config: Draft::from(IProfiles::new()),
runtime_config: Draft::from(IRuntime::new()),
clash_config: Draft::from(Box::new(IClashTemp::new())),
verge_config: Draft::from(Box::new(IVerge::new())),
profiles_config: Draft::from(Box::new(IProfiles::new())),
runtime_config: Draft::from(Box::new(IRuntime::new())),
})
}
pub fn clash() -> Draft<IClashTemp> {
pub fn clash() -> Draft<Box<IClashTemp>> {
Self::global().clash_config.clone()
}
pub fn verge() -> Draft<IVerge> {
pub fn verge() -> Draft<Box<IVerge>> {
Self::global().verge_config.clone()
}
pub fn profiles() -> Draft<IProfiles> {
pub fn profiles() -> Draft<Box<IProfiles>> {
Self::global().profiles_config.clone()
}
pub fn runtime() -> Draft<IRuntime> {
pub fn runtime() -> Draft<Box<IRuntime>> {
Self::global().runtime_config.clone()
}
/// 初始化订阅
pub async fn init_config() -> Result<()> {
if Self::profiles()
.data()
.latest_ref()
.get_item(&"Merge".to_string())
.is_err()
{
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
Self::profiles().data().append_item(merge_item.clone())?;
Self::profiles()
.data_mut()
.append_item(merge_item.clone())?;
}
if Self::profiles()
.data()
.latest_ref()
.get_item(&"Script".to_string())
.is_err()
{
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles().data().append_item(script_item.clone())?;
Self::profiles()
.data_mut()
.append_item(script_item.clone())?;
}
// 生成运行时配置
if let Err(err) = Self::generate().await {
@@ -135,7 +139,7 @@ impl Config {
};
let runtime = Config::runtime();
let runtime = runtime.latest();
let runtime = runtime.latest_ref();
let config = runtime
.config
.as_ref()
@@ -149,11 +153,11 @@ impl Config {
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().draft() = IRuntime {
*Config::runtime().draft_mut() = Box::new(IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
};
});
Ok(())
}
@@ -164,3 +168,42 @@ pub enum ConfigType {
Run,
Check,
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem;
#[test]
fn test_prfitem_from_merge_size() {
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
dbg!(&merge_item);
let prfitem_size = mem::size_of_val(&merge_item);
dbg!(prfitem_size);
// Boxed version
let boxed_merge_item = Box::new(merge_item);
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
dbg!(box_prfitem_size);
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
assert!(box_prfitem_size < prfitem_size);
}
#[test]
fn test_draft_size_non_boxed() {
let draft = Draft::from(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
dbg!(iruntime_size);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
#[test]
fn test_draft_size_boxed() {
let draft = Draft::from(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
dbg!(box_iruntime_size);
assert_eq!(
box_iruntime_size,
std::mem::size_of::<Draft<Box<IRuntime>>>()
);
}
}
+121 -101
View File
@@ -1,127 +1,147 @@
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
use std::sync::Arc;
use parking_lot::{
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
RwLockUpgradableReadGuard, RwLockWriteGuard,
};
#[derive(Debug, Clone)]
pub struct Draft<T: Clone + ToOwned> {
inner: Arc<Mutex<(T, Option<T>)>>,
inner: Arc<RwLock<(T, Option<T>)>>,
}
macro_rules! draft_define {
($id: ident) => {
impl Draft<$id> {
#[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
}
pub fn latest(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
&mut inner.0
} else {
inner.1.as_mut().unwrap()
}
})
}
pub fn draft(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
inner.1 = Some(inner.0.clone());
}
inner.1.as_mut().unwrap()
})
}
pub fn apply(&self) -> Option<$id> {
let mut inner = self.inner.lock();
match inner.1.take() {
Some(draft) => {
let old_value = inner.0.to_owned();
inner.0 = draft.to_owned();
Some(old_value)
}
None => None,
}
}
pub fn discard(&self) -> Option<$id> {
let mut inner = self.inner.lock();
inner.1.take()
}
impl<T: Clone + ToOwned> From<T> for Draft<T> {
fn from(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((data, None))),
}
impl From<$id> for Draft<$id> {
fn from(data: $id) -> Self {
Draft {
inner: Arc::new(Mutex::new((data, None))),
}
}
}
};
}
}
// draft_define!(IClash);
draft_define!(IClashTemp);
draft_define!(IProfiles);
draft_define!(IRuntime);
draft_define!(IVerge);
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data.
/// # Type Parameters
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
///
/// # Methods
/// - `data_mut`: Returns a mutable reference to the committed data.
/// - `data_ref`: Returns an immutable reference to the committed data.
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
/// - `discard`: Discards the draft data and returns it if it existed.
impl<T: Clone + ToOwned> Draft<Box<T>> {
/// 可写正式数据
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
}
/// 返回正式数据的只读视图(不包含草稿)
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
}
/// 创建或获取草稿并返回可写引用
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
let guard = self.inner.upgradable_read();
if guard.1.is_none() {
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
guard.1 = Some(guard.0.clone());
return RwLockWriteGuard::map(guard, |inner| inner.1.as_mut().unwrap());
}
// 已存在草稿,升级为写锁映射
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
inner.1.as_mut().unwrap()
})
}
/// 零拷贝只读视图:返回草稿(若存在)或正式值
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| {
inner.1.as_ref().unwrap_or(&inner.0)
})
}
/// 提交草稿,返回旧正式数据
pub fn apply(&self) -> Option<Box<T>> {
let mut inner = self.inner.write();
inner
.1
.take()
.map(|draft| std::mem::replace(&mut inner.0, draft))
}
/// 丢弃草稿,返回被丢弃的草稿
pub fn discard(&self) -> Option<Box<T>> {
self.inner.write().1.take()
}
}
#[test]
fn test_draft() {
let verge = IVerge {
fn test_draft_box() {
use super::IVerge;
// 1. 创建 Draft<Box<IVerge>>
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..IVerge::default()
};
});
let draft = Draft::from(verge);
assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false));
// 2. 读取正式数据(data_mut)
{
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert_eq!(draft.draft().enable_tun_mode, Some(false));
// 3. 初次获取草稿(draft_mut 会自动 clone 一份)
{
let draft_view = draft.draft_mut();
assert_eq!(draft_view.enable_auto_launch, Some(true));
assert_eq!(draft_view.enable_tun_mode, Some(false));
}
let mut d = draft.draft();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
drop(d);
// 4. 修改草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
}
assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false));
// 正式数据未变
assert_eq!(draft.data_mut().enable_auto_launch, Some(true));
assert_eq!(draft.data_mut().enable_tun_mode, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true));
// 草稿已变
{
let latest = draft.latest_ref();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(true));
}
assert_eq!(draft.latest().enable_auto_launch, Some(false));
assert_eq!(draft.latest().enable_tun_mode, Some(true));
// 5. 提交草稿
assert!(draft.apply().is_some()); // 第一次提交应有返回
assert!(draft.apply().is_none()); // 第二次提交返回 None
assert!(draft.apply().is_some());
assert!(draft.apply().is_none());
// 正式数据已更新
{
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.data().enable_tun_mode, Some(true));
// 6. 新建并修改下一轮草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(true);
}
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true));
// 7. 丢弃草稿
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
assert!(draft.discard().is_none()); // 再次丢弃返回 None
let mut d = draft.draft();
d.enable_auto_launch = Some(true);
drop(d);
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert!(draft.discard().is_some());
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert!(draft.discard().is_none());
assert_eq!(draft.draft().enable_auto_launch, Some(false));
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
}
+2 -2
View File
@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
// Encrypt data
let ciphertext = cipher
.encrypt(nonce.as_slice().into(), data.as_bytes())
.map_err(|e| format!("Encryption failed: {}", e))?;
.map_err(|e| format!("Encryption failed: {e}"))?;
// Concatenate nonce and ciphertext and encode them in base64
let mut combined = nonce;
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
// Decrypt data
let plaintext = cipher
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {}", e))?;
.map_err(|e| format!("Decryption failed: {e}"))?;
String::from_utf8(plaintext).map_err(|e| e.into())
}
+26 -10
View File
@@ -186,29 +186,37 @@ impl PrfItem {
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
Config::profiles().data().append_item(merge_item.clone())?;
Config::profiles()
.data_mut()
.append_item(merge_item.clone())?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
Config::profiles().data().append_item(script_item.clone())?;
Config::profiles()
.data_mut()
.append_item(script_item.clone())?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
Config::profiles().data().append_item(rules_item.clone())?;
Config::profiles()
.data_mut()
.append_item(rules_item.clone())?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
Config::profiles()
.data()
.data_mut()
.append_item(proxies_item.clone())?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
Config::profiles().data().append_item(groups_item.clone())?;
Config::profiles()
.data_mut()
.append_item(groups_item.clone())?;
groups = groups_item.uid;
}
Ok(PrfItem {
@@ -366,29 +374,37 @@ impl PrfItem {
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
Config::profiles().data().append_item(merge_item.clone())?;
Config::profiles()
.data_mut()
.append_item(merge_item.clone())?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
Config::profiles().data().append_item(script_item.clone())?;
Config::profiles()
.data_mut()
.append_item(script_item.clone())?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
Config::profiles().data().append_item(rules_item.clone())?;
Config::profiles()
.data_mut()
.append_item(rules_item.clone())?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
Config::profiles()
.data()
.data_mut()
.append_item(proxies_item.clone())?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
Config::profiles().data().append_item(groups_item.clone())?;
Config::profiles()
.data_mut()
.append_item(groups_item.clone())?;
groups = groups_item.uid;
}
+206 -5
View File
@@ -3,7 +3,7 @@ use crate::utils::{dirs, help};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::{fs, io::Write};
use std::{collections::HashSet, fs, io::Write};
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -15,6 +15,14 @@ pub struct IProfiles {
pub items: Option<Vec<PrfItem>>,
}
/// 清理结果
#[derive(Debug, Clone)]
pub struct CleanupResult {
pub total_files: usize,
pub deleted_files: Vec<String>,
pub failed_deletions: Vec<String>,
}
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
@@ -123,9 +131,9 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path)
.with_context(|| format!("failed to create file \"{}\"", file))?
.with_context(|| format!("failed to create file \"{file}\""))?
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{}\"", file))?;
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
if self.current.is_none()
@@ -226,9 +234,9 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path)
.with_context(|| format!("failed to create file \"{}\"", file))?
.with_context(|| format!("failed to create file \"{file}\""))?
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{}\"", file))?;
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
break;
@@ -485,4 +493,197 @@ impl IProfiles {
.collect()
})
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
pub fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
let profiles_dir = dirs::app_profiles_dir()?;
if !profiles_dir.exists() {
return Ok(CleanupResult {
total_files: 0,
deleted_files: vec![],
failed_deletions: vec![],
});
}
// 获取所有 active profile 的文件名集合
let active_files = self.get_all_active_files();
// 添加全局扩展配置文件到保护列表
let protected_files = self.get_protected_global_files();
// 扫描 profiles 目录下的所有文件
let mut total_files = 0;
let mut deleted_files = vec![];
let mut failed_deletions = vec![];
for entry in std::fs::read_dir(&profiles_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
total_files += 1;
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
// 检查是否为活跃文件
if !active_files.contains(file_name) {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
}
}
}
}
}
let result = CleanupResult {
total_files,
deleted_files,
failed_deletions,
};
log::info!(
target: "app",
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
result.total_files,
result.deleted_files.len(),
result.failed_deletions.len()
);
Ok(result)
}
/// 不删除全局扩展配置
fn get_protected_global_files(&self) -> HashSet<String> {
let mut protected_files = HashSet::new();
protected_files.insert("Merge.yaml".to_string());
protected_files.insert("Script.js".to_string());
protected_files
}
/// 获取所有 active profile 关联的文件名
fn get_all_active_files(&self) -> HashSet<String> {
let mut active_files = HashSet::new();
if let Some(items) = &self.items {
for item in items {
// 收集所有类型 profile 的文件
if let Some(file) = &item.file {
active_files.insert(file.clone());
}
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
if let Some(itype) = &item.itype {
if itype == "remote" || itype == "local" {
if let Some(option) = &item.option {
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge {
if let Ok(merge_item) = self.get_item(merge_uid) {
if let Some(file) = &merge_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(script_uid) = &option.script {
if let Ok(script_item) = self.get_item(script_uid) {
if let Some(file) = &script_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(rules_uid) = &option.rules {
if let Ok(rules_item) = self.get_item(rules_uid) {
if let Some(file) = &rules_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(proxies_uid) = &option.proxies {
if let Ok(proxies_item) = self.get_item(proxies_uid) {
if let Some(file) = &proxies_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(groups_uid) = &option.groups {
if let Ok(groups_item) = self.get_item(groups_uid) {
if let Some(file) = &groups_item.file {
active_files.insert(file.clone());
}
}
}
}
}
}
}
}
active_files
}
/// 检查文件名是否符合 profile 文件的命名规则
fn is_profile_file(filename: &str) -> bool {
// 匹配各种 profile 文件格式
// R12345678.yaml (remote)
// L12345678.yaml (local)
// m12345678.yaml (merge)
// s12345678.js (script)
// r12345678.yaml (rules)
// p12345678.yaml (proxies)
// g12345678.yaml (groups)
let patterns = [
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
r"^s[a-zA-Z0-9]+\.js$", // Script files
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
];
patterns.iter().any(|pattern| {
regex::Regex::new(pattern)
.map(|re| re.is_match(filename))
.unwrap_or(false)
})
}
pub fn auto_cleanup(&self) -> Result<()> {
match self.cleanup_orphaned_files() {
Ok(result) => {
if !result.deleted_files.is_empty() {
log::info!(
target: "app",
"自动清理完成,删除了 {} 个冗余文件",
result.deleted_files.len()
);
}
Ok(())
}
Err(e) => {
log::warn!(target: "app", "自动清理失败: {e}");
Ok(())
}
}
}
}
+100 -3
View File
@@ -1,6 +1,7 @@
use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
utils::{dirs, help, i18n},
logging,
utils::{dirs, help, i18n, logging::Type},
};
use anyhow::Result;
use log::LevelFilter;
@@ -200,6 +201,9 @@ pub struct IVerge {
/// 自动进入轻量模式的延迟(分钟)
pub auto_light_weight_minutes: Option<u64>,
/// 启用代理页面自动滚动
pub enable_hover_jump_navigator: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
@@ -229,6 +233,93 @@ pub struct IVergeTheme {
}
impl IVerge {
/// 有效的clash核心名称
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
/// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> {
let config_path = dirs::verge_path()?;
let mut config = match help::read_yaml::<IVerge>(&config_path) {
Ok(config) => config,
Err(_) => Self::template(),
};
let mut needs_fix = false;
if let Some(ref core) = config.clash_core {
let core_str = core.trim();
if core_str.is_empty() || !Self::VALID_CLASH_CORES.contains(&core_str) {
logging!(
warn,
Type::Config,
true,
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
core
);
config.clash_core = Some("verge-mihomo".to_string());
needs_fix = true;
}
} else {
logging!(
info,
Type::Config,
true,
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
);
config.clash_core = Some("verge-mihomo".to_string());
needs_fix = true;
}
// 修正后保存配置
if needs_fix {
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
logging!(
info,
Type::Config,
true,
"配置文件修正完成,需要重新加载配置"
);
Self::reload_config_after_fix(config)?;
} else {
logging!(
info,
Type::Config,
true,
"clash_core配置验证通过: {:?}",
config.clash_core
);
}
Ok(())
}
/// 配置修正后重新加载配置
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
use crate::config::Config;
let config_draft = Config::verge();
*config_draft.draft_mut() = Box::new(updated_config.clone());
config_draft.apply();
logging!(
info,
Type::Config,
true,
"内存配置已强制更新,新的clash_core: {:?}",
updated_config.clash_core
);
Ok(())
}
pub fn get_valid_clash_core(&self) -> String {
self.clash_core
.clone()
.unwrap_or_else(|| "verge-mihomo".to_string())
}
fn get_system_language() -> String {
let sys_lang = sys_locale::get_locale()
.unwrap_or_else(|| String::from("en"))
@@ -275,6 +366,7 @@ impl IVerge {
tun_tray_icon: Some(false),
enable_auto_launch: Some(false),
enable_silent_start: Some(false),
enable_hover_jump_navigator: Some(true),
enable_system_proxy: Some(false),
proxy_auto_config: Some(false),
pac_file_content: Some(DEFAULT_PAC.into()),
@@ -299,7 +391,7 @@ impl IVerge {
auto_close_connection: Some(true),
auto_check_update: Some(true),
enable_builtin_enhanced: Some(true),
auto_log_clean: Some(3),
auto_log_clean: Some(2),
webdav_url: None,
webdav_username: None,
webdav_password: None,
@@ -351,6 +443,7 @@ impl IVerge {
patch!(enable_tun_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_hover_jump_navigator);
patch!(enable_random_port);
#[cfg(not(target_os = "windows"))]
patch!(verge_redir_port);
@@ -492,11 +585,14 @@ pub struct IVergeResponse {
pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
}
impl From<IVerge> for IVergeResponse {
fn from(verge: IVerge) -> Self {
// 先获取验证后的clash_core值,避免后续借用冲突
let valid_clash_core = verge.get_valid_clash_core();
Self {
app_log_level: verge.app_log_level,
language: verge.language,
@@ -528,7 +624,7 @@ impl From<IVerge> for IVergeResponse {
proxy_host: verge.proxy_host,
theme_setting: verge.theme_setting,
web_ui_list: verge.web_ui_list,
clash_core: verge.clash_core,
clash_core: Some(valid_clash_core),
hotkeys: verge.hotkeys,
auto_close_connection: verge.auto_close_connection,
auto_check_update: verge.auto_check_update,
@@ -561,6 +657,7 @@ impl From<IVerge> for IVergeResponse {
auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
service_state: verge.service_state,
}
}
+531
View File
@@ -0,0 +1,531 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::time::{timeout, Duration};
#[cfg(target_os = "linux")]
use anyhow::anyhow;
#[cfg(not(target_os = "windows"))]
use tokio::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AsyncAutoproxy {
pub enable: bool,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AsyncSysproxy {
pub enable: bool,
pub host: String,
pub port: u16,
pub bypass: String,
}
impl Default for AsyncSysproxy {
fn default() -> Self {
Self {
enable: false,
host: "127.0.0.1".to_string(),
port: 7890,
bypass: String::new(),
}
}
}
pub struct AsyncProxyQuery;
impl AsyncProxyQuery {
/// 异步获取自动代理配置(PAC)
pub async fn get_auto_proxy() -> AsyncAutoproxy {
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "异步获取自动代理失败: {e}");
AsyncAutoproxy::default()
}
Err(_) => {
log::warn!(target: "app", "异步获取自动代理超时");
AsyncAutoproxy::default()
}
}
}
/// 异步获取系统代理配置
pub async fn get_system_proxy() -> AsyncSysproxy {
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "异步获取系统代理失败: {e}");
AsyncSysproxy::default()
}
Err(_) => {
log::warn!(target: "app", "异步获取系统代理超时");
AsyncSysproxy::default()
}
}
}
#[cfg(target_os = "windows")]
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
// Windows: 从注册表读取PAC配置
tokio::task::spawn_blocking(move || -> Result<AsyncAutoproxy> {
Self::get_pac_config_from_registry()
})
.await?
}
#[cfg(target_os = "windows")]
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
.encode_utf16()
.collect::<Vec<u16>>();
let mut hkey: HKEY = ptr::null_mut();
let result =
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
if result != 0 {
log::debug!(target: "app", "无法打开注册表项");
return Ok(AsyncAutoproxy::default());
}
// 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用)
let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::<Vec<u16>>();
let mut url_buffer = vec![0u16; 1024];
let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD;
let mut url_value_type: DWORD = 0;
let url_query_result = RegQueryValueExW(
hkey,
auto_config_url_name.as_ptr(),
ptr::null_mut(),
&mut url_value_type,
url_buffer.as_mut_ptr() as *mut u8,
&mut url_buffer_size,
);
let mut pac_url = String::new();
if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 {
let end_pos = url_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(url_buffer.len());
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
log::debug!(target: "app", "从注册表读取到PAC URL: {pac_url}");
}
// 2. 检查自动检测设置是否启用
let auto_detect_name = "AutoDetect\0".encode_utf16().collect::<Vec<u16>>();
let mut auto_detect: DWORD = 0;
let mut detect_buffer_size: DWORD = 4;
let mut detect_value_type: DWORD = 0;
let detect_query_result = RegQueryValueExW(
hkey,
auto_detect_name.as_ptr(),
ptr::null_mut(),
&mut detect_value_type,
&mut auto_detect as *mut DWORD as *mut u8,
&mut detect_buffer_size,
);
RegCloseKey(hkey);
// PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用
let pac_enabled = !pac_url.is_empty()
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
if pac_enabled {
log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}");
if pac_url.is_empty() && auto_detect != 0 {
pac_url = "auto-detect".to_string();
}
Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
})
} else {
log::debug!(target: "app", "PAC配置未启用");
Ok(AsyncAutoproxy::default())
}
}
}
#[cfg(target_os = "macos")]
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
// macOS: 使用 scutil --proxy 命令
let output = Command::new("scutil").args(["--proxy"]).output().await?;
if !output.status.success() {
return Ok(AsyncAutoproxy::default());
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::debug!(target: "app", "scutil output: {stdout}");
let mut pac_enabled = false;
let mut pac_url = String::new();
// 解析 scutil 输出
for line in stdout.lines() {
let line = line.trim();
if line.contains("ProxyAutoConfigEnable") && line.contains("1") {
pac_enabled = true;
} else if line.contains("ProxyAutoConfigURLString") {
// 正确解析包含冒号的URL
// 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac"
if let Some(colon_pos) = line.find(" : ") {
pac_url = line[colon_pos + 3..].trim().to_string();
}
}
}
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
Ok(AsyncAutoproxy {
enable: pac_enabled && !pac_url.is_empty(),
url: pac_url,
})
}
#[cfg(target_os = "linux")]
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
// Linux: 检查环境变量和GNOME设置
// 首先检查环境变量
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
if !auto_proxy.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: auto_proxy,
});
}
}
// 尝试使用 gsettings 获取 GNOME 代理设置
let output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "mode"])
.output()
.await;
if let Ok(output) = output {
if output.status.success() {
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
if mode.contains("auto") {
// 获取 PAC URL
let pac_output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
.output()
.await;
if let Ok(pac_output) = pac_output {
if pac_output.status.success() {
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if !pac_url.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
});
}
}
}
}
}
}
Ok(AsyncAutoproxy::default())
}
#[cfg(target_os = "windows")]
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
// Windows: 使用注册表直接读取代理设置
tokio::task::spawn_blocking(move || -> Result<AsyncSysproxy> {
Self::get_system_proxy_from_registry()
})
.await?
}
#[cfg(target_os = "windows")]
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
.encode_utf16()
.collect::<Vec<u16>>();
let mut hkey: HKEY = ptr::null_mut();
let result =
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
if result != 0 {
return Ok(AsyncSysproxy::default());
}
// 检查代理是否启用
let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::<Vec<u16>>();
let mut proxy_enable: DWORD = 0;
let mut buffer_size: DWORD = 4;
let mut value_type: DWORD = 0;
let enable_result = RegQueryValueExW(
hkey,
proxy_enable_name.as_ptr(),
ptr::null_mut(),
&mut value_type,
&mut proxy_enable as *mut DWORD as *mut u8,
&mut buffer_size,
);
if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 {
RegCloseKey(hkey);
return Ok(AsyncSysproxy::default());
}
// 读取代理服务器设置
let proxy_server_name = "ProxyServer\0".encode_utf16().collect::<Vec<u16>>();
let mut buffer = vec![0u16; 1024];
let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD;
let mut value_type: DWORD = 0;
let server_result = RegQueryValueExW(
hkey,
proxy_server_name.as_ptr(),
ptr::null_mut(),
&mut value_type,
buffer.as_mut_ptr() as *mut u8,
&mut buffer_size,
);
let mut proxy_server = String::new();
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
}
// 读取代理绕过列表
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
let mut bypass_buffer = vec![0u16; 1024];
let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD;
let mut bypass_value_type: DWORD = 0;
let override_result = RegQueryValueExW(
hkey,
proxy_override_name.as_ptr(),
ptr::null_mut(),
&mut bypass_value_type,
bypass_buffer.as_mut_ptr() as *mut u8,
&mut bypass_buffer_size,
);
let mut bypass_list = String::new();
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
let end_pos = bypass_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(bypass_buffer.len());
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
}
RegCloseKey(hkey);
if !proxy_server.is_empty() {
// 解析服务器地址和端口
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
let host = proxy_server[..colon_pos].to_string();
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
(host, port)
} else {
(proxy_server, 8080)
};
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}");
Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: bypass_list,
})
} else {
Ok(AsyncSysproxy::default())
}
}
}
#[cfg(target_os = "macos")]
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
let output = Command::new("scutil").args(["--proxy"]).output().await?;
if !output.status.success() {
return Ok(AsyncSysproxy::default());
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::debug!(target: "app", "scutil proxy output: {stdout}");
let mut http_enabled = false;
let mut http_host = String::new();
let mut http_port = 8080u16;
let mut exceptions = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.contains("HTTPEnable") && line.contains("1") {
http_enabled = true;
} else if line.contains("HTTPProxy") && !line.contains("Port") {
if let Some(host_part) = line.split(':').nth(1) {
http_host = host_part.trim().to_string();
}
} else if line.contains("HTTPPort") {
if let Some(port_part) = line.split(':').nth(1) {
if let Ok(port) = port_part.trim().parse::<u16>() {
http_port = port;
}
}
} else if line.contains("ExceptionsList") {
// 解析异常列表
if let Some(list_part) = line.split(':').nth(1) {
let list = list_part.trim();
if !list.is_empty() {
exceptions.push(list.to_string());
}
}
}
}
Ok(AsyncSysproxy {
enable: http_enabled && !http_host.is_empty(),
host: http_host,
port: http_port,
bypass: exceptions.join(","),
})
}
#[cfg(target_os = "linux")]
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
// Linux: 检查环境变量和桌面环境设置
// 首先检查环境变量
if let Ok(http_proxy) = std::env::var("http_proxy") {
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
return Ok(proxy_info);
}
}
if let Ok(https_proxy) = std::env::var("https_proxy") {
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
return Ok(proxy_info);
}
}
// 尝试使用 gsettings 获取 GNOME 代理设置
let mode_output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "mode"])
.output()
.await;
if let Ok(mode_output) = mode_output {
if mode_output.status.success() {
let mode = String::from_utf8_lossy(&mode_output.stdout)
.trim()
.to_string();
if mode.contains("manual") {
// 获取HTTP代理设置
let host_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "host"])
.output()
.await;
let port_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "port"])
.output()
.await;
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
if host_output.status.success() && port_output.status.success() {
let host = String::from_utf8_lossy(&host_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
let port = String::from_utf8_lossy(&port_output.stdout)
.trim()
.parse::<u16>()
.unwrap_or(8080);
if !host.is_empty() {
return Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: String::new(),
});
}
}
}
}
}
}
Ok(AsyncSysproxy::default())
}
#[cfg(target_os = "linux")]
fn parse_proxy_url(proxy_url: &str) -> Result<AsyncSysproxy> {
// 解析形如 "http://proxy.example.com:8080" 的URL
let url = proxy_url.trim();
// 移除协议前缀
let url = if let Some(stripped) = url.strip_prefix("http://") {
stripped
} else if let Some(stripped) = url.strip_prefix("https://") {
stripped
} else {
url
};
// 解析主机和端口
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
let host = url[..colon_pos].to_string();
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
(host, port)
} else {
(url.to_string(), 8080)
};
if host.is_empty() {
return Err(anyhow!("无效的代理URL"));
}
Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: std::env::var("no_proxy").unwrap_or_default(),
})
}
}
+4 -7
View File
@@ -78,7 +78,7 @@ impl WebDavClient {
if let Some(cfg) = lock.as_ref() {
cfg.clone()
} else {
let verge = Config::verge().latest().clone();
let verge = Config::verge().latest_ref().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
@@ -108,10 +108,7 @@ impl WebDavClient {
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout()))
.user_agent(format!(
"clash-verge/{} ({} WebDAV-Client)",
APP_VERSION, OS
))
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
.redirect(reqwest::redirect::Policy::custom(|attempt| {
// 允许所有请求类型的重定向,包括PUT
if attempt.previous().len() >= 5 {
@@ -177,7 +174,7 @@ impl WebDavClient {
}
Ok(Err(e)) => {
log::warn!("Upload failed, retrying once: {}", e);
log::warn!("Upload failed, retrying once: {e}");
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -237,7 +234,7 @@ impl WebDavClient {
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_file_name = format!("{OS}-backup-{now}.zip");
let zip_path = temp_dir().join(&zip_file_name);
let file = fs::File::create(&zip_path)?;
+333 -29
View File
@@ -1,5 +1,3 @@
#[cfg(target_os = "macos")]
use crate::core::tray::Tray;
use crate::{
config::*,
core::{
@@ -54,7 +52,7 @@ impl fmt::Display for RunningMode {
}
}
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
use crate::config::IVerge;
impl CoreManager {
/// 检查文件是否为脚本文件
@@ -140,14 +138,14 @@ impl CoreManager {
/// 使用默认配置
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
*Config::runtime().draft() = IRuntime {
config: Some(Config::clash().latest().0.clone()),
*Config::runtime().draft_mut() = Box::new(IRuntime {
config: Some(Config::clash().latest_ref().0.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
};
});
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
&Config::clash().latest_ref().0,
Some("# Clash Verge Runtime"),
)?;
handle::Handle::notice_message(msg_type, msg_content);
@@ -174,7 +172,7 @@ impl CoreManager {
// 检查文件是否存在
if !std::path::Path::new(config_path).exists() {
let error_msg = format!("File not found: {}", config_path);
let error_msg = format!("File not found: {config_path}");
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
return Ok((false, error_msg));
}
@@ -249,8 +247,7 @@ impl CoreManager {
config_path
);
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -287,7 +284,7 @@ impl CoreManager {
} else if !stderr.is_empty() {
stderr.to_string()
} else if let Some(code) = output.status.code() {
format!("验证进程异常退出,退出码: {}", code)
format!("验证进程异常退出,退出码: {code}")
} else {
"验证进程被终止".to_string()
};
@@ -308,7 +305,7 @@ impl CoreManager {
let content = match std::fs::read_to_string(config_path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {}", err);
let error_msg = format!("Failed to read file: {err}");
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
return Ok((false, error_msg));
}
@@ -322,7 +319,7 @@ impl CoreManager {
}
Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {}", err);
let error_msg = format!("YAML syntax error: {err}");
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
Ok((false, error_msg))
}
@@ -334,7 +331,7 @@ impl CoreManager {
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {}", err);
let error_msg = format!("Failed to read script file: {err}");
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg));
@@ -367,7 +364,7 @@ impl CoreManager {
Ok((true, String::new()))
}
Err(err) => {
let error_msg = format!("Script syntax error: {}", err);
let error_msg = format!("Script syntax error: {err}");
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
Ok((false, error_msg))
@@ -436,17 +433,313 @@ impl CoreManager {
}
impl CoreManager {
/// 清理多余的 mihomo 进程
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
// 获取当前管理的进程 PID
let current_pid = {
let child_guard = self.child_sidecar.lock().await;
child_guard.as_ref().map(|child| child.pid())
};
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
// 并行查找所有目标进程
let mut process_futures = Vec::new();
for &target in &target_processes {
let process_name = if cfg!(windows) {
format!("{target}.exe")
} else {
target.to_string()
};
process_futures.push(self.find_processes_by_name(process_name, target));
}
let process_results = futures::future::join_all(process_futures).await;
// 收集所有需要终止的进程PID
let mut pids_to_kill = Vec::new();
for result in process_results {
match result {
Ok((pids, process_name)) => {
for pid in pids {
// 跳过当前管理的进程
if let Some(current) = current_pid {
if pid == current {
logging!(
debug,
Type::Core,
true,
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
);
continue;
}
}
pids_to_kill.push((pid, process_name.clone()));
}
}
Err(e) => {
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
}
}
}
if pids_to_kill.is_empty() {
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
return Ok(());
}
let mut kill_futures = Vec::new();
for (pid, process_name) in &pids_to_kill {
kill_futures.push(self.kill_process_with_verification(*pid, process_name.clone()));
}
let kill_results = futures::future::join_all(kill_futures).await;
let killed_count = kill_results.into_iter().filter(|&success| success).count();
if killed_count > 0 {
logging!(
info,
Type::Core,
true,
"清理完成,共终止了 {} 个多余的 mihomo 进程",
killed_count
);
}
Ok(())
}
/// 根据进程名查找进程PID列
async fn find_processes_by_name(
&self,
process_name: String,
_target: &str,
) -> Result<(Vec<u32>, String)> {
#[cfg(windows)]
{
use std::mem;
use winapi::um::handleapi::CloseHandle;
use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
};
use winapi::um::winnt::HANDLE;
let process_name_clone = process_name.clone();
let pids = tokio::task::spawn_blocking(move || -> Result<Vec<u32>> {
let mut pids = Vec::new();
unsafe {
// 创建进程快照
let snapshot: HANDLE = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
return Err(anyhow::anyhow!("Failed to create process snapshot"));
}
let mut pe32: PROCESSENTRY32W = mem::zeroed();
pe32.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
// 获取第一个进程
if Process32FirstW(snapshot, &mut pe32) != 0 {
loop {
// 将宽字符转换为String
let end_pos = pe32
.szExeFile
.iter()
.position(|&x| x == 0)
.unwrap_or(pe32.szExeFile.len());
let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]);
// 检查进程名是否匹配
if exe_file.eq_ignore_ascii_case(&process_name_clone) {
pids.push(pe32.th32ProcessID);
}
if Process32NextW(snapshot, &mut pe32) == 0 {
break;
}
}
}
// 关闭句柄
CloseHandle(snapshot);
}
Ok(pids)
})
.await??;
Ok((pids, process_name))
}
#[cfg(not(windows))]
{
let output = if cfg!(target_os = "macos") {
tokio::process::Command::new("pgrep")
.arg(&process_name)
.output()
.await?
} else {
// Linux
tokio::process::Command::new("pidof")
.arg(&process_name)
.output()
.await?
};
if !output.status.success() {
return Ok((Vec::new(), process_name));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut pids = Vec::new();
// Unix系统直接解析PID列表
for pid_str in stdout.split_whitespace() {
if let Ok(pid) = pid_str.parse::<u32>() {
pids.push(pid);
}
}
Ok((pids, process_name))
}
}
/// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效
async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool {
logging!(
info,
Type::Core,
true,
"尝试终止进程: {} (PID: {})",
process_name,
pid
);
#[cfg(windows)]
let success = {
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
tokio::task::spawn_blocking(move || -> bool {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
if process_handle.is_null() {
return false;
}
let result = TerminateProcess(process_handle, 1);
CloseHandle(process_handle);
result != 0
}
})
.await
.unwrap_or(false)
};
#[cfg(not(windows))]
let success = {
tokio::process::Command::new("kill")
.args(["-9", &pid.to_string()])
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false)
};
if success {
// 短暂等待并验证进程是否真正终止
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let still_running = self.is_process_running(pid).await.unwrap_or(false);
if still_running {
logging!(
warn,
Type::Core,
true,
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
process_name,
pid
);
false
} else {
logging!(
info,
Type::Core,
true,
"成功终止进程: {} (PID: {})",
process_name,
pid
);
true
}
} else {
logging!(
warn,
Type::Core,
true,
"无法终止进程: {} (PID: {})",
process_name,
pid
);
false
}
}
/// Windows API检查进程
async fn is_process_running(&self, pid: u32) -> Result<bool> {
#[cfg(windows)]
{
use winapi::shared::minwindef::DWORD;
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::GetExitCodeProcess;
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
let result = tokio::task::spawn_blocking(move || -> Result<bool> {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if process_handle.is_null() {
return Ok(false);
}
let mut exit_code: DWORD = 0;
let result = GetExitCodeProcess(process_handle, &mut exit_code);
CloseHandle(process_handle);
if result == 0 {
return Ok(false);
}
Ok(exit_code == 259)
}
})
.await?;
result
}
#[cfg(not(windows))]
{
let output = tokio::process::Command::new("ps")
.args(["-p", &pid.to_string()])
.output()
.await?;
Ok(output.status.success() && !output.stdout.is_empty())
}
}
async fn start_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by sidecar");
let config_file = &Config::generate_file(ConfigType::Run)?;
let app_handle = handle::Handle::global()
.app_handle()
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
let clash_core = Config::verge()
.latest()
.clash_core
.clone()
.unwrap_or("verge-mihomo".to_string());
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
@@ -455,7 +748,7 @@ impl CoreManager {
let now = Local::now();
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
let log_path = service_log_dir.join(format!("sidecar_{}.log", timestamp));
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
let mut log_file = File::create(log_path)?;
@@ -571,7 +864,7 @@ impl CoreManager {
let mut state = service::ServiceState::get();
if !state.prefer_sidecar {
state.prefer_sidecar = true;
state.last_error = Some(format!("通过服务启动核心失败: {}", e));
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
if let Err(save_err) = state.save() {
logging!(
error,
@@ -590,6 +883,17 @@ impl CoreManager {
pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core");
// 应用启动时先清理任何遗留的 mihomo 进程
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
logging!(
warn,
Type::Core,
true,
"应用初始化时清理多余 mihomo 进程失败: {}",
e
);
}
let mut core_started_successfully = false;
if service::is_service_available().await.is_ok() {
@@ -737,9 +1041,8 @@ impl CoreManager {
}
logging!(trace, Type::Core, "Initied core logic completed");
#[cfg(target_os = "macos")]
logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
// #[cfg(target_os = "macos")]
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
Ok(())
}
@@ -792,6 +1095,7 @@ impl CoreManager {
/// 重启内核
pub async fn restart_core(&self) -> Result<()> {
self.stop_core().await?;
self.start_core().await?;
Ok(())
}
@@ -804,15 +1108,15 @@ impl CoreManager {
return Err(error_message.to_string());
}
let core: &str = &clash_core.clone().unwrap();
if !CLASH_CORES.contains(&core) {
let error_message = format!("Clash core invalid name: {}", core);
if !IVerge::VALID_CLASH_CORES.contains(&core) {
let error_message = format!("Clash core invalid name: {core}");
logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message);
}
Config::verge().draft().clash_core = clash_core.clone();
Config::verge().draft_mut().clash_core = clash_core.clone();
Config::verge().apply();
logging_error!(Type::Core, true, Config::verge().latest().save_file());
logging_error!(Type::Core, true, Config::verge().latest_ref().save_file());
let run_path = Config::generate_file(ConfigType::Run).map_err(|e| {
let msg = e.to_string();
+582
View File
@@ -0,0 +1,582 @@
use parking_lot::RwLock;
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep, timeout, Duration};
use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery;
use crate::logging_error;
use crate::utils::logging::Type;
use once_cell::sync::Lazy;
use sysproxy::{Autoproxy, Sysproxy};
#[derive(Debug, Clone)]
pub enum ProxyEvent {
/// 配置变更事件
ConfigChanged,
/// 强制检查代理状态
#[allow(dead_code)]
ForceCheck,
/// 启用系统代理
#[allow(dead_code)]
EnableProxy,
/// 禁用系统代理
#[allow(dead_code)]
DisableProxy,
/// 切换到PAC模式
#[allow(dead_code)]
SwitchToPac,
/// 切换到HTTP代理模式
#[allow(dead_code)]
SwitchToHttp,
/// 应用启动事件
AppStarted,
/// 应用关闭事件
#[allow(dead_code)]
AppStopping,
}
#[derive(Debug, Clone)]
pub struct ProxyState {
pub sys_enabled: bool,
pub pac_enabled: bool,
pub auto_proxy: Autoproxy,
pub sys_proxy: Sysproxy,
pub last_updated: std::time::Instant,
pub is_healthy: bool,
}
impl Default for ProxyState {
fn default() -> Self {
Self {
sys_enabled: false,
pac_enabled: false,
auto_proxy: Autoproxy {
enable: false,
url: "".to_string(),
},
sys_proxy: Sysproxy {
enable: false,
host: "127.0.0.1".to_string(),
port: 7890,
bypass: "".to_string(),
},
last_updated: std::time::Instant::now(),
is_healthy: true,
}
}
}
pub struct EventDrivenProxyManager {
state: Arc<RwLock<ProxyState>>,
event_sender: mpsc::UnboundedSender<ProxyEvent>,
query_sender: mpsc::UnboundedSender<QueryRequest>,
}
#[derive(Debug)]
struct QueryRequest {
response_tx: oneshot::Sender<Autoproxy>,
}
// 配置结构体移到外部
struct ProxyConfig {
sys_enabled: bool,
pac_enabled: bool,
guard_enabled: bool,
}
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
impl EventDrivenProxyManager {
pub fn global() -> &'static EventDrivenProxyManager {
&PROXY_MANAGER
}
fn new() -> Self {
let state = Arc::new(RwLock::new(ProxyState::default()));
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (query_tx, query_rx) = mpsc::unbounded_channel();
Self::start_event_loop(state.clone(), event_rx, query_rx);
Self {
state,
event_sender: event_tx,
query_sender: query_tx,
}
}
/// 获取自动代理配置(缓存)
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
self.state.read().auto_proxy.clone()
}
/// 异步获取最新的自动代理配置
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
let (tx, rx) = oneshot::channel();
let query = QueryRequest { response_tx: tx };
if self.query_sender.send(query).is_err() {
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
return self.get_auto_proxy_cached();
}
match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result,
_ => {
log::warn!(target: "app", "查询超时,返回缓存数据");
self.get_auto_proxy_cached()
}
}
}
/// 通知配置变更
pub fn notify_config_changed(&self) {
self.send_event(ProxyEvent::ConfigChanged);
}
/// 通知应用启动
pub fn notify_app_started(&self) {
self.send_event(ProxyEvent::AppStarted);
}
/// 通知应用即将关闭
#[allow(dead_code)]
pub fn notify_app_stopping(&self) {
self.send_event(ProxyEvent::AppStopping);
}
/// 启用系统代理
#[allow(dead_code)]
pub fn enable_proxy(&self) {
self.send_event(ProxyEvent::EnableProxy);
}
/// 禁用系统代理
#[allow(dead_code)]
pub fn disable_proxy(&self) {
self.send_event(ProxyEvent::DisableProxy);
}
/// 强制检查代理状态
#[allow(dead_code)]
pub fn force_check(&self) {
self.send_event(ProxyEvent::ForceCheck);
}
fn send_event(&self, event: ProxyEvent) {
if let Err(e) = self.event_sender.send(event) {
log::error!(target: "app", "发送代理事件失败: {e}");
}
}
fn start_event_loop(
state: Arc<RwLock<ProxyState>>,
mut event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) {
tokio::spawn(async move {
log::info!(target: "app", "事件驱动代理管理器启动");
loop {
tokio::select! {
event = event_rx.recv() => {
match event {
Some(event) => {
log::debug!(target: "app", "处理代理事件: {event:?}");
Self::handle_event(&state, event).await;
}
None => {
log::info!(target: "app", "事件通道关闭,代理管理器停止");
break;
}
}
}
query = query_rx.recv() => {
match query {
Some(query) => {
let result = Self::handle_query(&state).await;
let _ = query.response_tx.send(result);
}
None => {
log::info!(target: "app", "查询通道关闭");
break;
}
}
}
}
}
});
}
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
match event {
ProxyEvent::ConfigChanged | ProxyEvent::ForceCheck => {
Self::update_proxy_config(state).await;
}
ProxyEvent::EnableProxy => {
Self::enable_system_proxy(state).await;
}
ProxyEvent::DisableProxy => {
Self::disable_system_proxy(state).await;
}
ProxyEvent::SwitchToPac => {
Self::switch_proxy_mode(state, true).await;
}
ProxyEvent::SwitchToHttp => {
Self::switch_proxy_mode(state, false).await;
}
ProxyEvent::AppStarted => {
Self::initialize_proxy_state(state).await;
}
ProxyEvent::AppStopping => {
log::info!(target: "app", "清理代理状态");
}
}
}
async fn handle_query(state: &Arc<RwLock<ProxyState>>) -> Autoproxy {
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.auto_proxy = auto_proxy.clone();
});
auto_proxy
}
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "初始化代理状态");
let config = Self::get_proxy_config();
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.sys_enabled = config.sys_enabled;
s.pac_enabled = config.pac_enabled;
s.auto_proxy = auto_proxy;
s.sys_proxy = sys_proxy;
s.is_healthy = true;
});
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
}
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
log::debug!(target: "app", "更新代理配置");
let config = Self::get_proxy_config();
Self::update_state_timestamp(state, |s| {
s.sys_enabled = config.sys_enabled;
s.pac_enabled = config.pac_enabled;
});
if config.guard_enabled && config.sys_enabled {
Self::check_and_restore_proxy(state).await;
}
}
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
let (sys_enabled, pac_enabled) = {
let s = state.read();
(s.sys_enabled, s.pac_enabled)
};
if !sys_enabled {
return;
}
log::debug!(target: "app", "检查代理状态");
if pac_enabled {
Self::check_and_restore_pac_proxy(state).await;
} else {
Self::check_and_restore_sys_proxy(state).await;
}
}
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
let current = Self::get_auto_proxy_with_timeout().await;
let expected = Self::get_expected_pac_config();
Self::update_state_timestamp(state, |s| {
s.auto_proxy = current.clone();
});
if !current.enable || current.url != expected.url {
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
Self::restore_pac_proxy(&expected.url).await;
sleep(Duration::from_millis(500)).await;
let restored = Self::get_auto_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.is_healthy = restored.enable && restored.url == expected.url;
s.auto_proxy = restored;
});
}
}
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
let current = Self::get_sys_proxy_with_timeout().await;
let expected = Self::get_expected_sys_proxy();
Self::update_state_timestamp(state, |s| {
s.sys_proxy = current.clone();
});
if !current.enable || current.host != expected.host || current.port != expected.port {
log::info!(target: "app", "系统代理设置异常,正在恢复...");
Self::restore_sys_proxy(&expected).await;
sleep(Duration::from_millis(500)).await;
let restored = Self::get_sys_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.is_healthy = restored.enable
&& restored.host == expected.host
&& restored.port == expected.port;
s.sys_proxy = restored;
});
}
}
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "启用系统代理");
let pac_enabled = state.read().pac_enabled;
if pac_enabled {
let expected = Self::get_expected_pac_config();
Self::restore_pac_proxy(&expected.url).await;
} else {
let expected = Self::get_expected_sys_proxy();
Self::restore_sys_proxy(&expected).await;
}
Self::check_and_restore_proxy(state).await;
}
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "禁用系统代理");
#[cfg(not(target_os = "windows"))]
{
let disabled_sys = Sysproxy::default();
let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
}
}
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
if to_pac {
let disabled_sys = Sysproxy::default();
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
let expected = Self::get_expected_pac_config();
Self::restore_pac_proxy(&expected.url).await;
} else {
let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
let expected = Self::get_expected_sys_proxy();
Self::restore_sys_proxy(&expected).await;
}
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
Self::check_and_restore_proxy(state).await;
}
async fn get_auto_proxy_with_timeout() -> Autoproxy {
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
// 转换为兼容的结构
Autoproxy {
enable: async_proxy.enable,
url: async_proxy.url,
}
}
async fn get_sys_proxy_with_timeout() -> Sysproxy {
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
// 转换为兼容的结构
Sysproxy {
enable: async_proxy.enable,
host: async_proxy.host,
port: async_proxy.port,
bypass: async_proxy.bypass,
}
}
// 统一的状态更新方法
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
where
F: FnOnce(&mut ProxyState),
{
let mut state_guard = state.write();
update_fn(&mut state_guard);
state_guard.last_updated = std::time::Instant::now();
}
fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled) = {
let verge_config = Config::verge();
let verge = verge_config.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
)
};
ProxyConfig {
sys_enabled,
pac_enabled,
guard_enabled,
}
}
fn get_expected_pac_config() -> Autoproxy {
let proxy_host = {
let verge_config = Config::verge();
let verge = verge_config.latest_ref();
verge
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string())
};
let pac_port = IVerge::get_singleton_port();
Autoproxy {
enable: true,
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
}
}
fn get_expected_sys_proxy() -> Sysproxy {
let verge_config = Config::verge();
let verge = verge_config.latest_ref();
let port = verge
.verge_mixed_port
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
let proxy_host = verge
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string());
Sysproxy {
enable: true,
host: proxy_host,
port,
bypass: Self::get_bypass_config(),
}
}
fn get_bypass_config() -> String {
let (use_default, custom_bypass) = {
let verge_config = Config::verge();
let verge = verge_config.latest_ref();
(
verge.use_default_bypass.unwrap_or(true),
verge.system_proxy_bypass.clone().unwrap_or_default(),
)
};
#[cfg(target_os = "windows")]
let default_bypass = "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")]
let default_bypass =
"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")]
let default_bypass = "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>";
if custom_bypass.is_empty() {
default_bypass.to_string()
} else if use_default {
format!("{default_bypass},{custom_bypass}")
} else {
custom_bypass
}
}
async fn restore_pac_proxy(expected_url: &str) {
#[cfg(not(target_os = "windows"))]
{
let new_autoproxy = Autoproxy {
enable: true,
url: expected_url.to_string(),
};
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
}
#[cfg(target_os = "windows")]
{
Self::execute_sysproxy_command(&["pac", expected_url]).await;
}
}
async fn restore_sys_proxy(expected: &Sysproxy) {
#[cfg(not(target_os = "windows"))]
{
logging_error!(Type::System, true, expected.set_system_proxy());
}
#[cfg(target_os = "windows")]
{
let address = format!("{}:{}", expected.host, expected.port);
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await;
}
}
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: &[&str]) {
use crate::utils::dirs;
#[allow(unused_imports)] // creation_flags必须
use std::os::windows::process::CommandExt;
use tokio::process::Command;
let binary_path = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "获取服务路径失败: {e}");
return;
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe 不存在");
return;
}
let output = Command::new(sysproxy_exe)
.args(args)
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
.output()
.await;
match output {
Ok(output) => {
if !output.status.success() {
log::error!(target: "app", "执行sysproxy命令失败: {args:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
log::error!(target: "app", "sysproxy错误输出: {stderr}");
}
} else {
log::debug!(target: "app", "成功执行sysproxy命令: {args:?}");
}
}
Err(e) => {
log::error!(target: "app", "执行sysproxy命令出错: {e}");
}
}
}
}
+28 -10
View File
@@ -125,7 +125,7 @@ impl NotificationSystem {
match serde_json::to_value((status, message)) {
Ok(p) => ("verge://notice-message", Ok(p)),
Err(e) => {
log::error!("Failed to serialize NoticeMessage payload: {}", e);
log::error!("Failed to serialize NoticeMessage payload: {e}");
("verge://notice-message", Err(e))
}
}
@@ -151,9 +151,13 @@ impl NotificationSystem {
match window.emit(event_name_str, payload) {
Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {event_name_str}");
}
}
Err(e) => {
log::warn!("Failed to emit event {}: {}", event_name_str, e);
log::warn!("Failed to emit event {event_name_str}: {e}");
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
@@ -161,8 +165,7 @@ impl NotificationSystem {
const EMIT_ERROR_THRESHOLD: u64 = 10;
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
log::warn!(
"Reached {} emit errors, entering emergency mode",
EMIT_ERROR_THRESHOLD
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
);
*system.emergency_mode.write() = true;
}
@@ -171,7 +174,7 @@ impl NotificationSystem {
} else {
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
log::warn!("Skipped emitting event due to payload serialization error for {}", event_name_str);
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
}
} else {
log::warn!("No window found, skipping event emit.");
@@ -211,7 +214,7 @@ impl NotificationSystem {
match sender.send(event) {
Ok(_) => true,
Err(e) => {
log::warn!("Failed to send event to notification queue: {:?}", e);
log::warn!("Failed to send event to notification queue: {e:?}");
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*self.stats.last_error_time.write() = Some(Instant::now());
false
@@ -224,12 +227,27 @@ impl NotificationSystem {
}
fn shutdown(&mut self) {
log::info!("NotificationSystem shutdown initiated");
self.is_running = false;
self.sender = None;
if let Some(handle) = self.worker_handle.take() {
let _ = handle.join();
// 先关闭发送端,让接收端知道不会再有新消息
if let Some(sender) = self.sender.take() {
drop(sender);
}
// 设置超时避免无限等待
if let Some(handle) = self.worker_handle.take() {
match handle.join() {
Ok(_) => {
log::info!("NotificationSystem worker thread joined successfully");
}
Err(e) => {
log::error!("NotificationSystem worker thread join failed: {e:?}");
}
}
}
log::info!("NotificationSystem shutdown completed");
}
}
@@ -481,7 +499,7 @@ impl Handle {
});
if let Err(e) = thread_result {
log::error!("Failed to spawn startup errors thread: {}", e);
log::error!("Failed to spawn startup errors thread: {e}");
}
}
+92 -65
View File
@@ -1,10 +1,7 @@
use crate::utils::notification::{notify_event, NotificationEvent};
use crate::{
config::Config,
core::handle,
feat, logging, logging_error,
module::lightweight::entry_lightweight_mode,
process::AsyncHandler,
utils::{logging::Type, resolve},
config::Config, core::handle, feat, logging, logging_error,
module::lightweight::entry_lightweight_mode, utils::logging::Type,
};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
@@ -14,7 +11,7 @@ use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
current: Arc<Mutex<Vec<String>>>,
}
impl Hotkey {
@@ -28,7 +25,7 @@ impl Hotkey {
pub fn init(&self) -> Result<()> {
let verge = Config::verge();
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -38,12 +35,11 @@ impl Hotkey {
enable_global_hotkey
);
// 如果全局热键被禁用,则不注册热键
if !enable_global_hotkey {
return Ok(());
}
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
if let Some(hotkeys) = verge.latest_ref().hotkeys.as_ref() {
logging!(
debug,
Type::Hotkey,
@@ -138,14 +134,11 @@ impl Hotkey {
manager.unregister(hotkey)?;
}
let f = match func.trim() {
let app_handle_clone = app_handle.clone();
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
"open_or_close_dashboard" => {
logging!(
debug,
Type::Hotkey,
"Registering open_or_close_dashboard function"
);
|| {
let app_handle = app_handle_clone.clone();
Box::new(move || {
logging!(
debug,
Type::Hotkey,
@@ -153,53 +146,89 @@ impl Hotkey {
"=== Hotkey Dashboard Window Operation Start ==="
);
// 使用 spawn_blocking 来确保在正确的线程上执行
AsyncHandler::spawn_blocking(|| {
logging!(debug, Type::Hotkey, "Toggle dashboard window visibility");
logging!(
info,
Type::Hotkey,
true,
"Using unified WindowManager for hotkey operation (bypass debounce)"
);
// 检查窗口是否存在
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏它
if window.is_visible().unwrap_or(false) {
logging!(info, Type::Window, "Window is visible, hiding it");
let _ = window.hide();
} else {
// 如果窗口不可见,则显示它
logging!(info, Type::Window, "Window is hidden, showing it");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
} else {
// 如果窗口不存在,创建一个新窗口
logging!(
info,
Type::Window,
"Window does not exist, creating a new one"
);
resolve::create_window(true);
}
});
crate::feat::open_or_close_dashboard_hotkey();
logging!(
debug,
Type::Hotkey,
"=== Hotkey Dashboard Window Operation End ==="
);
}
notify_event(&app_handle, NotificationEvent::DashboardToggled);
})
}
"clash_mode_rule" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::change_clash_mode("rule".into());
notify_event(
&app_handle,
NotificationEvent::ClashModeChanged { mode: "Rule" },
);
})
}
"clash_mode_global" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::change_clash_mode("global".into());
notify_event(
&app_handle,
NotificationEvent::ClashModeChanged { mode: "Global" },
);
})
}
"clash_mode_direct" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::change_clash_mode("direct".into());
notify_event(
&app_handle,
NotificationEvent::ClashModeChanged { mode: "Direct" },
);
})
}
"toggle_system_proxy" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::toggle_system_proxy();
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
})
}
"toggle_tun_mode" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::toggle_tun_mode(None);
notify_event(&app_handle, NotificationEvent::TunModeToggled);
})
}
"entry_lightweight_mode" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
entry_lightweight_mode();
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
})
}
"quit" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::quit();
notify_event(&app_handle, NotificationEvent::AppQuit);
})
}
"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(None),
"entry_lightweight_mode" => || entry_lightweight_mode(),
"quit" => || feat::quit(),
#[cfg(target_os = "macos")]
"hide" => || feat::hide(),
"hide" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::hide();
notify_event(&app_handle, NotificationEvent::AppHidden);
})
}
_ => {
logging!(error, Type::Hotkey, "Invalid function: {}", func);
bail!("invalid function \"{func}\"");
@@ -220,21 +249,19 @@ impl Hotkey {
}
}
} else {
// 直接执行函数,不做任何状态检查
logging!(debug, Type::Hotkey, "Executing function directly");
// 获取全局热键状态
let is_enable_global_hotkey = Config::verge()
.latest()
.latest_ref()
.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);
} else {
use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible {
f();
@@ -289,9 +316,9 @@ impl Hotkey {
let func = iter.next();
let key = iter.next();
if func.is_some() && key.is_some() {
let func = func.unwrap().trim();
let key = key.unwrap().trim();
if let (Some(func), Some(key)) = (func, key) {
let func = func.trim();
let key = key.trim();
map.insert(key, func);
}
});
+3 -1
View File
@@ -1,6 +1,8 @@
pub mod async_proxy_query;
pub mod backup;
#[allow(clippy::module_inception)]
mod core;
pub mod event_driven_proxy;
pub mod handle;
pub mod hotkey;
pub mod service;
@@ -10,4 +12,4 @@ pub mod timer;
pub mod tray;
pub mod win_uwp;
pub use self::{core::*, timer::Timer};
pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};
+20 -25
View File
@@ -32,7 +32,7 @@ pub struct ServiceState {
impl ServiceState {
// 获取当前的服务状态
pub fn get() -> Self {
if let Some(state) = Config::verge().latest().service_state.clone() {
if let Some(state) = Config::verge().latest_ref().service_state.clone() {
return state;
}
Self::default()
@@ -41,11 +41,11 @@ impl ServiceState {
// 保存服务状态
pub fn save(&self) -> Result<()> {
let config = Config::verge();
let mut latest = config.latest().clone();
let mut latest = config.latest_ref().clone();
latest.service_state = Some(self.clone());
*config.draft() = latest;
*config.draft_mut() = latest;
config.apply();
let result = config.latest().save_file();
let result = config.latest_ref().save_file();
result
}
@@ -218,7 +218,7 @@ pub async fn reinstall_service() -> Result<()> {
Ok(())
}
Err(err) => {
let error = format!("failed to install service: {}", err);
let error = format!("failed to install service: {err}");
service_state.last_error = Some(error.clone());
service_state.prefer_sidecar = true;
service_state.save()?;
@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
Ok(())
}
Err(err) => {
let error = format!("failed to install service: {}", err);
let error = format!("failed to install service: {err}");
service_state.last_error = Some(error.clone());
service_state.prefer_sidecar = true;
service_state.save()?;
@@ -466,7 +466,7 @@ pub async fn reinstall_service() -> Result<()> {
Ok(())
}
Err(err) => {
let error = format!("failed to install service: {}", err);
let error = format!("failed to install service: {err}");
service_state.last_error = Some(error.clone());
service_state.prefer_sidecar = true;
service_state.save()?;
@@ -686,7 +686,7 @@ pub async fn check_service_needs_reinstall() -> bool {
// 检查版本和可用性
match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
/* logging!(
info,
Type::Service,
@@ -698,8 +698,7 @@ pub async fn check_service_needs_reinstall() -> bool {
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
if needs_reinstall {
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
version, REQUIRED_SERVICE_VERSION);
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
@@ -717,7 +716,7 @@ pub async fn check_service_needs_reinstall() -> bool {
// 检查服务是否可用
match is_service_available().await {
Ok(()) => {
log::info!(target: "app", "服务正在运行但版本检查失败: {}", err);
log::info!(target: "app", "服务正在运行但版本检查失败: {err}");
/* logging!(
info,
Type::Service,
@@ -742,8 +741,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");
@@ -828,8 +826,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
// 先检查服务版本,不受冷却期限制
let version_check = match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
version, REQUIRED_SERVICE_VERSION);
log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}");
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
log::warn!(target: "app", "服务版本不匹配,需要重装");
@@ -840,16 +837,14 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
}
}
Err(err) => {
log::warn!(target: "app", "无法获取服务版本: {}", err);
log::warn!(target: "app", "无法获取服务版本: {err}");
false
}
};
if version_check {
if is_service_available().await.is_ok() {
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
return start_with_existing_service(config_file).await;
}
if version_check && is_service_available().await.is_ok() {
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
return start_with_existing_service(config_file).await;
}
if !version_check {
@@ -868,7 +863,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
log::info!(target: "app", "开始重装服务");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
log::warn!(target: "app", "服务重装失败: {err}");
log::info!(target: "app", "尝试使用现有服务");
return start_with_existing_service(config_file).await;
@@ -887,7 +882,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
}
}
Err(err) => {
log::warn!(target: "app", "服务检查失败: {}", err);
log::warn!(target: "app", "服务检查失败: {err}");
}
}
@@ -896,7 +891,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
log::info!(target: "app", "服务需要重装");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
log::warn!(target: "app", "服务重装失败: {err}");
bail!("Failed to reinstall service: {}", err);
}
@@ -989,7 +984,7 @@ pub async fn force_reinstall_service() -> Result<()> {
Ok(())
}
Err(err) => {
log::error!(target: "app", "强制重装服务失败: {}", err);
log::error!(target: "app", "强制重装服务失败: {err}");
bail!("强制重装服务失败: {}", err)
}
}
+2 -2
View File
@@ -131,7 +131,7 @@ pub async fn send_ipc_request(
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
let command_type = format!("{:?}", command);
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
@@ -257,7 +257,7 @@ pub async fn send_ipc_request(
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
let command_type = format!("{:?}", command);
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
+51 -194
View File
@@ -2,25 +2,21 @@
use crate::utils::autostart as startup_shortcut;
use crate::{
config::{Config, IVerge},
core::handle::Handle,
core::{handle::Handle, EventDrivenProxyManager},
logging, logging_error,
process::AsyncHandler,
utils::logging::Type,
};
use anyhow::Result;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::sync::Arc;
#[cfg(not(target_os = "windows"))]
use sysproxy::{Autoproxy, Sysproxy};
use tauri::async_runtime::Mutex as TokioMutex;
use tauri_plugin_autostart::ManagerExt;
use tokio::time::{sleep, Duration};
pub struct Sysopt {
update_sysproxy: Arc<TokioMutex<bool>>,
reset_sysproxy: Arc<TokioMutex<bool>>,
/// record whether the guard async is running or not
guard_state: Arc<Mutex<bool>>,
}
#[cfg(target_os = "windows")]
@@ -33,10 +29,13 @@ static DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
fn get_bypass() -> String {
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
let use_default = Config::verge()
.latest_ref()
.use_default_bypass
.unwrap_or(true);
let res = {
let verge = Config::verge();
let verge = verge.latest();
let verge = verge.latest_ref();
verge.system_proxy_bypass.clone()
};
let custom_bypass = match res {
@@ -47,7 +46,7 @@ fn get_bypass() -> String {
if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
format!("{DEFAULT_BYPASS},{custom_bypass}")
} else {
custom_bypass
}
@@ -59,12 +58,15 @@ impl Sysopt {
SYSOPT.get_or_init(|| Sysopt {
update_sysproxy: Arc::new(TokioMutex::new(false)),
reset_sysproxy: Arc::new(TokioMutex::new(false)),
guard_state: Arc::new(false.into()),
})
}
pub fn init_guard_sysproxy(&self) -> Result<()> {
self.guard_proxy();
// 使用事件驱动代理管理器
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_app_started();
log::info!(target: "app", "已启用事件驱动代理守卫");
Ok(())
}
@@ -73,14 +75,14 @@ impl Sysopt {
let _lock = self.update_sysproxy.lock().await;
let port = Config::verge()
.latest()
.latest_ref()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host) = {
let verge = Config::verge();
let verge = verge.latest();
let verge = verge.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -101,12 +103,14 @@ impl Sysopt {
};
let mut auto = Autoproxy {
enable: false,
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port),
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
};
if !sys_enable {
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(());
}
@@ -115,6 +119,8 @@ impl Sysopt {
auto.enable = true;
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(());
}
@@ -123,13 +129,18 @@ impl Sysopt {
sys.enable = true;
auto.set_auto_proxy()?;
sys.set_system_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(());
}
}
#[cfg(target_os = "windows")]
{
if !sys_enable {
return self.reset_sysproxy().await;
let result = self.reset_sysproxy().await;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return result;
}
use crate::{core::handle::Handle, utils::dirs};
use anyhow::bail;
@@ -145,7 +156,7 @@ impl Sysopt {
let shell = app_handle.shell();
let output = if pac_enable {
let address = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["pac", address.as_str()])
@@ -154,7 +165,7 @@ impl Sysopt {
.unwrap();
output
} else {
let address = format!("{}:{}", proxy_host, port);
let address = format!("{proxy_host}:{port}");
let bypass = get_bypass();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
@@ -169,6 +180,8 @@ impl Sysopt {
bail!("sysproxy exe run failed");
}
}
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
Ok(())
}
@@ -180,7 +193,16 @@ impl Sysopt {
#[cfg(not(target_os = "windows"))]
{
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
let mut autoproxy = Autoproxy::get_auto_proxy()?;
let mut autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap,
Err(e) => {
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
Autoproxy {
enable: false,
url: "".to_string(),
}
}
};
sysproxy.enable = false;
autoproxy.enable = false;
autoproxy.set_auto_proxy()?;
@@ -220,7 +242,7 @@ impl Sysopt {
/// update the startup
pub fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().latest().enable_auto_launch };
let enable_auto_launch = { Config::verge().latest_ref().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false);
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
@@ -229,19 +251,17 @@ impl Sysopt {
{
if is_enable {
if let Err(e) = startup_shortcut::create_shortcut() {
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
log::error!(target: "app", "创建启动快捷方式失败: {e}");
// 如果快捷方式创建失败,回退到原来的方法
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
} else if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "删除启动快捷方式失败: {e}");
self.try_original_autostart_method(is_enable);
} else {
if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
return Ok(());
}
}
@@ -273,11 +293,11 @@ impl Sysopt {
{
match startup_shortcut::is_shortcut_enabled() {
Ok(enabled) => {
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
return Ok(enabled);
}
Err(e) => {
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
}
}
}
@@ -288,176 +308,13 @@ impl Sysopt {
match autostart_manager.is_enabled() {
Ok(status) => {
log::info!(target: "app", "Auto launch status: {}", status);
log::info!(target: "app", "Auto launch status: {status}");
Ok(status)
}
Err(e) => {
log::error!(target: "app", "Failed to get auto launch status: {}", e);
log::error!(target: "app", "Failed to get auto launch status: {e}");
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
}
}
}
fn guard_proxy(&self) {
let _lock = self.guard_state.lock();
AsyncHandler::spawn(move || async move {
// default duration is 10s
let mut wait_secs = 10u64;
loop {
sleep(Duration::from_secs(wait_secs)).await;
let (enable, guard, guard_duration, pac, proxy_host) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
verge.proxy_guard_duration.unwrap_or(10),
verge.proxy_auto_config.unwrap_or(false),
verge
.proxy_host
.clone()
.unwrap_or_else(|| String::from("127.0.0.1")),
)
};
// stop loop
if !enable || !guard {
continue;
}
// update duration
wait_secs = guard_duration;
log::debug!(target: "app", "try to guard the system proxy");
// 获取期望的代理端口
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let bypass = get_bypass();
// 检查系统代理配置
if pac {
// 检查 PAC 代理设置
let expected_url = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
let autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap,
Err(e) => {
log::error!(target: "app", "failed to get the auto proxy: {}", e);
continue;
}
};
// 检查自动代理是否启用且URL是否正确
if !autoproxy.enable || autoproxy.url != expected_url {
log::info!(target: "app", "auto proxy settings changed, restoring...");
#[cfg(not(target_os = "windows"))]
{
let new_autoproxy = Autoproxy {
enable: true,
url: expected_url,
};
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
}
#[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 = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "failed to get service path: {}", e);
continue;
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe not found");
continue;
}
let shell = app_handle.shell();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["pac", expected_url.as_str()])
.output()
.await
.unwrap();
if !output.status.success() {
log::error!(target: "app", "failed to set auto proxy");
}
}
}
} else {
// 检查常规系统代理设置
let sysproxy = match Sysproxy::get_system_proxy() {
Ok(sp) => sp,
Err(e) => {
log::error!(target: "app", "failed to get the system proxy: {}", e);
continue;
}
};
// 检查系统代理是否启用且配置是否匹配
if !sysproxy.enable || sysproxy.host != proxy_host || sysproxy.port != port {
log::info!(target: "app", "system proxy settings changed, restoring...");
#[cfg(not(target_os = "windows"))]
{
let new_sysproxy = Sysproxy {
enable: true,
host: proxy_host.clone(),
port,
bypass: bypass.clone(),
};
logging_error!(Type::System, true, new_sysproxy.set_system_proxy());
}
#[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 = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "failed to get service path: {}", e);
continue;
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe not found");
continue;
}
let address = format!("{}:{}", proxy_host, port);
let shell = app_handle.shell();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap();
if !output.status.success() {
log::error!(target: "app", "failed to set system proxy");
}
}
}
}
}
});
}
}
+4 -4
View File
@@ -91,7 +91,7 @@ impl Timer {
let cur_timestamp = chrono::Local::now().timestamp();
// Collect profiles that need immediate update
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
let profiles_to_update = if let Some(items) = Config::profiles().latest_ref().get_items() {
items
.iter()
.filter_map(|item| {
@@ -229,7 +229,7 @@ impl Timer {
fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().latest().get_items() {
if let Some(items) = Config::profiles().latest_ref().get_items() {
for item in items.iter() {
if let Some(option) = item.option.as_ref() {
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
@@ -376,7 +376,7 @@ impl Timer {
// Get the profile updated timestamp
let profiles_config = Config::profiles();
let profiles = profiles_config.latest();
let profiles = profiles_config.latest_ref();
let items = match profiles.get_items() {
Some(i) => i,
None => {
@@ -438,7 +438,7 @@ impl Timer {
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(&uid, true);
let is_current = Config::profiles().latest().current.as_ref() == Some(&uid);
let is_current = Config::profiles().latest_ref().current.as_ref() == Some(&uid);
logging!(
info,
Type::Timer,
+178 -262
View File
@@ -6,25 +6,13 @@ use crate::{
cmd,
config::Config,
feat, logging,
module::{
lightweight::{entry_lightweight_mode, is_in_lightweight_mode},
mihomo::Rate,
},
resolve,
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
Type,
};
use anyhow::Result;
#[cfg(target_os = "macos")]
use futures::StreamExt;
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::sync::Arc;
use std::{
fs,
sync::atomic::{AtomicBool, Ordering},
@@ -35,20 +23,37 @@ use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconEvent},
AppHandle, Wry,
};
#[cfg(target_os = "macos")]
use tokio::sync::broadcast;
use super::handle;
#[derive(Clone)]
struct TrayState {}
// 托盘点击防抖机制
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
}
fn should_handle_tray_click() -> bool {
let debounce_lock = get_tray_click_debounce();
let mut last_click = debounce_lock.lock();
let now = Instant::now();
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
*last_click = now;
true
} else {
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
now.duration_since(*last_click).as_millis());
false
}
}
#[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 rate_cache: Arc<Mutex<Option<Rate>>>,
last_menu_update: Mutex<Option<Instant>>,
menu_updating: AtomicBool,
}
@@ -61,7 +66,7 @@ pub struct Tray {
impl TrayState {
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().latest().clone();
let verge = Config::verge().latest_ref().clone();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon {
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
@@ -95,7 +100,7 @@ impl TrayState {
}
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().latest().clone();
let verge = Config::verge().latest_ref().clone();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon {
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
@@ -109,7 +114,7 @@ impl TrayState {
if tray_icon_colorful == "monochrome" {
(
false,
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(),
)
} else {
(
@@ -129,7 +134,7 @@ impl TrayState {
}
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().latest().clone();
let verge = Config::verge().latest_ref().clone();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon {
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
@@ -143,7 +148,7 @@ impl TrayState {
if tray_icon_colorful == "monochrome" {
(
false,
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(),
)
} else {
(
@@ -168,10 +173,6 @@ impl Tray {
#[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)),
rate_cache: Arc::new(Mutex::new(None)),
last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false),
});
@@ -184,18 +185,13 @@ impl 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 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 = { Config::verge().latest_ref().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() {
@@ -205,33 +201,38 @@ impl Tray {
Ok(())
}
/// 更新托盘菜单(带频率限制)
/// 更新托盘菜单
pub fn update_menu(&self) -> Result<()> {
// 检查是否正在更新或距离上次更新太近
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
// 调整最小更新间隔,确保状态及时刷新
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
// 检查是否已有更新任务在执行
// 检查是否正在更新
if self.menu_updating.load(Ordering::Acquire) {
log::debug!(target: "app", "托盘菜单正在更新中,跳过本次更新");
return Ok(());
}
// 检查更新频率
{
let last_update = self.last_menu_update.lock();
if let Some(last_time) = *last_update {
if last_time.elapsed() < MIN_UPDATE_INTERVAL {
log::debug!(target: "app", "托盘菜单更新频率过高,跳过本次更新");
return Ok(());
// 检查更新频率,但允许重要事件跳过频率限制
let should_force_update = match std::thread::current().name() {
Some("main") => true,
_ => {
let last_update = self.last_menu_update.lock();
if let Some(last_time) = *last_update {
last_time.elapsed() >= MIN_UPDATE_INTERVAL
} else {
true
}
}
};
if !should_force_update {
return Ok(());
}
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
return Ok(()); // 早期返回,避免panic
return Ok(());
}
};
@@ -248,13 +249,14 @@ impl Tray {
result
}
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
let verge = Config::verge().latest().clone();
let verge = Config::verge().latest_ref().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()
.latest_ref()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
@@ -262,7 +264,7 @@ impl Tray {
.to_owned()
};
let profile_uid_and_name = Config::profiles()
.data()
.data_mut()
.all_profile_uid_and_name()
.unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode();
@@ -270,7 +272,7 @@ impl Tray {
match app_handle.tray_by_id("main") {
Some(tray) => {
let _ = tray.set_menu(Some(create_tray_menu(
&app_handle,
app_handle,
Some(mode.as_str()),
*system_proxy,
*tun_mode,
@@ -288,7 +290,8 @@ impl Tray {
}
/// 更新托盘图标
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
#[cfg(target_os = "macos")]
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
@@ -305,68 +308,67 @@ impl Tray {
}
};
let verge = Config::verge().latest().clone();
let verge = Config::verge().latest_ref().clone();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
(true, true) => TrayState::get_tun_tray_icon(),
(true, false) => TrayState::get_sysproxy_tray_icon(),
(false, true) => TrayState::get_tun_tray_icon(),
(false, false) => TrayState::get_common_tray_icon(),
};
#[cfg(target_os = "macos")]
{
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let is_colorful = colorful == "colorful";
if !enable_tray_speed {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
let _ = tray.set_icon_as_template(!is_colorful);
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let is_colorful = colorful == "colorful";
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
let _ = tray.set_icon_as_template(!is_colorful);
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(());
}
};
let rate = if let Some(rate) = rate {
Some(rate)
} else {
let guard = self.speed_rate.lock();
if let Some(guard) = guard.as_ref() {
if let Some(rate) = guard.get_curent_rate() {
Some(rate)
} else {
Some(Rate::default())
}
} 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)
} else {
None
};
let rate = rate_guard.as_ref();
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
}
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(());
}
Ok(())
}
};
#[cfg(not(target_os = "macos"))]
{
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
Ok(())
}
let verge = Config::verge().latest_ref().clone();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
(true, true) => TrayState::get_tun_tray_icon(),
(true, false) => TrayState::get_sysproxy_tray_icon(),
(false, true) => TrayState::get_tun_tray_icon(),
(false, false) => TrayState::get_common_tray_icon(),
};
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
Ok(())
}
/// 更新托盘显示状态的函数
pub fn update_tray_display(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let _tray = app_handle.tray_by_id("main").unwrap();
// 更新菜单
self.update_menu()?;
Ok(())
}
/// 更新托盘提示
@@ -387,7 +389,7 @@ impl Tray {
}
};
let verge = Config::verge().latest().clone();
let verge = Config::verge().latest_ref().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);
@@ -400,7 +402,7 @@ impl Tray {
let mut current_profile_name = "None".to_string();
let profiles = Config::profiles();
let profiles = profiles.latest();
let profiles = profiles.latest_ref();
if let Some(current_profile_uid) = profiles.get_current() {
if let Ok(profile) = profiles.get_item(&current_profile_uid) {
current_profile_name = match &profile.name {
@@ -431,158 +433,14 @@ impl Tray {
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();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
let (shutdown_tx, shutdown_rx) = broadcast::channel(3);
*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);
// 使用单线程防止阻塞主线程
std::thread::Builder::new()
.name("traffic-monitor".into())
.spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to build tokio runtime for traffic monitor");
// 在单独的运行时中执行异步任务
rt.block_on(async move {
let mut shutdown = shutdown_rx;
let speed_rate = speed_rate.clone();
let is_subscribed = is_subscribed.clone();
let mut consecutive_errors = 0;
let max_consecutive_errors = 5;
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
'outer: loop {
if !*is_subscribed.read() {
log::info!(target: "app", "Traffic subscription has been cancelled");
break;
}
match tokio::time::timeout(
std::time::Duration::from_secs(5),
Traffic::get_traffic_stream()
).await {
Ok(stream_result) => {
match stream_result {
Ok(mut stream) => {
consecutive_errors = 0;
loop {
tokio::select! {
traffic_result = stream.next() => {
match traffic_result {
Some(Ok(traffic)) => {
if let Ok(Some(rate)) = tokio::time::timeout(
std::time::Duration::from_millis(50),
async {
let guard = speed_rate.try_lock();
if let Some(guard) = guard {
if let Some(sr) = guard.as_ref() {
sr.update_and_check_changed(traffic.up, traffic.down)
} else {
None
}
} else {
None
}
}
).await {
let _ = tokio::time::timeout(
std::time::Duration::from_millis(100),
async { let _ = Tray::global().update_icon(Some(rate)); }
).await;
}
},
Some(Err(e)) => {
log::error!(target: "app", "Traffic stream error: {}", e);
consecutive_errors += 1;
if consecutive_errors >= max_consecutive_errors {
log::error!(target: "app", "Too many errors, reconnecting traffic stream");
break;
}
},
None => {
log::info!(target: "app", "Traffic stream ended, reconnecting");
break;
}
}
},
_ = shutdown.recv() => {
log::info!(target: "app", "Received shutdown signal for traffic stream");
break 'outer;
},
_ = interval.tick() => {
if !*is_subscribed.read() {
log::info!(target: "app", "Traffic monitor detected subscription cancelled");
break 'outer;
}
log::debug!(target: "app", "Traffic subscription periodic health check");
},
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
log::info!(target: "app", "Traffic stream max active time reached, reconnecting");
break;
}
}
}
},
Err(e) => {
log::error!(target: "app", "Failed to get traffic stream: {}", e);
consecutive_errors += 1;
if consecutive_errors >= max_consecutive_errors {
log::error!(target: "app", "Too many consecutive errors, pausing traffic monitoring");
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
consecutive_errors = 0;
} else {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
}
},
Err(_) => {
log::error!(target: "app", "Traffic stream initialization timed out");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
if !*is_subscribed.read() {
break;
}
}
log::info!(target: "app", "Traffic subscription thread terminated");
});
})
.expect("Failed to spawn traffic monitor thread");
// 更新轻量模式显示状态
self.update_tray_display()?;
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);
}
}
pub fn unsubscribe_traffic(&self) {}
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
log::info!(target: "app", "正在从AppHandle创建系统托盘");
@@ -591,13 +449,19 @@ impl Tray {
let icon_bytes = TrayState::get_common_tray_icon().1;
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
#[cfg(target_os = "linux")]
let builder = TrayIconBuilder::with_id("main")
.icon(icon)
.icon_as_template(false);
#[cfg(not(target_os = "linux"))]
let mut builder = TrayIconBuilder::with_id("main")
.icon(icon)
.icon_as_template(false);
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event = { Config::verge().latest_ref().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);
@@ -607,9 +471,9 @@ impl Tray {
let tray = builder.build(app_handle)?;
tray.on_tray_icon_event(|_, event| {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
log::debug!(target: "app","tray event: {:?}", tray_event);
log::debug!(target: "app","tray event: {tray_event:?}");
if let TrayIconEvent::Click {
button: MouseButton::Left,
@@ -617,14 +481,23 @@ impl Tray {
..
} = event
{
// 添加防抖检查,防止快速连击
if !should_handle_tray_click() {
return;
}
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray点击事件: 显示主窗口");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
crate::module::lightweight::exit_lightweight_mode();
}
let _ = resolve::create_window(true);
let result = WindowManager::show_main_window();
log::info!(target: "app", "窗口显示结果: {result:?}");
}
_ => {}
}
@@ -634,6 +507,17 @@ impl Tray {
log::info!(target: "app", "系统托盘创建成功");
Ok(())
}
// 托盘统一的状态更新函数
pub fn update_all_states(&self) -> Result<()> {
// 确保所有状态更新完成
self.update_menu()?;
self.update_icon(None)?;
self.update_tooltip()?;
self.update_tray_display()?;
Ok(())
}
}
fn create_tray_menu(
@@ -650,7 +534,7 @@ fn create_tray_menu(
let version = VERSION.get().unwrap_or(&unknown_version);
let hotkeys = Config::verge()
.latest()
.latest_ref()
.hotkeys
.as_ref()
.map(|h| {
@@ -670,11 +554,11 @@ fn create_tray_menu(
.iter()
.map(|(profile_uid, profile_name)| {
let is_current_profile = Config::profiles()
.data()
.data_mut()
.is_current_profile_index(profile_uid.to_string());
CheckMenuItem::with_id(
app_handle,
format!("profiles_{}", profile_uid),
format!("profiles_{profile_uid}"),
t(profile_name),
true,
is_current_profile,
@@ -885,15 +769,26 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into());
}
"open_window" => {
// 如果在轻量模式中,先退出轻量模式
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() {
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出");
crate::module::lightweight::exit_lightweight_mode();
}
// 然后创建窗口
let _ = resolve::create_window(true);
let result = WindowManager::show_main_window();
log::info!(target: "app", "窗口显示结果: {result:?}");
}
"system_proxy" => {
feat::toggle_system_proxy();
}
"tun_mode" => {
feat::toggle_tun_mode(None);
}
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => {
let _ = cmd::open_app_dir();
@@ -906,7 +801,24 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
"restart_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(),
"entry_lightweight_mode" => entry_lightweight_mode(),
"entry_lightweight_mode" => {
if !should_handle_tray_click() {
return;
}
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
if was_lightweight {
crate::module::lightweight::exit_lightweight_mode();
} else {
crate::module::lightweight::entry_lightweight_mode();
}
if was_lightweight {
use crate::utils::window_manager::WindowManager;
let result = WindowManager::show_main_window();
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
}
}
"quit" => {
feat::quit();
}
@@ -916,4 +828,8 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
_ => {}
}
if let Err(e) = Tray::global().update_all_states() {
log::warn!(target: "app", "更新托盘状态失败: {e}");
}
}
-335
View File
@@ -1,336 +1 @@
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;
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(
is_custom_icon: bool,
icon_bytes: Option<Vec<u8>>,
rate: Option<&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.clone() {
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 total_width = match (is_custom_icon, icon_bytes.is_some()) {
(true, true) => 510,
(true, false) => 740,
(false, false) => 740,
(false, true) => icon_width + 740,
};
// println!(
// "icon_height: {}, icon_wight: {}, total_width: {}",
// icon_height, icon_width, total_width
// );
// 创建新的透明画布
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 is_colorful = if let Some(bytes) = icon_bytes.clone() {
!crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
} else {
false
};
// 选择文本颜色
let (text_color, shadow_color) = if is_colorful {
(
Rgba([144u8, 144u8, 144u8, 255u8]),
// Rgba([255u8, 255u8, 255u8, 128u8]),
Rgba([0u8, 0u8, 0u8, 128u8]),
)
// (
// Rgba([160u8, 160u8, 160u8, 255u8]),
// // Rgba([255u8, 255u8, 255u8, 128u8]),
// Rgba([0u8, 0u8, 0u8, 255u8]),
// )
} else {
(
Rgba([255u8, 255u8, 255u8, 255u8]),
Rgba([0u8, 0u8, 0u8, 128u8]),
)
};
// 减小字体大小以适应文本区域
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!("↑ {}", format_bytes_speed(rate.up));
let down_text = format!("↓ {}", format_bytes_speed(rate.down));
// For test rate display
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
// 计算文本位置,确保垂直间距合适
// 修改文本位置为居右显示
// 计算右对齐的文本位置
// 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 (up_text_x, down_text_x) = {
if is_custom_icon || icon_bytes.is_some() {
let text_left_offset = 30;
let left_begin = icon_width + text_left_offset;
(left_begin, left_begin)
} else {
(icon_width, icon_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::{
future::FutureExt,
stream::{self, StreamExt},
};
use std::time::Duration;
// 先处理错误和超时情况
let stream = Box::pin(
stream::unfold((), move |_| async move {
'retry: loop {
log::info!(target: "app", "establishing traffic websocket connection");
let (url, token) = MihomoManager::get_traffic_ws_url();
let mut request = match url.into_client_request() {
Ok(req) => req,
Err(e) => {
log::error!(target: "app", "failed to create websocket request: {}", e);
tokio::time::sleep(Duration::from_secs(2)).await;
continue 'retry;
}
};
request.headers_mut().insert(http::header::AUTHORIZATION, token);
match tokio::time::timeout(Duration::from_secs(3),
tokio_tungstenite::connect_async(request)
).await {
Ok(Ok((ws_stream, _))) => {
log::info!(target: "app", "traffic websocket connection established");
// 设置流超时控制
let traffic_stream = ws_stream
.take_while(|msg| {
let continue_stream = msg.is_ok();
async move { continue_stream }.boxed()
})
.filter_map(|msg| async move {
match msg {
Ok(msg) => {
if !msg.is_text() {
return None;
}
match tokio::time::timeout(
Duration::from_millis(200),
async { msg.into_text() }
).await {
Ok(Ok(text)) => {
match serde_json::from_str::<serde_json::Value>(&text) {
Ok(json) => {
let up = json["up"].as_u64().unwrap_or(0);
let down = json["down"].as_u64().unwrap_or(0);
Some(Ok(Traffic { up, down }))
},
Err(e) => {
log::warn!(target: "app", "traffic json parse error: {} for {}", e, text);
None
}
}
},
Ok(Err(e)) => {
log::warn!(target: "app", "traffic text conversion error: {}", e);
None
},
Err(_) => {
log::warn!(target: "app", "traffic text processing timeout");
None
}
}
},
Err(e) => {
log::error!(target: "app", "traffic websocket error: {}", e);
None
}
}
});
return Some((traffic_stream, ()));
},
Ok(Err(e)) => {
log::error!(target: "app", "traffic websocket connection failed: {}", e);
},
Err(_) => {
log::error!(target: "app", "traffic websocket connection timed out");
}
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
})
.flatten(),
);
Ok(stream)
}
}
+12 -6
View File
@@ -16,13 +16,13 @@ type ResultLog = Vec<(String, String)>;
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
let clash_config = { Config::clash().latest().0.clone() };
let clash_config = { Config::clash().latest_ref().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
let verge = Config::verge();
let verge = verge.latest();
let verge = verge.latest_ref();
(
verge.clash_core.clone(),
Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(false),
@@ -33,13 +33,13 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge();
let verge = verge.latest();
let verge = verge.latest_ref();
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge();
let verge = verge.latest();
let verge = verge.latest_ref();
verge.verge_tproxy_enabled.unwrap_or(false)
};
@@ -56,7 +56,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
profile_name,
) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let profiles = profiles.latest_ref();
let current = profiles.current_mapping().unwrap_or_default();
let merge = profiles
@@ -214,6 +214,12 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
config.remove("port");
continue;
}
#[cfg(target_os = "windows")]
{
if key.as_str() == Some("redir-port") || key.as_str() == Some("tproxy-port") {
continue;
}
}
#[cfg(not(target_os = "windows"))]
{
if key.as_str() == Some("redir-port") && !redir_enabled {
+66 -16
View File
@@ -8,7 +8,8 @@ pub fn use_script(
name: String,
) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use std::sync::{Arc, Mutex};
use parking_lot::Mutex;
use std::sync::Arc;
let mut context = Context::default();
let outputs = Arc::new(Mutex::new(vec![]));
@@ -24,7 +25,7 @@ pub fn use_script(
let level = level.to_std_string().unwrap();
let data = args.get(1).unwrap().to_string(context)?;
let data = data.to_std_string().unwrap();
let mut out = copy_outputs.lock().unwrap();
let mut out = copy_outputs.lock();
out.push((level, data));
Ok(JsValue::undefined())
},
@@ -33,38 +34,41 @@ pub fn use_script(
}
let _ = context.eval(Source::from_bytes(
r#"var console = Object.freeze({
log(data){__verge_log__("log",JSON.stringify(data))},
info(data){__verge_log__("info",JSON.stringify(data))},
error(data){__verge_log__("error",JSON.stringify(data))},
debug(data){__verge_log__("debug",JSON.stringify(data))},
log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
debug(data){__verge_log__("debug",JSON.stringify(data, null, 2))},
warn(data){__verge_log__("warn",JSON.stringify(data, null, 2))},
table(data){__verge_log__("table",JSON.stringify(data, null, 2))},
});"#,
));
let config = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?;
// 仅处理 name 参数中的特殊字符
let safe_name = escape_js_string_for_single_quote(&name);
let code = format!(
r#"try{{
{script};
JSON.stringify(main({config_str},'{name}')||'')
JSON.stringify(main({config_str},'{safe_name}')||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
);
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
if !result.is_string() {
anyhow::bail!("main function should return object");
}
let result = result.to_string(&mut context).unwrap();
let result = result.to_std_string().unwrap();
if result.starts_with("__error_flag__") {
anyhow::bail!(result[15..].to_owned());
}
if result == "\"\"" {
anyhow::bail!("main function should return object");
}
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
let mut out = outputs.lock().unwrap();
// 直接解析JSON结果,不做其他解析
let res: Result<Mapping, Error> = parse_json_safely(&result);
let mut out = outputs.lock();
match res {
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
Err(err) => {
@@ -77,6 +81,27 @@ pub fn use_script(
}
}
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
let json_str = strip_outer_quotes(json_str);
Ok(serde_json::from_str::<Mapping>(json_str)?)
}
// 移除字符串外层的引号
fn strip_outer_quotes(s: &str) -> &str {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
&s[1..s.len() - 1]
} else {
s
}
}
// 转义单引号和反斜杠,用于单引号包裹的JavaScript字符串
fn escape_js_string_for_single_quote(s: &str) -> String {
s.replace('\\', "\\\\").replace('\'', "\\'")
}
#[test]
fn test_script() {
let script = r#"
@@ -104,6 +129,31 @@ fn test_script() {
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let _ = serde_yaml::to_string(&config).unwrap();
let yaml_config_size = std::mem::size_of_val(&config);
dbg!(yaml_config_size);
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
dbg!(box_yaml_config_size);
dbg!(results);
assert!(box_yaml_config_size < yaml_config_size);
}
// 测试特殊字符转义功能
#[test]
fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string_for_single_quote(test_string);
println!("Original: {test_string}");
println!("Escaped: {escaped}");
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
let parsed = parse_json_safely(json_str).unwrap();
assert!(parsed.contains_key("key"));
assert!(parsed.contains_key("nested"));
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
let parsed_quoted = parse_json_safely(quoted_json_str).unwrap();
assert!(parsed_quoted.contains_key("key"));
assert!(parsed_quoted.contains_key("nested"));
}
-1
View File
@@ -1 +0,0 @@
pub mod service;
-1
View File
@@ -1 +0,0 @@
+7 -7
View File
@@ -11,7 +11,7 @@ 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);
log::error!(target: "app", "Failed to create backup: {err:#?}");
err
})?;
@@ -19,12 +19,12 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
.upload(temp_file_path.clone(), file_name)
.await
{
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
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);
log::warn!(target: "app", "Failed to remove temp file: {err:#?}");
}
Ok(())
@@ -33,7 +33,7 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
/// 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);
log::error!(target: "app", "Failed to list WebDAV backup files: {err:#?}");
err
})
}
@@ -44,7 +44,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
.delete(filename)
.await
.map_err(|err| {
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
log::error!(target: "app", "Failed to delete WebDAV backup file: {err:#?}");
err
})
}
@@ -52,7 +52,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
/// Restore WebDAV backup
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge();
let verge_data = verge.data().clone();
let verge_data = verge.latest_ref().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();
@@ -62,7 +62,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
.download(filename, backup_storage_path.clone())
.await
.map_err(|err| {
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
log::error!(target: "app", "Failed to download WebDAV backup file: {err:#?}");
err
})?;
+11 -8
View File
@@ -49,7 +49,7 @@ fn after_change_clash_mode() {
}
}
Err(err) => {
log::error!(target: "app", "Failed to get connections: {}", err);
log::error!(target: "app", "Failed to get connections: {err}");
}
}
});
@@ -68,23 +68,23 @@ pub fn change_clash_mode(mode: String) {
match MihomoManager::global().patch_configs(json_value).await {
Ok(_) => {
// 更新订阅
Config::clash().data().patch_config(mapping);
Config::clash().data_mut().patch_config(mapping);
if Config::clash().data().save_config().is_ok() {
if Config::clash().data_mut().save_config().is_ok() {
handle::Handle::refresh_clash();
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
}
let is_auto_close_connection = Config::verge()
.data()
.data_mut()
.auto_close_connection
.unwrap_or(false);
if is_auto_close_connection {
after_change_clash_mode();
}
}
Err(err) => println!("{err}"),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
@@ -94,7 +94,10 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
use crate::utils::network::{NetworkManager, ProxyType};
use tokio::time::Instant;
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
let tun_mode = Config::verge()
.latest_ref()
.enable_tun_mode
.unwrap_or(false);
// 如果是TUN模式,不使用代理,否则使用自身代理
let proxy_type = if !tun_mode {
@@ -113,7 +116,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
match response {
Ok(response) => {
log::trace!(target: "app", "test_delay response: {:#?}", response);
log::trace!(target: "app", "test_delay response: {response:#?}");
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
@@ -121,7 +124,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
}
}
Err(err) => {
log::trace!(target: "app", "test_delay error: {:#?}", err);
log::trace!(target: "app", "test_delay error: {err:#?}");
Err(err)
}
}
+6 -6
View File
@@ -10,7 +10,7 @@ use serde_yaml::Mapping;
/// Patch Clash configuration
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
Config::clash().draft_mut().patch_config(patch.clone());
let res = {
// 激活订阅
@@ -22,7 +22,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
}
Config::runtime().latest().patch_config(patch);
Config::runtime().draft_mut().patch_config(patch);
CoreManager::global().update_config().await?;
}
handle::Handle::refresh_clash();
@@ -31,7 +31,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
match res {
Ok(()) => {
Config::clash().apply();
Config::clash().data().save_config()?;
Config::clash().data_mut().save_config()?;
Ok(())
}
Err(err) => {
@@ -60,7 +60,7 @@ enum UpdateFlags {
/// Patch Verge configuration
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
Config::verge().draft_mut().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
@@ -175,7 +175,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
handle::Handle::refresh_clash();
}
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
Config::verge().draft_mut().enable_global_hotkey = enable_global_hotkey;
handle::Handle::refresh_verge();
}
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
@@ -213,7 +213,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Ok(()) => {
Config::verge().apply();
if !not_save_file {
Config::verge().data().save_file()?;
Config::verge().data_mut().save_file()?;
}
Ok(())
+20 -18
View File
@@ -2,7 +2,9 @@ use crate::{
cmd,
config::{Config, PrfItem, PrfOption},
core::{handle, CoreManager, *},
logging,
process::AsyncHandler,
utils::logging::Type,
};
use anyhow::{bail, Result};
@@ -29,23 +31,23 @@ pub async fn update_profile(
option: Option<PrfOption>,
auto_refresh: Option<bool>,
) -> Result<()> {
println!("[订阅更新] 开始更新订阅 {}", uid);
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
let url_opt = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let profiles = profiles.latest_ref();
let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
None // 非远程订阅直接更新
} else if item.url.is_none() {
println!("[订阅更新] {} 缺少URL,无法更新", uid);
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
bail!("failed to get the profile item url");
} else {
println!(
log::info!(target: "app",
"[订阅更新] {} 是远程订阅,URL: {}",
uid,
item.url.clone().unwrap()
@@ -56,24 +58,24 @@ pub async fn update_profile(
let should_update = match url_opt {
Some((url, opt)) => {
println!("[订阅更新] 开始下载新的订阅内容");
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
// 尝试使用正常设置更新
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => {
println!("[订阅更新] 更新订阅配置成功");
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles();
let mut profiles = profiles.latest();
let mut profiles = profiles.draft_mut();
profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh
}
Err(err) => {
// 首次更新失败,尝试使用Clash代理
println!("[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新");
// 发送通知
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
@@ -90,7 +92,7 @@ pub async fn update_profile(
// 使用Clash代理重试
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => {
println!("[订阅更新] 使用Clash代理更新成功");
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
// 恢复原始代理设置到item
if let Some(option) = item.option.as_mut() {
@@ -100,7 +102,7 @@ pub async fn update_profile(
// 更新到配置
let profiles = Config::profiles();
let mut profiles = profiles.latest();
let mut profiles = profiles.draft_mut();
profiles.update_item(uid.clone(), item.clone())?;
// 获取配置名称用于通知
@@ -110,14 +112,14 @@ pub async fn update_profile(
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh
}
Err(retry_err) => {
println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{}", retry_err),
format!("{retry_err}"),
);
return Err(retry_err);
}
@@ -129,14 +131,14 @@ pub async fn update_profile(
};
if should_update {
println!("[订阅更新] 更新内核配置");
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await {
Ok(_) => {
println!("[订阅更新] 更新成功");
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
}
Err(err) => {
println!("[订阅更新] 更新失败: {}", err);
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}");
}
+14 -9
View File
@@ -8,10 +8,10 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off
pub fn toggle_system_proxy() {
let enable = Config::verge().draft().enable_system_proxy;
let enable = Config::verge().draft_mut().enable_system_proxy;
let enable = enable.unwrap_or(false);
let auto_close_connection = Config::verge()
.data()
.data_mut()
.auto_close_connection
.unwrap_or(false);
@@ -22,7 +22,7 @@ pub fn toggle_system_proxy() {
.close_all_connections()
.await
{
log::error!(target: "app", "Failed to close all connections: {}", err);
log::error!(target: "app", "Failed to close all connections: {err}");
}
}
@@ -43,7 +43,7 @@ pub fn toggle_system_proxy() {
/// Toggle TUN mode on/off
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().data().enable_tun_mode;
let enable = Config::verge().data_mut().enable_tun_mode;
let enable = enable.unwrap_or(false);
AsyncHandler::spawn(async move || {
@@ -67,19 +67,24 @@ pub fn copy_clash_env() {
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| {
Config::verge()
.latest()
.latest_ref()
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string())
});
let app_handle = handle::Handle::global().app_handle().unwrap();
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
let port = {
Config::verge()
.latest_ref()
.verge_mixed_port
.unwrap_or(7897)
};
let http_proxy = format!("http://{clash_verge_rev_ip}:{port}");
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard();
let env_type = { Config::verge().latest().env_type.clone() };
let env_type = { Config::verge().latest_ref().env_type.clone() };
let env_type = match env_type {
Some(env_type) => env_type,
None => {
+191 -67
View File
@@ -3,55 +3,68 @@ use crate::AppHandleManager;
use crate::{
config::Config,
core::{handle, sysopt, CoreManager},
logging,
module::mihomo::MihomoManager,
utils::resolve,
utils::logging::Type,
};
/// Open or close the dashboard window
#[allow(dead_code)]
pub fn open_or_close_dashboard() {
println!("Attempting to open/close dashboard");
log::info!(target: "app", "Attempting to open/close dashboard");
if let Some(window) = handle::Handle::global().get_window() {
println!("Found existing window");
log::info!(target: "app", "Found existing window");
// 如果窗口存在,则切换其显示状态
match window.is_visible() {
Ok(visible) => {
println!("Window visibility status: {}", visible);
log::info!(target: "app", "Window visibility status: {}", visible);
if visible {
println!("Attempting to hide window");
log::info!(target: "app", "Attempting to hide window");
let _ = window.hide();
} else {
println!("Attempting to show and focus window");
log::info!(target: "app", "Attempting to show and focus window");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
}
Err(e) => {
println!("Failed to get window visibility: {:?}", e);
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
}
}
} else {
println!("No existing window found, creating new window");
log::info!(target: "app", "No existing window found, creating new window");
resolve::create_window(true);
}
open_or_close_dashboard_internal(false)
}
/// 优化的应用退出函数
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
#[allow(dead_code)]
pub fn open_or_close_dashboard_hotkey() {
open_or_close_dashboard_internal(true)
}
/// Internal implementation for opening/closing dashboard
fn open_or_close_dashboard_internal(bypass_debounce: bool) {
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
// 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce {
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "主线程中执行热键窗口操作");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode();
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window();
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {result:?}");
});
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode();
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window();
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {result:?}");
}
/// 异步优化的应用退出函数
pub fn quit() {
log::debug!(target: "app", "启动退出流程");
use crate::process::AsyncHandler;
logging!(debug, Type::System, true, "启动退出流程");
// 获取应用句柄并设置退出标志
let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -60,55 +73,166 @@ pub fn quit() {
// 优先关闭窗口,提供立即反馈
if let Some(window) = handle::Handle::global().get_window() {
let _ = window.hide();
log::info!(target: "app", "窗口已隐藏");
}
// 在单独线程中处理资源清理,避免阻塞主线程
std::thread::spawn(move || {
let cleanup_result = clean();
app_handle.exit(match cleanup_result {
true => 0,
false => 1,
});
// 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始异步清理资源");
let cleanup_result = clean_async().await;
logging!(
info,
Type::System,
true,
"资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 }
);
app_handle.exit(if cleanup_result { 0 } else { 1 });
});
}
pub fn clean() -> bool {
async fn clean_async() -> bool {
use tokio::time::{timeout, Duration};
let rt = tokio::runtime::Runtime::new().unwrap();
let cleanup_result = rt.block_on(async {
// 1. 处理TUN模式
let tun_success = if Config::verge().data().enable_tun_mode.unwrap_or(false) {
logging!(info, Type::System, true, "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_task = async {
if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) {
let disable_tun = serde_json::json!({
"tun": {
"enable": false
}
});
timeout(
Duration::from_secs(1),
match timeout(
Duration::from_secs(2),
MihomoManager::global().patch_configs(disable_tun),
)
.await
.is_ok()
{
Ok(_) => {
log::info!(target: "app", "TUN模式已禁用");
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
}
}
} else {
true
};
}
};
// 2. 顺序执行关键清理
let proxy_res = timeout(
Duration::from_secs(1),
// 2. 系统代理重置
let proxy_task = async {
match timeout(
Duration::from_secs(3),
sysopt::Sysopt::global().reset_sysproxy(),
)
.await;
.await
{
Ok(_) => {
log::info!(target: "app", "系统代理已重置");
true
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时");
false
}
}
};
let core_res = timeout(Duration::from_secs(1), CoreManager::global().stop_core()).await;
// 3. 核心服务停止
let core_task = async {
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
Ok(_) => {
log::info!(target: "app", "核心服务已停止");
true
}
Err(_) => {
log::warn!(target: "app", "停止核心服务超时");
false
}
}
};
// 3. 平台特定清理
#[cfg(target_os = "macos")]
let _dns_res = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
// 4. DNS恢复(仅macOS)
#[cfg(target_os = "macos")]
let dns_task = async {
match timeout(
Duration::from_millis(1000),
crate::utils::resolve::restore_public_dns(),
)
.await
{
Ok(_) => {
log::info!(target: "app", "DNS设置已恢复");
true
}
Err(_) => {
log::warn!(target: "app", "恢复DNS设置超时");
false
}
}
};
tun_success && proxy_res.is_ok() && core_res.is_ok()
// 并行执行所有清理任务
let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task);
#[cfg(target_os = "macos")]
let dns_success = dns_task.await;
#[cfg(not(target_os = "macos"))]
let dns_success = true;
let all_success = tun_success && proxy_success && core_success && dns_success;
logging!(
info,
Type::System,
true,
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
tun_success,
proxy_success,
core_success,
dns_success,
all_success
);
all_success
}
pub fn clean() -> bool {
use crate::process::AsyncHandler;
let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始执行清理操作...");
// 使用已有的异步清理函数
let cleanup_result = clean_async().await;
// 发送结果
let _ = tx.send(cleanup_result);
});
cleanup_result
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => {
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
result
}
Err(_) => {
logging!(
warn,
Type::System,
true,
"清理操作超时,返回成功状态避免阻塞"
);
true
}
}
}
#[cfg(target_os = "macos")]
@@ -116,7 +240,7 @@ pub fn hide() {
use crate::module::lightweight::add_light_weight_timer;
let enable_auto_light_weight_mode = Config::verge()
.data()
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);
+31 -19
View File
@@ -1,11 +1,11 @@
mod cmd;
mod config;
pub mod config;
mod core;
mod enhance;
mod error;
mod feat;
mod module;
mod process;
mod state;
mod utils;
use crate::{
core::hotkey,
@@ -13,10 +13,12 @@ use crate::{
utils::{resolve, resolve::resolve_scheme, server},
};
use config::Config;
use std::sync::{Mutex, Once};
use parking_lot::Mutex;
use std::sync::Once;
use tauri::AppHandle;
#[cfg(target_os = "macos")]
use tauri::Manager;
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration};
@@ -41,14 +43,14 @@ impl AppHandleManager {
/// Initialize the app handle manager with an app handle.
pub fn init(&self, handle: AppHandle) {
self.init.call_once(|| {
let mut app_handle = self.inner.lock().unwrap();
let mut app_handle = self.inner.lock();
*app_handle = Some(handle);
});
}
/// Get the app handle if it has been initialized.
pub fn get(&self) -> Option<AppHandle> {
self.inner.lock().unwrap().clone()
self.inner.lock().clone()
}
/// Get the app handle, panics if it hasn't been initialized.
@@ -59,7 +61,7 @@ impl AppHandleManager {
pub fn set_activation_policy_regular(&self) {
#[cfg(target_os = "macos")]
{
let app_handle = self.inner.lock().unwrap();
let app_handle = self.inner.lock();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
}
@@ -68,7 +70,7 @@ impl AppHandleManager {
pub fn set_activation_policy_accessory(&self) {
#[cfg(target_os = "macos")]
{
let app_handle = self.inner.lock().unwrap();
let app_handle = self.inner.lock();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
}
@@ -77,7 +79,7 @@ impl AppHandleManager {
pub fn set_activation_policy_prohibited(&self) {
#[cfg(target_os = "macos")]
{
let app_handle = self.inner.lock().unwrap();
let app_handle = self.inner.lock();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
}
@@ -90,17 +92,20 @@ pub fn run() {
let _ = utils::dirs::init_portable_flag();
// 单例检测
let app_exists: bool = AsyncHandler::block_on(move || async move {
// 异步单例检测
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
true
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
false
}
}
Err(_) => {
@@ -110,13 +115,9 @@ pub fn run() {
true,
"单例检查超时,假定没有其他实例运行"
);
false
}
}
});
if app_exists {
return;
}
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
@@ -126,6 +127,7 @@ pub fn run() {
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
@@ -134,6 +136,7 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.manage(Mutex::new(state::lightweight::LightWeightState::default()))
.setup(|app| {
logging!(info, Type::Setup, true, "开始应用初始化...");
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
@@ -163,6 +166,14 @@ pub fn run() {
});
});
// 窗口管理
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
let _ = app.handle().plugin(window_state_plugin);
// 异步处理
let app_handle = app.handle().clone();
AsyncHandler::spawn(move || async move {
@@ -253,6 +264,7 @@ pub fn run() {
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::get_proxies,
cmd::force_refresh_proxies,
cmd::get_providers_proxies,
cmd::save_dns_config,
cmd::apply_dns_config,
@@ -366,7 +378,7 @@ pub fn run() {
if core::handle::Handle::global().is_exiting() {
return;
}
println!("closing window...");
log::info!(target: "app", "closing window...");
api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide();
@@ -390,7 +402,7 @@ pub fn run() {
}
{
let is_enable_global_hotkey = Config::verge()
.latest()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
@@ -414,7 +426,7 @@ pub fn run() {
}
{
let is_enable_global_hotkey = Config::verge()
.latest()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
+140 -62
View File
@@ -1,47 +1,105 @@
use crate::{
config::Config,
core::{handle, timer::Timer},
log_err, logging, logging_error,
core::{handle, timer::Timer, tray::Tray},
log_err, logging,
state::lightweight::LightWeightState,
utils::logging::Type,
AppHandleManager,
};
#[cfg(target_os = "macos")]
use crate::logging_error;
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder;
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use std::{
sync::Arc,
time::{Duration, Instant},
};
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{Listener, Manager};
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 轻量模式状态标志
static IS_LIGHTWEIGHT_MODE: OnceCell<Arc<RwLock<bool>>> = OnceCell::new();
// 添加退出轻量模式的锁,防止并发调用
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
// 添加一个锁来防止并发退出轻量模式
static EXIT_LOCK: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
fn get_lightweight_mode() -> &'static Arc<RwLock<bool>> {
IS_LIGHTWEIGHT_MODE.get_or_init(|| Arc::new(RwLock::new(false)))
fn with_lightweight_status<F, R>(f: F) -> R
where
F: FnOnce(&mut LightWeightState) -> R,
{
let app_handle = handle::Handle::global().app_handle().unwrap();
let state = app_handle.state::<Mutex<LightWeightState>>();
let mut guard = state.lock();
f(&mut guard)
}
fn get_exit_lock() -> &'static Mutex<(bool, Instant)> {
EXIT_LOCK.get_or_init(|| Mutex::new((false, Instant::now())))
pub fn run_once_auto_lightweight() {
LightWeightState::default().run_once_time(|| {
let is_silent_start = Config::verge()
.latest_ref()
.enable_silent_start
.unwrap_or(false);
let enable_auto = Config::verge()
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);
if enable_auto && is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
);
set_lightweight_mode(false);
enable_auto_light_weight_mode();
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}");
}
}
});
}
pub fn auto_lightweight_mode_init() {
if let Some(app_handle) = handle::Handle::global().app_handle() {
let _ = app_handle.state::<Mutex<LightWeightState>>();
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto =
{ Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false);
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_lightweight_mode(true);
enable_auto_light_weight_mode();
// 确保托盘状态更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}");
}
}
}
}
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
*get_lightweight_mode().read()
with_lightweight_status(|state| state.is_lightweight)
}
// 设置轻量模式状态
fn set_lightweight_mode(value: bool) {
let mut mode = get_lightweight_mode().write();
*mode = value;
logging!(info, Type::Lightweight, true, "轻量模式状态: {}", value);
pub fn set_lightweight_mode(value: bool) {
with_lightweight_status(|state| {
state.set_lightweight_mode(value);
});
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}");
}
}
pub fn enable_auto_light_weight_mode() {
@@ -58,60 +116,72 @@ pub fn disable_auto_light_weight_mode() {
}
pub fn entry_lightweight_mode() {
use crate::utils::window_manager::WindowManager;
let result = WindowManager::hide_main_window();
logging!(
info,
Type::Lightweight,
true,
"轻量模式隐藏窗口结果: {:?}",
result
);
if let Some(window) = handle::Handle::global().get_window() {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
}
if let Some(webview) = window.get_webview_window("main") {
let _ = webview.destroy();
}
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
logging!(info, Type::Lightweight, true, "轻量模式已开启");
}
// 标记已进入轻量模式
set_lightweight_mode(true);
let _ = cancel_light_weight_timer();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
}
// 添加从轻量模式恢复的函数
pub fn exit_lightweight_mode() {
// 获取锁,检查是否已经有退出操作在进行中
let mut exit_lock = get_exit_lock().lock();
let (is_exiting, last_exit_time) = *exit_lock;
let now = Instant::now();
// 如果已经有一个退出操作在进行,并且距离上次退出时间不超过2秒,跳过本次退出
if is_exiting && now.duration_since(last_exit_time) < Duration::from_secs(2) {
// 使用原子操作检查是否已经在退出过程中,防止并发调用
if EXITING_LIGHTWEIGHT
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
logging!(
warn,
info,
Type::Lightweight,
true,
"已有退出轻量模式操作正在进行中,跳过本次请求"
"轻量模式退出操作已在进行中,跳过重复调用"
);
return;
}
*exit_lock = (true, now);
// 使用defer确保无论如何都会重置标志
let _guard = scopeguard::guard((), |_| {
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
});
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
exit_lock.0 = false;
return;
}
// 标记退出轻量模式
set_lightweight_mode(false);
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
// macOS激活策略
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_regular();
// 重置UI就绪状态
crate::utils::resolve::reset_ui_ready();
// 释放锁
exit_lock.0 = false;
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
}
#[cfg(target_os = "macos")]
pub fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer());
}
@@ -156,19 +226,20 @@ fn cancel_window_close_listener() {
fn setup_light_weight_timer() -> Result<()> {
Timer::global().init()?;
let mut timer_map = Timer::global().timer_map.write();
let delay_timer = Timer::global().delay_timer.write();
let mut timer_count = Timer::global().timer_count.lock();
let task_id = *timer_count;
*timer_count += 1;
let once_by_minutes = Config::verge()
.latest()
.latest_ref()
.auto_light_weight_minutes
.unwrap_or(10);
// 获取task_id
let task_id = {
let mut timer_count = Timer::global().timer_count.lock();
let id = *timer_count;
*timer_count += 1;
id
};
// 创建任务
let task = TaskBuilder::default()
.set_task_id(task_id)
.set_maximum_parallel_runnable_num(1)
@@ -179,17 +250,24 @@ fn setup_light_weight_timer() -> Result<()> {
})
.context("failed to create timer task")?;
delay_timer
.add_task(task)
.context("failed to add timer task")?;
// 添加任务到定时器
{
let delay_timer = Timer::global().delay_timer.write();
delay_timer
.add_task(task)
.context("failed to add timer task")?;
}
let timer_task = crate::core::timer::TimerTask {
task_id,
interval_minutes: once_by_minutes,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
// 更新任务映射
{
let mut timer_map = Timer::global().timer_map.write();
let timer_task = crate::core::timer::TimerTask {
task_id,
interval_minutes: once_by_minutes,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
}
logging!(
info,
+4 -29
View File
@@ -3,7 +3,7 @@ use mihomo_api;
use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock};
use std::time::{Duration, Instant};
use tauri::http::{HeaderMap, HeaderValue};
use tauri::http::HeaderMap;
// 缓存的最大有效期(5秒)
const CACHE_TTL: Duration = Duration::from_secs(5);
@@ -92,42 +92,17 @@ impl MihomoManager {
impl MihomoManager {
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
let client = { Config::clash().data().get_client_info() };
let client = { Config::clash().latest_ref().get_client_info() };
let server = format!("http://{}", client.server);
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
if let Some(secret) = client.secret {
let secret = format!("Bearer {}", secret).parse().unwrap();
let secret = format!("Bearer {secret}").parse().unwrap();
headers.insert("Authorization", secret);
}
Some((server, headers))
}
// 提供默认值的版本,避免在connection_info为None时panic
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
Self::get_clash_client_info().unwrap_or_else(|| {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
("http://127.0.0.1:9090".to_string(), headers)
})
}
#[cfg(target_os = "macos")]
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
let (url, headers) = MihomoManager::get_clash_client_info_or_default();
let ws_url = url.replace("http://", "ws://") + "/traffic";
let auth = headers
.get("Authorization")
.map(|val| val.to_str().unwrap_or("").to_string())
.unwrap_or_default();
// 创建默认的空HeaderValue而不是使用unwrap_or_default
let token = match HeaderValue::from_str(&auth) {
Ok(v) => v,
Err(_) => HeaderValue::from_static(""),
};
(ws_url, token)
}
// 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法
}
-16
View File
@@ -11,20 +11,4 @@ impl AsyncHandler {
{
async_runtime::spawn(f())
}
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
async_runtime::spawn_blocking(f)
}
pub fn block_on<F, Fut, R>(f: F) -> R
where
F: FnOnce() -> Fut,
Fut: Future<Output = R>,
{
async_runtime::block_on(f())
}
}
+44
View File
@@ -0,0 +1,44 @@
use std::sync::{Arc, Once, OnceLock};
use crate::{logging, utils::logging::Type};
#[derive(Clone)]
pub struct LightWeightState {
#[allow(unused)]
once: Arc<Once>,
pub is_lightweight: bool,
}
impl LightWeightState {
pub fn new() -> Self {
Self {
once: Arc::new(Once::new()),
is_lightweight: false,
}
}
#[allow(unused)]
pub fn run_once_time<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
self.once.call_once(f);
}
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
self.is_lightweight = value;
if value {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} else {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
self
}
}
impl Default for LightWeightState {
fn default() -> Self {
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
INSTANCE.get_or_init(LightWeightState::new).clone()
}
}
+5
View File
@@ -0,0 +1,5 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc
pub mod lightweight;
pub mod proxy;
+65
View File
@@ -0,0 +1,65 @@
use std::time::{Duration, Instant};
pub struct CacheEntry {
pub value: Arc<Value>,
pub expires_at: Instant,
}
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::OnceCell;
pub struct ProxyRequestCache {
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
}
impl ProxyRequestCache {
pub fn global() -> &'static Self {
static INSTANCE: once_cell::sync::OnceCell<ProxyRequestCache> =
once_cell::sync::OnceCell::new();
INSTANCE.get_or_init(|| ProxyRequestCache {
map: DashMap::new(),
})
}
pub fn make_key(prefix: &str, id: &str) -> String {
format!("{prefix}:{id}")
}
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Value>,
{
let now = Instant::now();
let key_cloned = key.clone();
let cell = self
.map
.entry(key)
.or_insert_with(|| Arc::new(OnceCell::new()))
.clone();
if let Some(entry) = cell.get() {
if entry.expires_at > now {
return Arc::clone(&entry.value);
}
}
if let Some(entry) = cell.get() {
if entry.expires_at <= now {
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
let new_cell = Arc::new(OnceCell::new());
self.map.insert(key_cloned.clone(), new_cell.clone());
return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await;
}
}
let value = fetch_fn().await;
let entry = CacheEntry {
value: Arc::new(value),
expires_at: Instant::now() + ttl,
};
let _ = cell.set(entry);
Arc::clone(&cell.get().unwrap().value)
}
}
+38 -15
View File
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
use log::info;
#[cfg(target_os = "windows")]
use std::{fs, path::Path, path::PathBuf};
use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
/// Windows 下的开机启动文件夹路径
#[cfg(target_os = "windows")]
@@ -39,10 +39,20 @@ pub fn get_exe_path() -> Result<PathBuf> {
pub fn create_shortcut() -> Result<()> {
let exe_path = get_exe_path()?;
let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
let old_shortcut_path = startup_dir.join("Clash-Verge.lnk");
let new_shortcut_path = startup_dir.join("Clash Verge.lnk");
// 如果快捷方式已存在,直接返回成功
if shortcut_path.exists() {
// 移除旧的快捷方式
if old_shortcut_path.exists() {
if let Err(e) = fs::remove_file(&old_shortcut_path) {
info!(target: "app", "移除旧快捷方式失败: {e}");
} else {
info!(target: "app", "成功移除旧快捷方式");
}
}
// 如果新快捷方式已存在,直接返回成功
if new_shortcut_path.exists() {
info!(target: "app", "启动快捷方式已存在");
return Ok(());
}
@@ -53,12 +63,14 @@ pub fn create_shortcut() -> Result<()> {
$Shortcut = $WshShell.CreateShortcut('{}'); \
$Shortcut.TargetPath = '{}'; \
$Shortcut.Save()",
shortcut_path.to_string_lossy().replace("\\", "\\\\"),
new_shortcut_path.to_string_lossy().replace("\\", "\\\\"),
exe_path.to_string_lossy().replace("\\", "\\\\")
);
let output = std::process::Command::new("powershell")
.args(["-Command", &powershell_command])
// 隐藏 PowerShell 窗口
.creation_flags(0x08000000) // CREATE_NO_WINDOW
.output()
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
@@ -75,18 +87,29 @@ pub fn create_shortcut() -> Result<()> {
#[cfg(target_os = "windows")]
pub fn remove_shortcut() -> Result<()> {
let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
let old_shortcut_path = startup_dir.join("Clash-Verge.lnk");
let new_shortcut_path = startup_dir.join("Clash Verge.lnk");
// 如果快捷方式不存在,直接返回成功
if !shortcut_path.exists() {
info!(target: "app", "启动快捷方式不存在,无需删除");
return Ok(());
let mut removed_any = false;
// 删除旧的快捷方式
if old_shortcut_path.exists() {
fs::remove_file(&old_shortcut_path).map_err(|e| anyhow!("删除旧快捷方式失败: {}", e))?;
info!(target: "app", "成功删除旧启动快捷方式");
removed_any = true;
}
// 删除快捷方式
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
// 删除新的快捷方式
if new_shortcut_path.exists() {
fs::remove_file(&new_shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
info!(target: "app", "成功删除启动快捷方式");
removed_any = true;
}
if !removed_any {
info!(target: "app", "启动快捷方式不存在,无需删除");
}
info!(target: "app", "成功删除启动快捷方式");
Ok(())
}
@@ -94,9 +117,9 @@ pub fn remove_shortcut() -> Result<()> {
#[cfg(target_os = "windows")]
pub fn is_shortcut_enabled() -> Result<bool> {
let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
let new_shortcut_path = startup_dir.join("Clash Verge.lnk");
Ok(shortcut_path.exists())
Ok(new_shortcut_path.exists())
}
// 非 Windows 平台使用的空方法
+4 -4
View File
@@ -94,7 +94,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
// 如果无法获取系统目录,则回退到可执行文件目录
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
log::warn!(target: "app", "Using fallback data directory: {:?}", fallback_dir);
log::warn!(target: "app", "Using fallback data directory: {fallback_dir:?}");
return Ok(fallback_dir);
}
};
@@ -102,7 +102,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
match app_handle.path().data_dir() {
Ok(dir) => Ok(dir.join(APP_ID)),
Err(e) => {
log::error!(target: "app", "Failed to get the app home directory: {}", e);
log::error!(target: "app", "Failed to get the app home directory: {e}");
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
}
}
@@ -127,7 +127,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
match app_handle.path().resource_dir() {
Ok(dir) => Ok(dir.join("resources")),
Err(e) => {
log::error!(target: "app", "Failed to get the resource directory: {}", e);
log::error!(target: "app", "Failed to get the resource directory: {e}");
Err(anyhow::anyhow!("Failed to get the resource directory"))
}
}
@@ -203,7 +203,7 @@ pub fn service_log_file() -> Result<PathBuf> {
let log_dir = app_logs_dir()?.join("service");
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{}.log", local_time);
let log_file = format!("{local_time}.log");
let log_file = log_dir.join(log_file);
let _ = std::fs::create_dir_all(&log_dir);
-49
View File
@@ -125,19 +125,6 @@ pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
Ok(())
}
#[cfg(target_os = "macos")]
pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result<bool> {
let img = image::load_from_memory(data)?;
let rgb_img = img.to_rgb8();
for pixel in rgb_img.pixels() {
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
return Ok(false);
}
}
Ok(true)
}
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> String {
use std::process::Command;
@@ -176,39 +163,3 @@ macro_rules! t {
}
};
}
/// 将字节数转换为可读的流量字符串
/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换
///
/// # Examples
/// ```not_run
/// format_bytes_speed(1000) // returns "1000B/s"
/// format_bytes_speed(1024) // returns "1.0KB/s"
/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s"
/// ```
/// ```
#[cfg(target_os = "macos")]
pub fn format_bytes_speed(speed: u64) -> String {
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
let mut size = speed as f64;
let mut unit_index = 0;
while size >= 1000.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
format!("{:.1}{}/s", size, UNITS[unit_index])
}
#[cfg(target_os = "macos")]
#[test]
fn test_format_bytes_speed() {
assert_eq!(format_bytes_speed(0), "0B/s");
assert_eq!(format_bytes_speed(1023), "1023B/s");
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s");
assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s");
}
+2 -2
View File
@@ -38,7 +38,7 @@ static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
if let Some(locales_dir) = get_locales_dir() {
for lang in get_supported_languages() {
let file_path = locales_dir.join(format!("{}.json", lang));
let file_path = locales_dir.join(format!("{lang}.json"));
if let Ok(content) = fs::read_to_string(file_path) {
if let Ok(json) = serde_json::from_str(&content) {
translations.insert(lang.to_string(), json);
@@ -59,7 +59,7 @@ fn get_system_language() -> String {
pub fn t(key: &str) -> String {
let current_lang = Config::verge()
.latest()
.latest_ref()
.language
.as_deref()
.map(String::from)
+7 -4
View File
@@ -25,13 +25,13 @@ fn init_log() -> Result<()> {
let _ = fs::create_dir_all(&log_dir);
}
let log_level = Config::verge().data().get_log_level();
let log_level = Config::verge().latest_ref().get_log_level();
if log_level == LevelFilter::Off {
return Ok(());
}
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{}.log", local_time);
let log_file = format!("{local_time}.log");
let log_file = log_dir.join(log_file);
let log_pattern = match log_level {
@@ -74,7 +74,7 @@ pub fn delete_log() -> Result<()> {
let auto_log_clean = {
let verge = Config::verge();
let verge = verge.data();
let verge = verge.latest_ref();
verge.auto_log_clean.unwrap_or(0)
};
@@ -286,6 +286,9 @@ pub fn init_config() -> Result<()> {
<Result<()>>::Ok(())
}));
// 验证并修正verge.yaml中的clash_core配置
crate::log_err!(IVerge::validate_and_fix_config());
crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
@@ -402,7 +405,7 @@ pub async fn startup_script() -> Result<()> {
let script_path = {
let verge = Config::verge();
let verge = verge.latest();
let verge = verge.latest_ref();
verge.startup_script.clone().unwrap_or("".to_string())
};
+2
View File
@@ -5,6 +5,8 @@ pub mod i18n;
pub mod init;
pub mod logging;
pub mod network;
pub mod notification;
pub mod resolve;
pub mod server;
pub mod tmpl;
pub mod window_manager;
+15 -123
View File
@@ -1,8 +1,9 @@
use anyhow::Result;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response};
use std::{
sync::{Arc, Mutex, Once},
sync::{Arc, Once},
time::{Duration, Instant},
};
use tokio::runtime::{Builder, Runtime};
@@ -78,7 +79,7 @@ impl NetworkManager {
.build()
.expect("Failed to build no_proxy client");
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock().unwrap();
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock();
*no_proxy_guard = Some(no_proxy_client);
logging!(info, Type::Network, true, "网络管理器初始化完成");
@@ -87,16 +88,16 @@ impl NetworkManager {
}
fn record_connection_error(&self, error: &str) {
let mut last_error = self.last_connection_error.lock().unwrap();
let mut last_error = self.last_connection_error.lock();
*last_error = Some((Instant::now(), error.to_string()));
let mut error_count = self.connection_error_count.lock().unwrap();
let mut error_count = self.connection_error_count.lock();
*error_count += 1;
}
fn should_reset_clients(&self) -> bool {
let error_count = *self.connection_error_count.lock().unwrap();
let last_error = self.last_connection_error.lock().unwrap();
let error_count = *self.connection_error_count.lock();
let last_error = self.last_connection_error.lock();
if error_count > 5 {
return true;
@@ -114,132 +115,23 @@ impl NetworkManager {
pub fn reset_clients(&self) {
logging!(info, Type::Network, true, "正在重置所有HTTP客户端");
{
let mut client = self.self_proxy_client.lock().unwrap();
let mut client = self.self_proxy_client.lock();
*client = None;
}
{
let mut client = self.system_proxy_client.lock().unwrap();
let mut client = self.system_proxy_client.lock();
*client = None;
}
{
let mut client = self.no_proxy_client.lock().unwrap();
let mut client = self.no_proxy_client.lock();
*client = None;
}
{
let mut error_count = self.connection_error_count.lock().unwrap();
let mut error_count = self.connection_error_count.lock();
*error_count = 0;
}
}
/*
/// 获取或创建自代理客户端
fn get_or_create_self_proxy_client(&self) -> Client {
if self.should_reset_clients() {
self.reset_clients();
}
let mut client_guard = self.self_proxy_client.lock().unwrap();
if client_guard.is_none() {
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let proxy_scheme = format!("http://127.0.0.1:{port}");
let mut builder = ClientBuilder::new()
.use_rustls_tls()
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
.timeout(DEFAULT_REQUEST_TIMEOUT)
.http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE)
.http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE)
.http2_adaptive_window(true)
.http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
.http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT)
.http2_max_frame_size(H2_MAX_FRAME_SIZE)
.tcp_keepalive(Some(Duration::from_secs(10)))
.http2_max_header_list_size(16 * 1024);
if let Ok(proxy) = Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
let client = builder.build().expect("Failed to build self_proxy client");
*client_guard = Some(client);
}
client_guard.as_ref().unwrap().clone()
}
/// 获取或创建系统代理客户端
fn get_or_create_system_proxy_client(&self) -> Client {
if self.should_reset_clients() {
self.reset_clients();
}
let mut client_guard = self.system_proxy_client.lock().unwrap();
if client_guard.is_none() {
use sysproxy::Sysproxy;
let mut builder = ClientBuilder::new()
.use_rustls_tls()
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
.timeout(DEFAULT_REQUEST_TIMEOUT)
.http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE)
.http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE)
.http2_adaptive_window(true)
.http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
.http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT)
.http2_max_frame_size(H2_MAX_FRAME_SIZE)
.tcp_keepalive(Some(Duration::from_secs(10)))
.http2_max_header_list_size(16 * 1024);
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
if let Ok(proxy) = Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
let client = builder
.build()
.expect("Failed to build system_proxy client");
*client_guard = Some(client);
}
client_guard.as_ref().unwrap().clone()
}
/// 根据代理设置选择合适的客户端
pub fn get_client(&self, proxy_type: ProxyType) -> Client {
match proxy_type {
ProxyType::NoProxy => {
let client_guard = self.no_proxy_client.lock().unwrap();
client_guard.as_ref().unwrap().clone()
}
ProxyType::SelfProxy => self.get_or_create_self_proxy_client(),
ProxyType::SystemProxy => self.get_or_create_system_proxy_client(),
}
}
*/
/// 创建带有自定义选项的HTTP请求
pub fn create_request(
&self,
@@ -279,9 +171,9 @@ impl NetworkManager {
}
ProxyType::Localhost => {
let port = Config::verge()
.latest()
.latest_ref()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
let proxy_scheme = format!("http://127.0.0.1:{port}");
@@ -322,7 +214,7 @@ impl NetworkManager {
use crate::utils::resolve::VERSION;
let version = match VERSION.get() {
Some(v) => format!("clash-verge/v{}", v),
Some(v) => format!("clash-verge/v{v}"),
None => "clash-verge/unknown".to_string(),
};
@@ -401,7 +293,7 @@ impl NetworkManager {
let result = tokio::select! {
result = request.send() => result,
_ = cancel_rx => {
self.record_connection_error(&format!("Request interrupted for: {}", url));
self.record_connection_error(&format!("Request interrupted for: {url}"));
return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration));
}
};
+70
View File
@@ -0,0 +1,70 @@
use tauri::AppHandle;
use tauri_plugin_notification::NotificationExt;
pub enum NotificationEvent<'a> {
DashboardToggled,
ClashModeChanged {
mode: &'a str,
},
SystemProxyToggled,
TunModeToggled,
LightweightModeEntered,
AppQuit,
#[cfg(target_os = "macos")]
AppHidden,
}
fn notify(app: &AppHandle, title: &str, body: &str) {
app.notification()
.builder()
.title(title)
.body(body)
.show()
.ok();
}
pub fn notify_event(app: &AppHandle, event: NotificationEvent) {
use crate::utils::i18n::t;
match event {
NotificationEvent::DashboardToggled => {
notify(app, &t("DashboardToggledTitle"), &t("DashboardToggledBody"));
}
NotificationEvent::ClashModeChanged { mode } => {
notify(
app,
&t("ClashModeChangedTitle"),
&t_with_args("ClashModeChangedBody", mode),
);
}
NotificationEvent::SystemProxyToggled => {
notify(
app,
&t("SystemProxyToggledTitle"),
&t("SystemProxyToggledBody"),
);
}
NotificationEvent::TunModeToggled => {
notify(app, &t("TunModeToggledTitle"), &t("TunModeToggledBody"));
}
NotificationEvent::LightweightModeEntered => {
notify(
app,
&t("LightweightModeEnteredTitle"),
&t("LightweightModeEnteredBody"),
);
}
NotificationEvent::AppQuit => {
notify(app, &t("AppQuitTitle"), &t("AppQuitBody"));
}
#[cfg(target_os = "macos")]
NotificationEvent::AppHidden => {
notify(app, &t("AppHiddenTitle"), &t("AppHiddenBody"));
}
}
}
// 辅助函数,带参数的i18n
fn t_with_args(key: &str, mode: &str) -> String {
use crate::utils::i18n::t;
t(key).replace("{mode}", mode)
}
+216 -78
View File
@@ -1,8 +1,10 @@
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use crate::{
config::{Config, IVerge, PrfItem},
core::*,
logging, logging_error,
module::lightweight,
module::lightweight::{self, auto_lightweight_mode_init},
process::AsyncHandler,
utils::{init, logging::Type, server},
wrap_err,
@@ -11,6 +13,7 @@ use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use percent_encoding::percent_decode_str;
use scopeguard;
use serde_yaml::Mapping;
use std::{
sync::Arc,
@@ -49,14 +52,12 @@ pub enum UiReadyStage {
#[derive(Debug)]
struct UiReadyState {
stage: RwLock<UiReadyStage>,
last_update: RwLock<Instant>,
}
impl Default for UiReadyState {
fn default() -> Self {
Self {
stage: RwLock::new(UiReadyStage::NotStarted),
last_update: RwLock::new(Instant::now()),
}
}
}
@@ -80,20 +81,8 @@ fn get_ui_ready_state() -> &'static Arc<UiReadyState> {
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
let mut stage_lock = state.stage.write();
let mut time_lock = state.last_update.write();
*stage_lock = stage;
*time_lock = Instant::now();
logging!(
info,
Type::Window,
true,
"UI准备阶段更新: {:?}, 耗时: {:?}ms",
stage,
time_lock.elapsed().as_millis()
);
// 如果是最终阶段,标记UI完全就绪
if stage == UiReadyStage::Ready {
mark_ui_ready();
@@ -116,9 +105,7 @@ pub fn reset_ui_ready() {
{
let state = get_ui_ready_state();
let mut stage = state.stage.write();
let mut time = state.last_update.write();
*stage = UiReadyStage::NotStarted;
*time = Instant::now();
}
logging!(info, Type::Window, true, "UI就绪状态已重置");
}
@@ -131,10 +118,10 @@ pub async fn find_unused_port() -> Result<u16> {
}
Err(_) => {
let port = Config::verge()
.latest()
.latest_ref()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
log::warn!(target: "app", "use default port: {}", port);
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
log::warn!(target: "app", "use default port: {port}");
Ok(port)
}
}
@@ -142,7 +129,8 @@ pub async fn find_unused_port() -> Result<u16> {
/// 异步方式处理启动后的额外任务
pub async fn resolve_setup_async(app_handle: &AppHandle) {
logging!(info, Type::Setup, true, "执行异步设置任务...");
let start_time = std::time::Instant::now();
logging!(info, Type::Setup, true, "开始执行异步设置任务...");
if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string();
@@ -169,6 +157,15 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
logging!(trace, Type::Config, true, "初始化配置...");
logging_error!(Type::Config, true, Config::init_config().await);
// 启动时清理冗余的 Profile 文件
logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
let profiles = Config::profiles();
if let Err(e) = profiles.latest_ref().auto_cleanup() {
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
} else {
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
}
logging!(trace, Type::Core, true, "启动核心管理器...");
logging_error!(Type::Core, true, CoreManager::global().init().await);
@@ -207,25 +204,47 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
);
// 创建窗口
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
{
if is_silent_start {
use crate::AppHandleManager;
AppHandleManager::global().set_activation_policy_accessory();
}
}
create_window(!is_silent_start);
// 初始化定时器
logging_error!(Type::System, true, timer::Timer::global().init());
// 自动进入轻量模式
let enable_auto_light_weight_mode =
{ Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
if enable_auto_light_weight_mode && !is_silent_start {
lightweight::enable_auto_light_weight_mode();
}
auto_lightweight_mode_init();
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
logging!(trace, Type::System, true, "初始化热键...");
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
logging!(info, Type::Setup, true, "异步设置任务完成");
let elapsed = start_time.elapsed();
logging!(
info,
Type::Setup,
true,
"异步设置任务完成,耗时: {:?}",
elapsed
);
// 如果初始化时间过长,记录警告
if elapsed.as_secs() > 10 {
logging!(
warn,
Type::Setup,
true,
"异步设置任务耗时较长({:?})",
elapsed
);
}
}
/// reset system proxy (异步)
@@ -258,12 +277,28 @@ pub fn create_window(is_show: bool) -> bool {
is_show
);
if !is_show {
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
lightweight::set_lightweight_mode(true);
handle::Handle::notify_startup_completed();
return false;
}
if let Some(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
}
return true;
}
@@ -288,6 +323,12 @@ pub fn create_window(is_show: bool) -> bool {
*creating = (true, Instant::now());
// ScopeGuard 确保创建状态重置,防止 webview 卡死
let _guard = scopeguard::guard(creating, |mut creating_guard| {
*creating_guard = (false, Instant::now());
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
});
match tauri::WebviewWindowBuilder::new(
&handle::Handle::global().app_handle().unwrap(),
"main", /* the unique window label */
@@ -299,14 +340,77 @@ pub fn create_window(is_show: bool) -> bool {
.fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(520.0, 520.0)
.visible(false)
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(
r#"
console.log('[Tauri] 窗口初始化脚本开始执行');
function createLoadingOverlay() {
if (document.getElementById('initial-loading-overlay')) {
console.log('[Tauri] 加载指示器已存在');
return;
}
console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay';
loadingDiv.innerHTML = `
<div style="
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: opacity 0.3s ease;
">
<div style="margin-bottom: 20px;">
<div style="
width: 40px; height: 40px; border: 3px solid #e3e3e3;
border-top: 3px solid #3498db; border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
</div>
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (prefers-color-scheme: dark) {
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
}
</style>
`;
if (document.body) {
document.body.appendChild(loadingDiv);
} else {
document.addEventListener('DOMContentLoaded', () => {
if (document.body && !document.getElementById('initial-loading-overlay')) {
document.body.appendChild(loadingDiv);
}
});
}
}
createLoadingOverlay();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
} else {
createLoadingOverlay();
}
console.log('[Tauri] 窗口初始化脚本执行完成');
"#,
)
.build()
{
Ok(newly_created_window) => {
logging!(debug, Type::Window, true, "主窗口实例创建成功");
*creating = (false, Instant::now());
update_ui_ready_stage(UiReadyStage::NotStarted);
AsyncHandler::spawn(move || async move {
@@ -318,84 +422,115 @@ pub fn create_window(is_show: bool) -> bool {
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
lightweight::run_once_auto_lightweight();
// 发送启动完成事件,触发前端开始加载
logging!(
debug,
Type::Window,
true,
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
if is_show {
let window_clone = newly_created_window.clone();
// Attempt to show and focus the window first.
// 立即显示窗口
let _ = window_clone.show();
let _ = window_clone.set_focus();
logging!(debug, Type::Window, true, "窗口已尝试显示和聚焦");
tokio::time::sleep(Duration::from_millis(100)).await; // Crucial delay
logging!(
debug,
Type::Window,
true,
"延时后,尝试发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
2
3
} else {
5
8
};
logging!(
info,
Type::Window,
true,
"等待UI就绪 (最多{}秒)...",
"开始监控UI加载状态 (最多{}秒)...",
timeout_seconds
);
let wait_result =
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
while !*get_ui_ready().read() {
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await;
// 异步监控UI状态,不影响窗口显示
tokio::spawn(async move {
let wait_result =
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
let mut check_count = 0;
while !*get_ui_ready().read() {
tokio::time::sleep(Duration::from_millis(100)).await;
check_count += 1;
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI就绪");
// 每2秒记录一次等待状态
if check_count % 20 == 0 {
logging!(
debug,
Type::Window,
true,
"UI加载状态检查... ({}秒)",
check_count / 10
);
}
}
})
.await;
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI已完全加载就绪");
// 移除初始加载指示器
if let Some(window) = handle::Handle::global().get_window() {
let _ = window.eval(r#"
const overlay = document.getElementById('initial-loading-overlay');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 300);
}
"#);
}
}
Err(_) => {
logging!(
warn,
Type::Window,
true,
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
*get_ui_ready().write() = true;
}
}
Err(_) => {
logging!(
warn,
Type::Window,
true,
"等待UI就绪超时({}秒),强制标记就绪",
timeout_seconds
);
*get_ui_ready().write() = true;
}
}
});
logging!(info, Type::Window, true, "窗口显示流程完成");
} else {
logging!(
debug,
Type::Window,
true,
"is_show为false,窗口保持隐藏。尝试发送启动事件。"
"is_show为false,窗口保持隐藏状态"
);
handle::Handle::notify_startup_completed();
}
});
true
}
Err(e) => {
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
*creating = (false, Instant::now()); // Reset the creating state if window creation failed
false
}
}
}
pub async fn resolve_scheme(param: String) -> Result<()> {
log::info!(target:"app", "received deep link: {}", param);
log::info!(target:"app", "received deep link: {param}");
let param_str = if param.starts_with("[") && param.len() > 4 {
param
@@ -433,13 +568,13 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {}", url);
log::info!(target:"app", "decoded subscription url: {url}");
create_window(false);
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item));
let _ = wrap_err!(Config::profiles().data_mut().append_item(item));
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
@@ -457,12 +592,15 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
async fn resolve_random_port_config() -> Result<()> {
let verge_config = Config::verge();
let clash_config = Config::clash();
let enable_random_port = verge_config.latest().enable_random_port.unwrap_or(false);
let enable_random_port = verge_config
.latest_ref()
.enable_random_port
.unwrap_or(false);
let default_port = verge_config
.latest()
.latest_ref()
.verge_mixed_port
.unwrap_or(clash_config.data().get_mixed_port());
.unwrap_or(clash_config.latest_ref().get_mixed_port());
let port = if enable_random_port {
find_unused_port().await.unwrap_or(default_port)
@@ -474,7 +612,7 @@ async fn resolve_random_port_config() -> Result<()> {
tokio::task::spawn_blocking(move || {
let verge_config_accessor = Config::verge();
let mut verge_data = verge_config_accessor.data();
let mut verge_data = verge_config_accessor.data_mut();
verge_data.patch_config(IVerge {
verge_mixed_port: Some(port_to_save),
..IVerge::default()
@@ -485,7 +623,7 @@ async fn resolve_random_port_config() -> Result<()> {
tokio::task::spawn_blocking(move || {
let clash_config_accessor = Config::clash(); // Extend lifetime of the accessor
let mut clash_data = clash_config_accessor.data(); // Access within blocking task, made mutable
let mut clash_data = clash_config_accessor.data_mut(); // Access within blocking task, made mutable
let mut mapping = Mapping::new();
mapping.insert("mixed-port".into(), port_to_save.into());
clash_data.patch_config(mapping);
+4 -4
View File
@@ -56,15 +56,15 @@ pub fn embed_server() {
let pac = warp::path!("commands" / "pac").map(move || {
let content = Config::verge()
.latest()
.latest_ref()
.pac_file_content
.clone()
.unwrap_or(DEFAULT_PAC.to_string());
let port = Config::verge()
.latest()
.latest_ref()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let content = content.replace("%mixed-port%", &format!("{}", port));
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
let content = content.replace("%mixed-port%", &format!("{port}"));
warp::http::Response::builder()
.header("Content-Type", "application/x-ns-proxy-autoconfig")
.body(content)
+382
View File
@@ -0,0 +1,382 @@
use crate::{core::handle, logging, utils::logging::Type};
use tauri::{Manager, WebviewWindow, Wry};
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use scopeguard;
use std::{
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
/// 窗口操作结果
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowOperationResult {
/// 窗口已显示并获得焦点
Shown,
/// 窗口已隐藏
Hidden,
/// 创建了新窗口
Created,
/// 操作失败
Failed,
/// 无需操作
NoAction,
}
/// 窗口状态
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowState {
/// 窗口可见且有焦点
VisibleFocused,
/// 窗口可见但无焦点
VisibleUnfocused,
/// 窗口最小化
Minimized,
/// 窗口隐藏
Hidden,
/// 窗口不存在
NotExist,
}
// 窗口操作防抖机制
static WINDOW_OPERATION_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
static WINDOW_OPERATION_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 500;
fn get_window_operation_debounce() -> &'static Mutex<Instant> {
WINDOW_OPERATION_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
}
fn should_handle_window_operation() -> bool {
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) {
log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用");
return false;
}
let debounce_lock = get_window_operation_debounce();
let mut last_operation = debounce_lock.lock();
let now = Instant::now();
let elapsed = now.duration_since(*last_operation);
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
*last_operation = now;
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
true
} else {
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
false
}
}
fn finish_window_operation() {
WINDOW_OPERATION_IN_PROGRESS.store(false, Ordering::Release);
}
/// 统一的窗口管理器
pub struct WindowManager;
impl WindowManager {
pub fn get_main_window_state() -> WindowState {
match Self::get_main_window() {
Some(window) => {
let is_minimized = window.is_minimized().unwrap_or(false);
let is_visible = window.is_visible().unwrap_or(false);
let is_focused = window.is_focused().unwrap_or(false);
if is_minimized {
return WindowState::Minimized;
}
if !is_visible {
return WindowState::Hidden;
}
if is_focused {
WindowState::VisibleFocused
} else {
WindowState::VisibleUnfocused
}
}
None => WindowState::NotExist,
}
}
/// 获取主窗口实例
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
handle::Handle::global()
.app_handle()
.and_then(|app| app.get_webview_window("main"))
}
/// 智能显示主窗口
pub fn show_main_window() -> WindowOperationResult {
// 防抖检查
if !should_handle_window_operation() {
return WindowOperationResult::NoAction;
}
let _guard = scopeguard::guard((), |_| {
finish_window_operation();
});
logging!(info, Type::Window, true, "开始智能显示主窗口");
logging!(
debug,
Type::Window,
true,
"{}",
Self::get_window_status_info()
);
let current_state = Self::get_main_window_state();
match current_state {
WindowState::NotExist => {
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
if Self::create_new_window() {
logging!(info, Type::Window, true, "窗口创建成功");
std::thread::sleep(std::time::Duration::from_millis(100));
WindowOperationResult::Created
} else {
logging!(warn, Type::Window, true, "窗口创建失败");
WindowOperationResult::Failed
}
}
WindowState::VisibleFocused => {
logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作");
WindowOperationResult::NoAction
}
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
if let Some(window) = Self::get_main_window() {
let state_after_check = Self::get_main_window_state();
if state_after_check == WindowState::VisibleFocused {
logging!(
info,
Type::Window,
true,
"窗口在检查期间已变为可见和有焦点状态"
);
return WindowOperationResult::NoAction;
}
Self::activate_window(&window)
} else {
WindowOperationResult::Failed
}
}
}
}
/// 切换主窗口显示状态(显示/隐藏)
pub fn toggle_main_window() -> WindowOperationResult {
// 防抖检查
if !should_handle_window_operation() {
return WindowOperationResult::NoAction;
}
let _guard = scopeguard::guard((), |_| {
finish_window_operation();
});
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
let current_state = Self::get_main_window_state();
logging!(
info,
Type::Window,
true,
"当前窗口状态: {:?} | 详细状态: {}",
current_state,
Self::get_window_status_info()
);
match current_state {
WindowState::NotExist => {
// 窗口不存在,创建新窗口
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
// 由于已经有防抖保护,直接调用内部方法
if Self::create_new_window() {
WindowOperationResult::Created
} else {
WindowOperationResult::Failed
}
}
WindowState::VisibleFocused | WindowState::VisibleUnfocused => {
logging!(
info,
Type::Window,
true,
"窗口可见(焦点状态: {}),将隐藏窗口",
if current_state == WindowState::VisibleFocused {
"有焦点"
} else {
"无焦点"
}
);
if let Some(window) = Self::get_main_window() {
match window.hide() {
Ok(_) => {
logging!(info, Type::Window, true, "窗口已成功隐藏");
WindowOperationResult::Hidden
}
Err(e) => {
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed
}
}
} else {
logging!(warn, Type::Window, true, "无法获取窗口实例");
WindowOperationResult::Failed
}
}
WindowState::Minimized | WindowState::Hidden => {
logging!(
info,
Type::Window,
true,
"窗口存在但被隐藏或最小化,将激活窗口"
);
if let Some(window) = Self::get_main_window() {
Self::activate_window(&window)
} else {
logging!(warn, Type::Window, true, "无法获取窗口实例");
WindowOperationResult::Failed
}
}
}
}
/// 激活窗口(取消最小化、显示、设置焦点)
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
logging!(info, Type::Window, true, "开始激活窗口");
let mut operations_successful = true;
// 1. 如果窗口最小化,先取消最小化
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
if let Err(e) = window.unminimize() {
logging!(warn, Type::Window, true, "取消最小化失败: {}", e);
operations_successful = false;
}
}
// 2. 显示窗口
if let Err(e) = window.show() {
logging!(warn, Type::Window, true, "显示窗口失败: {}", e);
operations_successful = false;
}
// 3. 设置焦点
if let Err(e) = window.set_focus() {
logging!(warn, Type::Window, true, "设置窗口焦点失败: {}", e);
operations_successful = false;
}
// 4. 平台特定的激活策略
#[cfg(target_os = "macos")]
{
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
AppHandleManager::global().set_activation_policy_regular();
}
#[cfg(target_os = "windows")]
{
// Windows 尝试额外的激活方法
if let Err(e) = window.set_always_on_top(true) {
logging!(
debug,
Type::Window,
true,
"设置置顶失败(非关键错误): {}",
e
);
}
// 立即取消置顶
if let Err(e) = window.set_always_on_top(false) {
logging!(
debug,
Type::Window,
true,
"取消置顶失败(非关键错误): {}",
e
);
}
}
if operations_successful {
logging!(info, Type::Window, true, "窗口激活成功");
WindowOperationResult::Shown
} else {
logging!(warn, Type::Window, true, "窗口激活部分失败");
WindowOperationResult::Failed
}
}
/// 隐藏主窗口
pub fn hide_main_window() -> WindowOperationResult {
logging!(info, Type::Window, true, "开始隐藏主窗口");
match Self::get_main_window() {
Some(window) => match window.hide() {
Ok(_) => {
logging!(info, Type::Window, true, "窗口已隐藏");
WindowOperationResult::Hidden
}
Err(e) => {
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed
}
},
None => {
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
WindowOperationResult::NoAction
}
}
}
/// 检查窗口是否可见
pub fn is_main_window_visible() -> bool {
Self::get_main_window()
.map(|window| window.is_visible().unwrap_or(false))
.unwrap_or(false)
}
/// 检查窗口是否有焦点
pub fn is_main_window_focused() -> bool {
Self::get_main_window()
.map(|window| window.is_focused().unwrap_or(false))
.unwrap_or(false)
}
/// 检查窗口是否最小化
pub fn is_main_window_minimized() -> bool {
Self::get_main_window()
.map(|window| window.is_minimized().unwrap_or(false))
.unwrap_or(false)
}
/// 创建新窗口,防抖避免重复调用
fn create_new_window() -> bool {
use crate::utils::resolve;
resolve::create_window(true)
}
/// 获取详细的窗口状态信息
pub fn get_window_status_info() -> String {
let state = Self::get_main_window_state();
let is_visible = Self::is_main_window_visible();
let is_focused = Self::is_main_window_focused();
let is_minimized = Self::is_main_window_minimized();
format!(
"窗口状态: {state:?} | 可见: {is_visible} | 有焦点: {is_focused} | 最小化: {is_minimized}"
)
}
}
@@ -2,13 +2,10 @@
name = "mihomo_api"
edition = "2024"
[features]
debug = []
[dependencies]
reqwest = { version = "0.12.15", features = ["json"] }
reqwest = { version = "0.12.22", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tokio = { version = "1.44.1", features = ["rt", "macros", "time"] }
tokio = { version = "1.46.1", features = ["rt", "macros", "time"] }
[dev-dependencies]
@@ -1,11 +1,8 @@
use reqwest::{Method, header::HeaderMap};
use serde_json::json;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
use serde_json::{Value, json};
use std::time::Duration;
pub mod model;
pub use model::{MihomoData, MihomoManager};
pub use model::MihomoManager;
impl MihomoManager {
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
@@ -20,38 +17,10 @@ impl MihomoManager {
Self {
mihomo_server,
data: Arc::new(Mutex::new(MihomoData {
proxies: serde_json::Value::Null,
providers_proxies: serde_json::Value::Null,
})),
client,
}
}
fn update_proxies(&self, proxies: serde_json::Value) {
let mut data = self.data.lock().expect("Mutex poisoned");
data.proxies = proxies;
}
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
let mut data = self.data.lock().expect("Mutex poisoned");
data.providers_proxies = providers_proxies;
}
pub fn get_mihomo_server(&self) -> String {
self.mihomo_server.clone()
}
pub fn get_proxies(&self) -> serde_json::Value {
let data = self.data.lock().expect("Mutex poisoned");
data.proxies.clone()
}
pub fn get_providers_proxies(&self) -> serde_json::Value {
let data = self.data.lock().expect("Mutex poisoned");
data.providers_proxies.clone()
}
async fn send_request(
&self,
method: Method,
@@ -87,18 +56,16 @@ impl MihomoManager {
Ok(response)
}
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
pub async fn get_refresh_proxies(&self) -> Result<Value, String> {
let url = format!("{}/proxies", self.mihomo_server);
let proxies = self.send_request(Method::GET, url, None).await?;
self.update_proxies(proxies);
Ok(self)
Ok(proxies)
}
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
pub async fn get_providers_proxies(&self) -> Result<Value, String> {
let url = format!("{}/providers/proxies", self.mihomo_server);
let providers_proxies = self.send_request(Method::GET, url, None).await?;
self.update_providers_proxies(providers_proxies);
Ok(self)
Ok(providers_proxies)
}
pub async fn close_all_connections(&self) -> Result<(), String> {
@@ -1,27 +1,5 @@
use std::sync::{Arc, Mutex};
pub struct MihomoData {
pub(crate) proxies: serde_json::Value,
pub(crate) providers_proxies: serde_json::Value,
}
#[derive(Clone)]
pub struct MihomoManager {
pub(crate) mihomo_server: String,
pub(crate) data: Arc<Mutex<MihomoData>>,
pub(crate) client: reqwest::Client,
}
#[cfg(feature = "debug")]
impl Drop for MihomoData {
fn drop(&mut self) {
println!("Dropping MihomoData");
}
}
#[cfg(feature = "debug")]
impl Drop for MihomoManager {
fn drop(&mut self) {
println!("Dropping MihomoManager");
}
}
@@ -1,29 +1,7 @@
use mihomo_api;
use reqwest::header::HeaderMap;
#[test]
fn test_mihomo_manager_init() {
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
assert_eq!(manager.get_proxies(), serde_json::Value::Null);
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
let manager = manager.refresh_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_ne!(proxies, serde_json::Value::Null);
assert_eq!(providers, serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_providers_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
let manager = manager.refresh_providers_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_eq!(proxies, serde_json::Value::Null);
assert_ne!(providers, serde_json::Value::Null);
let _ = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
assert_eq!(true, true);
}
+7 -23
View File
@@ -1,5 +1,5 @@
{
"version": "2.3.0",
"version": "2.3.2",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
@@ -11,15 +11,9 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"resources",
"resources/locales/*"
],
"resources": ["resources", "resources/locales/*"],
"publisher": "Clash Verge Rev",
"externalBin": [
"sidecar/verge-mihomo",
"sidecar/verge-mihomo-alpha"
],
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"copyright": "GNU General Public License v3.0",
"category": "DeveloperTool",
"shortDescription": "Clash Verge Rev",
@@ -50,28 +44,18 @@
},
"deep-link": {
"desktop": {
"schemes": [
"clash",
"clash-verge"
]
"schemes": ["clash", "clash-verge"]
}
}
},
"app": {
"security": {
"capabilities": [
"desktop-capability",
"migrated"
],
"capabilities": ["desktop-capability", "migrated"],
"assetProtocol": {
"scope": [
"$APPDATA/**",
"$RESOURCE/../**",
"**"
],
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
"enable": true
},
"csp": null
}
}
}
}

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