180 Commits

Author SHA1 Message Date
coolcoala
a1e1fedc3f v0.2.7 2025-09-29 02:52:35 +03:00
coolcoala
84dc631d80 updated UPDATELOG.md 2025-09-29 02:52:05 +03:00
coolcoala
6a3072fe04 hide the icon from the dock on macOS when clicking the close button 2025-09-29 02:33:53 +03:00
coolcoala
98d943f39d update dependencies 2025-09-29 02:30:28 +03:00
coolcoala
bcf724273d added message about global mode enabled 2025-09-29 02:29:13 +03:00
coolcoala
8703918a8c fixed some bugs with UI 2025-09-29 02:21:50 +03:00
coolcoala
7e88f3ba29 fixed localization 2025-09-29 02:02:34 +03:00
coolcoala
d9a2f221db v0.2.6 2025-08-25 01:08:34 +03:00
coolcoala
a4b3a257ed updated UPDATELOG.md 2025-08-25 01:05:48 +03:00
coolcoala
10397d0847 logs translated from Chinese into English 2025-08-25 01:04:56 +03:00
coolcoala
db442b2746 Fixed renaming installer 2025-08-23 03:33:19 +03:00
coolcoala
8cb3c69b78 Fixed issue with deep links 2025-08-23 03:15:29 +03:00
coolcoala
967f21cc23 Improved proxy selector view 2025-08-23 03:10:46 +03:00
coolcoala
3ecd73f430 Added some animations 2025-08-23 03:10:18 +03:00
coolcoala
ca7f6b86d7 Fixed an issue with the selector width 2025-08-23 03:06:20 +03:00
coolcoala
00cee81812 Fixed an issue with adding a profile when making changes in advanced settings. 2025-08-23 03:03:07 +03:00
coolcoala
25f5db82dc fixed dns leak 2025-08-19 16:43:50 +03:00
coolcoala
9e5c5d5e69 Merge remote-tracking branch 'origin/dev' into dev 2025-08-17 16:09:48 +03:00
coolcoala
2cfd1784d8 Merge pull request #8 from vffuunnyy/dev
Features + logs
2025-08-17 13:39:56 +03:00
vffuunnyy
bec1b95ad3 refactor: fix lifetime annotations in draft.rs
ci: add cargo retry settings
2025-08-16 15:31:30 +07:00
vffuunnyy
e26f500ad0 refactor: format code with prettier and fix quotation marks 2025-08-16 15:23:43 +07:00
vffuunnyy
9c33f007a1 refactor: fix formating in rust 2025-08-16 15:22:38 +07:00
vffuunnyy
902256d461 refactor: translate Chinese log messages to English in core modules 2025-08-16 04:00:00 +07:00
vffuunnyy
6051bd6d06 Merge branch 'dev' of https://github.com/vffuunnyy/clash-verge-rev-lite into dev
* 'dev' of https://github.com/vffuunnyy/clash-verge-rev-lite:
  the Add Profile button has been moved, and the layout has been slightly changed.
  the connections page has been slightly revised.
  fixed an issue with the dialog box when the profile name is long.
  added glass effect to components
  fixed icon background
2025-08-16 01:57:32 +07:00
vffuunnyy
c82f4e50d2 feat: add compression support and fix tun config overwrite issue 2025-08-16 01:56:56 +07:00
coolcoala
94e785c75c fixed deeplinks on windows 2025-08-12 21:19:26 +03:00
coolcoala
8b8daa7b4c the Add Profile button has been moved, and the layout has been slightly changed. 2025-08-09 02:54:05 +03:00
coolcoala
c95e63014f the connections page has been slightly revised. 2025-08-09 02:54:05 +03:00
coolcoala
32bf42cbb9 fixed an issue with the dialog box when the profile name is long. 2025-08-09 02:54:05 +03:00
coolcoala
175ec98947 added glass effect to components 2025-08-09 02:53:56 +03:00
coolcoala
0abd9343a9 Merge remote-tracking branch 'origin/dev' into dev 2025-08-09 02:35:53 +03:00
coolcoala
c9976382a9 Merge pull request #6 from vffuunnyy/dev
feat: replace AliDNS with Google and Cloudflare DNS servers
2025-08-08 01:45:17 +03:00
vffuunnyy
d38e93ac7e feat: replace AliDNS with Google and Cloudflare DNS servers 2025-08-08 05:24:25 +07:00
coolcoala
e51f1d20c0 fixed icon background 2025-08-06 18:49:36 +03:00
coolcoala
df595f4835 v0.2.5 2025-08-05 19:14:31 +03:00
coolcoala
63e4d2f686 updated UPDATELOG.md 2025-08-05 19:12:40 +03:00
coolcoala
971580def8 fixed map in background 2025-08-05 19:06:28 +03:00
coolcoala
ffd32426b5 fixed an issue with enabling tun and system proxy if profiles are missing 2025-08-05 19:05:44 +03:00
coolcoala
d2d26cc822 fixed icon for windows 2025-08-05 19:03:38 +03:00
coolcoala
a373b0b6eb fixed locales 2025-08-05 18:52:02 +03:00
coolcoala
f515fa1443 unnecessary menu items removed 2025-08-05 17:51:44 +03:00
coolcoala
e32e83d45e renamed to koala clash 2025-08-05 17:51:26 +03:00
coolcoala
7be3cdeb65 fixed command for macOS 2025-08-05 09:29:25 +03:00
coolcoala
b234b9166d code signing for macOS 2025-08-03 15:41:18 +03:00
coolcoala
2c485b5efb fixed an issue with opening a window via a shortcut when the application is already active 2025-08-03 12:56:46 +03:00
coolcoala
b7d7e1a1af fixed issue with clicking on shortcut 2025-08-03 11:21:02 +03:00
coolcoala
01be6ae70a fixing the tray customization issue 2025-08-03 11:12:25 +03:00
coolcoala
445eaadac3 minor fix in russian localization 2025-08-03 11:11:36 +03:00
coolcoala
d5b1dfddee new homepage 2025-08-03 11:10:00 +03:00
coolcoala
c68ea04f06 fixed icon in sidebar 2025-08-03 11:09:36 +03:00
coolcoala
9abc30b60c fix for dark mode in pop-up notifications, system theme detection 2025-08-03 11:08:32 +03:00
coolcoala
1f7561298c minor fix 2025-08-02 08:51:44 +03:00
coolcoala
611c5757e0 new icons 2025-08-02 04:21:34 +03:00
coolcoala
ab56e82173 fix logo in sidebar 2025-08-02 03:31:12 +03:00
coolcoala
34350fadb6 v0.2.4 2025-08-01 19:41:28 +03:00
coolcoala
77786da53f fix icons 2025-08-01 19:41:21 +03:00
coolcoala
f794ca5426 added links for donate 2025-08-01 19:41:15 +03:00
coolcoala
a2010e6d1d fixed renaming files 2025-07-30 09:11:57 +03:00
coolcoala
4ce6e9bfd7 updated UPDATELOG.md 2025-07-30 06:59:48 +03:00
coolcoala
9a3794073b fixed flag display when adding a link via vless:// 2025-07-30 06:53:02 +03:00
coolcoala
d6197d6d21 added traffic information display to the main page 2025-07-30 06:53:02 +03:00
coolcoala
1f321cf6bc fixed translations 2025-07-30 06:52:55 +03:00
coolcoala
5c6d3f4078 unused settings removed 2025-07-30 06:31:49 +03:00
coolcoala
6b8b95e4ca traffic information has been reworked 2025-07-30 06:31:49 +03:00
coolcoala
ae08d48641 added application icon to sidebar 2025-07-30 06:31:45 +03:00
coolcoala
d1ce5566cf added new background for dmg installer 2025-07-28 08:52:39 +03:00
coolcoala
5f027ebc79 started the process of renaming to Koala Clash 2025-07-28 08:43:36 +03:00
coolcoala
8cf83f8338 minor fix 2025-07-28 08:43:36 +03:00
coolcoala
b96e2c1fe0 notification of exceeding the number of devices in the subscription, support for vless:// links with templates by @legiz-ru 2025-07-28 08:43:30 +03:00
coolcoala
4ad1379773 new icons 2025-07-28 06:53:48 +03:00
coolcoala
ef0883f732 notifications in Telegram, and changes have been made so that the link to the release does not change over time 2025-07-26 09:28:24 +03:00
coolcoala
a2076b4e2d minor fix 2025-07-26 06:54:34 +03:00
coolcoala
0a3998530e the alphabetical index has been removed, and additional information about proxies is now hidden by default 2025-07-26 06:54:25 +03:00
coolcoala
ed2ec56a44 the size of modal windows has been adjusted due to an increase in the minimum window size 2025-07-26 06:53:35 +03:00
coolcoala
87473bdf92 fixed log color when dark theme is enabled 2025-07-26 06:53:00 +03:00
coolcoala
8186a6841a added icons for proxy groups 2025-07-26 06:52:36 +03:00
coolcoala
0a0b5b6612 direct was removed, and the translation for rules and global was replaced 2025-07-26 06:52:07 +03:00
coolcoala
72704f9dc9 the minimum window size has been changed 2025-07-26 06:50:42 +03:00
coolcoala
06ad23d904 added auto-scaling and scaling via key combination 2025-07-26 06:50:18 +03:00
coolcoala
fbd1c55f44 v0.2.3 2025-07-22 02:11:13 +03:00
coolcoala
9668a04a1a updated UPDATELOG.md 2025-07-21 03:41:07 +03:00
coolcoala
24af375a8e started work on translating console logs from Chinese to English 2025-07-21 03:40:47 +03:00
coolcoala
a32c973ab8 fixed problem with profile inactivation after adding via deeplink on windows 2025-07-21 03:06:37 +03:00
coolcoala
50beb913de fixed command mapping for macos installation 2025-07-21 03:06:22 +03:00
coolcoala
05f1ec7b34 added that it is not possible to enable proxy if no profile is available 2025-07-21 01:57:37 +03:00
coolcoala
9271b107b6 fixed a layout issue in the proxy menu, now all cards are the same size 2025-07-21 01:56:24 +03:00
coolcoala
e7208dd7d2 fixed problem with menu reopening when opening a page in a compressed window 2025-07-21 01:55:33 +03:00
coolcoala
e5dfb34082 v0.2.2 2025-07-19 03:57:29 +03:00
coolcoala
2ba5c4e706 new menu added, layout corrected in some places 2025-07-19 03:57:07 +03:00
coolcoala
27bcc5f4f8 v0.2.1 2025-07-18 04:51:35 +03:00
coolcoala
d884bd539b fixed release body and configs for webview 2025-07-18 04:30:43 +03:00
coolcoala
580a56727c new localization lines added 2025-07-18 04:18:19 +03:00
coolcoala
ac3163d061 added button to turn off hwid sending 2025-07-18 04:17:58 +03:00
coolcoala
8bc7a6c3e1 added saving the location on the main page when adding a profile via deeplink, profile update button on the main page, corrected url for telegram icon for support, and announcement-url header support. 2025-07-18 04:17:18 +03:00
coolcoala
31d368979e added update profiles at startup, “announce-url” header, and also when adding check if the profile already exists and if it does, just update it 2025-07-18 04:12:55 +03:00
coolcoala
5e855e4755 fixed locales and number of version in files 2025-07-15 03:24:35 +03:00
coolcoala
a8b75aeabd updated UPDATELOG.md 2025-07-15 03:09:38 +03:00
coolcoala
854d42180a corrected information in the README.md 2025-07-15 03:09:10 +03:00
coolcoala
e94724595c fixes for possible problems with updates 2025-07-15 03:08:23 +03:00
coolcoala
6f1d9ba1b4 added support for special headers and displaying their information on the main page 2025-07-15 03:07:42 +03:00
coolcoala
c090ae3b11 removed icons that are not used anywhere else 2025-07-15 03:06:11 +03:00
coolcoala
3303e95713 fixed actions for release 2025-07-15 03:05:37 +03:00
coolcoala
5cdc5075f8 code formatting with prettier 2025-07-14 05:23:32 +03:00
coolcoala
eb1e4fe0c3 added a setting to change the proxy mode controlled by a switch on the homepage 2025-07-14 05:23:02 +03:00
coolcoala
b1e3283a24 fixed automatic activation of profile after importing 2025-07-14 05:07:57 +03:00
coolcoala
ce3b0bb479 fixed selection from list and sticking of modal windows to window edges 2025-07-14 05:07:15 +03:00
coolcoala
25b295f2a8 latency is now measured only when opening the proxy list 2025-07-14 01:13:54 +03:00
coolcoala
18b7366258 simplified the proxy import menu 2025-07-14 01:13:20 +03:00
coolcoala
565771a3ea updated UPDATELOG.md 2025-07-13 23:04:24 +03:00
coolcoala
f9376f6903 added notification that profiles are missing 2025-07-13 22:55:20 +03:00
coolcoala
8e0f5b6abd fixed layout of profile cards 2025-07-13 22:54:44 +03:00
coolcoala
41f32231f0 fixed localization and made minor bug fixes 2025-07-11 21:15:52 +03:00
coolcoala
e1968891ac corrected display of flags 2025-07-11 20:18:33 +03:00
coolcoala
f04e707b10 corrected localization of the installer, and also corrected project name in configs 2025-07-11 20:18:09 +03:00
coolcoala
0bb795b0e1 fixed proxy guard in sysproxy settings 2025-07-11 20:16:54 +03:00
coolcoala
1c5e43690e added tooltips for the sorting icon in the proxy list 2025-07-11 20:16:24 +03:00
coolcoala
f604416532 another attempt to fix emoji display on windows 2025-07-11 04:58:53 +03:00
coolcoala
87ee07d481 fixed display of proxy groups and emoji on windows 2025-07-11 04:26:01 +03:00
coolcoala
7dec9cbe9b fix actions 2025-07-10 23:18:38 +03:00
coolcoala
1274ba2324 HWID implementation 2025-07-10 20:50:12 +03:00
coolcoala
d6014865d6 fixed display of profiles 2025-07-10 20:49:18 +03:00
coolcoala
48a5ff6948 fixed localization 2025-07-10 20:48:51 +03:00
coolcoala
dd3950e46d corrected triggers for launching a build 2025-07-10 02:55:41 +03:00
coolcoala
1708246866 fixed bug with displaying alert about the need to install the service 2025-07-09 11:01:39 +03:00
coolcoala
b0734f5935 fixed github link 2025-07-09 10:27:53 +03:00
coolcoala
11768862d3 increased the size of the proxy enable button 2025-07-09 10:25:58 +03:00
coolcoala
ef409216d8 attempting to configure the autobuild 2025-07-09 05:00:24 +03:00
coolcoala
f739afea3d fixed problem with language switching 2025-07-09 04:49:47 +03:00
coolcoala
d5266fa003 fixed theme viewer 2025-07-09 04:49:05 +03:00
coolcoala
149bdd5175 corrected layout for better visibility 2025-07-09 04:47:59 +03:00
coolcoala
ec99e24ca1 fixed dark mode 2025-07-09 04:43:16 +03:00
coolcoala
7cc893383e README has been corrected 2025-07-09 04:30:20 +03:00
coolcoala
3902480d39 corrected layout on profiles and rules pages 2025-07-08 15:42:18 +03:00
coolcoala
686490ded1 New Interface (initial commit) 2025-07-04 02:28:27 +03: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
❤是纱雾酱哟~
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]
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]
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]
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]
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]
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]
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]
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
希亚的西红柿
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]
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]
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
273 changed files with 26530 additions and 19230 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
custom:
[
"https://t.me/tribute/app?startapp=dtfk",
"https://t.me/tribute/app?startapp=dtLE",
]

View File

@@ -61,5 +61,12 @@ body:
attributes: attributes:
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly) label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
description: 请提供完整或相关部分的Debug日志请在“软件左侧菜单”->“设置”->“日志等级”调整到debugVerge错误请把“杂项设置”->“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") description: 请提供完整或相关部分的Debug日志请在“软件左侧菜单”->“设置”->“日志等级”调整到debugVerge错误请把“杂项设置”->“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")
value: |
<details><summary>日志内容 / Log Content</summary>
```log
<!-- 在此处粘贴完整日志 / Paste the full log here -->
```
</details>
validations: validations:
required: true required: true

View File

@@ -2,9 +2,9 @@ name: Auto Build
on: on:
workflow_dispatch: workflow_dispatch:
schedule: # schedule:
# UTC+8 0,6,12,18 # # UTC+8 0,6,12,18
- cron: "0 16,22,4,10 * * *" # - cron: "0 16,22,4,10 * * *"
permissions: write-all permissions: write-all
env: env:
TAG_NAME: autobuild TAG_NAME: autobuild
@@ -77,6 +77,15 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Update git tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -f ${{ env.TAG_NAME }}
git push --force origin ${{ env.TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch UPDATE logs - name: Fetch UPDATE logs
id: fetch_update_logs id: fetch_update_logs
run: | run: |
@@ -111,31 +120,25 @@ jobs:
cat > release.txt << EOF cat > release.txt << EOF
$UPDATE_LOGS $UPDATE_LOGS
## 我应该下载哪个版本? ## Which version should I download?
### MacOS ### MacOS
- MacOS intel芯片: x64.dmg - MacOS Intel Chip: x64.dmg
- MacOS apple M芯片: aarch64.dmg - MacOS Apple M Chip: aarch64.dmg
### Linux ### Linux
- Linux 64: amd64.deb/amd64.rpm - Linux 64-bit: amd64.deb/amd64.rpm
- Linux arm64 architecture: arm64.deb/aarch64.rpm - Linux arm64: arm64.deb/aarch64.rpm
- Linux armv7架构: armhf.deb/armhfp.rpm - Linux armv7: armhf.deb/armhfp.rpm
### Windows (不再支持Win7) ### Windows (Win7 is no longer supported)
#### 正常版本(推荐) #### Normal version (recommended)
- 64: x64-setup.exe - 64-bit: x64-setup.exe
- arm64架构: arm64-setup.exe - arm64: arm64-setup.exe
#### 便携版问题很多不再提供 #### Portable version is no longer available with many problems
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) #### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
- 64: x64_fixed_webview2-setup.exe - 64-bit: x64_fixed_webview2-setup.exe
- arm64架构: arm64_fixed_webview2-setup.exe - arm64: arm64_fixed_webview2-setup.exe
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}. Created at ${{ env.BUILDTIME }}.
EOF EOF
@@ -150,6 +153,28 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true 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: autobuild-x86-windows-macos-linux:
name: Autobuild x86 Windows, MacOS and Linux name: Autobuild x86 Windows, MacOS and Linux
needs: update_tag needs: update_tag
@@ -168,6 +193,9 @@ jobs:
- os: ubuntu-22.04 - os: ubuntu-22.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -217,12 +245,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
tagName: ${{ env.TAG_NAME }} tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"

View File

@@ -17,6 +17,9 @@ jobs:
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -28,6 +28,9 @@ jobs:
bundle: dmg bundle: dmg
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -10,6 +10,9 @@ on:
jobs: jobs:
rustfmt: rustfmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -26,11 +29,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: npm i -g --force corepack
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "lts/*" node-version: "lts/*"
- run: pnpm i --frozen-lockfile - run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm format:check - run: pnpm format:check
# taplo: # taplo:

View File

@@ -40,9 +40,91 @@ jobs:
fi fi
echo "Tag and package.json version are consistent." echo "Tag and package.json version are consistent."
create_release_notes:
name: Create Release Notes
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
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 update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
- run: |
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 update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## Which version should I download?
### macOS
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_aarch64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Apple%20Silicon"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Intel"></a><br>
> :warning: **Warning**
If you get a notification that the application is corrupted when you run it on macOS, run this command:<br>
<code>sudo xattr -r -c /Applications/Koala\ Clash.app</code>
### Linux
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64.deb"><img src="https://img.shields.io/badge/arm64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.aarch64.rpm"><img src="https://img.shields.io/badge/aarch64-default?style=flat&logo=fedora&label=RPM"> </a>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_armhf.deb"><img src="https://img.shields.io/badge/armhf-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.armhfp.rpm"><img src="https://img.shields.io/badge/armhfp-default?style=flat&logo=fedora&label=RPM"> </a>
### Windows (Win7 is no longer supported)
#### Normal version (recommended)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
#### Portable version is no longer available with many problems
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{env.VERSION}}
name: "Koala Clash v${{env.VERSION}}"
body_path: release.txt
token: ${{ secrets.GITHUB_TOKEN }}
release: release:
name: Release Build name: Release Build
needs: check_tag_version needs: [check_tag_version, create_release_notes]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -96,7 +178,13 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Create .p8 file
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "${{ secrets.APPLE_API_KEY_CONTENT }}" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
- name: Tauri build - name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
env: env:
NODE_OPTIONS: "--max_old_space_size=4096" NODE_OPTIONS: "--max_old_space_size=4096"
@@ -106,16 +194,61 @@ jobs:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_API_KEY_PATH: "~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8"
with: with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev v__VERSION__"
releaseBody: "More new features are now supported."
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
- name: Rename Artifact (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_"
Rename-Item $file.FullName $newName
}
- name: Rename Artifact (Linux/macOS)
if: runner.os == 'Linux' || runner.os == 'macOS'
shell: bash
run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
dir_path=$(dirname "$old_path")
old_filename=$(basename "$old_path")
new_filename=$(echo "$old_filename" \
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
)
new_path="${dir_path}/${new_filename}"
if [ "$old_path" != "$new_path" ]; then
echo " - '$old_filename' -> '$new_filename'"
mv "$old_path" "$new_path"
fi
done
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig
release-for-linux-arm: release-for-linux-arm:
name: Release Build for Linux ARM name: Release Build for Linux ARM
strategy: strategy:
@@ -225,14 +358,36 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install jq sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
- name: Rename
shell: bash
run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
dir_path=$(dirname "$old_path")
old_filename=$(basename "$old_path")
new_filename=$(echo "$old_filename" \
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
)
new_path="${dir_path}/${new_filename}"
if [ "$old_path" != "$new_path" ]; then
echo " - '$old_filename' -> '$new_filename'"
mv "$old_path" "$new_path"
fi
done
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{env.VERSION}} tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}" name: "Koala Clash v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: | files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
@@ -281,8 +436,8 @@ jobs:
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
run: | run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
@@ -302,19 +457,19 @@ jobs:
run: | run: |
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe" $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) { foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe" $newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName Rename-Item $file.FullName $newName
} }
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip" $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) { foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip" $newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName Rename-Item $file.FullName $newName
} }
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig" $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) { foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig" $newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
Rename-Item $file.FullName $newName Rename-Item $file.FullName $newName
} }
@@ -322,8 +477,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{steps.build.outputs.appVersion}} tag_name: v${{steps.build.outputs.appVersion}}
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}" name: "Koala Clash v${{steps.build.outputs.appVersion}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
@@ -383,25 +537,68 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget: push-notify-to-telegram:
name: Submit to Winget
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release-update] needs: [release-update, release-update-for-fixed-webview2]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0 - name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
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 update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Get Version - name: Get Version
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install jq sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Submit to Winget echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
uses: vedantmgoyal9/winget-releaser@main
- run: |
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 update logs"
UPDATE_LOGS=$(echo "$UPDATE_LOGS" | sed 's/^## \(v.*\)/\*\1\*/')
fi
cat > release.txt << EOF
Вышло обновление!
$UPDATE_LOGS
[Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest)
EOF
- name: notify to channel
uses: appleboy/telegram-action@master
with: with:
identifier: ClashVergeRev.ClashVergeRev to: ${{ secrets.TELEGRAM_TO_CHANNEL }}
version: ${{env.VERSION}} token: ${{ secrets.TELEGRAM_TOKEN }}
release-tag: v${{env.VERSION}} message_file: release.txt
installers-regex: '_(arm64|x64|x86)-setup\.exe$' format: markdown
token: ${{ secrets.WINGET_TOKEN }}
- name: notify to group
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_TO_GROUP }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message_file: release.txt
format: markdown

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ scripts/_env.sh
.tool-versions .tool-versions
.idea .idea
.old .old
bun.lock

View File

@@ -1,5 +1,8 @@
#!/bin/bash #!/bin/bash
# $1: remote name (e.g., origin)
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
if git diff --cached --name-only | grep -q '^src-tauri/'; then if git diff --cached --name-only | grep -q '^src-tauri/'; then
cargo clippy --manifest-path ./src-tauri/Cargo.toml cargo clippy --manifest-path ./src-tauri/Cargo.toml
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -8,11 +11,9 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then
fi fi
fi fi
remote_name="$1" # 只在 push 到 origin 并且 origin 指向目标仓库时执行格式检查
remote_url=$(git remote get-url "$remote_name") if [ "$1" = "origin" ] && echo "$2" | grep -Eq 'github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$'; then
echo "[pre-push] Detected push to origin (clash-verge-rev/clash-verge-rev)"
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..." echo "[pre-push] Running pnpm format:check..."
pnpm format:check pnpm format:check

View File

@@ -1,6 +1,16 @@
{ {
"singleQuote": true, "printWidth": 80,
"semi": false, "tabWidth": 2,
"trailingComma": "none", "useTabs": false,
"experimentalOperatorPosition": "start" "semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto"
} }

View File

@@ -1,7 +1,7 @@
<h1 align="center"> <h1 align="center">
<img src="./src-tauri/icons/icon.png" alt="Clash" width="128" /> <img src="./src-tauri/icons/icon.png" alt="Clash" width="128" />
<br> <br>
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a> Fork of <a href="https://github.com/clash-verge-rev/clash-verge-rev">Clash Verge Rev</a>
<br> <br>
</h1> </h1>
@@ -11,74 +11,26 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
## Preview ## Preview
| Dark | Light | | Dark | Light |
| -------------------------------- | --------------------------------- | | ----------------------------------- | ------------------------------------ |
| ![预览](./docs/preview_dark.png) | ![预览](./docs/preview_light.png) | | ![Preview](./docs/preview_dark.png) | ![Preview](./docs/preview_light.png) |
## Install ## Install
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br> Go to the [Release page](https://github.com/coolcoala/clash-verge-rev-lite/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). Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
#### 我应当怎样选择发行版 ### Telegram channel: ---
| 版本 | 特征 | 链接 |
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| 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/) 查看
---
### TG 频道: [@clash_verge_rev](https://t.me/clash_verge_re)
## Promotion
#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
- 优惠套餐每月仅需 15.8 元160G 流量,年付 8 折
- 海外团队,无跑路风险,高达 50% 返佣
- 集群负载均衡设计,高速专线(兼容老客户端)极低延迟无视晚高峰4K 秒开
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
- 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
🧩 YXVM 独立服务器优势:
- 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源非VPS可比性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
- 💡 支持即开即用多机房选择CN2 / IEPL 可选
- 📦 本项目使用配置已在售,欢迎同款入手!
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
## Features ## Features
- 基于性能强劲的 Rust Tauri 2 框架 - Based on the powerful Rust and Tauri 2 frameworks.
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。 - Built-in [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) kernel with support for switching between `Alpha` versions of the kernel.
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection` - Simple and beautiful user interface with support for custom theme colors, agent group/tray icons, and `CSS Injection`.
- 配置文件管理和增强Merge 和 Script配置文件语法提示。 - Configuration file management, configuration file syntax hints.
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。 - System Agent and Guard, `TUN (Virtual NIC)` mode.
- 可视化节点和规则编辑 - Visual node and rule editing
- WebDav 配置备份和同步 - WebDav configuration backup and synchronization
### FAQ
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
### Donation
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
## Development ## Development

File diff suppressed because it is too large Load Diff

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@root/src/components",
"utils": "@root/lib/utils",
"ui": "@root/src/components/ui",
"lib": "@root/lib",
"hooks": "@root/hooks"
},
"iconLibrary": "lucide"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 671 KiB

21
hooks/use-mobile.ts Normal file
View File

@@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "koala-clash",
"version": "2.3.1", "version": "0.2.7",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -29,55 +29,82 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^5.2.2",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.1.1", "@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.13", "@mui/lab": "7.0.0-beta.13",
"@mui/material": "^7.1.1", "@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.5.2", "@mui/x-data-grid": "^8.11.3",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "2.5.0", "@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.3", "@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.2.2", "@tauri-apps/plugin-deep-link": "~2.4.3",
"@tauri-apps/plugin-fs": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.1", "@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-notification": "^2.2.3", "@tauri-apps/plugin-global-shortcut": "^2.3.0",
"@tauri-apps/plugin-process": "^2.2.2", "@tauri-apps/plugin-notification": "^2.3.1",
"@tauri-apps/plugin-shell": "2.2.2", "@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-updater": "2.8.1", "@tauri-apps/plugin-shell": "2.2.1",
"@tauri-apps/plugin-window-state": "^2.2.3", "@tauri-apps/plugin-updater": "2.7.1",
"@tauri-apps/plugin-window-state": "^2.4.0",
"@types/d3-shape": "^3.1.7", "@types/d3-shape": "^3.1.7",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.8.5", "ahooks": "^3.9.5",
"axios": "^1.10.0", "axios": "^1.12.2",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"class-variance-authority": "^0.7.1",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"foxact": "^0.2.49", "foxact": "^0.2.49",
"framer-motion": "^12.23.16",
"glob": "^11.0.3", "glob": "^11.0.3",
"i18next": "^25.2.1", "i18next": "^25.5.2",
"js-base64": "^3.7.7", "js-base64": "^3.7.8",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.514.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"monaco-yaml": "^5.4.0", "monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"peggy": "^5.0.3", "next-themes": "^0.4.6",
"peggy": "^5.0.6",
"react": "19.1.0", "react": "19.1.0",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-colorful": "^5.6.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "6.0.0", "react-error-boundary": "6.0.0",
"react-hook-form": "^7.58.1", "react-hook-form": "^7.63.0",
"react-i18next": "15.5.3", "react-i18next": "15.5.2",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0", "react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.2", "react-router-dom": "7.6.2",
"react-virtuoso": "^4.13.0", "react-virtuoso": "^4.14.0",
"sockette": "^2.0.6", "sockette": "^2.0.6",
"swr": "^2.3.3", "sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"tar": "^7.4.3", "tar": "^7.4.3",
"types-pac": "^1.0.3", "types-pac": "^1.0.3",
"zustand": "^5.0.5" "zod": "^3.25.76",
"zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
@@ -85,25 +112,30 @@
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "19.1.8", "@types/node": "^24.5.2",
"@types/react": "19.1.6",
"@types/react-dom": "19.1.6", "@types/react-dom": "19.1.6",
"@vitejs/plugin-legacy": "^6.1.1", "@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-react": "4.5.2", "@vitejs/plugin-react": "4.5.1",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"commander": "^14.0.0", "autoprefixer": "^10.4.21",
"commander": "^14.0.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"meta-json-schema": "^1.19.10", "meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^3.5.3", "postcss": "^8.5.6",
"prettier": "^3.6.2",
"pretty-quick": "^4.2.2", "pretty-quick": "^4.2.2",
"sass": "^1.89.2", "sass": "^1.93.0",
"terser": "^5.43.0", "tailwindcss": "^4.1.13",
"typescript": "^5.8.3", "terser": "^5.44.0",
"vite": "^6.3.5", "tw-animate-css": "^1.3.8",
"typescript": "^5.9.2",
"vite": "^6.3.6",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0" "vite-plugin-svgr": "^4.5.0"
}, },
"prettier": { "prettier": {
"tabWidth": 2, "tabWidth": 2,
@@ -113,4 +145,4 @@
}, },
"type": "module", "type": "module",
"packageManager": "pnpm@9.13.2" "packageManager": "pnpm@9.13.2"
} }

4059
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,16 +42,16 @@ async function resolvePortable() {
const zip = new AdmZip(); const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe")); zip.addLocalFile(path.join(releaseDir, "Koala Clash.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe")); zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe")); zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources"); zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder( zip.addLocalFolder(
path.join( path.join(
releaseDir, releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`, `Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
), ),
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`, `Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
); );
zip.addLocalFolder(configDir, ".config"); zip.addLocalFolder(configDir, ".config");

View File

@@ -35,9 +35,9 @@ async function resolvePortable() {
} }
const zip = new AdmZip(); const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe")); zip.addLocalFile(path.join(releaseDir, "koala-clash.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe")); zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe")); zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources"); zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder(configDir, ".config"); zip.addLocalFolder(configDir, ".config");

View File

@@ -175,8 +175,8 @@ function clashMetaAlpha() {
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`; const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
return { return {
name: "verge-mihomo-alpha", name: "koala-mihomo-alpha",
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, targetFile: `koala-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile, exeFile,
zipFile, zipFile,
downloadURL, downloadURL,
@@ -192,8 +192,8 @@ function clashMeta() {
const zipFile = `${name}-${META_VERSION}.${urlExt}`; const zipFile = `${name}-${META_VERSION}.${urlExt}`;
return { return {
name: "verge-mihomo", name: "koala-mihomo",
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, targetFile: `koala-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile, exeFile,
zipFile, zipFile,
downloadURL, downloadURL,
@@ -381,7 +381,7 @@ const resolvePlugin = async () => {
// service chmod // service chmod
const resolveServicePermission = async () => { const resolveServicePermission = async () => {
const serviceExecutables = [ const serviceExecutables = [
"clash-verge-service*", "koala-clash-service*",
"install-service*", "install-service*",
"uninstall-service*", "uninstall-service*",
]; ];
@@ -429,14 +429,14 @@ async function resolveLocales() {
/** /**
* main * main
*/ */
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`; const SERVICE_URL = `https://github.com/coolcoala/koala-clash-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => { const resolveService = () => {
let ext = platform === "win32" ? ".exe" : ""; let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : ""; let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({ resolveResource({
file: "clash-verge-service" + suffix + ext, file: "koala-clash-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`, downloadURL: `${SERVICE_URL}/koala-clash-service${ext}`,
}); });
}; };
@@ -489,13 +489,13 @@ const resolveWinSysproxy = () =>
const tasks = [ const tasks = [
// { name: "clash", func: resolveClash, retry: 5 }, // { name: "clash", func: resolveClash, retry: 5 },
{ {
name: "verge-mihomo-alpha", name: "koala-mihomo-alpha",
func: () => func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())), getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5, retry: 5,
}, },
{ {
name: "verge-mihomo", name: "koala-mihomo",
func: () => func: () =>
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())), getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5, retry: 5,

3305
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,24 @@
[package] [package]
name = "clash-verge" name = "koala-clash"
version = "2.3.1" version = "0.2.7"
description = "clash verge" description = "koala clash"
authors = ["zzzgydi", "wonfen", "MystiPanda"] authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
default-run = "clash-verge" default-run = "koala-clash"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
[package.metadata.bundle] [package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev" identifier = "io.github.koala-clash"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.2.0", features = [] } tauri-build = { version = "2.3.0", features = [] }
[dependencies] [dependencies]
url = "2.5.4"
os_info = "3.0"
machine-uid = "0.5.3"
warp = "0.3.7" warp = "0.3.7"
anyhow = "1.0.98" anyhow = "1.0.98"
dirs = "6.0" dirs = "6.0"
@@ -25,7 +28,7 @@ dunce = "1.0.5"
log4rs = "1.3.0" log4rs = "1.3.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.41"
sysinfo = "0.35.2" sysinfo = "0.36.1"
boa_engine = "0.20.0" boa_engine = "0.20.0"
serde_json = "1.0.140" serde_json = "1.0.140"
serde_yaml = "0.9.34-deprecated" serde_yaml = "0.9.34-deprecated"
@@ -42,12 +45,12 @@ tokio = { version = "1.45.1", features = [
"sync", "sync",
] } ] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] } reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies", "brotli", "gzip", "zstd"] }
regex = "1.11.1" regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6" image = "0.25.6"
imageproc = "0.25.0" imageproc = "0.25.0"
tauri = { version = "2.5.1", features = [ tauri = { version = "2.6.2", features = [
"protocol-asset", "protocol-asset",
"devtools", "devtools",
"tray-icon", "tray-icon",
@@ -55,15 +58,14 @@ tauri = { version = "2.5.1", features = [
"image-png", "image-png",
] } ] }
network-interface = { version = "2.0.1", features = ["serde"] } network-interface = { version = "2.0.1", features = ["serde"] }
tauri-plugin-shell = "2.2.2" tauri-plugin-shell = "2.3.0"
tauri-plugin-dialog = "2.2.2" tauri-plugin-dialog = "2.3.0"
tauri-plugin-fs = "2.3.0" tauri-plugin-fs = "2.4.0"
tauri-plugin-process = "2.2.2" tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.2.3" tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.3.0"
tauri-plugin-devtools = "2.0.0" tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.2.3" tauri-plugin-window-state = "2.3.0"
zip = "4.1.0" zip = "4.2.0"
reqwest_dav = "0.2.1" reqwest_dav = "0.2.1"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"
@@ -75,11 +77,14 @@ async-trait = "0.1.88"
mihomo_api = { path = "src_crates/crate_mihomo_api" } mihomo_api = { path = "src_crates/crate_mihomo_api" }
ab_glyph = "0.2.29" ab_glyph = "0.2.29"
tungstenite = "0.27.0" tungstenite = "0.27.0"
libc = "0.2.173" libc = "0.2.174"
gethostname = "1.0.2" gethostname = "1.0.2"
hmac = "0.12.1" hmac = "0.12.1"
sha2 = "0.10.9" sha2 = "0.10.9"
hex = "0.4.3" hex = "0.4.3"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.0"
tauri-plugin-deep-link = "2"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
@@ -93,15 +98,20 @@ winapi = { version = "0.3.9", features = [
"errhandlingapi", "errhandlingapi",
"minwindef", "minwindef",
"winerror", "winerror",
"tlhelp32",
"processthreadsapi",
"winhttp",
"winreg",
] } ] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0" users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.4.0" tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.2.1" tauri-plugin-global-shortcut = "2.3.0"
tauri-plugin-updater = "2.8.1" tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
tauri-plugin-updater = "2.9.0"
[features] [features]
default = ["custom-protocol"] default = ["custom-protocol"]

View File

@@ -17,6 +17,8 @@
"autostart:allow-enable", "autostart:allow-enable",
"autostart:allow-disable", "autostart:allow-disable",
"autostart:allow-is-enabled", "autostart:allow-is-enabled",
"core:window:allow-set-theme" "core:window:allow-set-theme",
"notification:default",
"core:webview:allow-set-webview-zoom"
] ]
} }

236
src-tauri/deny.toml Normal file
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.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
chmod +x /usr/bin/install-service chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-service chmod +x /usr/bin/uninstall-service
chmod +x /usr/bin/clash-verge-service chmod +x /usr/bin/koala-clash-service

View File

@@ -6,7 +6,7 @@
<false/> <false/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>io.github.clash-verge-rev.clash-verge-rev</string> <string>io.github.koala-clash</string>
</array> </array>
<key>com.apple.security.inherit</key> <key>com.apple.security.inherit</key>
<true/> <true/>

View File

@@ -427,52 +427,52 @@ Function .onInit
!endif !endif
FunctionEnd FunctionEnd
!macro CheckAllVergeProcesses !macro CheckAllKoalaProcesses
; Check if clash-verge-service.exe is running ; Check if koala-clash-service.exe is running
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "clash-verge-service.exe" nsis_tauri_utils::FindProcessCurrentUser "koala-clash-service.exe"
!else !else
nsis_tauri_utils::FindProcess "clash-verge-service.exe" nsis_tauri_utils::FindProcess "koala-clash-service.exe"
!endif !endif
Pop $R0 Pop $R0
${If} $R0 = 0 ${If} $R0 = 0
DetailPrint "Kill clash-verge-service.exe..." DetailPrint "Kill koala-clash-service.exe..."
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe" nsis_tauri_utils::KillProcessCurrentUser "koala-clash-service.exe"
!else !else
nsis_tauri_utils::KillProcess "clash-verge-service.exe" nsis_tauri_utils::KillProcess "koala-clash-service.exe"
!endif !endif
${EndIf} ${EndIf}
; Check if verge-mihomo-alpha.exe is running ; Check if koala-mihomo-alpha.exe is running
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo-alpha.exe" nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo-alpha.exe"
!else !else
nsis_tauri_utils::FindProcess "verge-mihomo-alpha.exe" nsis_tauri_utils::FindProcess "koala-mihomo-alpha.exe"
!endif !endif
Pop $R0 Pop $R0
${If} $R0 = 0 ${If} $R0 = 0
DetailPrint "Kill verge-mihomo-alpha.exe..." DetailPrint "Kill koala-mihomo-alpha.exe..."
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo-alpha.exe" nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo-alpha.exe"
!else !else
nsis_tauri_utils::KillProcess "verge-mihomo-alpha.exe" nsis_tauri_utils::KillProcess "koala-mihomo-alpha.exe"
!endif !endif
${EndIf} ${EndIf}
; Check if verge-mihomo.exe is running ; Check if koala-mihomo.exe is running
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo.exe" nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo.exe"
!else !else
nsis_tauri_utils::FindProcess "verge-mihomo.exe" nsis_tauri_utils::FindProcess "koala-mihomo.exe"
!endif !endif
Pop $R0 Pop $R0
${If} $R0 = 0 ${If} $R0 = 0
DetailPrint "Kill verge-mihomo.exe..." DetailPrint "Kill koala-mihomo.exe..."
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo.exe" nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo.exe"
!else !else
nsis_tauri_utils::KillProcess "verge-mihomo.exe" nsis_tauri_utils::KillProcess "koala-mihomo.exe"
!endif !endif
${EndIf} ${EndIf}
@@ -509,22 +509,22 @@ FunctionEnd
${EndIf} ${EndIf}
!macroend !macroend
!macro StartVergeService !macro StartKoalaService
; Check if the service exists ; Check if the service exists
SimpleSC::ExistsService "clash_verge_service" SimpleSC::ExistsService "koala_clash_service"
Pop $0 ; 0service existsother: service not exists Pop $0 ; 0service existsother: service not exists
; Service exists ; Service exists
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
; Check if the service is running ; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service" SimpleSC::ServiceIsRunning "koala_clash_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0) Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running) Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
${If} $1 == 0 ${If} $1 == 0
DetailPrint "Restart Clash Verge Service..." DetailPrint "Restart Koala Clash Service..."
SimpleSC::StartService "clash_verge_service" "" 30 SimpleSC::StartService "koala_clash_service" "" 30
${EndIf} ${EndIf}
${ElseIf} $0 != 0 ${ElseIf} $0 != 0
Push $0 Push $0
@@ -535,35 +535,35 @@ FunctionEnd
${EndIf} ${EndIf}
!macroend !macroend
!macro RemoveVergeService !macro RemoveKoalaService
; Check if the service exists ; Check if the service exists
SimpleSC::ExistsService "clash_verge_service" SimpleSC::ExistsService "koala_clash_service"
Pop $0 ; 0service existsother: service not exists Pop $0 ; 0service existsother: service not exists
; Service exists ; Service exists
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
; Check if the service is running ; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service" SimpleSC::ServiceIsRunning "koala_clash_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0) Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running) Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
${If} $1 == 1 ${If} $1 == 1
DetailPrint "Stop Clash Verge Service..." DetailPrint "Stop Koala Clash Service..."
SimpleSC::StopService "clash_verge_service" 1 30 SimpleSC::StopService "koala_clash_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0) Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0 ${If} $0 == 0
DetailPrint "Removing Clash Verge Service..." DetailPrint "Removing Koala Clash Service..."
SimpleSC::RemoveService "clash_verge_service" SimpleSC::RemoveService "koala_clash_service"
${ElseIf} $0 != 0 ${ElseIf} $0 != 0
Push $0 Push $0
SimpleSC::GetErrorMessage SimpleSC::GetErrorMessage
Pop $0 Pop $0
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)" MessageBox MB_OK|MB_ICONSTOP "Koala Clash Service Stop Error ($0)"
${EndIf} ${EndIf}
${ElseIf} $1 == 0 ${ElseIf} $1 == 0
DetailPrint "Removing Clash Verge Service..." DetailPrint "Removing Koala Clash Service..."
SimpleSC::RemoveService "clash_verge_service" SimpleSC::RemoveService "koala_clash_service"
${EndIf} ${EndIf}
${ElseIf} $0 != 0 ${ElseIf} $0 != 0
Push $0 Push $0
@@ -764,7 +764,7 @@ Section Install
SetOutPath $INSTDIR SetOutPath $INSTDIR
nsExec::Exec 'netsh int tcp res' nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning !insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses !insertmacro CheckAllKoalaProcesses
; 清理自启动注册表项 ; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..." DetailPrint "Cleaning auto-launch registry entries..."
@@ -772,32 +772,32 @@ Section Install
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run" StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64 SetRegView 64
; 清理旧版本的注册表项 (Clash Verge) ; 清理旧版本的注册表项 (Koala Clash)
ReadRegStr $R2 HKCU "$R1" "Clash Verge" ReadRegStr $R2 HKCU "$R1" "Koala Clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge" DeleteRegValue HKCU "$R1" "Koala Clash"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge" ReadRegStr $R2 HKLM "$R1" "Koala Clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge" DeleteRegValue HKLM "$R1" "Koala Clash"
${EndIf} ${EndIf}
; 清理新版本的注册表项 (clash-verge) ; 清理新版本的注册表项 (koala-clash)
ReadRegStr $R2 HKCU "$R1" "clash-verge" ReadRegStr $R2 HKCU "$R1" "koala-clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge" DeleteRegValue HKCU "$R1" "koala-clash"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge" ReadRegStr $R2 HKLM "$R1" "koala-clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge" DeleteRegValue HKLM "$R1" "koala-clash"
${EndIf} ${EndIf}
; Delete old files before installation ; Delete old files before installation
; Delete clash-verge.desktop ; Delete koala-clash.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2 IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe" Delete "$INSTDIR\Koala Clash.exe"
; Copy main executable ; Copy main executable
File "${MAINBINARYSRCPATH}" File "${MAINBINARYSRCPATH}"
@@ -815,7 +815,7 @@ Section Install
File /a "/oname={{this}}" "{{@key}}" File /a "/oname={{this}}" "{{@key}}"
{{/each}} {{/each}}
!insertmacro StartVergeService !insertmacro StartKoalaService
; Create uninstaller ; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe" WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -918,11 +918,11 @@ FunctionEnd
Section Uninstall Section Uninstall
;删除 window-state.json 文件 ;删除 window-state.json 文件
SetShellVarContext current SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json" Delete "$APPDATA\io.github.koala-clash\window-state.json"
!insertmacro CheckIfAppIsRunning !insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses !insertmacro CheckAllKoalaProcesses
!insertmacro RemoveVergeService !insertmacro RemoveKoalaService
; 清理自启动注册表项 ; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..." DetailPrint "Cleaning auto-launch registry entries..."
@@ -930,26 +930,26 @@ Section Uninstall
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run" StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64 SetRegView 64
; 清理旧版本的注册表项 (Clash Verge) ; 清理旧版本的注册表项 (Koala Clash)
ReadRegStr $R2 HKCU "$R1" "Clash Verge" ReadRegStr $R2 HKCU "$R1" "Koala Clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge" DeleteRegValue HKCU "$R1" "Koala Clash"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge" ReadRegStr $R2 HKLM "$R1" "Koala Clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge" DeleteRegValue HKLM "$R1" "Koala Clash"
${EndIf} ${EndIf}
; 清理新版本的注册表项 (clash-verge) ; 清理新版本的注册表项 (koala-clash)
ReadRegStr $R2 HKCU "$R1" "clash-verge" ReadRegStr $R2 HKCU "$R1" "koala-clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge" DeleteRegValue HKCU "$R1" "koala-clash"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge" ReadRegStr $R2 HKLM "$R1" "koala-clash"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge" DeleteRegValue HKLM "$R1" "koala-clash"
${EndIf} ${EndIf}
; Delete the app directory and its content from disk ; Delete the app directory and its content from disk
@@ -966,9 +966,9 @@ Section Uninstall
Delete "$INSTDIR\\{{this}}" Delete "$INSTDIR\\{{this}}"
{{/each}} {{/each}}
; Delete clash-verge.desktop ; Delete koala-clash.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2 IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe" Delete "$INSTDIR\Koala Clash.exe"
; Delete uninstaller ; Delete uninstaller
Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\uninstall.exe"
@@ -982,20 +982,20 @@ Section Uninstall
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk" !insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式 ; 兼容旧名称快捷方式
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk" !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk" !insertmacro UnpinShortcut "$DESKTOP\koala-clash.lnk"
; Remove start menu shortcut ; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式 ; 兼容旧名称快捷方式
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk" Delete "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
RMDir "$SMPROGRAMS\$AppStartMenuFolder" RMDir "$SMPROGRAMS\$AppStartMenuFolder"
; Remove desktop shortcuts ; Remove desktop shortcuts
Delete "$DESKTOP\${PRODUCTNAME}.lnk" Delete "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式 ; 兼容旧名称快捷方式
Delete "$DESKTOP\clash-verge.lnk" Delete "$DESKTOP\koala-clash.lnk"
; Remove registry information for add/remove programs ; Remove registry information for add/remove programs
!if "${INSTALLMODE}" == "both" !if "${INSTALLMODE}" == "both"
@@ -1017,7 +1017,7 @@ Section Uninstall
;删除 window-state.json 文件 ;删除 window-state.json 文件
SetShellVarContext current SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json" Delete "$APPDATA\io.github.koala-clash\window-state.json"
${GetOptions} $CMDLINE "/P" $R0 ${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0 IfErrors +2 0

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()) Ok(icon_path.to_string_lossy().to_string())
} else { } else {
let _ = std::fs::remove_file(&temp_path); let _ = std::fs::remove_file(&temp_path);
Err(format!("下载的内容不是有效图片: {}", url)) Err(format!("Downloaded content is not a valid image: {url}"))
} }
} }
@@ -209,15 +209,17 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
/// 通知UI已准备就绪 /// 通知UI已准备就绪
#[tauri::command] #[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> { pub fn notify_ui_ready() -> CmdResult<()> {
log::info!(target: "app", "前端UI已准备就绪"); log::info!(target: "app", "Frontend UI is ready");
crate::utils::resolve::mark_ui_ready(); crate::utils::resolve::mark_ui_ready();
// Flush any pending messages queued while UI was not ready (e.g. minimized to tray)
crate::core::handle::Handle::global().flush_ui_pending_messages();
Ok(()) Ok(())
} }
/// UI加载阶段 /// UI加载阶段
#[tauri::command] #[tauri::command]
pub fn update_ui_stage(stage: String) -> CmdResult<()> { pub fn update_ui_stage(stage: String) -> CmdResult<()> {
log::info!(target: "app", "UI加载阶段更新: {}", stage); log::info!(target: "app", "UI loading stage updated: {stage}");
use crate::utils::resolve::UiReadyStage; use crate::utils::resolve::UiReadyStage;
@@ -228,8 +230,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded, "ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready, "Ready" => UiReadyStage::Ready,
_ => { _ => {
log::warn!(target: "app", "未知的UI加载阶段: {}", stage); log::warn!(target: "app", "Unknown UI loading stage: {stage}");
return Err(format!("未知的UI加载阶段: {}", stage)); return Err(format!("Unknown UI loading stage: {stage}"));
} }
}; };
@@ -240,7 +242,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
/// 重置UI就绪状态 /// 重置UI就绪状态
#[tauri::command] #[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> { pub fn reset_ui_ready_state() -> CmdResult<()> {
log::info!(target: "app", "重置UI就绪状态"); log::info!(target: "app", "Reset UI ready state");
crate::utils::resolve::reset_ui_ready(); crate::utils::resolve::reset_ui_ready();
Ok(()) Ok(())
} }

View File

@@ -49,8 +49,8 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
Ok(None) Ok(None)
} }
Err(err) => { Err(err) => {
let error_msg = format!("Core changed but failed to restart: {}", err); let error_msg = format!("Core changed but failed to restart: {err}");
log::error!(target: "app", "{}", error_msg); log::error!(target: "app", "{error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg); handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg)) Ok(Some(error_msg))
} }
@@ -116,7 +116,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
// 保存DNS配置到文件 // 保存DNS配置到文件
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?; 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())?; 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(()) Ok(())
} }
@@ -137,7 +137,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
let dns_path = match dirs::app_home_dir() { let dns_path = match dirs::app_home_dir() {
Ok(path) => path.join("dns_config.yaml"), Ok(path) => path.join("dns_config.yaml"),
Err(e) => { Err(e) => {
log::error!(target: "app", "Failed to get home dir: {}", e); log::error!(target: "app", "Failed to get home dir: {e}");
return; return;
} }
}; };
@@ -150,7 +150,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
let dns_yaml = match std::fs::read_to_string(&dns_path) { let dns_yaml = match std::fs::read_to_string(&dns_path) {
Ok(content) => content, Ok(content) => content,
Err(e) => { Err(e) => {
log::error!(target: "app", "Failed to read DNS config: {}", e); log::error!(target: "app", "Failed to read DNS config: {e}");
return; return;
} }
}; };
@@ -163,7 +163,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
patch patch
} }
Err(e) => { Err(e) => {
log::error!(target: "app", "Failed to parse DNS config: {}", e); log::error!(target: "app", "Failed to parse DNS config: {e}");
return; return;
} }
}; };
@@ -178,13 +178,13 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
// 首先重新生成配置 // 首先重新生成配置
if let Err(err) = Config::generate().await { 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; return;
} }
// 然后应用新配置 // 然后应用新配置
if let Err(err) = CoreManager::global().update_config().await { 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 { } else {
log::info!(target: "app", "DNS config successfully applied"); log::info!(target: "app", "DNS config successfully applied");
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
@@ -196,7 +196,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
// 重新生成配置 // 重新生成配置
if let Err(err) = Config::generate().await { 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; return;
} }
@@ -207,7 +207,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
Err(err) => { Err(err) => {
log::error!(target: "app", "Failed to apply regenerated config: {}", err); log::error!(target: "app", "Failed to apply regenerated config: {err}");
} }
} }
} }

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); let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
char::from_u32(c1) 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() .unwrap_or_default()
} }
@@ -163,7 +163,7 @@ async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
map.get("loc").map(|loc| { map.get("loc").map(|loc| {
let emoji = country_code_to_emoji(loc); let emoji = country_code_to_emoji(loc);
format!("{}{}", emoji, loc) format!("{emoji}{loc}")
}) })
} else { } else {
None None
@@ -255,7 +255,7 @@ async fn check_gemini(client: &Client) -> UnlockItem {
caps.get(1).map(|m| { caps.get(1).map(|m| {
let country_code = m.as_str(); let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code); 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| { caps.get(1).map(|m| {
let country_code = m.as_str().trim(); let country_code = m.as_str().trim();
let emoji = country_code_to_emoji(country_code); 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) // 第二步使用设备ID检查访问权限 (使用相同的Cookie)
let url = format!( let url =
"https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={}", format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
device_id
);
let token_result = match client_with_cookies.get(&url).send().await { let token_result = match client_with_cookies.get(&url).send().await {
Ok(response) => { Ok(response) => {
@@ -431,7 +429,7 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
.map(|m| { .map(|m| {
let country_code = m.as_str(); let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code); let emoji = country_code_to_emoji(country_code);
format!("{}{}", emoji, country_code) format!("{emoji}{country_code}")
}) })
} }
Err(_) => None, Err(_) => None,
@@ -470,7 +468,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
// 检查连接失败情况 // 检查连接失败情况
if let Err(e) = &result1 { if let Err(e) = &result1 {
eprintln!("Netflix请求错误: {}", e); eprintln!("Netflix请求错误: {e}");
return UnlockItem { return UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: "Failed".to_string(), status: "Failed".to_string(),
@@ -487,7 +485,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
.await; .await;
if let Err(e) = &result2 { if let Err(e) = &result2 {
eprintln!("Netflix请求错误: {}", e); eprintln!("Netflix请求错误: {e}");
return UnlockItem { return UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: "Failed".to_string(), status: "Failed".to_string(),
@@ -541,7 +539,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
return UnlockItem { return UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: "Yes".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()), check_time: Some(get_local_date_string()),
}; };
} }
@@ -557,7 +555,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
} }
} }
Err(e) => { Err(e) => {
eprintln!("获取Netflix区域信息失败: {}", e); eprintln!("获取Netflix区域信息失败: {e}");
UnlockItem { UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: "Yes (但无法获取区域)".to_string(), status: "Yes (但无法获取区域)".to_string(),
@@ -570,7 +568,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
// 其他未知错误状态 // 其他未知错误状态
UnlockItem { UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: format!("Failed (状态码: {}_{}", status1, status2), status: format!("Failed (状态码: {status1}_{status2}"),
region: None, region: None,
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
} }
@@ -614,7 +612,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
return UnlockItem { return UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: "Yes".to_string(), status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, country)), region: Some(format!("{emoji}{country}")),
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
}; };
} }
@@ -631,7 +629,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
} }
} }
Err(e) => { Err(e) => {
eprintln!("解析Fast.com API响应失败: {}", e); eprintln!("解析Fast.com API响应失败: {e}");
UnlockItem { UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: "Failed (解析错误)".to_string(), status: "Failed (解析错误)".to_string(),
@@ -642,7 +640,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
} }
} }
Err(e) => { Err(e) => {
eprintln!("Fast.com API请求失败: {}", e); eprintln!("Fast.com API请求失败: {e}");
UnlockItem { UnlockItem {
name: "Netflix".to_string(), name: "Netflix".to_string(),
status: "Failed (CDN API)".to_string(), status: "Failed (CDN API)".to_string(),
@@ -884,7 +882,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem { return UnlockItem {
name: "Disney+".to_string(), name: "Disney+".to_string(),
status: "Yes".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()), check_time: Some(get_local_date_string()),
}; };
} }
@@ -947,7 +945,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem { return UnlockItem {
name: "Disney+".to_string(), name: "Disney+".to_string(),
status: "Yes".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()), check_time: Some(get_local_date_string()),
}; };
} }
@@ -968,7 +966,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem { return UnlockItem {
name: "Disney+".to_string(), name: "Disney+".to_string(),
status: "Yes".to_string(), status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)), region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
}; };
} }
@@ -990,7 +988,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
UnlockItem { UnlockItem {
name: "Disney+".to_string(), name: "Disney+".to_string(),
status: "Soon".to_string(), status: "Soon".to_string(),
region: Some(format!("{}{}(即将上线)", emoji, region)), region: Some(format!("{emoji}{region}(即将上线)")),
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
} }
} }
@@ -999,13 +997,13 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
UnlockItem { UnlockItem {
name: "Disney+".to_string(), name: "Disney+".to_string(),
status: "Yes".to_string(), status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)), region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
} }
} }
None => UnlockItem { None => UnlockItem {
name: "Disney+".to_string(), name: "Disney+".to_string(),
status: format!("Failed (Error: Unknown region status for {})", region), status: format!("Failed (Error: Unknown region status for {region})"),
region: None, region: None,
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
}, },
@@ -1056,7 +1054,7 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
return UnlockItem { return UnlockItem {
name: "Prime Video".to_string(), name: "Prime Video".to_string(),
status: "Yes".to_string(), status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)), region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
}; };
} }
@@ -1170,7 +1168,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
.connection_verbose(true) // 详细连接信息 .connection_verbose(true) // 详细连接信息
.build() { .build() {
Ok(client) => client, 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 { while let Some(res) = tasks.join_next().await {
if let Err(e) = res { if let Err(e) = res {
eprintln!("任务执行失败: {}", e); eprintln!("任务执行失败: {e}");
} }
} }

View File

@@ -1,17 +1,15 @@
use super::CmdResult; use super::CmdResult;
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
use crate::wrap_err; use crate::wrap_err;
use network_interface::NetworkInterface; use network_interface::NetworkInterface;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use sysproxy::{Autoproxy, Sysproxy};
use tokio::task::spawn_blocking;
/// get the system proxy /// get the system proxy
#[tauri::command] #[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> { pub async fn get_sys_proxy() -> CmdResult<Mapping> {
let current = spawn_blocking(Sysproxy::get_system_proxy) log::debug!(target: "app", "Asynchronously getting system proxy configuration");
.await
.map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))? let current = AsyncProxyQuery::get_system_proxy().await;
.map_err(|e| format!("Failed to get system proxy: {}", e))?;
let mut map = Mapping::new(); let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into()); 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()); map.insert("bypass".into(), current.bypass.into());
log::debug!(target: "app", "Return system proxy configuration: enable={}, {}:{}", current.enable, current.host, current.port);
Ok(map) Ok(map)
} }
/// get the system proxy /// 获取自动代理配置
#[tauri::command] #[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> { pub async fn get_auto_proxy() -> CmdResult<Mapping> {
let current = spawn_blocking(Autoproxy::get_auto_proxy) log::debug!(target: "app", "Start retrieving auto proxy configuration (event-driven)");
.await
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))? let proxy_manager = EventDrivenProxyManager::global();
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
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(); let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into()); 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", "Return auto proxy configuration (cached): enable={}, url={}", current.enable, current.url);
Ok(map) Ok(map)
} }
@@ -49,7 +54,7 @@ pub fn get_system_hostname() -> CmdResult<String> {
Ok(name) => name, Ok(name) => name,
Err(os_string) => { Err(os_string) => {
// 对于包含非UTF-8的主机名使用调试格式化 // 对于包含非UTF-8的主机名使用调试格式化
let fallback = format!("{:?}", os_string); let fallback = format!("{os_string:?}");
// 去掉可能存在的引号 // 去掉可能存在的引号
fallback.trim_matches('"').to_string() fallback.trim_matches('"').to_string()
} }

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
state.proxies = Box::new(proxies); state.proxies = Box::new(proxies);
state.need_refresh = false; state.need_refresh = false;
} }
log::debug!(target: "app", "proxies刷新成功"); log::debug!(target: "app", "Proxies refreshed successfully");
} }
let proxies = { let proxies = {
@@ -50,7 +50,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>(); let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
log::debug!(target: "app", "强制刷新代理缓存"); log::debug!(target: "app", "Force refresh proxy cache");
let proxies = manager.get_refresh_proxies().await?; let proxies = manager.get_refresh_proxies().await?;
@@ -61,7 +61,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
state.last_refresh_time = Instant::now(); state.last_refresh_time = Instant::now();
} }
log::debug!(target: "app", "强制刷新代理缓存完成"); log::debug!(target: "app", "Force refresh proxy cache completed");
Ok(proxies) Ok(proxies)
} }
@@ -88,7 +88,7 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
state.providers_proxies = Box::new(providers); state.providers_proxies = Box::new(providers);
state.need_refresh = false; state.need_refresh = false;
} }
log::debug!(target: "app", "providers_proxies刷新成功"); log::debug!(target: "app", "providers_proxies refreshed successfully");
} }
let providers_proxies = { let providers_proxies = {

View File

@@ -84,7 +84,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知 // 发送合并文件专用错误通知
let result = (false, error_msg.clone()); let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); crate::cmd::validate::handle_yaml_validation_notice(&result, "Merge config file");
return Ok(()); return Ok(());
} }
Err(e) => { Err(e) => {
@@ -133,17 +133,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|| (!file_path_str.ends_with(".js") && !is_script_error) || (!file_path_str.ends_with(".js") && !is_script_error)
{ {
// 普通YAML错误使用YAML通知处理 // 普通YAML错误使用YAML通知处理
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知"); log::info!(target: "app", "[cmd config save] YAML config file validation failed, sending notification");
let result = (false, error_msg.clone()); let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件"); crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML config file");
} else if is_script_error { } else if is_script_error {
// 脚本错误使用专门的通知处理 // 脚本错误使用专门的通知处理
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知"); log::info!(target: "app", "[cmd config save] Script file validation failed, sending notification");
let result = (false, error_msg.clone()); let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件"); crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else { } else {
// 普通配置错误使用一般通知 // 普通配置错误使用一般通知
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知"); log::info!(target: "app", "[cmd config save] Other validation failure type, sending general notification");
handle::Handle::notice_message("config_validate::error", &error_msg); handle::Handle::notice_message("config_validate::error", &error_msg);
} }
@@ -154,7 +154,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
error, error,
Type::Config, Type::Config,
true, true,
"[cmd配置save] 验证过程发生错误: {}", "[cmd config save] Error occurred during validation: {}",
e e
); );
// 恢复原始配置文件 // 恢复原始配置文件

View File

@@ -24,7 +24,7 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
#[tauri::command] #[tauri::command]
pub async fn export_diagnostic_info() -> CmdResult<()> { pub async fn export_diagnostic_info() -> CmdResult<()> {
let sysinfo = PlatformSpecification::new_async().await; let sysinfo = PlatformSpecification::new_async().await;
let info = format!("{:?}", sysinfo); let info = format!("{sysinfo:?}");
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
let cliboard = app_handle.clipboard(); let cliboard = app_handle.clipboard();
@@ -37,7 +37,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
#[tauri::command] #[tauri::command]
pub async fn get_system_info() -> CmdResult<String> { pub async fn get_system_info() -> CmdResult<String> {
let sysinfo = PlatformSpecification::new_async().await; let sysinfo = PlatformSpecification::new_async().await;
let info = format!("{:?}", sysinfo); let info = format!("{sysinfo:?}");
Ok(info) Ok(info)
} }

View File

@@ -32,7 +32,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
warn, warn,
Type::Config, Type::Config,
true, true,
"{} 验证失败: {}", "{} validation failed: {}",
file_type, file_type,
error_msg error_msg
); );
@@ -43,14 +43,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
/// 验证指定脚本文件 /// 验证指定脚本文件
#[tauri::command] #[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> { pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path); logging!(info, Type::Config, true, "Validating script file: {}", file_path);
match CoreManager::global() match CoreManager::global()
.validate_config_file(&file_path, None) .validate_config_file(&file_path, None)
.await .await
{ {
Ok(result) => { Ok(result) => {
handle_script_validation_notice(&result, "脚本文件"); handle_script_validation_notice(&result, "Script file");
Ok(result.0) // 返回验证结果布尔值 Ok(result.0) // 返回验证结果布尔值
} }
Err(e) => { Err(e) => {
@@ -129,7 +129,7 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
info, info,
Type::Config, Type::Config,
true, true,
"[通知] 发送通知: status={}, msg={}", "[Notice] Sending notice: status={}, msg={}",
status, status,
error_msg error_msg
); );

View File

@@ -42,7 +42,7 @@ impl IClashTemp {
tun.insert("enable".into(), false.into()); tun.insert("enable".into(), false.into());
tun.insert("stack".into(), "gvisor".into()); tun.insert("stack".into(), "gvisor".into());
tun.insert("auto-route".into(), true.into()); tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into()); tun.insert("strict-route".into(), true.into());
tun.insert("auto-detect-interface".into(), true.into()); tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].into()); tun.insert("dns-hijack".into(), vec!["any:53"].into());
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@@ -129,7 +129,7 @@ impl IClashTemp {
help::save_yaml( help::save_yaml(
&dirs::clash_path()?, &dirs::clash_path()?,
&self.0, &self.0,
Some("# Generated by Clash Verge"), Some("# Generated by Koala Clash"),
) )
} }

View File

@@ -11,8 +11,8 @@ use once_cell::sync::OnceCell;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; pub const RUNTIME_CONFIG: &str = "koala-clash.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; pub const CHECK_CONFIG: &str = "koala-clash-check.yaml";
pub struct Config { pub struct Config {
clash_config: Draft<Box<IClashTemp>>, clash_config: Draft<Box<IClashTemp>>,
@@ -69,9 +69,9 @@ impl Config {
} }
// 生成运行时配置 // 生成运行时配置
if let Err(err) = Self::generate().await { if let Err(err) = Self::generate().await {
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err); logging!(error, Type::Config, true, "Failed to generate runtime config: {}", err);
} else { } else {
logging!(info, Type::Config, true, "生成运行时配置成功"); logging!(info, Type::Config, true, "Runtime config generated successfully");
} }
// 生成运行时配置文件并验证 // 生成运行时配置文件并验证
@@ -79,7 +79,7 @@ impl Config {
let validation_result = if config_result.is_ok() { let validation_result = if config_result.is_ok() {
// 验证配置文件 // 验证配置文件
logging!(info, Type::Config, true, "开始验证配置"); logging!(info, Type::Config, true, "Starting config validation");
match CoreManager::global().validate_config().await { match CoreManager::global().validate_config().await {
Ok((is_valid, error_msg)) => { Ok((is_valid, error_msg)) => {
@@ -88,7 +88,7 @@ impl Config {
warn, warn,
Type::Config, Type::Config,
true, true,
"[首次启动] 配置验证失败,使用默认最小配置启动: {}", "[First launch] Config validation failed, starting with minimal default config: {}",
error_msg error_msg
); );
CoreManager::global() CoreManager::global()
@@ -96,12 +96,12 @@ impl Config {
.await?; .await?;
Some(("config_validate::boot_error", error_msg)) Some(("config_validate::boot_error", error_msg))
} else { } else {
logging!(info, Type::Config, true, "配置验证成功"); logging!(info, Type::Config, true, "Config validation succeeded");
Some(("config_validate::success", String::new())) Some(("config_validate::success", String::new()))
} }
} }
Err(err) => { Err(err) => {
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err); logging!(warn, Type::Config, true, "Validation process execution failed: {}", err);
CoreManager::global() CoreManager::global()
.use_default_config("config_validate::process_terminated", "") .use_default_config("config_validate::process_terminated", "")
.await?; .await?;
@@ -109,7 +109,7 @@ impl Config {
} }
} }
} else { } else {
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置"); logging!(warn, Type::Config, true, "Failed to generate config file; using default config");
CoreManager::global() CoreManager::global()
.use_default_config("config_validate::error", "") .use_default_config("config_validate::error", "")
.await?; .await?;
@@ -141,7 +141,7 @@ impl Config {
.as_ref() .as_ref()
.ok_or(anyhow!("failed to get runtime config"))?; .ok_or(anyhow!("failed to get runtime config"))?;
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?; help::save_yaml(&path, &config, Some("# Generated by Koala Clash"))?;
Ok(path) Ok(path)
} }

View File

@@ -19,11 +19,11 @@ macro_rules! draft_define {
impl Draft<Box<$id>> { impl Draft<Box<$id>> {
#[allow(unused)] #[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<Box<$id>> { pub fn data(&self) -> MappedMutexGuard<'_, Box<$id>> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0) MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
} }
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> { pub fn latest(&self) -> MappedMutexGuard<'_, Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
&mut inner.0 &mut inner.0
@@ -33,7 +33,7 @@ macro_rules! draft_define {
}) })
} }
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> { pub fn draft(&self) -> MappedMutexGuard<'_, Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
inner.1 = Some(inner.0.clone()); inner.1 = Some(inner.0.clone());

View File

@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
// Encrypt data // Encrypt data
let ciphertext = cipher let ciphertext = cipher
.encrypt(nonce.as_slice().into(), data.as_bytes()) .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 // Concatenate nonce and ciphertext and encode them in base64
let mut combined = nonce; let mut combined = nonce;
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
// Decrypt data // Decrypt data
let plaintext = cipher let plaintext = cipher
.decrypt(nonce.into(), ciphertext) .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()) String::from_utf8(plaintext).map_err(|e| e.into())
} }

View File

@@ -4,10 +4,12 @@ use crate::utils::{
tmpl, tmpl,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{fs, time::Duration}; use std::{fs, time::Duration};
use url::Url;
use super::Config; use super::Config;
@@ -53,6 +55,18 @@ pub struct PrfItem {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub home: Option<String>, pub home: Option<String>,
/// profile support url
#[serde(skip_serializing_if = "Option::is_none")]
pub support_url: Option<String>,
/// profile announce
#[serde(skip_serializing_if = "Option::is_none")]
pub announce: Option<String>,
/// profile announce url
#[serde(skip_serializing_if = "Option::is_none")]
pub announce_url: Option<String>,
/// the file data /// the file data
#[serde(skip)] #[serde(skip)]
pub file_data: Option<String>, pub file_data: Option<String>,
@@ -113,6 +127,12 @@ pub struct PrfOption {
pub proxies: Option<String>, pub proxies: Option<String>,
pub groups: Option<String>, pub groups: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_hwid: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_always: Option<bool>,
} }
impl PrfOption { impl PrfOption {
@@ -132,6 +152,8 @@ impl PrfOption {
a.proxies = b.proxies.or(a.proxies); a.proxies = b.proxies.or(a.proxies);
a.groups = b.groups.or(a.groups); a.groups = b.groups.or(a.groups);
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds); a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
a.use_hwid = b.use_hwid.or(a.use_hwid);
a.update_always = b.update_always.or(a.update_always);
Some(a) Some(a)
} }
t => t.0.or(t.1), t => t.0.or(t.1),
@@ -230,6 +252,9 @@ impl PrfItem {
..PrfOption::default() ..PrfOption::default()
}), }),
home: None, home: None,
support_url: None,
announce: None,
announce_url: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
}) })
@@ -251,6 +276,7 @@ impl PrfItem {
let user_agent = opt_ref.and_then(|o| o.user_agent.clone()); let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval); let update_interval = opt_ref.and_then(|o| o.update_interval);
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20); let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
let use_hwid = Config::verge().latest().enable_send_hwid.unwrap_or(true);
let mut merge = opt_ref.and_then(|o| o.merge.clone()); let mut merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone()); let mut script = opt_ref.and_then(|o| o.script.clone());
let mut rules = opt_ref.and_then(|o| o.rules.clone()); let mut rules = opt_ref.and_then(|o| o.rules.clone());
@@ -274,6 +300,7 @@ impl PrfItem {
Some(timeout), Some(timeout),
user_agent.clone(), user_agent.clone(),
accept_invalid_certs, accept_invalid_certs,
use_hwid,
) )
.await .await
{ {
@@ -291,6 +318,21 @@ impl PrfItem {
let header = resp.headers(); let header = resp.headers();
let mut final_url = url.to_string();
if let Some(new_domain_value) = header.get("new-sub-domain") {
if let Ok(new_domain) = new_domain_value.to_str() {
if !new_domain.is_empty() {
if let Ok(mut parsed_url) = Url::parse(url) {
if parsed_url.set_host(Some(new_domain)).is_ok() {
final_url = parsed_url.to_string();
log::info!(target: "app", "URL host updated to -> {final_url}");
}
}
}
}
}
// parse the Subscription UserInfo // parse the Subscription UserInfo
let extra = match header.get("Subscription-Userinfo") { let extra = match header.get("Subscription-Userinfo") {
Some(value) => { Some(value) => {
@@ -340,6 +382,11 @@ impl PrfItem {
}, },
}; };
let update_always = match header.get("update-always") {
Some(value) => value.to_str().unwrap_or("false").parse::<bool>().ok(),
None => None,
};
let home = match header.get("profile-web-page-url") { let home = match header.get("profile-web-page-url") {
Some(value) => { Some(value) => {
let str_value = value.to_str().unwrap_or(""); let str_value = value.to_str().unwrap_or("");
@@ -348,9 +395,64 @@ impl PrfItem {
None => None, None => None,
}; };
let support_url = match header.get("support-url") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
Some(str_value.to_string())
}
None => None,
};
let announce = match header.get("announce") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD
.decode(b64_data)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
} else {
Some(str_value.to_string())
}
}
None => None,
};
if let Some(announce_msg) = &announce {
let lower_msg = announce_msg.to_lowercase();
if lower_msg.contains("device") || lower_msg.contains("устройств") {
bail!(announce_msg.clone());
}
}
let announce_url = match header.get("announce-url") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
Some(str_value.to_string())
}
None => None,
};
let profile_title = match header.get("profile-title") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD
.decode(b64_data)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
} else {
Some(str_value.to_string())
}
}
None => None,
};
let uid = help::get_uid("R"); let uid = help::get_uid("R");
let file = format!("{uid}.yaml"); let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); let name = name
.or(profile_title)
.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?; let data = resp.text_with_charset("utf-8").await?;
// process the charset "UTF-8 with BOM" // process the charset "UTF-8 with BOM"
@@ -398,19 +500,33 @@ impl PrfItem {
name: Some(name), name: Some(name),
desc, desc,
file: Some(file), file: Some(file),
url: Some(url.into()), url: Some(final_url),
selected: None, selected: None,
extra, extra,
option: Some(PrfOption { option: Some(PrfOption {
user_agent: user_agent.clone(),
with_proxy: if with_proxy { Some(true) } else { None },
self_proxy: if self_proxy { Some(true) } else { None },
update_interval, update_interval,
update_always,
timeout_seconds: Some(timeout),
danger_accept_invalid_certs: if accept_invalid_certs {
Some(true)
} else {
None
},
merge, merge,
script, script,
rules, rules,
proxies, proxies,
groups, groups,
use_hwid: Some(use_hwid),
..PrfOption::default() ..PrfOption::default()
}), }),
home, home,
support_url,
announce,
announce_url,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data.into()), file_data: Some(data.into()),
}) })
@@ -438,6 +554,9 @@ impl PrfItem {
extra: None, extra: None,
option: None, option: None,
home: None, home: None,
support_url: None,
announce: None,
announce_url: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(template), file_data: Some(template),
}) })
@@ -460,6 +579,9 @@ impl PrfItem {
file: Some(file), file: Some(file),
url: None, url: None,
home: None, home: None,
support_url: None,
announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
@@ -481,6 +603,9 @@ impl PrfItem {
file: Some(file), file: Some(file),
url: None, url: None,
home: None, home: None,
support_url: None,
announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
@@ -502,6 +627,9 @@ impl PrfItem {
file: Some(file), file: Some(file),
url: None, url: None,
home: None, home: None,
support_url: None,
announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
@@ -523,6 +651,9 @@ impl PrfItem {
file: Some(file), file: Some(file),
url: None, url: None,
home: None, home: None,
support_url: None,
announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,

View File

@@ -66,7 +66,7 @@ impl IProfiles {
help::save_yaml( help::save_yaml(
&dirs::profiles_path()?, &dirs::profiles_path()?,
self, self,
Some("# Profiles Config for Clash Verge"), Some("# Profiles Config for Koala Clash"),
) )
} }
@@ -131,15 +131,14 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file); let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path) 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()) .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() if item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()) {
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string())) // Always switch current to the newly created remote/local profile
{ self.current = uid.clone();
self.current = uid;
} }
if self.items.is_none() { if self.items.is_none() {
@@ -220,6 +219,11 @@ impl IProfiles {
each.extra = item.extra; each.extra = item.extra;
each.updated = item.updated; each.updated = item.updated;
each.home = item.home; each.home = item.home;
each.announce = item.announce;
each.announce_url = item.announce_url;
each.support_url = item.support_url;
each.name = item.name;
each.url = item.url;
each.option = PrfOption::merge(each.option.clone(), item.option); each.option = PrfOption::merge(each.option.clone(), item.option);
// save the file data // save the file data
// move the field value after save // move the field value after save
@@ -234,9 +238,9 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file); let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path) 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()) .write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{}\"", file))?; .with_context(|| format!("failed to write to file \"{file}\""))?;
} }
break; break;
@@ -531,7 +535,7 @@ impl IProfiles {
if Self::is_profile_file(file_name) { if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件 // 检查是否为全局扩展文件
if protected_files.contains(file_name) { if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name); log::debug!(target: "app", "Protect global extension config file: {file_name}");
continue; continue;
} }
@@ -540,11 +544,11 @@ impl IProfiles {
match std::fs::remove_file(&path) { match std::fs::remove_file(&path) {
Ok(_) => { Ok(_) => {
deleted_files.push(file_name.to_string()); deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {}", file_name); log::info!(target: "app", "Cleaned up redundant file: {file_name}");
} }
Err(e) => { Err(e) => {
failed_deletions.push(format!("{}: {}", file_name, e)); failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e); log::warn!(target: "app", "Failed to clean file: {file_name} - {e}");
} }
} }
} }
@@ -674,14 +678,14 @@ impl IProfiles {
if !result.deleted_files.is_empty() { if !result.deleted_files.is_empty() {
log::info!( log::info!(
target: "app", target: "app",
"自动清理完成,删除了 {} 个冗余文件", "Auto cleanup completed, deleted {} redundant files",
result.deleted_files.len() result.deleted_files.len()
); );
} }
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
log::warn!(target: "app", "自动清理失败: {}", e); log::warn!(target: "app", "Auto cleanup failed: {e}");
Ok(()) Ok(())
} }
} }

View File

@@ -74,6 +74,10 @@ pub struct IVerge {
/// enable dns settings - this controls whether dns_config.yaml is applied /// enable dns settings - this controls whether dns_config.yaml is applied
pub enable_dns_settings: Option<bool>, pub enable_dns_settings: Option<bool>,
pub enable_send_hwid: Option<bool>,
pub primary_action: Option<String>,
/// always use default bypass /// always use default bypass
pub use_default_bypass: Option<bool>, pub use_default_bypass: Option<bool>,
@@ -234,7 +238,7 @@ pub struct IVergeTheme {
impl IVerge { impl IVerge {
/// 有效的clash核心名称 /// 有效的clash核心名称
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"]; pub const VALID_CLASH_CORES: &'static [&'static str] = &["koala-mihomo", "koala-mihomo-alpha"];
/// 验证并修正配置文件中的clash_core值 /// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> { pub fn validate_and_fix_config() -> Result<()> {
@@ -253,10 +257,10 @@ impl IVerge {
warn, warn,
Type::Config, Type::Config,
true, true,
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'", "Invalid clash_core config detected at startup: '{}', auto-fixing to 'koala-mihomo'",
core core
); );
config.clash_core = Some("verge-mihomo".to_string()); config.clash_core = Some("koala-mihomo".to_string());
needs_fix = true; needs_fix = true;
} }
} else { } else {
@@ -264,21 +268,21 @@ impl IVerge {
info, info,
Type::Config, Type::Config,
true, true,
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'" "clash_core not configured at startup; setting default to 'koala-mihomo'"
); );
config.clash_core = Some("verge-mihomo".to_string()); config.clash_core = Some("koala-mihomo".to_string());
needs_fix = true; needs_fix = true;
} }
// 修正后保存配置 // 修正后保存配置
if needs_fix { if needs_fix {
logging!(info, Type::Config, true, "正在保存修正后的配置文件..."); logging!(info, Type::Config, true, "Saving fixed configuration file...");
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?; help::save_yaml(&config_path, &config, Some("# Koala Clash Config"))?;
logging!( logging!(
info, info,
Type::Config, Type::Config,
true, true,
"配置文件修正完成,需要重新加载配置" "Configuration file fixed; reloading config required"
); );
Self::reload_config_after_fix(config)?; Self::reload_config_after_fix(config)?;
@@ -287,7 +291,7 @@ impl IVerge {
info, info,
Type::Config, Type::Config,
true, true,
"clash_core配置验证通过: {:?}", "clash_core config validation passed: {:?}",
config.clash_core config.clash_core
); );
} }
@@ -317,7 +321,7 @@ impl IVerge {
pub fn get_valid_clash_core(&self) -> String { pub fn get_valid_clash_core(&self) -> String {
self.clash_core self.clash_core
.clone() .clone()
.unwrap_or_else(|| "verge-mihomo".to_string()) .unwrap_or_else(|| "koala-mihomo".to_string())
} }
fn get_system_language() -> String { fn get_system_language() -> String {
@@ -336,18 +340,17 @@ impl IVerge {
} }
pub fn new() -> Self { pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) { dirs::verge_path()
Ok(config) => config, .and_then(|path| help::read_yaml::<IVerge>(&path))
Err(err) => { .unwrap_or_else(|err| {
log::error!(target: "app", "{err}"); log::error!(target: "app", "{err}");
Self::template() Self::template()
} })
}
} }
pub fn template() -> Self { pub fn template() -> Self {
Self { Self {
clash_core: Some("verge-mihomo".into()), clash_core: Some("koala-mihomo".into()),
language: Some(Self::get_system_language()), language: Some(Self::get_system_language()),
theme_mode: Some("system".into()), theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@@ -391,7 +394,7 @@ impl IVerge {
auto_close_connection: Some(true), auto_close_connection: Some(true),
auto_check_update: Some(true), auto_check_update: Some(true),
enable_builtin_enhanced: Some(true), enable_builtin_enhanced: Some(true),
auto_log_clean: Some(3), auto_log_clean: Some(2),
webdav_url: None, webdav_url: None,
webdav_username: None, webdav_username: None,
webdav_password: None, webdav_password: None,
@@ -401,6 +404,8 @@ impl IVerge {
enable_auto_light_weight_mode: Some(false), enable_auto_light_weight_mode: Some(false),
auto_light_weight_minutes: Some(10), auto_light_weight_minutes: Some(10),
enable_dns_settings: Some(false), enable_dns_settings: Some(false),
enable_send_hwid: Some(true),
primary_action: Some("tun-mode".into()),
home_cards: None, home_cards: None,
service_state: None, service_state: None,
..Self::default() ..Self::default()
@@ -409,7 +414,7 @@ impl IVerge {
/// Save IVerge App Config /// Save IVerge App Config
pub fn save_file(&self) -> Result<()> { pub fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")) help::save_yaml(&dirs::verge_path()?, &self, Some("# Koala Clash Config"))
} }
/// patch verge config /// patch verge config
@@ -489,6 +494,8 @@ impl IVerge {
patch!(enable_auto_light_weight_mode); patch!(enable_auto_light_weight_mode);
patch!(auto_light_weight_minutes); patch!(auto_light_weight_minutes);
patch!(enable_dns_settings); patch!(enable_dns_settings);
patch!(enable_send_hwid);
patch!(primary_action);
patch!(home_cards); patch!(home_cards);
patch!(service_state); patch!(service_state);
} }
@@ -584,6 +591,8 @@ pub struct IVergeResponse {
pub enable_auto_light_weight_mode: Option<bool>, pub enable_auto_light_weight_mode: Option<bool>,
pub auto_light_weight_minutes: Option<u64>, pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>, pub enable_dns_settings: Option<bool>,
pub enable_send_hwid: Option<bool>,
pub primary_action: Option<String>,
pub home_cards: Option<serde_json::Value>, pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>, pub enable_hover_jump_navigator: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>, pub service_state: Option<crate::core::service::ServiceState>,
@@ -656,6 +665,8 @@ impl From<IVerge> for IVergeResponse {
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode, enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
auto_light_weight_minutes: verge.auto_light_weight_minutes, auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings, enable_dns_settings: verge.enable_dns_settings,
enable_send_hwid: verge.enable_send_hwid,
primary_action: verge.primary_action,
home_cards: verge.home_cards, home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator, enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
service_state: verge.service_state, service_state: verge.service_state,

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", "Async auto proxy fetch succeeded: enable={}, url={}", proxy.enable, proxy.url);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "Async auto proxy fetch failed: {e}");
AsyncAutoproxy::default()
}
Err(_) => {
log::warn!(target: "app", "Async auto proxy fetch timed out");
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", "Async system proxy fetch succeeded: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "Async system proxy fetch failed: {e}");
AsyncSysproxy::default()
}
Err(_) => {
log::warn!(target: "app", "Async system proxy fetch timed out");
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", "Unable to open registry key");
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", "Read PAC URL from registry: {}", 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 configuration enabled: URL={}, AutoDetect={}", pac_url, 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 configuration not enabled");
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", "Parse result: 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", "Read proxy settings from registry: {}:{}, bypass: {}", host, port, 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!("Invalid proxy URL"));
}
Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: std::env::var("no_proxy").unwrap_or_default(),
})
}
}

View File

@@ -108,14 +108,11 @@ impl WebDavClient {
reqwest::Client::builder() reqwest::Client::builder()
.danger_accept_invalid_certs(true) .danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout())) .timeout(Duration::from_secs(op.timeout()))
.user_agent(format!( .user_agent(format!("koala-clash/{APP_VERSION} ({OS} WebDAV-Client)"))
"clash-verge/{} ({} WebDAV-Client)",
APP_VERSION, OS
))
.redirect(reqwest::redirect::Policy::custom(|attempt| { .redirect(reqwest::redirect::Policy::custom(|attempt| {
// 允许所有请求类型的重定向包括PUT // 允许所有请求类型的重定向包括PUT
if attempt.previous().len() >= 5 { if attempt.previous().len() >= 5 {
attempt.error("重定向次数过多") attempt.error("Too many redirects")
} else { } else {
attempt.follow() attempt.follow()
} }
@@ -177,7 +174,7 @@ impl WebDavClient {
} }
Ok(Err(e)) => { 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; tokio::time::sleep(Duration::from_millis(500)).await;
timeout( timeout(
Duration::from_secs(TIMEOUT_UPLOAD), Duration::from_secs(TIMEOUT_UPLOAD),
@@ -237,7 +234,7 @@ impl WebDavClient {
pub fn create_backup() -> Result<(String, PathBuf), Error> { pub fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string(); 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 zip_path = temp_dir().join(&zip_file_name);
let file = fs::File::create(&zip_path)?; let file = fs::File::create(&zip_path)?;

View File

@@ -1,5 +1,3 @@
#[cfg(target_os = "macos")]
use crate::core::tray::Tray;
use crate::{ use crate::{
config::*, config::*,
core::{ core::{
@@ -74,7 +72,7 @@ impl CoreManager {
warn, warn,
Type::Config, Type::Config,
true, true,
"无法读取文件以检测类型: {}, 错误: {}", "Failed to read file to detect type: {}, error: {}",
path, path,
err err
); );
@@ -132,7 +130,7 @@ impl CoreManager {
debug, debug,
Type::Config, Type::Config,
true, true,
"无法确定文件类型默认当作YAML处理: {}", "Unable to determine file type, defaulting to YAML handling: {}",
path path
); );
Ok(false) Ok(false)
@@ -148,14 +146,19 @@ impl CoreManager {
help::save_yaml( help::save_yaml(
&runtime_path, &runtime_path,
&Config::clash().latest().0, &Config::clash().latest().0,
Some("# Clash Verge Runtime"), Some("# Koala Clash Runtime"),
)?; )?;
handle::Handle::notice_message(msg_type, msg_content); handle::Handle::notice_message(msg_type, msg_content);
Ok(()) Ok(())
} }
/// 验证运行时配置 /// 验证运行时配置
pub async fn validate_config(&self) -> Result<(bool, String)> { pub async fn validate_config(&self) -> Result<(bool, String)> {
logging!(info, Type::Config, true, "生成临时配置文件用于验证"); logging!(
info,
Type::Config,
true,
"Generate temporary config file for validation"
);
let config_path = Config::generate_file(ConfigType::Check)?; let config_path = Config::generate_file(ConfigType::Check)?;
let config_path = dirs::path_to_str(&config_path)?; let config_path = dirs::path_to_str(&config_path)?;
self.validate_config_internal(config_path).await self.validate_config_internal(config_path).await
@@ -168,13 +171,18 @@ impl CoreManager {
) -> Result<(bool, String)> { ) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证 // 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() { if handle::Handle::global().is_exiting() {
logging!(info, Type::Core, true, "应用正在退出,跳过验证"); logging!(
info,
Type::Core,
true,
"App is exiting, skipping validation"
);
return Ok((true, String::new())); return Ok((true, String::new()));
} }
// 检查文件是否存在 // 检查文件是否存在
if !std::path::Path::new(config_path).exists() { 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); //handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
return Ok((false, error_msg)); return Ok((false, error_msg));
} }
@@ -185,7 +193,7 @@ impl CoreManager {
info, info,
Type::Config, Type::Config,
true, true,
"检测到Merge文件仅进行语法检查: {}", "Detected merge file, performing syntax check only: {}",
config_path config_path
); );
return self.validate_file_syntax(config_path).await; return self.validate_file_syntax(config_path).await;
@@ -203,7 +211,7 @@ impl CoreManager {
warn, warn,
Type::Config, Type::Config,
true, true,
"无法确定文件类型: {}, 错误: {}", "Unable to determine file type: {}, error: {}",
config_path, config_path,
err err
); );
@@ -217,7 +225,7 @@ impl CoreManager {
info, info,
Type::Config, Type::Config,
true, true,
"检测到脚本文件,使用JavaScript验证: {}", "Detected script file, validating with JavaScript: {}",
config_path config_path
); );
return self.validate_script_file(config_path).await; return self.validate_script_file(config_path).await;
@@ -228,7 +236,7 @@ impl CoreManager {
info, info,
Type::Config, Type::Config,
true, true,
"使用Clash内核验证配置文件: {}", "Validating config file with Clash core: {}",
config_path config_path
); );
self.validate_config_internal(config_path).await self.validate_config_internal(config_path).await
@@ -237,7 +245,12 @@ impl CoreManager {
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> { async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证 // 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() { if handle::Handle::global().is_exiting() {
logging!(info, Type::Core, true, "应用正在退出,跳过验证"); logging!(
info,
Type::Core,
true,
"App is exiting, skipping validation"
);
return Ok((true, String::new())); return Ok((true, String::new()));
} }
@@ -245,17 +258,23 @@ impl CoreManager {
info, info,
Type::Config, Type::Config,
true, true,
"开始验证配置文件: {}", "Starting validation for config file: {}",
config_path config_path
); );
let clash_core = Config::verge().latest().get_valid_clash_core(); let clash_core = Config::verge().latest().get_valid_clash_core();
logging!(info, Type::Config, true, "使用内核: {}", clash_core); logging!(info, Type::Config, true, "Using core: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
let app_dir = dirs::app_home_dir()?; let app_dir = dirs::app_home_dir()?;
let app_dir_str = dirs::path_to_str(&app_dir)?; let app_dir_str = dirs::path_to_str(&app_dir)?;
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str); logging!(
info,
Type::Config,
true,
"Validation directory: {}",
app_dir_str
);
// 使用子进程运行clash验证配置 // 使用子进程运行clash验证配置
let output = app_handle let output = app_handle
@@ -273,56 +292,84 @@ impl CoreManager {
let has_error = let has_error =
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw)); !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
logging!(info, Type::Config, true, "-------- 验证结果 --------"); logging!(
info,
Type::Config,
true,
"-------- Validation Result --------"
);
if !stderr.is_empty() { if !stderr.is_empty() {
logging!(info, Type::Config, true, "stderr输出:\n{}", stderr); logging!(info, Type::Config, true, "stderr output:\n{}", stderr);
} }
if has_error { if has_error {
logging!(info, Type::Config, true, "发现错误,开始处理错误信息"); logging!(
info,
Type::Config,
true,
"Errors found, processing error details"
);
let error_msg = if !stdout.is_empty() { let error_msg = if !stdout.is_empty() {
stdout.to_string() stdout.to_string()
} else if !stderr.is_empty() { } else if !stderr.is_empty() {
stderr.to_string() stderr.to_string()
} else if let Some(code) = output.status.code() { } else if let Some(code) = output.status.code() {
format!("验证进程异常退出,退出码: {}", code) format!("Validation process exited abnormally, exit code: {code}")
} else { } else {
"验证进程被终止".to_string() "Validation process was terminated".to_string()
}; };
logging!(info, Type::Config, true, "-------- 验证结束 --------"); logging!(info, Type::Config, true, "-------- Validation End --------");
Ok((false, error_msg)) // 返回错误消息给调用者处理 Ok((false, error_msg)) // 返回错误消息给调用者处理
} else { } else {
logging!(info, Type::Config, true, "验证成功"); logging!(info, Type::Config, true, "Validation succeeded");
logging!(info, Type::Config, true, "-------- 验证结束 --------"); logging!(info, Type::Config, true, "-------- Validation End --------");
Ok((true, String::new())) Ok((true, String::new()))
} }
} }
/// 只进行文件语法检查,不进行完整验证 /// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> { async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
logging!(info, Type::Config, true, "开始检查文件: {}", config_path); logging!(
info,
Type::Config,
true,
"Starting file check: {}",
config_path
);
// 读取文件内容 // 读取文件内容
let content = match std::fs::read_to_string(config_path) { let content = match std::fs::read_to_string(config_path) {
Ok(content) => content, Ok(content) => content,
Err(err) => { 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); logging!(
error,
Type::Config,
true,
"Failed to read file: {}",
error_msg
);
return Ok((false, error_msg)); return Ok((false, error_msg));
} }
}; };
// 对YAML文件尝试解析只检查语法正确性 // 对YAML文件尝试解析只检查语法正确性
logging!(info, Type::Config, true, "进行YAML语法检查"); logging!(info, Type::Config, true, "Performing YAML syntax check");
match serde_yaml::from_str::<serde_yaml::Value>(&content) { match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => { Ok(_) => {
logging!(info, Type::Config, true, "YAML语法检查通过"); logging!(info, Type::Config, true, "YAML syntax check passed");
Ok((true, String::new())) Ok((true, String::new()))
} }
Err(err) => { 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); logging!(
error,
Type::Config,
true,
"YAML syntax error: {}",
error_msg
);
Ok((false, error_msg)) Ok((false, error_msg))
} }
} }
@@ -333,14 +380,20 @@ impl CoreManager {
let content = match std::fs::read_to_string(path) { let content = match std::fs::read_to_string(path) {
Ok(content) => content, Ok(content) => content,
Err(err) => { 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); logging!(warn, Type::Config, true, "Script syntax error: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg)); return Ok((false, error_msg));
} }
}; };
logging!(debug, Type::Config, true, "验证脚本文件: {}", path); logging!(
debug,
Type::Config,
true,
"Validating script file: {}",
path
);
// 使用boa引擎进行基本语法检查 // 使用boa引擎进行基本语法检查
use boa_engine::{Context, Source}; use boa_engine::{Context, Source};
@@ -350,7 +403,13 @@ impl CoreManager {
match result { match result {
Ok(_) => { Ok(_) => {
logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path); logging!(
debug,
Type::Config,
true,
"Script syntax validation passed: {}",
path
);
// 检查脚本是否包含main函数 // 检查脚本是否包含main函数
if !content.contains("function main") if !content.contains("function main")
@@ -358,7 +417,13 @@ impl CoreManager {
&& !content.contains("let main") && !content.contains("let main")
{ {
let error_msg = "Script must contain a main function"; let error_msg = "Script must contain a main function";
logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path); logging!(
warn,
Type::Config,
true,
"Script missing main function: {}",
path
);
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg); //handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
return Ok((false, error_msg.to_string())); return Ok((false, error_msg.to_string()));
} }
@@ -366,8 +431,8 @@ impl CoreManager {
Ok((true, String::new())) Ok((true, String::new()))
} }
Err(err) => { Err(err) => {
let error_msg = format!("Script syntax error: {}", err); let error_msg = format!("Script syntax error: {err}");
logging!(warn, Type::Config, true, "脚本语法错误: {}", err); logging!(warn, Type::Config, true, "Script syntax error: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
Ok((false, error_msg)) Ok((false, error_msg))
} }
@@ -377,33 +442,55 @@ impl CoreManager {
pub async fn update_config(&self) -> Result<(bool, String)> { pub async fn update_config(&self) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过完整验证流程 // 检查程序是否正在退出,如果是则跳过完整验证流程
if handle::Handle::global().is_exiting() { if handle::Handle::global().is_exiting() {
logging!(info, Type::Config, true, "应用正在退出,跳过验证"); logging!(
info,
Type::Config,
true,
"App is exiting, skipping validation"
);
return Ok((true, String::new())); return Ok((true, String::new()));
} }
logging!(info, Type::Config, true, "开始更新配置"); logging!(info, Type::Config, true, "Starting config update");
// 1. 先生成新的配置内容 // 1. 先生成新的配置内容
logging!(info, Type::Config, true, "生成新的配置内容"); logging!(
info,
Type::Config,
true,
"Generating new configuration content"
);
Config::generate().await?; Config::generate().await?;
// 2. 验证配置 // 2. 验证配置
match self.validate_config().await { match self.validate_config().await {
Ok((true, _)) => { Ok((true, _)) => {
logging!(info, Type::Config, true, "配置验证通过"); logging!(info, Type::Config, true, "Configuration validation passed");
// 4. 验证通过后,生成正式的运行时配置 // 4. 验证通过后,生成正式的运行时配置
logging!(info, Type::Config, true, "生成运行时配置"); logging!(info, Type::Config, true, "Generating runtime configuration");
let run_path = Config::generate_file(ConfigType::Run)?; let run_path = Config::generate_file(ConfigType::Run)?;
logging_error!(Type::Config, true, self.put_configs_force(run_path).await); logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
Ok((true, "something".into())) Ok((true, "something".into()))
} }
Ok((false, error_msg)) => { Ok((false, error_msg)) => {
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg); logging!(
warn,
Type::Config,
true,
"Configuration validation failed: {}",
error_msg
);
Config::runtime().discard(); Config::runtime().discard();
Ok((false, error_msg)) Ok((false, error_msg))
} }
Err(e) => { Err(e) => {
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e); logging!(
warn,
Type::Config,
true,
"Error occurred during validation: {}",
e
);
Config::runtime().discard(); Config::runtime().discard();
Err(e) Err(e)
} }
@@ -435,6 +522,322 @@ impl CoreManager {
} }
impl CoreManager { impl CoreManager {
/// 清理多余的 mihomo 进程
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
logging!(
info,
Type::Core,
true,
"Starting cleanup of orphaned mihomo processes"
);
// 获取当前管理的进程 PID
let current_pid = {
let child_guard = self.child_sidecar.lock().await;
child_guard.as_ref().map(|child| child.pid())
};
let target_processes = ["koala-mihomo", "koala-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,
"Skipping currently managed process: {} (PID: {})",
process_name,
pid
);
continue;
}
}
pids_to_kill.push((pid, process_name.clone()));
}
}
Err(e) => {
logging!(
debug,
Type::Core,
true,
"Error occurred while finding processes: {}",
e
);
}
}
}
if pids_to_kill.is_empty() {
logging!(
debug,
Type::Core,
true,
"No orphaned mihomo processes found"
);
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,
"Cleanup complete, a total of {} redundant mihomo processes terminated",
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,
"Attempt to terminate process: {} (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,
"Process {} (PID: {}) Termination command successful, but process still running",
process_name,
pid
);
false
} else {
logging!(
info,
Type::Core,
true,
"Successfully terminated process: {} (PID: {})",
process_name,
pid
);
true
}
} else {
logging!(
warn,
Type::Core,
true,
"Unable to terminate process: {} (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<()> { async fn start_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by sidecar"); logging!(trace, Type::Core, true, "Running core by sidecar");
let config_file = &Config::generate_file(ConfigType::Run)?; let config_file = &Config::generate_file(ConfigType::Run)?;
@@ -450,7 +853,7 @@ impl CoreManager {
let now = Local::now(); let now = Local::now();
let timestamp = now.format("%Y%m%d_%H%M%S").to_string(); 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)?; let mut log_file = File::create(log_path)?;
@@ -539,19 +942,29 @@ impl CoreManager {
// 当服务安装失败时的回退逻辑 // 当服务安装失败时的回退逻辑
async fn attempt_service_init(&self) -> Result<()> { async fn attempt_service_init(&self) -> Result<()> {
if service::check_service_needs_reinstall().await { if service::check_service_needs_reinstall().await {
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装"); logging!(
info,
Type::Core,
true,
"Service version mismatch or abnormal status, performing reinstallation"
);
if let Err(e) = service::reinstall_service().await { if let Err(e) = service::reinstall_service().await {
logging!( logging!(
warn, warn,
Type::Core, Type::Core,
true, true,
"服务重装失败 during attempt_service_init: {}", "Service reinstallation failed during attempt_service_init: {}",
e e
); );
return Err(e); return Err(e);
} }
// 如果重装成功,还需要尝试启动服务 // 如果重装成功,还需要尝试启动服务
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务"); logging!(
info,
Type::Core,
true,
"Service reinstalled successfully, attempting to start"
);
} }
if let Err(e) = self.start_core_by_service().await { if let Err(e) = self.start_core_by_service().await {
@@ -559,20 +972,20 @@ impl CoreManager {
warn, warn,
Type::Core, Type::Core,
true, true,
"通过服务启动核心失败 during attempt_service_init: {}", "Failed to start core via service during attempt_service_init: {}",
e e
); );
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置 // 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
let mut state = service::ServiceState::get(); let mut state = service::ServiceState::get();
if !state.prefer_sidecar { if !state.prefer_sidecar {
state.prefer_sidecar = true; state.prefer_sidecar = true;
state.last_error = Some(format!("通过服务启动核心失败: {}", e)); state.last_error = Some(format!("Failed to start core via service: {e}"));
if let Err(save_err) = state.save() { if let Err(save_err) = state.save() {
logging!( logging!(
error, error,
Type::Core, Type::Core,
true, true,
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}", "Failed to save ServiceState (in attempt_service_init/start_core_by_service): {}",
save_err save_err
); );
} }
@@ -585,6 +998,17 @@ impl CoreManager {
pub async fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core"); logging!(trace, Type::Core, "Initializing core");
// 应用启动时先清理任何遗留的 mihomo 进程
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
logging!(
warn,
Type::Core,
true,
"Failed to clean up unnecessary mihomo processes during application initialization: {}",
e
);
}
let mut core_started_successfully = false; let mut core_started_successfully = false;
if service::is_service_available().await.is_ok() { if service::is_service_available().await.is_ok() {
@@ -592,11 +1016,16 @@ impl CoreManager {
info, info,
Type::Core, Type::Core,
true, true,
"服务当前可用或看似可用,尝试通过服务模式启动/重装" "Service currently available or appears available; attempting to start/reinstall via service mode"
); );
match self.attempt_service_init().await { match self.attempt_service_init().await {
Ok(_) => { Ok(_) => {
logging!(info, Type::Core, true, "服务模式成功启动核心"); logging!(
info,
Type::Core,
true,
"Service mode successfully started core"
);
core_started_successfully = true; core_started_successfully = true;
} }
Err(_err) => { Err(_err) => {
@@ -604,7 +1033,7 @@ impl CoreManager {
warn, warn,
Type::Core, Type::Core,
true, true,
"服务模式启动或重装失败。将尝试Sidecar模式回退。" "Service mode start or reinstall failed. Will attempt Sidecar fallback."
); );
} }
} }
@@ -613,7 +1042,7 @@ impl CoreManager {
info, info,
Type::Core, Type::Core,
true, true,
"服务初始不可用 (is_service_available 调用失败)" "Service initially unavailable (is_service_available call failed)"
); );
} }
@@ -622,7 +1051,7 @@ impl CoreManager {
info, info,
Type::Core, Type::Core,
true, true,
"核心未通过服务模式启动执行Sidecar回退或首次安装逻辑" "Core not started via service mode; performing Sidecar fallback or first-time install logic"
); );
let service_state = service::ServiceState::get(); let service_state = service::ServiceState::get();
@@ -632,7 +1061,7 @@ impl CoreManager {
info, info,
Type::Core, Type::Core,
true, true,
"用户偏好Sidecar模式或先前服务启动失败使用Sidecar模式启动" "User prefers Sidecar mode or previous service start failed; starting with Sidecar mode"
); );
self.start_core_by_sidecar().await?; self.start_core_by_sidecar().await?;
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束 // 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
@@ -644,26 +1073,41 @@ impl CoreManager {
info, info,
Type::Core, Type::Core,
true, true,
"无服务安装记录 (首次运行或状态重置),尝试安装服务" "No service installation record (first run or state reset); attempting to install service"
); );
match service::install_service().await { match service::install_service().await {
Ok(_) => { Ok(_) => {
logging!(info, Type::Core, true, "服务安装成功(首次尝试)"); logging!(
info,
Type::Core,
true,
"Service installed successfully (first attempt)"
);
let mut new_state = service::ServiceState::default(); let mut new_state = service::ServiceState::default();
new_state.record_install(); new_state.record_install();
new_state.prefer_sidecar = false; new_state.prefer_sidecar = false;
new_state.save()?; new_state.save()?;
if service::is_service_available().await.is_ok() { if service::is_service_available().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动"); logging!(
info,
Type::Core,
true,
"Newly installed service available; attempting to start"
);
if self.start_core_by_service().await.is_ok() { if self.start_core_by_service().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务启动成功"); logging!(
info,
Type::Core,
true,
"Newly installed service started successfully"
);
} else { } else {
logging!( logging!(
warn, warn,
Type::Core, Type::Core,
true, true,
"新安装的服务启动失败,回退到Sidecar模式" "Newly installed service failed to start; falling back to Sidecar mode"
); );
let mut final_state = service::ServiceState::get(); let mut final_state = service::ServiceState::get();
final_state.prefer_sidecar = true; final_state.prefer_sidecar = true;
@@ -677,7 +1121,7 @@ impl CoreManager {
warn, warn,
Type::Core, Type::Core,
true, true,
"服务安装成功但未能连接/立即可用,回退到Sidecar模式" "Service installed successfully but not connectable/immediately available; falling back to Sidecar mode"
); );
let mut final_state = service::ServiceState::get(); let mut final_state = service::ServiceState::get();
final_state.prefer_sidecar = true; final_state.prefer_sidecar = true;
@@ -690,7 +1134,13 @@ impl CoreManager {
} }
} }
Err(err) => { Err(err) => {
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err); logging!(
warn,
Type::Core,
true,
"Service first-time installation failed: {}",
err
);
let new_state = service::ServiceState { let new_state = service::ServiceState {
last_error: Some(err.to_string()), last_error: Some(err.to_string()),
prefer_sidecar: true, prefer_sidecar: true,
@@ -708,7 +1158,7 @@ impl CoreManager {
info, info,
Type::Core, Type::Core,
true, true,
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式" "There is a service installation record, but the service is unavailable/not started. Force switch to Sidecar mode"
); );
let mut final_state = service::ServiceState::get(); let mut final_state = service::ServiceState::get();
if !final_state.prefer_sidecar { if !final_state.prefer_sidecar {
@@ -716,7 +1166,7 @@ impl CoreManager {
warn, warn,
Type::Core, Type::Core,
true, true,
"prefer_sidecar false,因服务启动失败或不可用而强制设置为 true" "prefer_sidecar is false, but is forced to true due to service startup failure or unavailability"
); );
final_state.prefer_sidecar = true; final_state.prefer_sidecar = true;
final_state.last_error = final_state.last_error =
@@ -732,9 +1182,8 @@ impl CoreManager {
} }
logging!(trace, Type::Core, "Initied core logic completed"); logging!(trace, Type::Core, "Initied core logic completed");
#[cfg(target_os = "macos")] // #[cfg(target_os = "macos")]
logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); // logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
Ok(()) Ok(())
} }
@@ -754,7 +1203,12 @@ impl CoreManager {
if service::check_service_needs_reinstall().await { if service::check_service_needs_reinstall().await {
service::reinstall_service().await?; service::reinstall_service().await?;
} }
logging!(info, Type::Core, true, "服务可用,使用服务模式启动"); logging!(
info,
Type::Core,
true,
"Service available; starting in service mode"
);
self.start_core_by_service().await?; self.start_core_by_service().await?;
} else { } else {
// 服务不可用,检查用户偏好 // 服务不可用,检查用户偏好
@@ -764,11 +1218,16 @@ impl CoreManager {
info, info,
Type::Core, Type::Core,
true, true,
"服务不可用根据用户偏好使用Sidecar模式" "Service unavailable; starting in Sidecar mode per user preference"
); );
self.start_core_by_sidecar().await?; self.start_core_by_sidecar().await?;
} else { } else {
logging!(info, Type::Core, true, "服务不可用使用Sidecar模式"); logging!(
info,
Type::Core,
true,
"Service unavailable; starting in Sidecar mode"
);
self.start_core_by_sidecar().await?; self.start_core_by_sidecar().await?;
} }
} }
@@ -787,6 +1246,7 @@ impl CoreManager {
/// 重启内核 /// 重启内核
pub async fn restart_core(&self) -> Result<()> { pub async fn restart_core(&self) -> Result<()> {
self.stop_core().await?; self.stop_core().await?;
self.start_core().await?; self.start_core().await?;
Ok(()) Ok(())
} }
@@ -800,7 +1260,7 @@ impl CoreManager {
} }
let core: &str = &clash_core.clone().unwrap(); let core: &str = &clash_core.clone().unwrap();
if !IVerge::VALID_CLASH_CORES.contains(&core) { if !IVerge::VALID_CLASH_CORES.contains(&core) {
let error_message = format!("Clash core invalid name: {}", core); let error_message = format!("Clash core invalid name: {core}");
logging!(error, Type::Core, true, "{}", error_message); logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message); return Err(error_message);
} }

View File

@@ -0,0 +1,571 @@
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>,
}
// Configuration structure moved to external
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,
}
}
/// Get automatic proxy configuration (cached)
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
self.state.read().auto_proxy.clone()
}
/// Asynchronously get the latest automatic proxy configuration
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", "Failed to send query request, returning cached data");
return self.get_auto_proxy_cached();
}
match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result,
_ => {
log::warn!(target: "app", "Query timed out, returning cached data");
self.get_auto_proxy_cached()
}
}
}
/// Notify configuration changed
pub fn notify_config_changed(&self) {
self.send_event(ProxyEvent::ConfigChanged);
}
/// Notify application started
pub fn notify_app_started(&self) {
self.send_event(ProxyEvent::AppStarted);
}
/// Notify application stopping
#[allow(dead_code)]
pub fn notify_app_stopping(&self) {
self.send_event(ProxyEvent::AppStopping);
}
/// Enable system proxy
#[allow(dead_code)]
pub fn enable_proxy(&self) {
self.send_event(ProxyEvent::EnableProxy);
}
/// Disable system proxy
#[allow(dead_code)]
pub fn disable_proxy(&self) {
self.send_event(ProxyEvent::DisableProxy);
}
/// Force check proxy status
#[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", "Failed to send proxy event: {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", "Event-driven proxy manager started");
loop {
tokio::select! {
event = event_rx.recv() => {
match event {
Some(event) => {
log::debug!(target: "app", "Handling proxy event: {event:?}");
Self::handle_event(&state, event).await;
}
None => {
log::info!(target: "app", "Event channel closed, proxy manager stopped");
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", "Query channel closed");
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", "Cleaning up proxy state");
}
}
}
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", "Initializing proxy state");
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", "Proxy state initialized: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
}
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
log::debug!(target: "app", "Updating proxy configuration");
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", "Checking proxy status");
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 proxy setting abnormal, recovering...");
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", "System proxy setting abnormal, recovering...");
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", "Enabling system proxy");
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", "Disabling system proxy");
#[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", "Switching to {} mode", if to_pac { "PAC" } else { "HTTP Proxy" });
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;
// Convert to compatible structure
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;
// Convert to compatible structure
Sysproxy {
enable: async_proxy.enable,
host: async_proxy.host,
port: async_proxy.port,
bypass: async_proxy.bypass,
}
}
// Unified state update method
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 verge_config = Config::verge();
let verge = verge_config.latest();
ProxyConfig {
sys_enabled: verge.enable_system_proxy.unwrap_or(false),
pac_enabled: verge.proxy_auto_config.unwrap_or(false),
guard_enabled: verge.enable_proxy_guard.unwrap_or(false),
}
}
fn get_expected_pac_config() -> Autoproxy {
let verge_config = Config::verge();
let verge = verge_config.latest();
let (proxy_host, pac_port) = (
verge
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string()),
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();
let port = verge
.verge_mixed_port
.unwrap_or(Config::clash().data().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 verge_config = Config::verge();
let verge = verge_config.latest();
let use_default = verge.use_default_bypass.unwrap_or(true);
let custom_bypass = 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", "Failed to get service path: {}", e);
return;
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe does not exist");
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", "Failed to execute sysproxy command: {:?}", args);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
log::error!(target: "app", "sysproxy stderr: {}", stderr);
}
} else {
log::debug!(target: "app", "Successfully executed sysproxy command: {:?}", args);
}
}
Err(e) => {
log::error!(target: "app", "Error executing sysproxy command: {}", e);
}
}
}
}

View File

@@ -125,7 +125,7 @@ impl NotificationSystem {
match serde_json::to_value((status, message)) { match serde_json::to_value((status, message)) {
Ok(p) => ("verge://notice-message", Ok(p)), Ok(p) => ("verge://notice-message", Ok(p)),
Err(e) => { Err(e) => {
log::error!("Failed to serialize NoticeMessage payload: {}", e); log::error!("Failed to serialize NoticeMessage payload: {e}");
("verge://notice-message", Err(e)) ("verge://notice-message", Err(e))
} }
} }
@@ -153,11 +153,11 @@ impl NotificationSystem {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst); system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件 // 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {}", event_name_str); log::debug!("Successfully emitted event: {event_name_str}");
} }
} }
Err(e) => { 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.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now()); *system.stats.last_error_time.write() = Some(Instant::now());
@@ -165,8 +165,7 @@ impl NotificationSystem {
const EMIT_ERROR_THRESHOLD: u64 = 10; const EMIT_ERROR_THRESHOLD: u64 = 10;
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() { if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
log::warn!( log::warn!(
"Reached {} emit errors, entering emergency mode", "Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
EMIT_ERROR_THRESHOLD
); );
*system.emergency_mode.write() = true; *system.emergency_mode.write() = true;
} }
@@ -175,7 +174,7 @@ impl NotificationSystem {
} else { } else {
system.stats.total_errors.fetch_add(1, Ordering::SeqCst); system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now()); *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 { } else {
log::warn!("No window found, skipping event emit."); log::warn!("No window found, skipping event emit.");
@@ -215,7 +214,7 @@ impl NotificationSystem {
match sender.send(event) { match sender.send(event) {
Ok(_) => true, Ok(_) => true,
Err(e) => { 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.total_errors.fetch_add(1, Ordering::SeqCst);
*self.stats.last_error_time.write() = Some(Instant::now()); *self.stats.last_error_time.write() = Some(Instant::now());
false false
@@ -243,7 +242,7 @@ impl NotificationSystem {
log::info!("NotificationSystem worker thread joined successfully"); log::info!("NotificationSystem worker thread joined successfully");
} }
Err(e) => { Err(e) => {
log::error!("NotificationSystem worker thread join failed: {:?}", e); log::error!("NotificationSystem worker thread join failed: {e:?}");
} }
} }
} }
@@ -259,6 +258,8 @@ pub struct Handle {
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>, startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
startup_completed: Arc<RwLock<bool>>, startup_completed: Arc<RwLock<bool>>,
notification_system: Arc<RwLock<Option<NotificationSystem>>>, notification_system: Arc<RwLock<Option<NotificationSystem>>>,
/// Messages that should be emitted only after UI is really ready
ui_pending_messages: Arc<RwLock<Vec<ErrorMessage>>>,
} }
impl Default for Handle { impl Default for Handle {
@@ -269,6 +270,7 @@ impl Default for Handle {
startup_errors: Arc::new(RwLock::new(Vec::new())), startup_errors: Arc::new(RwLock::new(Vec::new())),
startup_completed: Arc::new(RwLock::new(false)), startup_completed: Arc::new(RwLock::new(false)),
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))), notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
ui_pending_messages: Arc::new(RwLock::new(Vec::new())),
} }
} }
} }
@@ -296,6 +298,10 @@ impl Handle {
} }
pub fn get_window(&self) -> Option<WebviewWindow> { pub fn get_window(&self) -> Option<WebviewWindow> {
// If we are in lightweight mode, treat as no window (webview may be destroyed)
if crate::module::lightweight::is_in_lightweight_mode() {
return None;
}
let app_handle = self.app_handle()?; let app_handle = self.app_handle()?;
let window: Option<WebviewWindow> = app_handle.get_webview_window("main"); let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
if window.is_none() { if window.is_none() {
@@ -412,12 +418,13 @@ impl Handle {
let status_str = status.into(); let status_str = status.into();
let msg_str = msg.into(); let msg_str = msg.into();
// If startup not completed, buffer messages (existing behavior)
if !*handle.startup_completed.read() { if !*handle.startup_completed.read() {
logging!( logging!(
info, info,
Type::Frontend, Type::Frontend,
true, true,
"启动过程中发现错误,加入消息队列: {} - {}", "Error found during startup; queued: {} - {}",
status_str, status_str,
msg_str msg_str
); );
@@ -430,6 +437,23 @@ impl Handle {
return; return;
} }
// If UI is not yet ready (e.g., window re-created from tray or lightweight mode),
// buffer messages to emit after UI signals readiness.
if !crate::utils::resolve::is_ui_ready() {
log::debug!(
target: "app",
"UI not ready, queue notice message: {} - {}",
status_str,
msg_str
);
let mut pendings = handle.ui_pending_messages.write();
pendings.push(ErrorMessage {
status: status_str,
message: msg_str,
});
return;
}
if handle.is_exiting() { if handle.is_exiting() {
return; return;
} }
@@ -443,6 +467,34 @@ impl Handle {
} }
} }
/// Flush messages buffered while UI was not ready
pub fn flush_ui_pending_messages(&self) {
let pending = {
let mut msgs = self.ui_pending_messages.write();
std::mem::take(&mut *msgs)
};
if pending.is_empty() {
return;
}
if self.is_exiting() {
return;
}
let system_opt = self.notification_system.read();
if let Some(system) = system_opt.as_ref() {
for msg in pending {
system.send_event(FrontendEvent::NoticeMessage {
status: msg.status,
message: msg.message,
});
// small pacing to avoid flooding immediately on resume
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
}
pub fn mark_startup_completed(&self) { pub fn mark_startup_completed(&self) {
{ {
let mut completed = self.startup_completed.write(); let mut completed = self.startup_completed.write();
@@ -467,7 +519,7 @@ impl Handle {
info, info,
Type::Frontend, Type::Frontend,
true, true,
"发送{}条启动时累积的错误消息", "Sending {} accumulated startup error messages",
errors.len() errors.len()
); );
@@ -500,7 +552,7 @@ impl Handle {
}); });
if let Err(e) = thread_result { if let Err(e) = thread_result {
log::error!("Failed to spawn startup errors thread: {}", e); log::error!("Failed to spawn startup errors thread: {e}");
} }
} }

View File

@@ -1,10 +1,7 @@
use crate::utils::notification::{notify_event, NotificationEvent};
use crate::{ use crate::{
config::Config, config::Config, core::handle, feat, logging, logging_error,
core::handle, module::lightweight::entry_lightweight_mode, utils::logging::Type,
feat, logging, logging_error,
module::lightweight::entry_lightweight_mode,
process::AsyncHandler,
utils::{logging::Type, resolve},
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@@ -14,7 +11,7 @@ use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
pub struct Hotkey { pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置 current: Arc<Mutex<Vec<String>>>,
} }
impl Hotkey { impl Hotkey {
@@ -38,7 +35,6 @@ impl Hotkey {
enable_global_hotkey enable_global_hotkey
); );
// 如果全局热键被禁用,则不注册热键
if !enable_global_hotkey { if !enable_global_hotkey {
return Ok(()); return Ok(());
} }
@@ -138,14 +134,11 @@ impl Hotkey {
manager.unregister(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" => { "open_or_close_dashboard" => {
logging!( let app_handle = app_handle_clone.clone();
debug, Box::new(move || {
Type::Hotkey,
"Registering open_or_close_dashboard function"
);
|| {
logging!( logging!(
debug, debug,
Type::Hotkey, Type::Hotkey,
@@ -153,94 +146,89 @@ impl Hotkey {
"=== Hotkey Dashboard Window Operation Start ===" "=== Hotkey Dashboard Window Operation Start ==="
); );
// 检查是否在轻量模式下,如果是,需要同步处理 logging!(
if crate::module::lightweight::is_in_lightweight_mode() { info,
logging!( Type::Hotkey,
info, true,
Type::Hotkey, "Using unified WindowManager for hotkey operation (bypass debounce)"
true, );
"In lightweight mode, calling open_or_close_dashboard directly"
);
crate::feat::open_or_close_dashboard();
} else {
AsyncHandler::spawn(move || async move {
logging!(
debug,
Type::Hotkey,
true,
"Toggle dashboard window visibility (async)"
);
// 检查窗口是否存在 crate::feat::open_or_close_dashboard_hotkey();
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏
match window.is_visible() {
Ok(visible) => {
if visible {
logging!(
info,
Type::Window,
true,
"Window is visible, hiding it"
);
let _ = window.hide();
} else {
// 如果窗口不可见,则显示
logging!(
info,
Type::Window,
true,
"Window is hidden, showing it"
);
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
}
Err(e) => {
logging!(
warn,
Type::Window,
true,
"Failed to check window visibility: {}",
e
);
let _ = window.show();
let _ = window.set_focus();
}
}
} else {
// 如果窗口不存在,创建一个新窗口
logging!(
info,
Type::Window,
true,
"Window does not exist, creating a new one"
);
resolve::create_window(true);
}
});
}
logging!( logging!(
debug, debug,
Type::Hotkey, Type::Hotkey,
"=== Hotkey Dashboard Window Operation End ===" "=== 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")] #[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); logging!(error, Type::Hotkey, "Invalid function: {}", func);
bail!("invalid function \"{func}\""); bail!("invalid function \"{func}\"");
@@ -261,10 +249,8 @@ impl Hotkey {
} }
} }
} else { } else {
// 直接执行函数,不做任何状态检查
logging!(debug, Type::Hotkey, "Executing function directly"); logging!(debug, Type::Hotkey, "Executing function directly");
// 获取全局热键状态
let is_enable_global_hotkey = Config::verge() let is_enable_global_hotkey = Config::verge()
.latest() .latest()
.enable_global_hotkey .enable_global_hotkey
@@ -274,7 +260,6 @@ impl Hotkey {
f(); f();
} else { } else {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
let is_visible = WindowManager::is_main_window_visible(); let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused(); let is_focused = WindowManager::is_main_window_focused();

View File

@@ -1,6 +1,8 @@
pub mod async_proxy_query;
pub mod backup; pub mod backup;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod core; mod core;
pub mod event_driven_proxy;
pub mod handle; pub mod handle;
pub mod hotkey; pub mod hotkey;
pub mod service; pub mod service;
@@ -10,4 +12,4 @@ pub mod timer;
pub mod tray; pub mod tray;
pub mod win_uwp; pub mod win_uwp;
pub use self::{core::*, timer::Timer}; pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};

View File

@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
Ok(()) Ok(())
} }
Err(err) => { 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.last_error = Some(error.clone());
service_state.prefer_sidecar = true; service_state.prefer_sidecar = true;
service_state.save()?; service_state.save()?;
@@ -466,7 +466,7 @@ pub async fn reinstall_service() -> Result<()> {
Ok(()) Ok(())
} }
Err(err) => { 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.last_error = Some(error.clone());
service_state.prefer_sidecar = true; service_state.prefer_sidecar = true;
service_state.save()?; service_state.save()?;
@@ -477,7 +477,12 @@ pub async fn reinstall_service() -> Result<()> {
/// 检查服务状态 - 使用IPC通信 /// 检查服务状态 - 使用IPC通信
pub async fn check_ipc_service_status() -> Result<JsonResponse> { pub async fn check_ipc_service_status() -> Result<JsonResponse> {
logging!(info, Type::Service, true, "开始检查服务状态 (IPC)"); logging!(
info,
Type::Service,
true,
"Starting service status check (IPC)"
);
// 使用IPC通信 // 使用IPC通信
let payload = serde_json::json!({}); let payload = serde_json::json!({});
@@ -495,8 +500,16 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
); */ ); */
if !response.success { if !response.success {
let err_msg = response.error.unwrap_or_else(|| "未知服务错误".to_string()); let err_msg = response
logging!(error, Type::Service, true, "服务响应错误: {}", err_msg); .error
.unwrap_or_else(|| "Unknown service error".to_string());
logging!(
error,
Type::Service,
true,
"Service response error: {}",
err_msg
);
bail!(err_msg); bail!(err_msg);
} }
@@ -516,7 +529,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
warn, warn,
Type::Service, Type::Service,
true, true,
"解析嵌套的ResponseBody失败: {}; 尝试其他方式", "Failed to parse nested ResponseBody: {}; trying alternative",
e e
); );
None None
@@ -536,7 +549,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
info, info,
Type::Service, Type::Service,
true, true,
"服务检测成功: code={}, msg={}, data存在={}", "Service check succeeded: code={}, msg={}, data_present={}",
json_response.code, json_response.code,
json_response.msg, json_response.msg,
json_response.data.is_some() json_response.data.is_some()
@@ -550,7 +563,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
info, info,
Type::Service, Type::Service,
true, true,
"服务检测成功: code={}, msg={}", "Service check succeeded: code={}, msg={}",
json_response.code, json_response.code,
json_response.msg json_response.msg
); );
@@ -561,31 +574,42 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
error, error,
Type::Service, Type::Service,
true, true,
"解析服务响应失败: {}; 原始数据: {:?}", "Failed to parse service response: {}; raw data: {:?}",
e, e,
data data
); );
bail!("无法解析服务响应数据: {}", e) bail!("Unable to parse service response data: {}", e)
} }
} }
} }
} }
None => { None => {
logging!(error, Type::Service, true, "服务响应中没有数据"); logging!(error, Type::Service, true, "No data in service response");
bail!("服务响应中没有数据") bail!("No data in service response")
} }
} }
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "IPC通信失败: {}", e); logging!(
bail!("无法连接到Clash Verge Service: {}", e) error,
Type::Service,
true,
"IPC communication failed: {}",
e
);
bail!("Unable to connect to Koala Clash Service: {}", e)
} }
} }
} }
/// 检查服务版本 - 使用IPC通信 /// 检查服务版本 - 使用IPC通信
pub async fn check_service_version() -> Result<String> { pub async fn check_service_version() -> Result<String> {
logging!(info, Type::Service, true, "开始检查服务版本 (IPC)"); logging!(
info,
Type::Service,
true,
"Starting service version check (IPC)"
);
let payload = serde_json::json!({}); let payload = serde_json::json!({});
// logging!(debug, Type::Service, true, "发送GetVersion请求"); // logging!(debug, Type::Service, true, "发送GetVersion请求");
@@ -604,8 +628,14 @@ pub async fn check_service_version() -> Result<String> {
if !response.success { if !response.success {
let err_msg = response let err_msg = response
.error .error
.unwrap_or_else(|| "获取服务版本失败".to_string()); .unwrap_or_else(|| "Failed to get service version".to_string());
logging!(error, Type::Service, true, "获取版本错误: {}", err_msg); logging!(
error,
Type::Service,
true,
"Failed to get service version: {}",
err_msg
);
bail!(err_msg); bail!(err_msg);
} }
@@ -618,7 +648,7 @@ pub async fn check_service_version() -> Result<String> {
info, info,
Type::Service, Type::Service,
true, true,
"获取到服务版本: {}", "Service version: {}",
version_str version_str
); );
return Ok(version_str.to_string()); return Ok(version_str.to_string());
@@ -628,7 +658,7 @@ pub async fn check_service_version() -> Result<String> {
error, error,
Type::Service, Type::Service,
true, true,
"嵌套数据中没有version字段: {:?}", "Nested data does not contain version field: {:?}",
nested_data nested_data
); );
} else { } else {
@@ -639,7 +669,7 @@ pub async fn check_service_version() -> Result<String> {
info, info,
Type::Service, Type::Service,
true, true,
"获取到服务版本: {}", "Received service version: {}",
version_response.version version_response.version
); );
return Ok(version_response.version); return Ok(version_response.version);
@@ -649,44 +679,55 @@ pub async fn check_service_version() -> Result<String> {
error, error,
Type::Service, Type::Service,
true, true,
"解析版本响应失败: {}; 原始数据: {:?}", "Failed to parse version response: {}; raw data: {:?}",
e, e,
data data
); );
bail!("无法解析服务版本数据: {}", e) bail!("Unable to parse service version data: {}", e)
} }
} }
} }
bail!("响应中未找到有效的版本信息") bail!("No valid version information found in response")
} }
None => { None => {
logging!(error, Type::Service, true, "版本响应中没有数据"); logging!(error, Type::Service, true, "No data in version response");
bail!("服务版本响应中没有数据") bail!("No data in service version response")
} }
} }
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "IPC通信失败: {}", e); logging!(
bail!("无法连接到Clash Verge Service: {}", e) error,
Type::Service,
true,
"IPC communication failed: {}",
e
);
bail!("Unable to connect to Koala Clash Service: {}", e)
} }
} }
} }
/// 检查服务是否需要重装 /// 检查服务是否需要重装
pub async fn check_service_needs_reinstall() -> bool { pub async fn check_service_needs_reinstall() -> bool {
logging!(info, Type::Service, true, "开始检查服务是否需要重装"); logging!(
info,
Type::Service,
true,
"Checking whether service needs reinstallation"
);
let service_state = ServiceState::get(); let service_state = ServiceState::get();
if !service_state.can_reinstall() { if !service_state.can_reinstall() {
log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数"); log::info!(target: "app", "Service reinstall check: in cooldown period or max attempts reached");
return false; return false;
} }
// 检查版本和可用性 // 检查版本和可用性
match check_service_version().await { match check_service_version().await {
Ok(version) => { Ok(version) => {
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION); log::info!(target: "app", "Service version check: current={version}, required={REQUIRED_SERVICE_VERSION}");
/* logging!( /* logging!(
info, info,
Type::Service, Type::Service,
@@ -698,26 +739,36 @@ pub async fn check_service_needs_reinstall() -> bool {
let needs_reinstall = version != REQUIRED_SERVICE_VERSION; let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
if needs_reinstall { if needs_reinstall {
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}", log::warn!(target: "app", "Service version mismatch detected, reinstallation required! current={version}, required={REQUIRED_SERVICE_VERSION}");
version, REQUIRED_SERVICE_VERSION); logging!(
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装"); warn,
Type::Service,
true,
"Service version mismatch, reinstallation required"
);
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes()); // log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
// log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes()); // log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
} else { } else {
log::info!(target: "app", "服务版本匹配,无需重装"); log::info!(target: "app", "Service version matches, no reinstallation needed");
// logging!(info, Type::Service, true, "服务版本匹配,无需重装"); // logging!(info, Type::Service, true, "服务版本匹配,无需重装");
} }
needs_reinstall needs_reinstall
} }
Err(err) => { Err(err) => {
logging!(error, Type::Service, true, "检查服务版本失败: {}", err); logging!(
error,
Type::Service,
true,
"Failed to check service version: {}",
err
);
// 检查服务是否可用 // 检查服务是否可用
match is_service_available().await { match is_service_available().await {
Ok(()) => { Ok(()) => {
log::info!(target: "app", "服务正在运行但版本检查失败: {}", err); log::info!(target: "app", "Service is running but version check failed: {err}");
/* logging!( /* logging!(
info, info,
Type::Service, Type::Service,
@@ -728,7 +779,7 @@ pub async fn check_service_needs_reinstall() -> bool {
false false
} }
_ => { _ => {
log::info!(target: "app", "服务不可用或未运行,需要重装"); log::info!(target: "app", "Service unavailable or not running, reinstallation needed");
// logging!(info, Type::Service, true, "服务不可用或未运行,需要重装"); // logging!(info, Type::Service, true, "服务不可用或未运行,需要重装");
true true
} }
@@ -739,7 +790,7 @@ pub async fn check_service_needs_reinstall() -> bool {
/// 尝试使用服务启动core /// 尝试使用服务启动core
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> { pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)"); log::info!(target:"app", "Attempting to start core with existing service (IPC)");
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心"); // logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
let clash_core = Config::verge().latest().get_valid_clash_core(); let clash_core = Config::verge().latest().get_valid_clash_core();
@@ -782,8 +833,16 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
); */ ); */
if !response.success { if !response.success {
let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string()); let err_msg = response
logging!(error, Type::Service, true, "启动核心失败: {}", err_msg); .error
.unwrap_or_else(|| "Failed to start core".to_string());
logging!(
error,
Type::Service,
true,
"Failed to start core: {}",
err_msg
);
bail!(err_msg); bail!(err_msg);
} }
@@ -794,128 +853,140 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
let msg = data let msg = data
.get("msg") .get("msg")
.and_then(|m| m.as_str()) .and_then(|m| m.as_str())
.unwrap_or("未知错误"); .unwrap_or("Unknown error");
if code_value != 0 { if code_value != 0 {
logging!( logging!(
error, error,
Type::Service, Type::Service,
true, true,
"启动核心返回错误: code={}, msg={}", "Start core returned error: code={}, msg={}",
code_value, code_value,
msg msg
); );
bail!("启动核心失败: {}", msg); bail!("Failed to start core: {}", msg);
} }
} }
} }
logging!(info, Type::Service, true, "服务成功启动核心"); logging!(
info,
Type::Service,
true,
"Service successfully started core"
);
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e); logging!(
bail!("无法连接到Clash Verge Service: {}", e) error,
Type::Service,
true,
"Failed to start core via IPC: {}",
e
);
bail!("Unable to connect to Koala Clash Service: {}", e)
} }
} }
} }
// 以服务启动core // 以服务启动core
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
log::info!(target: "app", "正在尝试通过服务启动核心"); log::info!(target: "app", "Attempting to start core via service");
// 先检查服务版本,不受冷却期限制 // 先检查服务版本,不受冷却期限制
let version_check = match check_service_version().await { let version_check = match check_service_version().await {
Ok(version) => { Ok(version) => {
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}", log::info!(target: "app", "Detected service version: {version}, required: {REQUIRED_SERVICE_VERSION}");
version, REQUIRED_SERVICE_VERSION);
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() { if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
log::warn!(target: "app", "服务版本不匹配,需要重装"); log::warn!(target: "app", "Service version mismatch, reinstallation required");
false false
} else { } else {
log::info!(target: "app", "服务版本匹配"); log::info!(target: "app", "Service version matches");
true true
} }
} }
Err(err) => { Err(err) => {
log::warn!(target: "app", "无法获取服务版本: {}", err); log::warn!(target: "app", "Failed to get service version: {err}");
false false
} }
}; };
if version_check && is_service_available().await.is_ok() { if version_check && is_service_available().await.is_ok() {
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用"); log::info!(target: "app", "Service is running and version matches, attempting to use it");
return start_with_existing_service(config_file).await; return start_with_existing_service(config_file).await;
} }
if !version_check { if !version_check {
log::info!(target: "app", "服务版本不匹配,尝试重装"); log::info!(target: "app", "Service version mismatch, attempting reinstallation");
let service_state = ServiceState::get(); let service_state = ServiceState::get();
if !service_state.can_reinstall() { if !service_state.can_reinstall() {
log::warn!(target: "app", "由于限制无法重装服务"); log::warn!(target: "app", "Cannot reinstall service due to limitations");
if let Ok(()) = start_with_existing_service(config_file).await { if let Ok(()) = start_with_existing_service(config_file).await {
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务"); log::info!(target: "app", "Service started successfully despite version mismatch");
return Ok(()); return Ok(());
} else { } else {
bail!("服务版本不匹配且无法重装,启动失败"); bail!("Service version mismatch and cannot reinstall; startup failed");
} }
} }
log::info!(target: "app", "开始重装服务"); log::info!(target: "app", "Starting service reinstallation");
if let Err(err) = reinstall_service().await { if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err); log::warn!(target: "app", "Service reinstallation failed: {err}");
log::info!(target: "app", "尝试使用现有服务"); log::info!(target: "app", "Attempting to use existing service");
return start_with_existing_service(config_file).await; return start_with_existing_service(config_file).await;
} }
log::info!(target: "app", "服务重装成功,尝试启动"); log::info!(target: "app", "Service reinstalled successfully, attempting to start");
return start_with_existing_service(config_file).await; return start_with_existing_service(config_file).await;
} }
// 检查服务状态 // Check service status
match check_ipc_service_status().await { match check_ipc_service_status().await {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "服务可用但未运行核心,尝试启动"); log::info!(target: "app", "Service available but core not running, attempting to start");
if let Ok(()) = start_with_existing_service(config_file).await { if let Ok(()) = start_with_existing_service(config_file).await {
return Ok(()); return Ok(());
} }
} }
Err(err) => { Err(err) => {
log::warn!(target: "app", "服务检查失败: {}", err); log::warn!(target: "app", "Service check failed: {err}");
} }
} }
// 服务不可用或启动失败,检查是否需要重装 // Service unavailable or startup failed, check if reinstallation is needed
if check_service_needs_reinstall().await { if check_service_needs_reinstall().await {
log::info!(target: "app", "服务需要重装"); log::info!(target: "app", "Service needs reinstallation");
if let Err(err) = reinstall_service().await { if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err); log::warn!(target: "app", "Service reinstallation failed: {err}");
bail!("Failed to reinstall service: {}", err); bail!("Failed to reinstall service: {}", err);
} }
log::info!(target: "app", "服务重装完成,尝试启动核心"); log::info!(target: "app", "Service reinstallation completed, attempting to start core");
start_with_existing_service(config_file).await start_with_existing_service(config_file).await
} else { } else {
log::warn!(target: "app", "服务不可用且无法重装"); log::warn!(target: "app", "Service unavailable and cannot be reinstalled");
bail!("Service is not available and cannot be reinstalled at this time") bail!("Service is not available and cannot be reinstalled at this time")
} }
} }
/// 通过服务停止core /// 通过服务停止core
pub(super) async fn stop_core_by_service() -> Result<()> { pub(super) async fn stop_core_by_service() -> Result<()> {
logging!(info, Type::Service, true, "通过服务停止核心 (IPC)"); logging!(info, Type::Service, true, "Stopping core via service (IPC)");
let payload = serde_json::json!({}); let payload = serde_json::json!({});
let response = send_ipc_request(IpcCommand::StopClash, payload) let response = send_ipc_request(IpcCommand::StopClash, payload)
.await .await
.context("无法连接到Clash Verge Service")?; .context("Unable to connect to Koala Clash Service")?;
if !response.success { if !response.success {
bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string())); bail!(response
.error
.unwrap_or_else(|| "Failed to stop core".to_string()));
} }
if let Some(data) = &response.data { if let Some(data) = &response.data {
@@ -924,18 +995,18 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
let msg = data let msg = data
.get("msg") .get("msg")
.and_then(|m| m.as_str()) .and_then(|m| m.as_str())
.unwrap_or("未知错误"); .unwrap_or("Unknown error");
if code_value != 0 { if code_value != 0 {
logging!( logging!(
error, error,
Type::Service, Type::Service,
true, true,
"停止核心返回错误: code={}, msg={}", "Stop core returned error: code={}, msg={}",
code_value, code_value,
msg msg
); );
bail!("停止核心失败: {}", msg); bail!("Failed to stop core: {}", msg);
} }
} }
} }
@@ -945,19 +1016,24 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
/// 检查服务是否正在运行 /// 检查服务是否正在运行
pub async fn is_service_available() -> Result<()> { pub async fn is_service_available() -> Result<()> {
logging!(info, Type::Service, true, "开始检查服务是否正在运行"); logging!(
info,
Type::Service,
true,
"Checking whether service is running"
);
match check_ipc_service_status().await { match check_ipc_service_status().await {
Ok(resp) => { Ok(resp) => {
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() { if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
logging!(info, Type::Service, true, "服务正在运行"); logging!(info, Type::Service, true, "Service is running");
Ok(()) Ok(())
} else { } else {
logging!( logging!(
warn, warn,
Type::Service, Type::Service,
true, true,
"服务未正常运行: code={}, msg={}", "Service not running normally: code={}, msg={}",
resp.code, resp.code,
resp.msg resp.msg
); );
@@ -965,7 +1041,13 @@ pub async fn is_service_available() -> Result<()> {
} }
} }
Err(err) => { Err(err) => {
logging!(error, Type::Service, true, "检查服务运行状态失败: {}", err); logging!(
error,
Type::Service,
true,
"Failed to check service running status: {}",
err
);
Err(err) Err(err)
} }
} }
@@ -973,21 +1055,21 @@ pub async fn is_service_available() -> Result<()> {
/// 强制重装服务UI修复按钮 /// 强制重装服务UI修复按钮
pub async fn force_reinstall_service() -> Result<()> { pub async fn force_reinstall_service() -> Result<()> {
log::info!(target: "app", "用户请求强制重装服务"); log::info!(target: "app", "User requested forced service reinstallation");
let service_state = ServiceState::default(); let service_state = ServiceState::default();
service_state.save()?; service_state.save()?;
log::info!(target: "app", "已重置服务状态,开始执行重装"); log::info!(target: "app", "Service state reset, starting reinstallation");
match reinstall_service().await { match reinstall_service().await {
Ok(()) => { Ok(()) => {
log::info!(target: "app", "服务重装成功"); log::info!(target: "app", "Service reinstalled successfully");
Ok(()) Ok(())
} }
Err(err) => { Err(err) => {
log::error!(target: "app", "强制重装服务失败: {}", err); log::error!(target: "app", "Forced service reinstallation failed: {err}");
bail!("强制重装服务失败: {}", err) bail!("Forced service reinstallation failed: {}", err)
} }
} }
} }

View File

@@ -6,9 +6,9 @@ use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
const IPC_SOCKET_NAME: &str = if cfg!(windows) { const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\clash-verge-service" r"\\.\pipe\koala-clash-service"
} else { } else {
"/tmp/clash-verge-service.sock" "/tmp/koala-clash-service.sock"
}; };
// 定义命令类型 // 定义命令类型
@@ -43,7 +43,7 @@ pub struct IpcResponse {
fn derive_secret_key() -> Vec<u8> { fn derive_secret_key() -> Vec<u8> {
// to do // to do
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取 // 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
let unique_app_id = "clash-verge-app-secret-fuck-me-until-daylight"; let unique_app_id = "koala-clash-app-secret-fuck-me-until-daylight";
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(unique_app_id.as_bytes()); hasher.update(unique_app_id.as_bytes());
hasher.finalize().to_vec() hasher.finalize().to_vec()
@@ -85,7 +85,7 @@ fn sign_message(message: &str) -> Result<String> {
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
let secret_key = derive_secret_key(); let secret_key = derive_secret_key();
let mut mac = HmacSha256::new_from_slice(&secret_key).context("HMAC初始化失败")?; let mut mac = HmacSha256::new_from_slice(&secret_key).context("Failed to initialize HMAC")?;
mac.update(message.as_bytes()); mac.update(message.as_bytes());
let result = mac.finalize(); let result = mac.finalize();
@@ -129,14 +129,25 @@ pub async fn send_ipc_request(
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}, winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
}; };
logging!(info, Type::Service, true, "正在连接服务 (Windows)..."); logging!(
info,
Type::Service,
true,
"Connecting to service (Windows)..."
);
let command_type = format!("{:?}", command); let command_type = format!("{:?}", command);
let request = match create_signed_request(command, payload) { let request = match create_signed_request(command, payload) {
Ok(req) => req, Ok(req) => req,
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e); logging!(
error,
Type::Service,
true,
"Failed to create signed request: {}",
e
);
return Err(e); return Err(e);
} }
}; };
@@ -147,8 +158,14 @@ pub async fn send_ipc_request(
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) { let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
Ok(name) => name, Ok(name) => name,
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "创建CString失败: {}", e); logging!(
return Err(anyhow::anyhow!("创建CString失败: {}", e)); error,
Type::Service,
true,
"Failed to create CString: {}",
e
);
return Err(anyhow::anyhow!("Failed to create CString: {}", e));
} }
}; };
@@ -170,64 +187,110 @@ pub async fn send_ipc_request(
error, error,
Type::Service, Type::Service,
true, true,
"连接到服务命名管道失败: {}", "Failed to connect to service named pipe: {}",
error error
); );
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error)); return Err(anyhow::anyhow!("Unable to connect to service named pipe: {}", error));
} }
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) }; let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
logging!(info, Type::Service, true, "服务连接成功 (Windows)"); logging!(
info,
Type::Service,
true,
"Service connection successful (Windows)"
);
let request_bytes = request_json.as_bytes(); let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes(); let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = pipe.write_all(&len_bytes) { if let Err(e) = pipe.write_all(&len_bytes) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e); logging!(
return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); error,
Type::Service,
true,
"Failed to write request length: {}",
e
);
return Err(anyhow::anyhow!("Failed to write request length: {}", e));
} }
if let Err(e) = pipe.write_all(request_bytes) { if let Err(e) = pipe.write_all(request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e); logging!(
return Err(anyhow::anyhow!("写入请求内容失败: {}", e)); error,
Type::Service,
true,
"Failed to write request body: {}",
e
);
return Err(anyhow::anyhow!("Failed to write request body: {}", e));
} }
if let Err(e) = pipe.flush() { if let Err(e) = pipe.flush() {
logging!(error, Type::Service, true, "刷新管道失败: {}", e); logging!(error, Type::Service, true, "Failed to flush pipe: {}", e);
return Err(anyhow::anyhow!("刷新管道失败: {}", e)); return Err(anyhow::anyhow!("Failed to flush pipe: {}", e));
} }
let mut response_len_bytes = [0u8; 4]; let mut response_len_bytes = [0u8; 4];
if let Err(e) = pipe.read_exact(&mut response_len_bytes) { if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
logging!(error, Type::Service, true, "读取响应长度失败: {}", e); logging!(
return Err(anyhow::anyhow!("读取响应长度失败: {}", e)); error,
Type::Service,
true,
"Failed to read response length: {}",
e
);
return Err(anyhow::anyhow!("Failed to read response length: {}", e));
} }
let response_len = u32::from_be_bytes(response_len_bytes) as usize; let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len]; let mut response_bytes = vec![0u8; response_len];
if let Err(e) = pipe.read_exact(&mut response_bytes) { if let Err(e) = pipe.read_exact(&mut response_bytes) {
logging!(error, Type::Service, true, "读取响应内容失败: {}", e); logging!(
return Err(anyhow::anyhow!("读取响应内容失败: {}", e)); error,
Type::Service,
true,
"Failed to read response body: {}",
e
);
return Err(anyhow::anyhow!("Failed to read response body: {}", e));
} }
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) { let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "服务响应解析失败: {}", e); logging!(
return Err(anyhow::anyhow!("解析响应失败: {}", e)); error,
Type::Service,
true,
"Failed to parse service response: {}",
e
);
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
} }
}; };
match verify_response_signature(&response) { match verify_response_signature(&response) {
Ok(valid) => { Ok(valid) => {
if !valid { if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败"); logging!(
bail!("服务响应签名验证失败"); error,
Type::Service,
true,
"Service response signature verification failed"
);
bail!("Service response signature verification failed");
} }
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e); logging!(
error,
Type::Service,
true,
"Error verifying response signature: {}",
e
);
return Err(e); return Err(e);
} }
} }
@@ -236,7 +299,7 @@ pub async fn send_ipc_request(
info, info,
Type::Service, Type::Service,
true, true,
"IPC请求完成: 命令={}, 成功={}", "IPC request completed: command={}, success={}",
command_type, command_type,
response.success response.success
); );
@@ -255,14 +318,14 @@ pub async fn send_ipc_request(
) -> Result<IpcResponse> { ) -> Result<IpcResponse> {
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
logging!(info, Type::Service, true, "正在连接服务 (Unix)..."); logging!(info, Type::Service, true, "Connecting to service (Unix)...");
let command_type = format!("{:?}", command); let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) { let request = match create_signed_request(command, payload) {
Ok(req) => req, Ok(req) => req,
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e); logging!(error, Type::Service, true, "Failed to create signed request: {}", e);
return Err(e); return Err(e);
} }
}; };
@@ -271,12 +334,23 @@ pub async fn send_ipc_request(
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) { let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
Ok(s) => { Ok(s) => {
logging!(info, Type::Service, true, "服务连接成功 (Unix)"); logging!(
info,
Type::Service,
true,
"Service connection successful (Unix)"
);
s s
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e); logging!(
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e)); error,
Type::Service,
true,
"Failed to connect to Unix socket: {}",
e
);
return Err(anyhow::anyhow!("Unable to connect to service Unix socket: {}", e));
} }
}; };
@@ -284,46 +358,58 @@ pub async fn send_ipc_request(
let len_bytes = (request_bytes.len() as u32).to_be_bytes(); let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) { if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e); logging!(error, Type::Service, true, "Failed to write request length: {}", e);
return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); return Err(anyhow::anyhow!("Failed to write request length: {}", e));
} }
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) { if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e); logging!(error, Type::Service, true, "Failed to write request body: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e)); return Err(anyhow::anyhow!("Failed to write request body: {}", e));
} }
let mut response_len_bytes = [0u8; 4]; let mut response_len_bytes = [0u8; 4];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) { if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
logging!(error, Type::Service, true, "读取响应长度失败: {}", e); logging!(error, Type::Service, true, "Failed to read response length: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e)); return Err(anyhow::anyhow!("Failed to read response length: {}", e));
} }
let response_len = u32::from_be_bytes(response_len_bytes) as usize; let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len]; let mut response_bytes = vec![0u8; response_len];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) { if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
logging!(error, Type::Service, true, "读取响应内容失败: {}", e); logging!(error, Type::Service, true, "Failed to read response body: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e)); return Err(anyhow::anyhow!("Failed to read response body: {}", e));
} }
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) { let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,); logging!(
return Err(anyhow::anyhow!("解析响应失败: {}", e)); error,
Type::Service,
true,
"Failed to parse service response: {}",
e,
);
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
} }
}; };
match verify_response_signature(&response) { match verify_response_signature(&response) {
Ok(valid) => { Ok(valid) => {
if !valid { if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败"); logging!(error, Type::Service, true, "Service response signature verification failed");
bail!("服务响应签名验证失败"); bail!("Service response signature verification failed");
} }
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e); logging!(
error,
Type::Service,
true,
"Error verifying response signature: {}",
e
);
return Err(e); return Err(e);
} }
} }
@@ -332,7 +418,7 @@ pub async fn send_ipc_request(
info, info,
Type::Service, Type::Service,
true, true,
"IPC请求完成: 命令={}, 成功={}", "IPC request completed: command={}, success={}",
command_type, command_type,
response.success response.success
); );

View File

@@ -2,25 +2,21 @@
use crate::utils::autostart as startup_shortcut; use crate::utils::autostart as startup_shortcut;
use crate::{ use crate::{
config::{Config, IVerge}, config::{Config, IVerge},
core::handle::Handle, core::{handle::Handle, EventDrivenProxyManager},
logging, logging_error, logging, logging_error,
process::AsyncHandler,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::Result; use anyhow::Result;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::sync::Arc; use std::sync::Arc;
#[cfg(not(target_os = "windows"))]
use sysproxy::{Autoproxy, Sysproxy}; use sysproxy::{Autoproxy, Sysproxy};
use tauri::async_runtime::Mutex as TokioMutex; use tauri::async_runtime::Mutex as TokioMutex;
use tauri_plugin_autostart::ManagerExt; use tauri_plugin_autostart::ManagerExt;
use tokio::time::{sleep, Duration};
pub struct Sysopt { pub struct Sysopt {
update_sysproxy: Arc<TokioMutex<bool>>, update_sysproxy: Arc<TokioMutex<bool>>,
reset_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")] #[cfg(target_os = "windows")]
@@ -47,7 +43,7 @@ fn get_bypass() -> String {
if custom_bypass.is_empty() { if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string() DEFAULT_BYPASS.to_string()
} else if use_default { } else if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass) format!("{DEFAULT_BYPASS},{custom_bypass}")
} else { } else {
custom_bypass custom_bypass
} }
@@ -59,12 +55,15 @@ impl Sysopt {
SYSOPT.get_or_init(|| Sysopt { SYSOPT.get_or_init(|| Sysopt {
update_sysproxy: Arc::new(TokioMutex::new(false)), update_sysproxy: Arc::new(TokioMutex::new(false)),
reset_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<()> { pub fn init_guard_sysproxy(&self) -> Result<()> {
self.guard_proxy(); // 使用事件驱动代理管理器
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_app_started();
log::info!(target: "app", "Event-driven proxy guard enabled");
Ok(()) Ok(())
} }
@@ -101,12 +100,14 @@ impl Sysopt {
}; };
let mut auto = Autoproxy { let mut auto = Autoproxy {
enable: false, enable: false,
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port), url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
}; };
if !sys_enable { if !sys_enable {
sys.set_system_proxy()?; sys.set_system_proxy()?;
auto.set_auto_proxy()?; auto.set_auto_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(()); return Ok(());
} }
@@ -115,6 +116,8 @@ impl Sysopt {
auto.enable = true; auto.enable = true;
sys.set_system_proxy()?; sys.set_system_proxy()?;
auto.set_auto_proxy()?; auto.set_auto_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(()); return Ok(());
} }
@@ -123,13 +126,18 @@ impl Sysopt {
sys.enable = true; sys.enable = true;
auto.set_auto_proxy()?; auto.set_auto_proxy()?;
sys.set_system_proxy()?; sys.set_system_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(()); return Ok(());
} }
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
if !sys_enable { 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 crate::{core::handle::Handle, utils::dirs};
use anyhow::bail; use anyhow::bail;
@@ -169,6 +177,8 @@ impl Sysopt {
bail!("sysproxy exe run failed"); bail!("sysproxy exe run failed");
} }
} }
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
Ok(()) Ok(())
} }
@@ -180,7 +190,16 @@ impl Sysopt {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
{ {
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?; 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", "Failed to get auto proxy config while resetting: {e}, using default config");
Autoproxy {
enable: false,
url: "".to_string(),
}
}
};
sysproxy.enable = false; sysproxy.enable = false;
autoproxy.enable = false; autoproxy.enable = false;
autoproxy.set_auto_proxy()?; autoproxy.set_auto_proxy()?;
@@ -229,14 +248,14 @@ impl Sysopt {
{ {
if is_enable { if is_enable {
if let Err(e) = startup_shortcut::create_shortcut() { if let Err(e) = startup_shortcut::create_shortcut() {
log::error!(target: "app", "创建启动快捷方式失败: {}", e); log::error!(target: "app", "Failed to create startup shortcut: {}", e);
// 如果快捷方式创建失败,回退到原来的方法 // 如果快捷方式创建失败,回退到原来的方法
self.try_original_autostart_method(is_enable); self.try_original_autostart_method(is_enable);
} else { } else {
return Ok(()); return Ok(());
} }
} else if let Err(e) = startup_shortcut::remove_shortcut() { } else if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "删除启动快捷方式失败: {}", e); log::error!(target: "app", "Failed to remove startup shortcut: {}", e);
self.try_original_autostart_method(is_enable); self.try_original_autostart_method(is_enable);
} else { } else {
return Ok(()); return Ok(());
@@ -271,11 +290,11 @@ impl Sysopt {
{ {
match startup_shortcut::is_shortcut_enabled() { match startup_shortcut::is_shortcut_enabled() {
Ok(enabled) => { Ok(enabled) => {
log::info!(target: "app", "快捷方式自启动状态: {}", enabled); log::info!(target: "app", "Shortcut auto-launch state: {}", enabled);
return Ok(enabled); return Ok(enabled);
} }
Err(e) => { Err(e) => {
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e); log::error!(target: "app", "Failed to check shortcut, falling back to original method: {}", e);
} }
} }
} }
@@ -286,176 +305,13 @@ impl Sysopt {
match autostart_manager.is_enabled() { match autostart_manager.is_enabled() {
Ok(status) => { Ok(status) => {
log::info!(target: "app", "Auto launch status: {}", status); log::info!(target: "app", "Auto launch status: {status}");
Ok(status) Ok(status)
} }
Err(e) => { 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)) 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");
}
}
}
}
}
});
}
} }

View File

@@ -73,7 +73,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"已注册的定时任务数量: {}", "Registered timer task count: {}",
timer_map.len() timer_map.len()
); );
@@ -81,7 +81,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"注册了定时任务 - uid={}, interval={}min, task_id={}", "Registered timer task - uid={}, interval={}min, task_id={}",
uid, uid,
task.interval_minutes, task.interval_minutes,
task.task_id task.task_id
@@ -100,7 +100,12 @@ impl Timer {
let uid = item.uid.as_ref()?; let uid = item.uid.as_ref()?;
if interval > 0 && cur_timestamp - updated >= interval * 60 { if interval > 0 && cur_timestamp - updated >= interval * 60 {
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid); logging!(
info,
Type::Timer,
"Profile requires immediate update: uid={}",
uid
);
Some(uid.clone()) Some(uid.clone())
} else { } else {
None None
@@ -116,7 +121,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"需要立即更新的配置数量: {}", "Number of profiles requiring immediate update: {}",
profiles_to_update.len() profiles_to_update.len()
); );
let timer_map = self.timer_map.read(); let timer_map = self.timer_map.read();
@@ -124,7 +129,7 @@ impl Timer {
for uid in profiles_to_update { for uid in profiles_to_update {
if let Some(task) = timer_map.get(&uid) { if let Some(task) = timer_map.get(&uid) {
logging!(info, Type::Timer, "立即执行任务: uid={}", uid); logging!(info, Type::Timer, "Executing task immediately: uid={}", uid);
if let Err(e) = delay_timer.advance_task(task.task_id) { if let Err(e) = delay_timer.advance_task(task.task_id) {
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e); logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
} }
@@ -237,7 +242,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"找到定时更新配置: uid={}, interval={}min", "Found scheduled update config: uid={}, interval={}min",
uid, uid,
interval interval
); );
@@ -251,7 +256,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"生成的定时更新配置数量: {}", "Generated scheduled update config count: {}",
new_map.len() new_map.len()
); );
new_map new_map
@@ -267,7 +272,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"当前 timer_map 大小: {}", "Current timer_map size: {}",
timer_map.len() timer_map.len()
); );
@@ -279,7 +284,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"定时任务间隔变更: uid={}, ={}, ={}", "Timer task interval changed: uid={}, old={}, new={}",
uid, uid,
task.interval_minutes, task.interval_minutes,
interval interval
@@ -288,12 +293,12 @@ impl Timer {
} }
None => { None => {
// Task no longer needed // Task no longer needed
logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid); logging!(debug, Type::Timer, "Timer task removed: uid={}", uid);
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id)); diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
} }
_ => { _ => {
// Task exists with same interval, no change needed // Task exists with same interval, no change needed
logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid); logging!(debug, Type::Timer, "Timer task unchanged: uid={}", uid);
} }
} }
} }
@@ -306,7 +311,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"新增定时任务: uid={}, interval={}min", "Added timer task: uid={}, interval={}min",
uid, uid,
interval interval
); );
@@ -320,7 +325,13 @@ impl Timer {
*self.timer_count.lock() = next_id; *self.timer_count.lock() = next_id;
} }
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len()); logging!(debug, Type::Timer, "Number of scheduled task changes: {}", diff_map.len());
logging!(
debug,
Type::Timer,
"Number of timer task changes: {}",
diff_map.len()
);
diff_map diff_map
} }
@@ -363,13 +374,18 @@ impl Timer {
/// Get next update time for a profile /// Get next update time for a profile
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> { pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid); logging!(info, Type::Timer, "Getting next update time, uid={}", uid);
let timer_map = self.timer_map.read(); let timer_map = self.timer_map.read();
let task = match timer_map.get(uid) { let task = match timer_map.get(uid) {
Some(t) => t, Some(t) => t,
None => { None => {
logging!(warn, Type::Timer, "找不到对应的定时任务uid={}", uid); logging!(
warn,
Type::Timer,
"Corresponding timer task not found, uid={}",
uid
);
return None; return None;
} }
}; };
@@ -380,7 +396,7 @@ impl Timer {
let items = match profiles.get_items() { let items = match profiles.get_items() {
Some(i) => i, Some(i) => i,
None => { None => {
logging!(warn, Type::Timer, "获取配置列表失败"); logging!(warn, Type::Timer, "Failed to get profile list");
return None; return None;
} }
}; };
@@ -388,7 +404,12 @@ impl Timer {
let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) { let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) {
Some(p) => p, Some(p) => p,
None => { None => {
logging!(warn, Type::Timer, "找不到对应的配置uid={}", uid); logging!(
warn,
Type::Timer,
"Corresponding profile not found, uid={}",
uid
);
return None; return None;
} }
}; };
@@ -401,7 +422,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"计算得到下次更新时间: {}, uid={}", "Calculated next update time: {}, uid={}",
next_time, next_time,
uid uid
); );
@@ -410,7 +431,7 @@ impl Timer {
logging!( logging!(
warn, warn,
Type::Timer, Type::Timer,
"更新时间或间隔无效,updated={}, interval={}", "Invalid update time or interval, updated={}, interval={}",
updated, updated,
task.interval_minutes task.interval_minutes
); );
@@ -442,7 +463,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"配置 {} 是否为当前激活配置: {}", "Is profile {} currently active: {}",
uid, uid,
is_current is_current
); );

View File

@@ -3,7 +3,6 @@ use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod speed_rate; pub mod speed_rate;
use crate::{ use crate::{
cmd,
config::Config, config::Config,
feat, logging, feat, logging,
module::{lightweight::is_in_lightweight_mode, mihomo::Rate}, module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
@@ -12,15 +11,7 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
#[cfg(target_os = "macos")]
use futures::StreamExt;
use parking_lot::Mutex; 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::{ use std::{
fs, fs,
sync::atomic::{AtomicBool, Ordering}, sync::atomic::{AtomicBool, Ordering},
@@ -31,20 +22,37 @@ use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconEvent}, tray::{MouseButton, MouseButtonState, TrayIconEvent},
AppHandle, Wry, AppHandle, Wry,
}; };
#[cfg(target_os = "macos")]
use tokio::sync::broadcast;
use super::handle; use super::handle;
#[derive(Clone)] #[derive(Clone)]
struct TrayState {} 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", "Tray click ignored by debounce; time since last click: {:?}ms",
now.duration_since(*last_click).as_millis());
false
}
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub struct Tray { 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>>, last_menu_update: Mutex<Option<Instant>>,
menu_updating: AtomicBool, menu_updating: AtomicBool,
} }
@@ -105,7 +113,7 @@ impl TrayState {
if tray_icon_colorful == "monochrome" { if tray_icon_colorful == "monochrome" {
( (
false, false,
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(), include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(),
) )
} else { } else {
( (
@@ -139,7 +147,7 @@ impl TrayState {
if tray_icon_colorful == "monochrome" { if tray_icon_colorful == "monochrome" {
( (
false, false,
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(), include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(),
) )
} else { } else {
( (
@@ -164,10 +172,6 @@ impl Tray {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
return TRAY.get_or_init(|| Tray { 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), last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false), menu_updating: AtomicBool::new(false),
}); });
@@ -180,11 +184,6 @@ impl Tray {
} }
pub fn init(&self) -> Result<()> { pub fn init(&self) -> Result<()> {
#[cfg(target_os = "macos")]
{
let mut speed_rate = self.speed_rate.lock();
*speed_rate = Some(SpeedRate::new());
}
Ok(()) Ok(())
} }
@@ -231,7 +230,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在"); log::warn!(target: "app", "Failed to update tray menu: app_handle not found");
return Ok(()); return Ok(());
} }
}; };
@@ -279,11 +278,11 @@ impl Tray {
profile_uid_and_name, profile_uid_and_name,
is_lightweight_mode, is_lightweight_mode,
)?)); )?));
log::debug!(target: "app", "托盘菜单更新成功"); log::debug!(target: "app", "Tray menu updated successfully");
Ok(()) Ok(())
} }
None => { None => {
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在"); log::warn!(target: "app", "Failed to update tray menu: tray not found");
Ok(()) Ok(())
} }
} }
@@ -291,11 +290,11 @@ impl Tray {
/// 更新托盘图标 /// 更新托盘图标
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> { pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在"); log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
return Ok(()); return Ok(());
} }
}; };
@@ -303,7 +302,7 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") { let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray, Some(tray) => tray,
None => { None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在"); log::warn!(target: "app", "Failed to update tray icon: tray not found");
return Ok(()); return Ok(());
} }
}; };
@@ -312,55 +311,18 @@ impl Tray {
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); 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 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, true) => TrayState::get_tun_tray_icon(),
(true, false) => TrayState::get_sysproxy_tray_icon(), (true, false) => TrayState::get_sysproxy_tray_icon(),
(false, true) => TrayState::get_tun_tray_icon(), (false, true) => TrayState::get_tun_tray_icon(),
(false, false) => TrayState::get_common_tray_icon(), (false, false) => TrayState::get_common_tray_icon(),
}; };
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 colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let is_colorful = colorful == "colorful"; 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(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); let _ = tray.set_icon_as_template(!is_colorful);
let _ = tray.set_icon_as_template(!is_colorful);
return Ok(());
}
let rate = if let Some(rate) = rate {
Some(rate)
} else {
let guard = self.speed_rate.lock();
if let Some(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);
}
}
Ok(()) Ok(())
} }
@@ -369,7 +331,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在"); log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
return Ok(()); return Ok(());
} }
}; };
@@ -377,7 +339,7 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") { let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray, Some(tray) => tray,
None => { None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在"); log::warn!(target: "app", "Failed to update tray icon: tray not found");
return Ok(()); return Ok(());
} }
}; };
@@ -413,7 +375,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "更新托盘提示失败: app_handle不存在"); log::warn!(target: "app", "Failed to update tray tooltip: app_handle not found");
return Ok(()); return Ok(());
} }
}; };
@@ -421,7 +383,7 @@ impl Tray {
let version = match VERSION.get() { let version = match VERSION.get() {
Some(v) => v, Some(v) => v,
None => { None => {
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在"); log::warn!(target: "app", "Failed to update tray tooltip: version info not found");
return Ok(()); return Ok(());
} }
}; };
@@ -451,7 +413,7 @@ impl Tray {
if let Some(tray) = app_handle.tray_by_id("main") { if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&format!( let _ = tray.set_tooltip(Some(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", "Koala Clash {version}\n{}: {}\n{}: {}\n{}: {}",
t("SysProxy"), t("SysProxy"),
switch_map[system_proxy], switch_map[system_proxy],
t("TUN"), t("TUN"),
@@ -460,7 +422,7 @@ impl Tray {
current_profile_name current_profile_name
))); )));
} else { } else {
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在"); log::warn!(target: "app", "Failed to update tray tooltip: tray not found");
} }
Ok(()) Ok(())
@@ -475,158 +437,12 @@ impl Tray {
Ok(()) 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");
Ok(())
}
/// 取消订阅 traffic 数据 /// 取消订阅 traffic 数据
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn unsubscribe_traffic(&self) { 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 create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
log::info!(target: "app", "正在从AppHandle创建系统托盘"); log::info!(target: "app", "Creating system tray from AppHandle");
// 获取图标 // 获取图标
let icon_bytes = TrayState::get_common_tray_icon().1; let icon_bytes = TrayState::get_common_tray_icon().1;
@@ -656,7 +472,7 @@ impl Tray {
tray.on_tray_icon_event(|_, event| { tray.on_tray_icon_event(|_, event| {
let tray_event = { Config::verge().latest().tray_event.clone() }; let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into()); 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 { if let TrayIconEvent::Click {
button: MouseButton::Left, button: MouseButton::Left,
@@ -664,25 +480,30 @@ impl Tray {
.. ..
} = event } = event
{ {
// 添加防抖检查,防止快速连击
if !should_handle_tray_click() {
return;
}
match tray_event.as_str() { match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None), "tun_mode" => feat::toggle_tun_mode(None),
"main_window" => { "main_window" => {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray点击事件: 显示主窗口"); log::info!(target: "app", "Tray click: show main window");
if crate::module::lightweight::is_in_lightweight_mode() { if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式"); log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode(); crate::module::lightweight::exit_lightweight_mode();
} }
let result = WindowManager::show_main_window(); let result = WindowManager::show_main_window();
log::info!(target: "app", "窗口显示结果: {:?}", result); log::info!(target: "app", "Window show result: {result:?}");
} }
_ => {} _ => {}
} }
} }
}); });
tray.on_menu_event(on_menu_event); tray.on_menu_event(on_menu_event);
log::info!(target: "app", "系统托盘创建成功"); log::info!(target: "app", "System tray created successfully");
Ok(()) Ok(())
} }
@@ -736,7 +557,7 @@ fn create_tray_menu(
.is_current_profile_index(profile_uid.to_string()); .is_current_profile_index(profile_uid.to_string());
CheckMenuItem::with_id( CheckMenuItem::with_id(
app_handle, app_handle,
format!("profiles_{}", profile_uid), format!("profiles_{profile_uid}"),
t(profile_name), t(profile_name),
true, true,
is_current_profile, is_current_profile,
@@ -779,16 +600,6 @@ fn create_tray_menu(
) )
.unwrap(); .unwrap();
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
t("Direct Mode"),
true,
mode == "direct",
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
)
.unwrap();
let profiles = &Submenu::with_id_and_items( let profiles = &Submenu::with_id_and_items(
app_handle, app_handle,
"profiles", "profiles",
@@ -828,45 +639,6 @@ fn create_tray_menu(
) )
.unwrap(); .unwrap();
let copy_env =
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
t("Conf Dir"),
true,
None::<&str>,
)
.unwrap();
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
t("Core Dir"),
true,
None::<&str>,
)
.unwrap();
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
t("Logs Dir"),
true,
None::<&str>,
)
.unwrap();
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
t("Open Dir"),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
let restart_clash = &MenuItem::with_id( let restart_clash = &MenuItem::with_id(
app_handle, app_handle,
"restart_clash", "restart_clash",
@@ -914,7 +686,6 @@ fn create_tray_menu(
separator, separator,
rule_mode, rule_mode,
global_mode, global_mode,
direct_mode,
separator, separator,
profiles, profiles,
separator, separator,
@@ -922,8 +693,6 @@ fn create_tray_menu(
tun_mode, tun_mode,
separator, separator,
lighteweight_mode, lighteweight_mode,
copy_env,
open_dir,
more, more,
separator, separator,
quit, quit,
@@ -948,15 +717,18 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
} }
"open_window" => { "open_window" => {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "托盘菜单点击: 打开窗口"); log::info!(target: "app", "Tray menu click: open window");
// 如果在轻量模式中,先退出轻量模式
if !should_handle_tray_click() {
return;
}
if crate::module::lightweight::is_in_lightweight_mode() { if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出"); log::info!(target: "app", "Currently in lightweight mode, exiting");
crate::module::lightweight::exit_lightweight_mode(); crate::module::lightweight::exit_lightweight_mode();
} }
// 使用统一的窗口管理器显示窗口
let result = WindowManager::show_main_window(); let result = WindowManager::show_main_window();
log::info!(target: "app", "窗口显示结果: {:?}", result); log::info!(target: "app", "Window show result: {result:?}");
} }
"system_proxy" => { "system_proxy" => {
feat::toggle_system_proxy(); feat::toggle_system_proxy();
@@ -964,20 +736,13 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
"tun_mode" => { "tun_mode" => {
feat::toggle_tun_mode(None); feat::toggle_tun_mode(None);
} }
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => {
let _ = cmd::open_app_dir();
}
"open_core_dir" => {
let _ = cmd::open_core_dir();
}
"open_logs_dir" => {
let _ = cmd::open_logs_dir();
}
"restart_clash" => feat::restart_clash_core(), "restart_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(), "restart_app" => feat::restart_app(),
"entry_lightweight_mode" => { "entry_lightweight_mode" => {
// 处理轻量模式的切换 if !should_handle_tray_click() {
return;
}
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode(); let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
if was_lightweight { if was_lightweight {
crate::module::lightweight::exit_lightweight_mode(); crate::module::lightweight::exit_lightweight_mode();
@@ -985,11 +750,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
crate::module::lightweight::entry_lightweight_mode(); crate::module::lightweight::entry_lightweight_mode();
} }
// 退出轻量模式后显示主窗口
if was_lightweight { if was_lightweight {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
let result = WindowManager::show_main_window(); let result = WindowManager::show_main_window();
log::info!(target: "app", "退出轻量模式后显示主窗口: {:?}", result); log::info!(target: "app", "Show main window after exiting lightweight mode: {result:?}");
} }
} }
"quit" => { "quit" => {
@@ -1002,8 +766,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
_ => {} _ => {}
} }
// 统一调用状态更新
if let Err(e) = Tray::global().update_all_states() { if let Err(e) = Tray::global().update_all_states() {
log::warn!(target: "app", "更新托盘状态失败: {}", e); log::warn!(target: "app", "Failed to update tray state: {e}");
} }
} }

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)
}
}

View File

@@ -108,8 +108,8 @@ impl ChainSupport {
(self, core.as_str()), (self, core.as_str()),
(ChainSupport::All, _) (ChainSupport::All, _)
| (ChainSupport::Clash, "clash") | (ChainSupport::Clash, "clash")
| (ChainSupport::ClashMeta, "verge-mihomo") | (ChainSupport::ClashMeta, "koala-mihomo")
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha") | (ChainSupport::ClashMetaAlpha, "koala-mihomo-alpha")
), ),
None => true, None => true,
} }

View File

@@ -202,7 +202,9 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
}); });
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new()); let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
for (key, value) in patch_tun.into_iter() { for (key, value) in patch_tun.into_iter() {
tun.insert(key, value); if !tun.contains_key(&key) {
tun.insert(key, value);
}
} }
config.insert("tun".into(), tun.into()); config.insert("tun".into(), tun.into());
} else { } else {
@@ -239,7 +241,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.filter(|(s, _)| s.is_support(clash_core.as_ref())) .filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c) .map(|(_, c)| c)
.for_each(|item| { .for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid); log::debug!(target: "app", "run builtin script {0}", item.uid);
if let ChainType::Script(script) = item.data { if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned(), "".to_string()) { match use_script(script, config.to_owned(), "".to_string()) {
Ok((res_config, _)) => { Ok((res_config, _)) => {

View File

@@ -141,8 +141,8 @@ fn test_script() {
fn test_escape_unescape() { fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#; let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string_for_single_quote(test_string); let escaped = escape_js_string_for_single_quote(test_string);
println!("Original: {}", test_string); println!("Original: {test_string}");
println!("Escaped: {}", escaped); println!("Escaped: {escaped}");
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#; let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
let parsed = parse_json_safely(json_str).unwrap(); let parsed = parse_json_safely(json_str).unwrap();

View File

@@ -60,7 +60,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
crate::utils::resolve::restore_public_dns().await; crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await; crate::utils::resolve::set_public_dns("8.8.8.8".to_string()).await;
} }
} }

View File

@@ -11,7 +11,7 @@ use std::fs;
/// Create a backup and upload to WebDAV /// Create a backup and upload to WebDAV
pub async fn create_backup_and_upload_webdav() -> Result<()> { pub async fn create_backup_and_upload_webdav() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| { 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 err
})?; })?;
@@ -19,12 +19,12 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
.upload(temp_file_path.clone(), file_name) .upload(temp_file_path.clone(), file_name)
.await .await
{ {
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err); log::error!(target: "app", "Failed to upload to WebDAV: {err:#?}");
return Err(err); return Err(err);
} }
if let Err(err) = std::fs::remove_file(&temp_file_path) { 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(()) Ok(())
@@ -33,7 +33,7 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
/// List WebDAV backups /// List WebDAV backups
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> { pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
backup::WebDavClient::global().list().await.map_err(|err| { 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 err
}) })
} }
@@ -44,7 +44,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
.delete(filename) .delete(filename)
.await .await
.map_err(|err| { .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 err
}) })
} }
@@ -62,7 +62,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
.download(filename, backup_storage_path.clone()) .download(filename, backup_storage_path.clone())
.await .await
.map_err(|err| { .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 err
})?; })?;

View File

@@ -49,7 +49,7 @@ fn after_change_clash_mode() {
} }
} }
Err(err) => { Err(err) => {
log::error!(target: "app", "Failed to get connections: {}", err); log::error!(target: "app", "Failed to get connections: {err}");
} }
} }
}); });
@@ -108,12 +108,12 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let start = Instant::now(); let start = Instant::now();
let response = NetworkManager::global() let response = NetworkManager::global()
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false) .get_with_interrupt(&url, proxy_type, Some(10), user_agent, false, false)
.await; .await;
match response { match response {
Ok(response) => { Ok(response) => {
log::trace!(target: "app", "test_delay response: {:#?}", response); log::trace!(target: "app", "test_delay response: {response:#?}");
if response.status().is_success() { if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32) Ok(start.elapsed().as_millis() as u32)
} else { } else {
@@ -121,7 +121,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
} }
} }
Err(err) => { Err(err) => {
log::trace!(target: "app", "test_delay error: {:#?}", err); log::trace!(target: "app", "test_delay error: {err:#?}");
Err(err) Err(err)
} }
} }

View File

@@ -31,7 +31,13 @@ pub async fn update_profile(
option: Option<PrfOption>, option: Option<PrfOption>,
auto_refresh: Option<bool>, auto_refresh: Option<bool>,
) -> Result<()> { ) -> Result<()> {
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid); logging!(
info,
Type::Config,
true,
"[Subscription Update] Start updating subscription {}",
uid
);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性 let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性
let url_opt = { let url_opt = {
@@ -41,14 +47,14 @@ pub async fn update_profile(
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote { if !is_remote {
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid); log::info!(target: "app", "[Subscription Update] {uid} is not a remote subscription, skipping update");
None // 非远程订阅直接更新 None // 非远程订阅直接更新
} else if item.url.is_none() { } else if item.url.is_none() {
log::warn!(target: "app", "[订阅更新] {} 缺少URL无法更新", uid); log::warn!(target: "app", "[Subscription Update] {uid} is missing URL, cannot update");
bail!("failed to get the profile item url"); bail!("failed to get the profile item url");
} else { } else {
log::info!(target: "app", log::info!(target: "app",
"[订阅更新] {} 是远程订阅,URL: {}", "[Subscription Update] {} is a remote subscription, URL: {}",
uid, uid,
item.url.clone().unwrap() item.url.clone().unwrap()
); );
@@ -58,24 +64,24 @@ pub async fn update_profile(
let should_update = match url_opt { let should_update = match url_opt {
Some((url, opt)) => { Some((url, opt)) => {
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容"); log::info!(target: "app", "[Subscription Update] Start downloading new subscription content");
let merged_opt = PrfOption::merge(opt.clone(), option.clone()); let merged_opt = PrfOption::merge(opt.clone(), option.clone());
// 尝试使用正常设置更新 // 尝试使用正常设置更新
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await { match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => { Ok(item) => {
log::info!(target: "app", "[订阅更新] 更新订阅配置成功"); log::info!(target: "app", "[Subscription Update] Subscription config updated successfully");
let profiles = Config::profiles(); let profiles = Config::profiles();
let mut profiles = profiles.latest(); let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?; profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current(); let is_current = Some(uid.clone()) == profiles.get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current); log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
is_current && auto_refresh is_current && auto_refresh
} }
Err(err) => { Err(err) => {
// 首次更新失败尝试使用Clash代理 // 首次更新失败尝试使用Clash代理
log::warn!(target: "app", "[订阅更新] 正常更新失败: {}尝试使用Clash代理更新", err); log::warn!(target: "app", "[Subscription Update] Normal update failed: {err}, trying to update via Clash proxy");
// 发送通知 // 发送通知
handle::Handle::notice_message("update_retry_with_clash", uid.clone()); handle::Handle::notice_message("update_retry_with_clash", uid.clone());
@@ -92,7 +98,7 @@ pub async fn update_profile(
// 使用Clash代理重试 // 使用Clash代理重试
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await { match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => { Ok(mut item) => {
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功"); log::info!(target: "app", "[Subscription Update] Update via Clash proxy succeeded");
// 恢复原始代理设置到item // 恢复原始代理设置到item
if let Some(option) = item.option.as_mut() { if let Some(option) = item.option.as_mut() {
@@ -112,14 +118,14 @@ pub async fn update_profile(
handle::Handle::notice_message("update_with_clash_proxy", profile_name); handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current(); let is_current = Some(uid.clone()) == profiles.get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current); log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
is_current && auto_refresh is_current && auto_refresh
} }
Err(retry_err) => { Err(retry_err) => {
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err); log::error!(target: "app", "[Subscription Update] Update via Clash proxy still failed: {retry_err}");
handle::Handle::notice_message( handle::Handle::notice_message(
"update_failed_even_with_clash", "update_failed_even_with_clash",
format!("{}", retry_err), format!("{retry_err}"),
); );
return Err(retry_err); return Err(retry_err);
} }
@@ -131,14 +137,30 @@ pub async fn update_profile(
}; };
if should_update { if should_update {
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置"); logging!(
info,
Type::Config,
true,
"[Subscription Update] Update core configuration"
);
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok(_) => { Ok(_) => {
logging!(info, Type::Config, true, "[订阅更新] 更新成功"); logging!(
info,
Type::Config,
true,
"[Subscription Update] Update succeeded"
);
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
Err(err) => { Err(err) => {
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err); logging!(
error,
Type::Config,
true,
"[Subscription Update] Update failed: {}",
err
);
handle::Handle::notice_message("update_failed", format!("{err}")); handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}"); log::error!(target: "app", "{err}");
} }

View File

@@ -22,7 +22,7 @@ pub fn toggle_system_proxy() {
.close_all_connections() .close_all_connections()
.await .await
{ {
log::error!(target: "app", "Failed to close all connections: {}", err); log::error!(target: "app", "Failed to close all connections: {err}");
} }
} }
@@ -75,8 +75,8 @@ pub fn copy_clash_env() {
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) }; let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port); let http_proxy = format!("http://{clash_verge_rev_ip}:{port}");
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port); let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard(); let cliboard = app_handle.clipboard();
let env_type = { Config::verge().latest().env_type.clone() }; let env_type = { Config::verge().latest().env_type.clone() };

View File

@@ -11,29 +11,60 @@ use crate::{
/// Open or close the dashboard window /// Open or close the dashboard window
#[allow(dead_code)] #[allow(dead_code)]
pub fn open_or_close_dashboard() { pub fn open_or_close_dashboard() {
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; use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Attempting to open/close dashboard"); log::info!(target: "app", "Attempting to open/close dashboard (bypass debounce: {bypass_debounce})");
// 检查是否在轻量模式下 // 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce {
log::info!(target: "app", "Hotkey invoked, dispatching window operation to main thread");
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "Executing hotkey window operation on main thread");
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() { if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode(); crate::module::lightweight::exit_lightweight_mode();
log::info!(target: "app", "Creating new window after exiting lightweight mode"); log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window(); let result = WindowManager::show_main_window();
log::info!(target: "app", "Window operation result: {:?}", result); log::info!(target: "app", "Window operation result: {result:?}");
return; return;
} }
// 使用统一的窗口管理器切换窗口状态
let result = WindowManager::toggle_main_window(); let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {:?}", result); log::info!(target: "app", "Window toggle result: {result:?}");
} }
/// 异步优化的应用退出函数 /// 异步优化的应用退出函数
pub fn quit() { pub fn quit() {
use crate::process::AsyncHandler; use crate::process::AsyncHandler;
logging!(debug, Type::System, true, "启动退出流程"); logging!(debug, Type::System, true, "Start exit process");
// 获取应用句柄并设置退出标志 // 获取应用句柄并设置退出标志
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -42,19 +73,24 @@ pub fn quit() {
// 优先关闭窗口,提供立即反馈 // 优先关闭窗口,提供立即反馈
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window() {
let _ = window.hide(); let _ = window.hide();
log::info!(target: "app", "窗口已隐藏"); log::info!(target: "app", "Window hidden");
} }
// 使用异步任务处理资源清理,避免阻塞 // 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始异步清理资源"); logging!(
info,
Type::System,
true,
"Start asynchronous resource cleanup"
);
let cleanup_result = clean_async().await; let cleanup_result = clean_async().await;
logging!( logging!(
info, info,
Type::System, Type::System,
true, true,
"资源清理完成,退出代码: {}", "Resource cleanup completed, exit code: {}",
if cleanup_result { 0 } else { 1 } if cleanup_result { 0 } else { 1 }
); );
app_handle.exit(if cleanup_result { 0 } else { 1 }); app_handle.exit(if cleanup_result { 0 } else { 1 });
@@ -64,7 +100,12 @@ pub fn quit() {
async fn clean_async() -> bool { async fn clean_async() -> bool {
use tokio::time::{timeout, Duration}; use tokio::time::{timeout, Duration};
logging!(info, Type::System, true, "开始执行异步清理操作..."); logging!(
info,
Type::System,
true,
"Start executing asynchronous cleanup..."
);
// 1. 处理TUN模式 // 1. 处理TUN模式
let tun_task = async { let tun_task = async {
@@ -81,11 +122,11 @@ async fn clean_async() -> bool {
.await .await
{ {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "TUN模式已禁用"); log::info!(target: "app", "TUN mode disabled");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时"); log::warn!(target: "app", "Timeout disabling TUN mode");
false false
} }
} }
@@ -103,11 +144,11 @@ async fn clean_async() -> bool {
.await .await
{ {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "系统代理已重置"); log::info!(target: "app", "System proxy reset");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "重置系统代理超时"); log::warn!(target: "app", "Timeout resetting system proxy");
false false
} }
} }
@@ -117,11 +158,11 @@ async fn clean_async() -> bool {
let core_task = async { let core_task = async {
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await { match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "核心服务已停止"); log::info!(target: "app", "Core service stopped");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "停止核心服务超时"); log::warn!(target: "app", "Timeout stopping core service");
false false
} }
} }
@@ -137,11 +178,11 @@ async fn clean_async() -> bool {
.await .await
{ {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "DNS设置已恢复"); log::info!(target: "app", "DNS settings restored");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "恢复DNS设置超时"); log::warn!(target: "app", "Timeout restoring DNS settings");
false false
} }
} }
@@ -161,7 +202,7 @@ async fn clean_async() -> bool {
info, info,
Type::System, Type::System,
true, true,
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}", "Asynchronous cleanup completed - TUN: {}, Proxy: {}, Core: {}, DNS: {}, Overall: {}",
tun_success, tun_success,
proxy_success, proxy_success,
core_success, core_success,
@@ -178,7 +219,7 @@ pub fn clean() -> bool {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始执行清理操作..."); logging!(info, Type::System, true, "Start executing cleanup...");
// 使用已有的异步清理函数 // 使用已有的异步清理函数
let cleanup_result = clean_async().await; let cleanup_result = clean_async().await;
@@ -189,7 +230,13 @@ pub fn clean() -> bool {
match rx.recv_timeout(std::time::Duration::from_secs(8)) { match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => { Ok(result) => {
logging!(info, Type::System, true, "清理操作完成,结果: {}", result); logging!(
info,
Type::System,
true,
"Cleanup completed, result: {}",
result
);
result result
} }
Err(_) => { Err(_) => {
@@ -197,7 +244,7 @@ pub fn clean() -> bool {
warn, warn,
Type::System, Type::System,
true, true,
"清理操作超时,返回成功状态避免阻塞" "Cleanup timed out, returning success to avoid blocking"
); );
true true
} }

View File

@@ -7,11 +7,7 @@ mod module;
mod process; mod process;
mod state; mod state;
mod utils; mod utils;
use crate::{ use crate::{core::hotkey, process::AsyncHandler, utils::resolve};
core::hotkey,
process::AsyncHandler,
utils::{resolve, resolve::resolve_scheme, server},
};
use config::Config; use config::Config;
use std::sync::{Mutex, Once}; use std::sync::{Mutex, Once};
use tauri::AppHandle; use tauri::AppHandle;
@@ -86,84 +82,75 @@ impl AppHandleManager {
#[allow(clippy::panic)] #[allow(clippy::panic)]
pub fn run() { pub fn run() {
utils::network::NetworkManager::global().init(); // Capture early deep link before any async setup (cold start on macOS)
utils::resolve::capture_early_deep_link_from_args();
utils::network::NetworkManager::global().init();
let _ = utils::dirs::init_portable_flag(); let _ = utils::dirs::init_portable_flag();
// 异步单例检测
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, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
}
}
Err(_) => {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
);
}
}
});
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let devtools = tauri_plugin_devtools::init(); let devtools = tauri_plugin_devtools::init();
#[allow(unused_mut)] #[allow(unused_mut)]
let mut builder = tauri::Builder::default() let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| {
.plugin(tauri_plugin_clipboard_manager::init()) // Handle deep link when a second instance is invoked: forward URL to the running instance
.plugin(tauri_plugin_process::init()) if let Some(url) = argv
.plugin(tauri_plugin_global_shortcut::Builder::new().build()) .iter()
.plugin(tauri_plugin_fs::init()) .find(|a| a.starts_with("clash://") || a.starts_with("koala-clash://"))
.plugin(tauri_plugin_dialog::init()) .cloned()
.plugin(tauri_plugin_shell::init()) {
.plugin(tauri_plugin_deep_link::init()) // Robust scheduling avoids races with lightweight/window
.setup(|app| { resolve::schedule_handle_deep_link(url);
logging!(info, Type::Setup, true, "开始应用初始化..."); }
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); }))
#[cfg(target_os = "macos")] .plugin(tauri_plugin_notification::init())
{ .plugin(tauri_plugin_updater::Builder::new().build())
auto_start_plugin_builder = auto_start_plugin_builder .plugin(tauri_plugin_clipboard_manager::init())
.macos_launcher(MacosLauncher::LaunchAgent) .plugin(tauri_plugin_process::init())
.app_name(app.config().identifier.clone()); .plugin(tauri_plugin_global_shortcut::Builder::new().build())
} .plugin(tauri_plugin_fs::init())
let _ = app.handle().plugin(auto_start_plugin_builder.build()); .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
logging!(info, Type::Setup, true, "Starting app initialization...");
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))] // Register deep link handler as early as possible to not miss cold-start events (macOS)
{ app.deep_link().on_open_url(|event| {
use tauri_plugin_deep_link::DeepLinkExt; let urls: Vec<String> = event.urls().iter().map(|u| u.to_string()).collect();
logging!(info, Type::Setup, true, "注册深层链接..."); logging!(info, Type::Setup, true, "on_open_url received: {:?}", urls);
logging_error!(Type::System, true, app.deep_link().register_all()); if let Some(url) = urls.first().cloned() {
} resolve::schedule_handle_deep_link(url);
}
});
app.deep_link().on_open_url(|event| { let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
AsyncHandler::spawn(move || { #[cfg(target_os = "macos")]
let url = event.urls().first().map(|u| u.to_string()); {
async move { auto_start_plugin_builder = auto_start_plugin_builder
if let Some(url) = url { .macos_launcher(MacosLauncher::LaunchAgent)
logging_error!(Type::Setup, true, resolve_scheme(url).await); .app_name(app.config().identifier.clone());
} }
} let _ = app.handle().plugin(auto_start_plugin_builder.build());
});
}); // Ensure URL schemes are registered with the OS (all platforms)
logging!(info, Type::Setup, true, "Registering deep links with OS...");
logging_error!(Type::System, true, app.deep_link().register_all());
// Deep link handler will be registered AFTER core handle init to ensure window creation works
// 窗口管理 // 窗口管理
logging!(info, Type::Setup, true, "初始化窗口状态管理..."); logging!(
info,
Type::Setup,
true,
"Initializing window state management..."
);
let window_state_plugin = tauri_plugin_window_state::Builder::new() let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json") .with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default()) .with_state_flags(tauri_plugin_window_state::StateFlags::default())
@@ -173,7 +160,12 @@ pub fn run() {
// 异步处理 // 异步处理
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "异步执行应用设置..."); logging!(
info,
Type::Setup,
true,
"Executing app setup asynchronously..."
);
match timeout( match timeout(
Duration::from_secs(30), Duration::from_secs(30),
resolve::resolve_setup_async(&app_handle), resolve::resolve_setup_async(&app_handle),
@@ -181,41 +173,81 @@ pub fn run() {
.await .await
{ {
Ok(_) => { Ok(_) => {
logging!(info, Type::Setup, true, "应用设置成功完成"); logging!(info, Type::Setup, true, "App setup completed successfully");
} }
Err(_) => { Err(_) => {
logging!( logging!(
error, error,
Type::Setup, Type::Setup,
true, true,
"应用设置超时(30秒),继续执行后续流程" "App setup timed out (30s), continuing with subsequent steps"
); );
} }
} }
}); });
logging!(info, Type::Setup, true, "执行主要设置操作..."); logging!(
info,
Type::Setup,
true,
"Executing main setup operations..."
);
logging!(info, Type::Setup, true, "初始化AppHandleManager..."); logging!(info, Type::Setup, true, "Initializing AppHandleManager...");
AppHandleManager::global().init(app.handle().clone()); AppHandleManager::global().init(app.handle().clone());
logging!(info, Type::Setup, true, "初始化核心句柄..."); logging!(info, Type::Setup, true, "Initializing core handle...");
core::handle::Handle::global().init(app.handle()); core::handle::Handle::global().init(app.handle());
logging!(info, Type::Setup, true, "初始化配置..."); logging!(info, Type::Setup, true, "Initializing config...");
if let Err(e) = utils::init::init_config() { if let Err(e) = utils::init::init_config() {
logging!(error, Type::Setup, true, "初始化配置失败: {}", e); logging!(
error,
Type::Setup,
true,
"Failed to initialize config: {}",
e
);
} }
logging!(info, Type::Setup, true, "初始化资源..."); logging!(info, Type::Setup, true, "Initializing resources...");
if let Err(e) = utils::init::init_resources() { if let Err(e) = utils::init::init_resources() {
logging!(error, Type::Setup, true, "初始化资源失败: {}", e); logging!(
error,
Type::Setup,
true,
"Failed to initialize resources: {}",
e
);
} }
app.manage(Mutex::new(state::proxy::CmdProxyState::default())); app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
app.manage(Mutex::new(state::lightweight::LightWeightState::default())); app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
logging!(info, Type::Setup, true, "初始化完成,继续执行"); // If an early deep link was captured from argv, schedule it now (after core and window can be created)
utils::resolve::replay_early_deep_link();
// (deep link handler already registered above)
tauri::async_runtime::spawn(async {
tokio::time::sleep(Duration::from_secs(5)).await;
logging!(
info,
Type::Cmd,
true,
"Running profile updates at startup..."
);
if let Err(e) = crate::cmd::update_profiles_on_startup().await {
log::error!("Failed to update profiles on startup: {e}");
}
});
logging!(
info,
Type::Setup,
true,
"Initialization completed, continuing"
);
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
@@ -294,6 +326,8 @@ pub fn run() {
cmd::read_profile_file, cmd::read_profile_file,
cmd::save_profile_file, cmd::save_profile_file,
cmd::get_next_update_time, cmd::get_next_update_time,
cmd::update_profiles_on_startup,
cmd::create_profile_from_share_link,
// script validation // script validation
cmd::script_validate_notice, cmd::script_validate_notice,
cmd::validate_script_file, cmd::validate_script_file,
@@ -333,7 +367,7 @@ pub fn run() {
app.run(|app_handle, e| match e { app.run(|app_handle, e| match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
logging!(info, Type::System, true, "应用就绪或恢复"); logging!(info, Type::System, true, "App ready or resumed");
AppHandleManager::global().init(app_handle.clone()); AppHandleManager::global().init(app_handle.clone());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
@@ -341,8 +375,8 @@ pub fn run() {
.get_handle() .get_handle()
.get_webview_window("main") .get_webview_window("main")
{ {
logging!(info, Type::Window, true, "设置macOS窗口标题"); logging!(info, Type::Window, true, "Setting macOS window title");
let _ = window.set_title("Clash Verge"); let _ = window.set_title("Koala Clash");
} }
} }
} }
@@ -353,6 +387,10 @@ pub fn run() {
} => { } => {
if !has_visible_windows { if !has_visible_windows {
AppHandleManager::global().set_activation_policy_regular(); AppHandleManager::global().set_activation_policy_regular();
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
} }
AppHandleManager::global().init(app_handle.clone()); AppHandleManager::global().init(app_handle.clone());
} }
@@ -382,7 +420,12 @@ pub fn run() {
if let Some(window) = core::handle::Handle::global().get_window() { if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide(); let _ = window.hide();
} else { } else {
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在"); logging!(
warn,
Type::Window,
true,
"Tried to hide window but it does not exist"
);
} }
} }
tauri::WindowEvent::Focused(true) => { tauri::WindowEvent::Focused(true) => {

View File

@@ -13,11 +13,17 @@ use crate::AppHandleManager;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder; use delay_timer::prelude::TaskBuilder;
use std::sync::Mutex; use std::sync::{
atomic::{AtomicBool, Ordering},
Mutex,
};
use tauri::{Listener, Manager}; use tauri::{Listener, Manager};
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 添加退出轻量模式的锁,防止并发调用
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
fn with_lightweight_status<F, R>(f: F) -> R fn with_lightweight_status<F, R>(f: F) -> R
where where
F: FnOnce(&mut LightWeightState) -> R, F: FnOnce(&mut LightWeightState) -> R,
@@ -30,24 +36,24 @@ where
pub fn run_once_auto_lightweight() { pub fn run_once_auto_lightweight() {
LightWeightState::default().run_once_time(|| { LightWeightState::default().run_once_time(|| {
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true); let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(false);
let enable_auto = Config::verge() let enable_auto = Config::verge()
.data() .data()
.enable_auto_light_weight_mode .enable_auto_light_weight_mode
.unwrap_or(true); .unwrap_or(false);
if enable_auto && is_silent_start { if enable_auto && is_silent_start {
logging!( logging!(
info, info,
Type::Lightweight, Type::Lightweight,
true, true,
"正常创建窗口和添加定时器监听器" "Silent start detected: create window, then attach auto lightweight-mode listener"
); );
set_lightweight_mode(false); set_lightweight_mode(false);
disable_auto_light_weight_mode(); enable_auto_light_weight_mode();
// 触发托盘更新 // 触发托盘更新
if let Err(e) = Tray::global().update_part() { if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {}", e); log::warn!("Failed to update tray: {e}");
} }
} }
}); });
@@ -59,14 +65,19 @@ pub fn auto_lightweight_mode_init() {
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false); let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false); let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
if enable_auto && is_silent_start { if enable_auto && !is_silent_start {
logging!(info, Type::Lightweight, true, "自动轻量模式静默启动"); logging!(
info,
Type::Lightweight,
true,
"Non-silent start: directly attach auto lightweight-mode listener"
);
set_lightweight_mode(true); set_lightweight_mode(true);
enable_auto_light_weight_mode(); enable_auto_light_weight_mode();
// 确保托盘状态更新 // 确保托盘状态更新
if let Err(e) = Tray::global().update_part() { if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {}", e); log::warn!("Failed to update tray: {e}");
} }
} }
} }
@@ -78,39 +89,39 @@ pub fn is_in_lightweight_mode() -> bool {
} }
// 设置轻量模式状态 // 设置轻量模式状态
fn set_lightweight_mode(value: bool) { pub fn set_lightweight_mode(value: bool) {
with_lightweight_status(|state| { with_lightweight_status(|state| {
state.set_lightweight_mode(value); state.set_lightweight_mode(value);
}); });
// 触发托盘更新 // 触发托盘更新
if let Err(e) = Tray::global().update_part() { if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {}", e); log::warn!("Failed to update tray: {e}");
} }
} }
pub fn enable_auto_light_weight_mode() { pub fn enable_auto_light_weight_mode() {
Timer::global().init().unwrap(); Timer::global().init().unwrap();
logging!(info, Type::Lightweight, true, "开启自动轻量模式"); logging!(info, Type::Lightweight, true, "Enable auto lightweight mode");
setup_window_close_listener(); setup_window_close_listener();
setup_webview_focus_listener(); setup_webview_focus_listener();
} }
pub fn disable_auto_light_weight_mode() { pub fn disable_auto_light_weight_mode() {
logging!(info, Type::Lightweight, true, "关闭自动轻量模式"); logging!(info, Type::Lightweight, true, "Disable auto lightweight mode");
let _ = cancel_light_weight_timer(); let _ = cancel_light_weight_timer();
cancel_window_close_listener(); cancel_window_close_listener();
} }
pub fn entry_lightweight_mode() { pub fn entry_lightweight_mode() {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
crate::utils::resolve::reset_ui_ready();
let result = WindowManager::hide_main_window(); let result = WindowManager::hide_main_window();
logging!( logging!(
info, info,
Type::Lightweight, Type::Lightweight,
true, true,
"轻量模式隐藏窗口结果: {:?}", "Lightweight mode window hide result: {:?}",
result result
); );
@@ -120,7 +131,6 @@ pub fn entry_lightweight_mode() {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory(); AppHandleManager::global().set_activation_policy_accessory();
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} }
set_lightweight_mode(true); set_lightweight_mode(true);
let _ = cancel_light_weight_timer(); let _ = cancel_light_weight_timer();
@@ -131,14 +141,32 @@ pub fn entry_lightweight_mode() {
// 添加从轻量模式恢复的函数 // 添加从轻量模式恢复的函数
pub fn exit_lightweight_mode() { pub fn exit_lightweight_mode() {
// 使用原子操作检查是否已经在退出过程中,防止并发调用
if EXITING_LIGHTWEIGHT
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
logging!(
info,
Type::Lightweight,
true,
"Lightweight mode exit already in progress; skipping duplicate call"
);
return;
}
// 使用defer确保无论如何都会重置标志
let _guard = scopeguard::guard((), |_| {
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
});
// 确保当前确实处于轻量模式才执行退出操作 // 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() { if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出"); logging!(info, Type::Lightweight, true, "Not in lightweight mode; skip exit");
return; return;
} }
set_lightweight_mode(false); set_lightweight_mode(false);
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
// macOS激活策略 // macOS激活策略
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -164,7 +192,7 @@ fn setup_window_close_listener() -> u32 {
info, info,
Type::Lightweight, Type::Lightweight,
true, true,
"监听到关闭请求,开始轻量模式计时" "Close requested; starting lightweight-mode timer"
); );
}); });
return handler; return handler;
@@ -179,7 +207,7 @@ fn setup_webview_focus_listener() -> u32 {
logging!( logging!(
info, info,
Type::Lightweight, Type::Lightweight,
"监听到窗口获得焦点,取消轻量模式计时" "Window focused; cancel lightweight-mode timer"
); );
}); });
return handler; return handler;
@@ -190,7 +218,7 @@ fn setup_webview_focus_listener() -> u32 {
fn cancel_window_close_listener() { fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window() {
window.unlisten(setup_window_close_listener()); window.unlisten(setup_window_close_listener());
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听"); logging!(info, Type::Lightweight, true, "Removed window close listener");
} }
} }
@@ -215,7 +243,7 @@ fn setup_light_weight_timer() -> Result<()> {
.set_maximum_parallel_runnable_num(1) .set_maximum_parallel_runnable_num(1)
.set_frequency_once_by_minutes(once_by_minutes) .set_frequency_once_by_minutes(once_by_minutes)
.spawn_async_routine(move || async move { .spawn_async_routine(move || async move {
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式"); logging!(info, Type::Timer, true, "Timer expired; entering lightweight mode");
entry_lightweight_mode(); entry_lightweight_mode();
}) })
.context("failed to create timer task")?; .context("failed to create timer task")?;
@@ -243,7 +271,7 @@ fn setup_light_weight_timer() -> Result<()> {
info, info,
Type::Timer, Type::Timer,
true, true,
"计时器已设置,{} 分钟后将自动进入轻量模式", "Timer set; will auto-enter lightweight mode after {} minute(s)",
once_by_minutes once_by_minutes
); );
@@ -258,7 +286,7 @@ fn cancel_light_weight_timer() -> Result<()> {
delay_timer delay_timer
.remove_task(task.task_id) .remove_task(task.task_id)
.context("failed to remove timer task")?; .context("failed to remove timer task")?;
logging!(info, Type::Timer, true, "计时器已取消"); logging!(info, Type::Timer, true, "Timer canceled");
} }
Ok(()) Ok(())

View File

@@ -4,8 +4,6 @@ use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tauri::http::HeaderMap; use tauri::http::HeaderMap;
#[cfg(target_os = "macos")]
use tauri::http::HeaderValue;
// 缓存的最大有效期5秒 // 缓存的最大有效期5秒
const CACHE_TTL: Duration = Duration::from_secs(5); const CACHE_TTL: Duration = Duration::from_secs(5);
@@ -99,38 +97,12 @@ impl MihomoManager {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap()); headers.insert("Content-Type", "application/json".parse().unwrap());
if let Some(secret) = client.secret { if let Some(secret) = client.secret {
let secret = format!("Bearer {}", secret).parse().unwrap(); let secret = format!("Bearer {secret}").parse().unwrap();
headers.insert("Authorization", secret); headers.insert("Authorization", secret);
} }
Some((server, headers)) Some((server, headers))
} }
// 提供默认值的版本避免在connection_info为None时panic // 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法
#[cfg(target_os = "macos")]
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)
}
} }

View File

@@ -28,9 +28,9 @@ impl LightWeightState {
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self { pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
self.is_lightweight = value; self.is_lightweight = value;
if value { if value {
logging!(info, Type::Lightweight, true, "轻量模式已开启"); logging!(info, Type::Lightweight, true, "Lightweight mode enabled");
} else { } else {
logging!(info, Type::Lightweight, true, "轻量模式已关闭"); logging!(info, Type::Lightweight, true, "Lightweight mode disabled");
} }
self self
} }

View File

@@ -4,12 +4,12 @@ use anyhow::{anyhow, Result};
use log::info; use log::info;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use std::{fs, path::Path, path::PathBuf}; use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
/// Windows 下的开机启动文件夹路径 /// Windows 下的开机启动文件夹路径
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_startup_dir() -> Result<PathBuf> { pub fn get_startup_dir() -> Result<PathBuf> {
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("无法获取 APPDATA 环境变量"))?; let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("Unable to obtain APPDATA environment variable"))?;
let startup_dir = Path::new(&appdata) let startup_dir = Path::new(&appdata)
.join("Microsoft") .join("Microsoft")
@@ -19,7 +19,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
.join("Startup"); .join("Startup");
if !startup_dir.exists() { if !startup_dir.exists() {
return Err(anyhow!("Startup 目录不存在: {:?}", startup_dir)); return Err(anyhow!("Startup directory does not exist: {:?}", startup_dir));
} }
Ok(startup_dir) Ok(startup_dir)
@@ -29,7 +29,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_exe_path() -> Result<PathBuf> { pub fn get_exe_path() -> Result<PathBuf> {
let exe_path = let exe_path =
std::env::current_exe().map_err(|e| anyhow!("无法获取当前可执行文件路径: {}", e))?; std::env::current_exe().map_err(|e| anyhow!("Unable to obtain the path of the current executable file: {}", e))?;
Ok(exe_path) Ok(exe_path)
} }
@@ -39,11 +39,11 @@ pub fn get_exe_path() -> Result<PathBuf> {
pub fn create_shortcut() -> Result<()> { pub fn create_shortcut() -> Result<()> {
let exe_path = get_exe_path()?; let exe_path = get_exe_path()?;
let startup_dir = get_startup_dir()?; let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk"); let shortcut_path = startup_dir.join("Koala-Clash.lnk");
// 如果快捷方式已存在,直接返回成功 // If the shortcut already exists, return success directly
if shortcut_path.exists() { if shortcut_path.exists() {
info!(target: "app", "启动快捷方式已存在"); info!(target: "app", "Startup shortcut already exists");
return Ok(()); return Ok(());
} }
@@ -59,34 +59,36 @@ pub fn create_shortcut() -> Result<()> {
let output = std::process::Command::new("powershell") let output = std::process::Command::new("powershell")
.args(["-Command", &powershell_command]) .args(["-Command", &powershell_command])
// Hide the PowerShell window
.creation_flags(0x08000000) // CREATE_NO_WINDOW
.output() .output()
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?; .map_err(|e| anyhow!("Failed to execute PowerShell command: {}", e))?;
if !output.status.success() { if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr); let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("创建快捷方式失败: {}", error_msg)); return Err(anyhow!("Failed to create shortcut: {}", error_msg));
} }
info!(target: "app", "成功创建启动快捷方式"); info!(target: "app", "Successfully created startup shortcut");
Ok(()) Ok(())
} }
/// 删除快捷方式 /// Remove the shortcut
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn remove_shortcut() -> Result<()> { pub fn remove_shortcut() -> Result<()> {
let startup_dir = get_startup_dir()?; let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk"); let shortcut_path = startup_dir.join("Koala-Clash.lnk");
// 如果快捷方式不存在,直接返回成功 // If the shortcut does not exist, return success directly
if !shortcut_path.exists() { if !shortcut_path.exists() {
info!(target: "app", "启动快捷方式不存在,无需删除"); info!(target: "app", "Startup shortcut does not exist, nothing to remove");
return Ok(()); return Ok(());
} }
// 删除快捷方式 // Delete the shortcut
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?; fs::remove_file(&shortcut_path).map_err(|e| anyhow!("Failed to delete shortcut: {}", e))?;
info!(target: "app", "成功删除启动快捷方式"); info!(target: "app", "Successfully removed startup shortcut");
Ok(()) Ok(())
} }
@@ -94,7 +96,7 @@ pub fn remove_shortcut() -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn is_shortcut_enabled() -> Result<bool> { pub fn is_shortcut_enabled() -> Result<bool> {
let startup_dir = get_startup_dir()?; let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk"); let shortcut_path = startup_dir.join("Koala-Clash.lnk");
Ok(shortcut_path.exists()) Ok(shortcut_path.exists())
} }

View File

@@ -5,14 +5,14 @@ use std::{fs, path::PathBuf};
use tauri::Manager; use tauri::Manager;
#[cfg(not(feature = "verge-dev"))] #[cfg(not(feature = "verge-dev"))]
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev"; pub static APP_ID: &str = "io.github.koala-clash";
#[cfg(not(feature = "verge-dev"))] #[cfg(not(feature = "verge-dev"))]
pub static BACKUP_DIR: &str = "clash-verge-rev-backup"; pub static BACKUP_DIR: &str = "io.github.koala-clash-backup";
#[cfg(feature = "verge-dev")] #[cfg(feature = "verge-dev")]
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev"; pub static APP_ID: &str = "io.github.koala-clash.dev";
#[cfg(feature = "verge-dev")] #[cfg(feature = "verge-dev")]
pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev"; pub static BACKUP_DIR: &str = "io.github.koala-clash-backup-dev";
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new(); pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
@@ -94,7 +94,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
// 如果无法获取系统目录,则回退到可执行文件目录 // 如果无法获取系统目录,则回退到可执行文件目录
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID); 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); return Ok(fallback_dir);
} }
}; };
@@ -102,7 +102,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
match app_handle.path().data_dir() { match app_handle.path().data_dir() {
Ok(dir) => Ok(dir.join(APP_ID)), Ok(dir) => Ok(dir.join(APP_ID)),
Err(e) => { 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")) 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() { match app_handle.path().resource_dir() {
Ok(dir) => Ok(dir.join("resources")), Ok(dir) => Ok(dir.join("resources")),
Err(e) => { 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")) Err(anyhow::anyhow!("Failed to get the resource directory"))
} }
} }
@@ -188,13 +188,13 @@ pub fn profiles_path() -> Result<PathBuf> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn service_path() -> Result<PathBuf> { pub fn service_path() -> Result<PathBuf> {
let res_dir = app_resources_dir()?; let res_dir = app_resources_dir()?;
Ok(res_dir.join("clash-verge-service")) Ok(res_dir.join("koala-clash-service"))
} }
#[cfg(windows)] #[cfg(windows)]
pub fn service_path() -> Result<PathBuf> { pub fn service_path() -> Result<PathBuf> {
let res_dir = app_resources_dir()?; let res_dir = app_resources_dir()?;
Ok(res_dir.join("clash-verge-service.exe")) Ok(res_dir.join("koala-clash-service.exe"))
} }
pub fn service_log_file() -> Result<PathBuf> { pub fn service_log_file() -> Result<PathBuf> {
@@ -203,7 +203,7 @@ pub fn service_log_file() -> Result<PathBuf> {
let log_dir = app_logs_dir()?.join("service"); let log_dir = app_logs_dir()?.join("service");
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string(); 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_file = log_dir.join(log_file);
let _ = std::fs::create_dir_all(&log_dir); let _ = std::fs::create_dir_all(&log_dir);

View File

@@ -125,19 +125,6 @@ pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
Ok(()) 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")] #[cfg(target_os = "linux")]
pub fn linux_elevator() -> String { pub fn linux_elevator() -> String {
use std::process::Command; 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), "0.0B/s");
assert_eq!(format_bytes_speed(1023), "1.0KB/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");
}

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