Compare commits

..
228 Commits
Author SHA1 Message Date
AetherWingandSlinetrac e1d914e61d Chore(i18n): Improve Chinese–English typesetting (#6351)
* chore(i18n): add spacing after "TUN"

* docs: Changelog

* chore(i18n): Improve Chinese–English typesetting

* Apply suggestions from code review

* chore: i18n

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-23 05:17:23 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
6dba62a3b4 chore(deps): lock file maintenance (#6348)
* chore(deps): lock file maintenance

* chore(deps): pnpm update

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-23 01:52:41 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
acab77a1b4 chore(deps): lock file maintenance (#6347)
* chore(deps): lock file maintenance

* chore(deps): update Cargo.toml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-23 01:52:27 +00:00
Slinetrac 1bf445ddcc fix(tun): avoid service IPC startup race on Windows (#6340)
* fix(tun): avoid service IPC startup race on Windows

* docs: Changelog.md

* style: prettier
2026-02-22 13:09:47 +00:00
wonfen 700011688b feat: mask all URLs embedded in an error/log string for safe logging 2026-02-22 07:37:40 +08:00
oomeowandTunglies 0cc9bb2f30 refactor: mihomo ipc (#6312)
* refactor: mihomo ipc

* feat: enable tauri-plugin-mihomo log

* chore: filter tauri ipc modules log

* chore: update tauri-plugin-mihomo dep

* chore: bump tauri-plugin-mihomo version to 0.1.6

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-02-21 18:36:49 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7d29c0c6ee chore(deps): lock file maintenance (#6318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 17:55:26 +00:00
wonfen 321017413d chore: replace node test with cp.cloudflare.com 2026-02-21 14:28:27 +08:00
wonfen 7f045943e2 chore: update changelog 2026-02-21 04:23:03 +08:00
wonfen fa07dfbc9a feat: mask sensitive parts of a subscription URL for safe logging 2026-02-21 04:19:46 +08:00
Tunglies 119aaee546 refactor: optimize profile change notifications to use references 2026-02-20 17:58:04 +08:00
Tunglies 5f573ca2d6 refactor: simplfy backend notify message to frontend (#6323) 2026-02-20 03:18:51 +00:00
Tunglies e5dd127bcc feat: enhance profile update handling with manual trigger 2026-02-20 11:12:18 +08:00
Tunglies 7528c238c4 chore: bump version to 2.4.7 2026-02-20 10:58:32 +08:00
wonfen 10601e873e fix: telegram release link 2026-02-20 05:46:59 +08:00
wonfen d77d655897 Release 2.4.6 2026-02-20 02:58:23 +08:00
wonfen ec6f259794 chore: replace node test with 1.0.0.1 2026-02-19 11:55:09 +08:00
Slinetrac 7a564b4ea9 fix(linux): handle empty KDE proxy schema without error (#6316)
* fix: kde sysproxy error

* docs: Changelog.md
2026-02-15 12:10:34 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> cd4ff68b5b chore(deps): update rust crate zip to v8 (#6314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 03:34:31 +00:00
Slinetrac 1927b5f957 build(linux): bundle linux service binaries from sidecar (#6311)
* refactor: prebuild

* style: prettier
2026-02-15 00:37:41 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 58047cbbd1 chore(deps): update dependency react-error-boundary to v6.1.1 (#6309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 02:06:10 +00:00
wonfen ddf455508f chore: replace node test with 104.16.132.229 2026-02-14 04:41:14 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> afde2f34f4 chore(deps): update dependency @types/react to v19.2.14 (#6305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 11:34:17 +00:00
wonfen 9f6eb46e90 chore: update changelog 2026-02-12 14:55:39 +08:00
wonfen cf9f235270 chore: replace node test with 8888 2026-02-12 14:52:36 +08:00
Jelipo 44851466cf fix: add margin and border radius to group svg image (#6300) 2026-02-12 04:24:30 +00:00
AetherWingandSlinetrac 847a0a6afd fix(tray): resolve tray open log failed (#6301)
* fix(tray): resolve tray open log failed

* fix(ci): allow missing_const_for_fn

* fix(windows): run snapshot log cleanup unconditionally and cover all temp dirs

* refactor: simplify snapshot_path

* fix: clippy

* refactor: delete_snapshot_logs

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-12 03:53:36 +00:00
wonfen 81c56d46c1 perf: optimize IP info card 2026-02-12 08:49:41 +08:00
Sline 31c0910919 feat: Masque (#6303) 2026-02-11 12:38:44 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 87f55cfce7 chore(deps): update dependency node to v24.13.1 (#6299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 06:41:24 +00:00
JingxinXuandSlinetrac bba71aaa4c feat: added clear btn in filter component (#6229) (#6295)
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-11 06:40:47 +00:00
Sline a019b26ceb fix: types (#6298) 2026-02-11 05:48:31 +00:00
wonfen a4617d1fed feat: add clear ingress/egress and data flow indicators for proxy chain 2026-02-11 04:41:46 +08:00
Sline 4d72d2d0df refactor: replace winapi w/ windows (#6291) 2026-02-10 06:06:27 +00:00
許景欣andSlinetrac 277ded4c44 fix: displays rule lineNo instead of index in rule page (#6230) (#6286)
* fix: displays rule lineNo instead of index in rule page (#6230)

* fix(rules): derive line numbers in view model without mutating shared rules

* doc: update changelog

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-10 05:11:14 +00:00
yuanyan3060 5f9096dd6e perf: update_tooltip (#6283) 2026-02-10 03:14:49 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
410b5bd317 chore(deps): lock file maintenance cargo dependencies (#6281)
* chore(deps): lock file maintenance cargo dependencies

* chore(deps): update Cargo.toml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-09 07:37:23 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
a7041657c9 chore(deps): lock file maintenance npm dependencies (#6282)
* chore(deps): lock file maintenance npm dependencies

* chore(deps): run pnpm update

* chore: add parserOptions

* refactor: fix @eslint-react/no-implicit-key

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-09 07:37:07 +00:00
Tunglies 88764d763c refactor(tray): simplify tray event handling and improve menu event processing (#6278)
* refactor(tray): simplify tray event handling and improve menu event processing

* refactor(tray): enhance error handling in tray menu and icon updates

* refactor(tray): enhance tray icon event handling and add debounce for click events

* fix: remove duplicated set tooltip

* refactor(tray): simplify tray icon event handling by removing redundant parameters
2026-02-08 13:05:07 +00:00
Sline 8edfbbb1c6 fix(scheme): auto refresh core config on first URL scheme subscription (#6277)
* fix(scheme): auto refresh core config on first URL scheme subscription

* docs: Changelog.md
2026-02-08 06:34:20 +00:00
Sline 7730cd1c5b refactor: fix eslint no-useless-assignment preserve-caught-error (#6276) 2026-02-08 05:19:41 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> cad1c983e1 chore(deps): update npm dependencies to v10 (#6273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 08:15:04 +00:00
TungliesandSukka 5480e57e67 feat: allow pass user-agent for IP detection (#6272)
* feat: allow pass user-agent when lookup ip API

* Update src/services/api.ts

Co-authored-by: Sukka <isukkaw@gmail.com>

* refactor: optimize user-agent retrieval with memoization

---------

Co-authored-by: Sukka <isukkaw@gmail.com>
2026-02-07 08:11:47 +00:00
Tunglies 5bcb057bf9 style: fix prettier 2026-02-07 16:15:43 +08:00
Tunglies c30eaa3678 refactor: reduce webview lock contention (#6271)
* refactor: replace handle::Handle::get_window() with WindowManager::get_main_window() in multiple files

* refactor: enhance WindowManager to return window state alongside the main window instance

* refactor: update useProfiles and ProfilePage to support profile overrides and improve patchProfilesConfig return type

* refactor: enhance handle_success to check main window existence before notifying profile changes

* refactor: simplify get_main_window_with_state by using pattern matching and improve window state handling

* refactor: fix window activation by removing unnecessary reference in activate_existing_main_window

* refactor: remove redundant macOS conditional for window_manager import
2026-02-07 05:45:15 +00:00
Tunglies 6c6e0812b8 chore: bump tauri-plugin-mihomo to 0.1.4 2026-02-07 08:11:24 +08:00
wonfen afa591c279 chore: replace node test with 1.1.1.1 2026-02-07 04:21:01 +08:00
SukkaandCopilot e9d63aee5e perf(ip-info-card): reduce IP API load even more (#6263)
* refactor(ip-info-card): reduce retry, use succeed as lastFetchTs

* refactor(ip-info-card): stop countdown during revalidation

* perf(ip-info-card): avoid aggressive schedule revalidation

* perf(ip-info-card): try stop interval on window minimized

* perf(ip-info-card): only mutate after card scroll into view once

* perf(ip-info-card): interval only when card has been visible

* chore: add more debug information

* Update src/components/home/ip-info-card.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: reset countdown state after mutate finishes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-06 09:09:36 +00:00
Tunglies 781313e8f0 fix: avoid register logger when enable tracing feature 2026-02-05 17:15:05 +08:00
Tunglies c3f7ff7aa2 feat: add debug-release profile and limit signal runtime thread to one 2026-02-05 13:53:58 +08:00
Tunglies 5397808f16 chore: bump pnpm packages version 2026-02-05 11:17:05 +08:00
Tunglies 279836151c chore: bump tauri version to 2.10.1 2026-02-05 11:13:35 +08:00
AetherWing 6f424ebd2b fix(home-ui): align signal icon delay states with formatDelay (#6249)
* fix(ui): align signal icon delay states with formatDelay

* docs: changelog.md
2026-02-04 12:11:11 +00:00
Tunglies c7f5bc4e0d perf(profiles): optimize item removal by uid in profiles management 2026-02-04 12:57:59 +08:00
SukkaandCopilot 90e193099f refactor(ip-info-card): make ip info card much better (#6226)
* perf(ip-info-card): make ip info card much better

* fix(ip-info-card): remove unused useEffect deps

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor(ip-info-card): use `async-retry`, bail out non-2XX resp

* feat(ip-info-card): add new backend

* feat(ip-info-card): only revalidate when window is visible

* perf(ip-info-card): reduce re-renders when window is hidden

* fix(ip-info-card): remove `mutate` from `useEffect` arg

* Update src/components/home/ip-info-card.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: drop AbortSignal.timeout for old safati compat

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-04 01:40:13 +00:00
Tunglies b3a1fb8d23 fix(macos): update DNS to use 114.114.114.114 for TUN overwrite (#6250) 2026-02-04 01:27:33 +00:00
Tunglies a8e51cc6bb fix: improve subscription URL handling by fixing query parameter parsing 2026-02-03 10:26:50 +08:00
Tunglies 53867bc3a9 fix: enhance error reporting during service installation (#6161)
* fix: enhance error reporting during service installation

* fix: correct variable name in install_service function for clarity

* fix(windows): improve output handling in install_service function
2026-01-31 10:09:50 +00:00
Tunglies ae5d3c478a fix: resolve issue with tray operations after system resume (#6216)
* feat(limiter): add Limiter struct with clock interface and tests

* feat(limiter): integrate Limiter into tray and window management for rate limiting

* fix(tray, window_manager): update debounce timing for tray click and window operations

* refactor(limiter): change time representation from u64 to u128 for improved precision

* fix: resolve issue with tray operations after system resume

* Revert "refactor(limiter): change time representation from u64 to u128 for improved precision"

This reverts commit 2198f40f7f.
2026-01-31 09:23:20 +00:00
Tunglies 654152391b fix: unexpected clippy error 2026-01-31 16:37:51 +08:00
Tunglies 63a77b1c7d fix(macos): prioritize network interfaces for reliable proxy setup 2026-01-30 08:53:42 +08:00
Tunglies 9a0703676b fix: update IPC version, improve service IPC handling 2026-01-29 23:54:58 +08:00
Tunglies 95281632a1 fix(tray): correct spelling of 'TrayMenu' in TrayAction enum and usage 2026-01-29 19:44:24 +08:00
b17dd39f31 feat(tunnels): add tunnels viewer UI with add/delete support (#6052)
* feat(settings): add tunnels viewer UI and management logic

* docs: Changelog.md

* refactor(notice): remove redundant t() call

* refactor(tunnels): make proxy optional and follow current configuration by default

* refactor(tunnels): save after close dialog

* feat(tunnel): update check

* refactor(tunnels): merge import

* refactor(tunnels): use ipaddr.js

* docs: Changelog.md

* refactor(tunnels): enhance validation

* feat: add tunnels ti PATCH_CONFIG_INNER

* fix: sanitize invalid proxy references in tunnels

* refactor: use smartstring alias String

* docs: Changelog.md

* perf: optimize tunnels proxy validation and collection logic

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-01-29 09:32:37 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 1af326cefc chore(deps): update dependency @actions/github to v9 (#6200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 03:17:16 +00:00
Tunglies 5103868119 perf: separate Clone implementation for Draft struct and remove trait bound from struct definition
Improve around 2% - 16% CPU performance
2026-01-28 10:28:26 +08:00
Tunglies c57a962109 refactor: replace useSWR with custom hooks for update and network interfaces (#6195) 2026-01-27 12:52:20 +00:00
Tunglies 36926df26c refactor: remove SWR_REALTIME configuration and simplify SWR usage in AppDataProvider 2026-01-27 20:07:48 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 9d81a13c58 chore(deps): update dependency @actions/github to v8 (#6184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 01:51:06 +00:00
Tunglies 511fab9a9d Revert "perf: improve config processing (#6091)"
This reverts commit bf189bb144.
2026-01-26 23:36:57 +08:00
TungliesandSline 88529af8c8 fix(Linux): use PKEXEC_UID #6159 (#6160)
* fix(Linux): add GID environment variable for Linux service installation #6159

* chore: bump clash_verge_service_ipc to 2.1.2

* chore: remove CLASH_VERGE_SERVICE_GID for linux

---------

Co-authored-by: Sline <realakayuki@gmail.com>
2026-01-26 12:41:45 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
425096e8af chore(deps): lock file maintenance cargo dependencies (#6167)
* chore(deps): lock file maintenance cargo dependencies

* chore: run cargo upgrade and cargo update

* chore: fix clippy

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-26 07:45:36 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
8a4e2327c1 chore(deps): lock file maintenance npm dependencies (#6168)
* chore(deps): lock file maintenance npm dependencies

* chore: pnpm update

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-26 07:33:53 +00:00
TungliesandSlinetrac 74b1687be9 feat: implement git-hook using cargo make and add Makefile.toml (#5498)
* feat: implement pre-push checks using cargo make and add Makefile.toml for task management

* feat: enhance Makefile.toml with condition checks for tasks and improve clippy args

* fix: update file patterns for format-check task in Makefile.toml

* feat: update file patterns for eslint and typecheck tasks in Makefile.toml

* feat: refactor Makefile.toml to consolidate Rust tasks and update pre-commit checks

* feat: update Makefile.toml to add i18n-check and lint-staged tasks; modify pre-commit script

* feat: update Makefile.toml to add i18n-check and lint-staged tasks; modify pre-commit script

* refactor: simplify Makefile.toml by removing unused conditions and consolidating dependencies

* feat: update Makefile.toml to define Rust and frontend tasks for pre-commit and pre-push checks

* chore: remove unnecessary tasks

* chore: add windows override

* chore: remove format and format-check

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-26 07:21:02 +00:00
Tunglies 6477dd61c3 perf: reduce various timeout and retry intervals for improved responsiveness to fetch proxy infomation (#6072) 2026-01-25 07:31:34 +00:00
Tunglies 6ded9bdcde doc: changelog 2026-01-25 15:40:58 +08:00
Tunglies 13dc3feb9f perf: migrate fs method to async (#6071)
* perf(profiles): migrate file handling to async and improve error handling

* refactor(profiles): simplify cleanup_orphaned_files and adjust CleanupResult structure
2026-01-25 07:20:12 +00:00
Tunglies c7462716e5 refactor: reduce duplicated separately useSWR (#6153)
* refactor: reduce duplicated seperatlly useSWR

* refactor: streamline useSWR integration and improve error handling
2026-01-25 07:14:45 +00:00
Tunglies bf189bb144 perf: improve config processing (#6091)
* perf: improve config processing

* perf: enhance profile reordering logic and adjust logging level

* perf: add PartialEq derive to PrfSelected and PrfExtra structs for improved comparison

* perf: refactor PrfOption merge logic and streamline update_item method in IProfiles

* perf: simplify current_mapping and profiles_preview methods in IProfiles for improved readability

* perf: optimize filename matching logic in IProfiles by using a static regex
2026-01-25 07:13:38 +00:00
Tunglies 0c6631ebb0 fix(ip-info-card): handle offline state and clashConfig absence in IP info fetching (#6085)
* fix(ip-info-card): handle offline state and clashConfig absence in IP info fetching

* fix: eslint errors
2026-01-25 07:12:17 +00:00
Sline 93e7ac1bce feat(webdav): cache connection status and adjust auto-refresh behavior (#6129) 2026-01-25 06:49:12 +00:00
Sline b921098182 refactor(connections): switch manager table to TanStack column accessors and IConnectionsItem rows (#6083)
* refactor(connection-table): drive column order/visibility/sorting by TanStack Table state

* refactor(connection-table): simplify table data flow and align with built-in API

* refactor(connection-table): let column manager consume TanStack Table columns directly
2026-01-25 06:49:10 +00:00
SlineandTunglies 440f95f617 feat(misc-viewer): optional delay check interval (#6145)
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2026-01-25 06:48:16 +00:00
Tunglies b9667ad349 chore: bump version to 2.4.6 2026-01-25 14:22:22 +08:00
Tunglies 4e7cdbfcc0 Release: 2.4.5 2026-01-25 14:05:57 +08:00
Tunglies 966fd68087 fix(unix): update clash_verge_service_ipc to 2.1.1 to fix directory permissions 2026-01-25 13:35:18 +08:00
Tunglies 334cec3bde fix: update tauri-plugin-mihomo version, improve error handling #6149 2026-01-24 09:19:52 +08:00
Tunglies 6e16133393 ci(Mergify): configuration update (#6152)
Signed-off-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-01-23 14:35:57 +00:00
Tunglies 5e976c2fe1 chore: inline crate clash-verge-types to module for better maintenance (#6142) 2026-01-23 14:00:51 +00:00
DikozImpact d81aa5f233 Ru language fix (#6143)
* Ru language fix

* Update proxies.json

* Update home.json
2026-01-23 07:42:31 +08:00
Tunglies e5fc0de39a ci: downgrade Ubuntu version in autobuild workflow 2026-01-22 22:08:19 +08:00
Tunglies 6c62350cc3 Release: bump version to 2.4.5-rc.2 2026-01-21 21:26:11 +08:00
Tunglies d1649e3017 fix: update service to 2.1.0 and improve service installation for Unix systems (#6114)
* fix: update service to 2.1.0 and improve service installation for Unix systems

* fix: set GID environment variable during service installation on Linux

* Revert "fix: set GID environment variable during service installation on Linux"

This reverts commit 373aec579b.
2026-01-19 14:02:25 +08:00
Slinetrac 2869a35f1e chore(i18n): update backend i18n keys
translated by GPT-5.2.
2026-01-19 12:35:30 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
98f12a9c72 chore(deps): lock file maintenance npm dependencies (#6119)
* chore(deps): lock file maintenance npm dependencies

* chore: run pnpm update

* fix(components): satisfy ESLint destructuring and narrow unknown errors

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-19 12:05:48 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
6dc8a2f232 chore(deps): lock file maintenance cargo dependencies (#6118)
* chore(deps): lock file maintenance cargo dependencies

* chore: update Cargo.toml

* refactor: use reqwest directly

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-19 12:05:36 +08:00
Tunglies 6511f3868e fix: log IPC path issues conditionally based on tun mode setting 2026-01-18 09:13:22 +08:00
Tunglies 7da5a804f9 chore: bump service top 2.0.30, reduce memory footpoint 2026-01-18 08:43:39 +08:00
wonfen 20ed7a3abe chore: temporarily remove promotion 2026-01-17 03:33:10 +08:00
Sline fd98caccd2 revert: use-app-data (#6088)
* Revert "refactor(app-data): split monolithic context into focused SWR hooks (#5576)"

This reverts commit 8e8182f707.

# Conflicts:
#	src/components/home/clash-info-card.tsx
#	src/components/home/clash-mode-card.tsx
#	src/components/home/current-proxy-card.tsx
#	src/components/home/home-profile-card.tsx
#	src/components/proxy/provider-button.tsx
#	src/components/proxy/proxy-chain.tsx
#	src/components/proxy/proxy-groups.tsx
#	src/components/proxy/use-render-list.ts
#	src/components/rule/provider-button.tsx
#	src/components/setting/mods/sysproxy-viewer.tsx
#	src/hooks/use-clash-data.ts
#	src/hooks/use-current-proxy.ts
#	src/hooks/use-shared-swr-poller.ts
#	src/hooks/use-system-proxy-state.ts
#	src/pages/rules.tsx

* docs: Changelog.md
2026-01-16 18:32:31 +08:00
Tunglies a5f494bda2 fix: ensure external control source settings take effect immediately #6103 2026-01-16 18:27:21 +08:00
Tunglies d4d8ef3849 chore: update Mihomo(Meta) kernel version to v1.19.19 2026-01-16 12:43:12 +08:00
Slinetrac b16cbd5379 feat(backup): restore starts automatically with loading overlay without closing dialog 2026-01-16 12:32:36 +08:00
Tunglies 9e6689ef08 bump: sysproxy-rs version to 0.4.3 2026-01-15 21:25:10 +08:00
Tunglies e0c35c5ee3 fix: unexpected port in use error when change ports 2026-01-15 17:40:06 +08:00
Slinetrac 670055aba1 docs: simplify Linux wording 2026-01-15 14:09:34 +08:00
Slinetrac a780e44e69 docs: add warning to Changelog.md 2026-01-15 13:31:46 +08:00
Tunglies 5c9b46f031 chore: bump version to prerelease 2.4.5-rc.1 2026-01-14 16:55:27 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f5e75d5287 chore(deps): update dependency node to v24.13.0 (#6087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-14 14:40:47 +08:00
Slinetrac c2d8277a1a fix(connections): allow full-width header sorting without triggering on resize 2026-01-14 11:23:50 +08:00
Tunglies 66e98518a7 chore(ci): update autobuild setup for ARM architecture support 2026-01-13 18:42:41 +08:00
Tunglies 089b73bbfd chore(deps): update clash_verge_service_ipc to version 2.0.29 (#6073) 2026-01-13 18:30:54 +08:00
Slinetrac d2c52d09e1 chore(renovate): disable lockfile maintenance automerge 2026-01-12 15:10:52 +08:00
Slinetrac 84143ec761 chore(deps): bump npm deps 2026-01-12 14:50:54 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
f451a26f8c chore(deps): lock file maintenance (#6063)
* chore(deps): lock file maintenance

* chore(deps): update Cargo.toml

* chore(deps): use git repo until the next release

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-12 14:45:33 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> e1220a189b chore(deps): lock file maintenance npm dependencies (#6064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 05:12:01 +00:00
Tunglies 57d4149807 fix(config): improve runtime config fallback handling 2026-01-11 15:27:54 +08:00
Slinetrac 86c3b241b1 docs: Changelog.md 2026-01-10 11:04:55 +08:00
Sline a49000712d feat(tun-viewer): route-exclude-address GUI support (#6053) 2026-01-10 10:50:44 +08:00
歳納七夏 35b2066d4c build(tauri): add libayatana-appindicator3 dependency for linux packages (#6051) 2026-01-10 08:42:57 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 92e0762fc4 chore(deps): update dependency @actions/github to v7 (#6042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 00:27:00 +08:00
Slinetrac 6b8630d357 docs: Changelog.md 2026-01-08 22:44:11 +08:00
Slinetrac a1e77070f0 chore(deps): bump clash-verge-service-ipc to 2.0.29 2026-01-08 22:29:10 +08:00
Slinetrac 6926744ca2 docs: Changelog.md 2026-01-08 14:12:48 +08:00
Slinetrac 13855b9bc2 perf(tun-viewer): run enhanceProfiles in background to avoid save blocking 2026-01-08 14:03:00 +08:00
Slinetrac 1889f18183 feat(notice): override context menu to copy error details 2026-01-07 13:17:56 +08:00
Slinetrac a981be80ef refactor(base): expand barrel exports and standardize imports 2026-01-06 15:02:10 +08:00
Slinetrac 60d3a1927b docs: Changelog.md 2026-01-05 16:18:44 +08:00
Slinetrac 620841592f fix(scheme): prevent double-decoding of subscription URLs #2609
Closes #2609
2026-01-05 15:36:01 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
2128e1f788 chore(deps): lock file maintenance npm dependencies (#6015)
* chore(deps): lock file maintenance npm dependencies

* chore(deps): bump npm deps

* chore: fix eslint ref-name

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-01-05 13:53:22 +08:00
Slinetrac 256a3f697b chore(deps): update Cargo.toml 2026-01-05 13:26:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a701450362 chore(deps): lock file maintenance (#6014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 10:47:37 +08:00
Tunglies 9e4e0c81a4 refactor: clean up and improve code structure (#6010) 2026-01-04 19:21:45 +08:00
oomeow 421bbd090e feat: reconfig log dynamically (#5724) 2026-01-04 16:56:16 +08:00
Tunglies 4adf678480 chore: update Cargo.lock dependencies and increment tauri-plugin-mihomo version to 0.1.3 2026-01-04 16:29:01 +08:00
Slinetrac a9a782d5c9 docs: Changelog.md 2026-01-04 15:05:12 +08:00
Sline ee5e5ee8a6 feat(sysproxy-viewer): add visual editor for bypass list with chips display (#6007) 2026-01-04 14:58:50 +08:00
Slinetrac a940445081 chore(deps): revert tray-icon patch
Patch PR is merged.
2026-01-04 11:47:32 +08:00
Tunglies 65653594c7 chore: allow to update Cargo.lockl from git repo 2026-01-04 06:59:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> ac8f62bea2 chore(deps): update tauri-apps/tauri-action action to v0.6.1 (#6005)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 20:25:57 +00:00
Tunglies eb8ba8b369 refactor(tray): replace Arc<str> with Cow<'static, str> for menu texts and update tray event handling 2026-01-03 19:25:36 +08:00
Slinetrac c18821288e feat(sysproxy-viewer): make proxy bypass validation optional #4012 2026-01-03 17:10:10 +08:00
Slinetrac 7d40410dea chore: update tray-icon patch 2026-01-03 15:44:46 +08:00
AetherWing 349be20a6c chore(windows): remove redundant 3s delay from autostart task (#5996)
* fix(tray): retry tray creation with delay on Windows

* fix(tray): retry tray creation with delay on Windows

* chore(windows): remove redundant 3s delay from autostart task

* chore(windows): remove redundant 3s delay from autostart task
2026-01-02 15:18:01 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 1901a6c97c chore(deps): update dependency globals to v17 (#5997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-02 15:07:31 +08:00
Slinetrac bb72b92ae9 refactor(tray): unify tray icon retrieval into get_tray_icon 2026-01-02 14:28:17 +08:00
Slinetrac 8a1740d38b fix(tray): apply patch for tray-icon #5984
Closes #5984.
2026-01-02 13:54:47 +08:00
Tunglies d75d3bd86e fix(proxy): update system proxy state references to use configState 2026-01-01 17:40:56 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> b277a1e760 chore(deps): update dependency react-i18next to v16.5.1 (#5994)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-01 16:55:15 +08:00
Slinetrac 522eccdd0e refactor(icon): extract icon cache logic into reusable hook 2026-01-01 16:35:36 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
f3b9eedcf7 chore(deps): update rust crate reqwest to 0.13.1 (#5990)
* chore(deps): update rust crate reqwest to 0.13.0

* fix: replace rustls-tls with rustls and update lock file

* fix(webdav): use tauri_plugin_http re-exported reqwest client

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2025-12-31 23:53:49 +08:00
Tunglies 3bbcdbe5ca fix(service): update update clash_verge_service_ipc to 2.0.27
chore(changelog): add IPC permission restrictions for macOS and Linux

refactor(service): remove outdated service version check logic
2025-12-31 21:35:34 +08:00
TungliesandKaguraNaku cceb0a6eb4 fix(permissions): manage umask for sidecar process in CoreManager on Unix
Co-authored-by: KaguraNaku <97681505+KaguraNaku@users.noreply.github.com>
2025-12-31 21:05:05 +08:00
Slinetrac cb5a2e7ce3 perf(proxy-chain): avoid duplicate proxy refresh in chain panel #5855 2025-12-31 16:42:56 +08:00
Slinetrac 609008f087 refactor(uri-parser): split parser into folder-based modules 2025-12-31 14:40:45 +08:00
Slinetrac bae3576e93 fix: update tauri-plugin-mihomo version in pnpm-lock.yaml 2025-12-30 23:58:43 +08:00
Tunglies 9ce343fb45 perf(i18n): update translate function to use Cow to aovid allocate 2025-12-30 18:32:19 +08:00
Tunglies cf08628200 fix(config): update home_cards reference to use as_ref() for improved handling 2025-12-30 18:09:10 +08:00
Tunglies c06c15450f perf(draft): update with_data_modify to use T instead of Box<T> for better performance
Performance improved around 11.7%, avoid to allocate stack and copy to heap.
2025-12-30 17:51:50 +08:00
Slinetrac 772b87e733 feat: add GUI support for AnyTLS/Mieru/Sudoku and AnyTLS URI parsing 2025-12-30 15:15:55 +08:00
Slinetrac c80c659180 fix(dns-viewer): disable spellcheck on DNS viewer text fields #5948
Closes #5948
2025-12-30 13:44:56 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 0cde6cfce9 chore(deps): update dependency react-error-boundary to v6.0.1 (#5969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 10:33:24 +08:00
Slinetrac e6a0369036 fix(ui): hide scrollbars on side menu and hover jump navigator #5945
Closes #5945
2025-12-29 10:26:34 +08:00
Slinetrac ca50e35435 chore(deps): bump deps 2025-12-28 17:55:58 +08:00
Slinetrac 0193ba7bf9 refactor(seq): use if let filter for selector group dedup 2025-12-28 11:05:28 +08:00
Slinetrac c40cdf6b55 fix(seq): prepend newly added proxies to first selector group 2025-12-28 10:55:27 +08:00
Sline a82bcbe86e feat(proxy): auto-append new proxies to first selector group (#5965)
* feat(proxy): auto-append new proxies to first selector group

* docs: Changelog.md
2025-12-28 10:33:10 +08:00
Slinetrac 895e54f7ec fix(config): restrict restart_core_needed mutability to non-Windows to avoid unused-mut 2025-12-28 09:45:54 +08:00
Tunglies c41db51f81 feat: add governor crate for rate limiting and improve window/tray operation handling 2025-12-27 20:27:49 +08:00
Tunglies 2c1303c2bd feat: add bitflags crate and refactor UpdateFlags implementation 2025-12-27 17:09:08 +08:00
SlineandTunglies c8aeae3f83 crate(i18n): add clash-verge-i18n crate and integrate localization support (#5961)
* crate(i18n): add clash-verge-i18n crate and integrate localization support

* refactor(service): remove redundant reinstall_service functions for Windows, Linux, and macOS

* chore(i18n): align i18n key

* feat(i18n): unify scan roots and add backend Rust/YAML support to cleanup script

* chore(i18n): add scripts to package.json

* fix(tray): initialize i18n locale before setup

* refactor(i18n): move locale initialization into Config::init_config

* fix(i18n): refresh systray tooltip on language change and correct docs reference

* fix(tray): remove unnecessary locale synchronization to improve performance

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-12-27 15:03:19 +08:00
Slinetrac 1b477ed0b2 chore: fix typo 2025-12-27 13:56:52 +08:00
Slinetrac 5aba848741 Revert "crate(i18n): add clash-verge-i18n crate and integrate localization support (#5959)"
This reverts commit 593751eda2.
2025-12-27 12:07:56 +08:00
TungliesandSlinetrac 593751eda2 crate(i18n): add clash-verge-i18n crate and integrate localization support (#5959)
* crate(i18n): add clash-verge-i18n crate and integrate localization support

* refactor(service): remove redundant reinstall_service functions for Windows, Linux, and macOS

* chore(i18n): align i18n key

* feat(i18n): unify scan roots and add backend Rust/YAML support to cleanup script

* chore(i18n): add scripts to package.json

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2025-12-27 11:33:58 +08:00
Slinetrac b53f54f3f4 fix(i18n): remove invalid i18n key usage 2025-12-27 09:24:15 +08:00
Tunglies bfb18cf003 refactor(profile): improve error handling for file not found case
refactor(merge): simplify deep_merge function signature
chore: remove unused fmt_bytes function and related tests
chore: clean up help module by removing unused macros
chore: remove format module from utils
2025-12-26 22:15:09 +08:00
Tunglies 9c6f5bc991 fix(profiles): add cleanup of orphaned files and adjust logging level 2025-12-26 17:17:18 +08:00
Slinetrac 63cd4905f9 refactor(notice): extract resolveNoticeMessage helper 2025-12-26 16:42:31 +08:00
歳納七夏 2417d064e1 feat: allow disabling proxy groups in tray icon (#5946)
* feat: allow disabling proxy groups in tray icon

* feat: allow disabling proxy groups in tray icon (update i11n)

* feat: allow disabling proxy groups in tray icon (fix)

* feat: allow disabling proxy groups in tray icon (less nesting)

* feat: allow disabling proxy groups in tray icon (changelog)
2025-12-26 16:29:05 +08:00
Slinetrac d91e19e166 chore: reorganize frontend files 2025-12-26 15:53:25 +08:00
Sline 65b4d8713d refactor: windows autostart (#5941)
* refactor(windows-autostart): switch to scheduled tasks with user/admin split

* fix(schtasks): decode stdout/stderr using Windows OEM/ANSI code pages

* refactor(ui): remove admin auto-launch warning and clean i18n

* feat(windows): user-level auto-launch via task XML

* docs: Changelog.md
2025-12-26 14:42:21 +08:00
Slinetrac a67abda72d refactor(theme): simplify useCustomTheme for Tauri only 2025-12-26 12:25:51 +08:00
Slinetrac 8e27834e35 fix(theme/windows): apply patch to tao to fix Windows theme sync 2025-12-26 12:01:44 +08:00
Slinetrac ee3f7df417 Revert "fix(theme/windows): switch to dark-light based theme detection"
This reverts commit 1c044f053f.
2025-12-26 11:59:13 +08:00
Tunglies f9b8a658a1 perf(draft): optimize memory layout by removing double indirection & implemented optimistic locking via Arc::ptr_eq for with_data_modify (#5942)
* perf(draft): optimize memory layout by removing double indirection

- Replace `Arc<Box<T>>` with `Arc<T>` to reduce pointer chasing and memory overhead.
- Leverage `Arc::from(Box<T>)` in async modify path for efficient ownership transfer.
- Fix race conditions in `edit_draft` by ensuring atomicity under write lock.
- Performance improved by ~16-24% across all operations (based on Criterion bench).

Benchmarks:
- latest_arc:     41.1ns (-24.2%)
- edit_draft:     92.2ns (-17.6%)
- apply:          89.8ns (-17.7%)
- async_modify:   66.0ns (-16.6%)

* perf(draft): implemented optimistic locking via Arc::ptr_eq for with_data_modify

Benchmarks confirm only a negligible ~2% (1.3ns) overhead for async operations, ensuring total data integrity during concurrent updates.
2025-12-25 16:44:23 +08:00
Slinetrac 1c044f053f fix(theme/windows): switch to dark-light based theme detection 2025-12-25 14:43:27 +08:00
Slinetrac 712b8ff19b refactor(linux): move Linux-specific mime.rs and workarounds.rs to utils/linux 2025-12-25 13:18:55 +08:00
Slinetrac 4ab2720ac4 refactor(tray): remove --no-tray code path and env-based tray bypass 2025-12-25 10:42:03 +08:00
Slinetrac af0e72d119 feat(linux): disable WebKit DMABUF renderer on NVIDIA GPUs at startup #5921 2025-12-25 10:34:27 +08:00
Slinetrac bd62a4ecc0 refactor(utils): extract freedesktop mimeapps handling into utils/mime 2025-12-25 10:02:07 +08:00
Tunglies 4ffb8b415f perf(traffic): optimize traffic data handling and improve performance 2025-12-24 13:40:03 +08:00
Slinetrac 0992556b4a feat(update-viewer): support rendering HTML in update markdown #5932
Closes #5932
2025-12-23 20:48:14 +08:00
Tunglies be6b53c760 ci: update Ubuntu sources to use 'noble' release and adjust package installation 2025-12-23 14:52:47 +08:00
Slinetrac 797c0f90aa chore(deps): bump npm deps 2025-12-23 11:59:49 +08:00
Slinetrac d52f00c1b1 feat(ui): add collapse/expand option to navbar context menu #5910
Closes #5910
2025-12-23 11:52:40 +08:00
Slinetrac f26abcd2a9 fix(backup): reload config.yaml and profiles.yaml after restore to avoid stale memory overwrite #5909
Closes #5909
2025-12-23 10:56:31 +08:00
Tunglies 863a80df43 ci: update Ubuntu version to 24.04 and adjust dependencies in autobuild workflow 2025-12-22 23:23:35 +08:00
oomeow 19accbd538 chore: rust related config for workspace (#5912)
* chore: move rust related config to workspace

* chore: cargo fmt
2025-12-22 15:28:55 +08:00
SukkaandSlinetrac 8e48e4ed10 chore(eslint): add eslint-plugin-react-compiler (#5918)
* chore(eslint): add `eslint-plugin-react-compiler`

* refactor: fix eslint warnings and avoid in-render mutations

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2025-12-22 12:51:20 +08:00
Slinetrac eafa08066d docs(notice): add example for passing Error directly with i18n key 2025-12-22 11:01:53 +08:00
Slinetrac 231517b5db chore(monaco): ignore worker errors in console 2025-12-21 23:52:16 +08:00
Slinetrac 45193e017f fix(monaco): Ensure monaco loader uses the bundled ESM instance instead of CDN 2025-12-21 21:38:05 +08:00
Slinetrac 2515deefed Revert "refactor(editor-viewer): simplify loading/save flow with timeout fallback (#5905)"
This reverts commit c84bb91f4a.
2025-12-21 20:28:38 +08:00
Sline c84bb91f4a refactor(editor-viewer): simplify loading/save flow with timeout fallback (#5905) 2025-12-21 17:10:52 +08:00
Tunglies af094bfcd7 refactor: remove port_scanner dependency and simplify port checking logic 2025-12-21 16:51:55 +08:00
Slinetrac a5752f7b00 docs: Changelog.md 2025-12-21 13:16:54 +08:00
Slinetrac 23e551e384 feat(tray): add optional inline outbound modes in tray menu #5881
Closes #5881
2025-12-21 10:33:19 +08:00
Slinetrac a0b12b8797 chore(deps): bump npm deps 2025-12-21 09:43:07 +08:00
Tunglies 16c3dcc616 fix(proxy): check if proxy port is in use #5891 2025-12-20 19:11:27 +08:00
Slinetrac 5afe11e55b fix(monaco): disable MUI Dialog focus enforcement for Monaco editor #5885
Closes #5885
2025-12-20 16:12:45 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 6f61759a39 chore(deps): update rust crate zip to v7 (#5886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 13:54:25 +08:00
CactusandSlinetrac 57b17ab8d3 feat: add navigation collapse functionality to layout (#5815)
* feat(layout): add collapsible navbar toggle in UI settings

* refactor(layout): adjust collapsed navbar styles

* docs: Changelog.md

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2025-12-19 18:15:20 +08:00
Sline fc84dc561c feat(notice): persist toast position preference (#5621)
* feat(notice): persist toast position preference

* docs: Changelog.md
2025-12-19 18:04:23 +08:00
Sline bd8eccdcea feat(backup): add local backup import flow (#5669)
* feat(backup): add local backup import flow

* refactor(backup): robustify history listing and propagate import error details

* docs: Changelog.md
2025-12-19 17:46:31 +08:00
SlineandTunglies b4e25951b4 feat(proxy): unify filter search box and sync filter options (#5819)
* feat(proxy): unify filter search box and sync filter options

* docs: fix JSDoc placement

* refactor(search): unify string matcher and stabilize regex filtering

* fix(search): treat invalid regex as matching nothing

* docs: update changelog to include advanced filter search feature for proxy page

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-12-19 17:12:36 +08:00
zhuo-githubzhuocanhe <zhuocanhe>Tunglies
aa72fa9a42 fix: add DNS hijacking fix for macOS wired network(#5728)
* [bugfix][issues-2701]mac有线网络下dns劫持失败

* [bugfix][issues-2701]mac有线网络下dns劫持失败。删除弃用代码,删除打印端口日志

* fix(changelog): add DNS hijacking fix for macOS wired network

---------

Co-authored-by: zhuocanhe <zhuocanhe>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-12-19 17:05:47 +08:00
Tunglies f0ae631cb0 chore: update version to 2.4.5 and enhance changelog management scripts 2025-12-19 16:51:11 +08:00
Tunglies 787463a226 chore: bump version to 2.4.4 2025-12-19 16:25:24 +08:00
Slinetrac 8d5d72957b fix(home): correct delay sorting for Current Node card #5870
Closes #5870
2025-12-19 14:24:04 +08:00
Slinetrac 7d805cb83e chore(deps): bump npm deps 2025-12-19 10:32:40 +08:00
oomeow a1286ad057 fix: always occupies hotkey globally when app launch silently (#5866)
* fix: always occupies hotkey when app launch silently

* docs: update Changelog.md

* chore: update
2025-12-18 22:21:15 +08:00
oomeow 9713343323 refactor: use sysproxy-rs to set system proxy on windows (#5846)
* refactor: use sysproxy-rs to set system proxy on windows

* fix: remove download sysproxy.exe task

* chore: unified processing system proxy reset

* docs: update Changelog.md
2025-12-18 20:23:29 +08:00
wonfen b282217e5c doc: update promotion 2025-12-18 18:42:18 +08:00
Slinetrac 4b29a140b5 docs: Changelog.md 2025-12-17 16:57:34 +08:00
Slinetrac b6af7b7440 fix(tun): set auto-redirect default to false to improve cross-distro compatibility #5845
Closes #5845
2025-12-17 16:20:06 +08:00
Tunglies bcb8e831c5 fix(runtime): aovid force re-reresh runtime after create proxy chain 2025-12-17 12:52:25 +08:00
Slinetrac a9e6391417 feat(layout/nav): add reset-to-default menu order action 2025-12-17 11:33:31 +08:00
Slinetrac 721929a2a1 refactor(layout/nav): extract nav menu order logic into hook 2025-12-17 11:06:38 +08:00
337 changed files with 14654 additions and 11953 deletions
+3 -3
View File
@@ -307,7 +307,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -377,7 +377,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -505,7 +505,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
+23 -17
View File
@@ -46,7 +46,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -137,7 +137,7 @@ jobs:
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-22.04
- os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
@@ -165,10 +165,19 @@ jobs:
cache-workspace-crates: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
if: matrix.os == 'ubuntu-24.04'
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
sudo apt install \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev
- name: Install x86 OpenSSL (macOS only)
if: matrix.target == 'x86_64-apple-darwin'
@@ -187,7 +196,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
@@ -244,6 +253,8 @@ jobs:
fail-fast: false
matrix:
include:
# It should be ubuntu-22.04 to match the cross-compilation environment
# ortherwise it is hard to resolve the dependencies
- os: ubuntu-22.04
target: aarch64-unknown-linux-gnu
arch: arm64
@@ -283,7 +294,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
@@ -302,8 +313,8 @@ jobs:
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Setup for linux
run: |
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
@@ -322,14 +333,9 @@ jobs:
sudo mv /tmp/sources.list /etc/apt/sources.list
sudo dpkg --add-architecture ${{ matrix.arch }}
sudo apt-get update -y
sudo apt-get -f install -y
sudo apt update
sudo apt-get install -y \
linux-libc-dev:${{ matrix.arch }} \
libc6-dev:${{ matrix.arch }}
sudo apt-get install -y \
sudo apt install -y \
libxslt1.1:${{ matrix.arch }} \
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
libayatana-appindicator3-dev:${{ matrix.arch }} \
@@ -433,7 +439,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
@@ -535,7 +541,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4.2.0
name: Install pnpm
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
+1 -1
View File
@@ -103,7 +103,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
- uses: actions/setup-node@v6
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: "24.12.0"
node-version: "24.13.1"
cache: "pnpm"
- name: Restore pnpm cache
+8 -8
View File
@@ -197,7 +197,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -218,7 +218,7 @@ jobs:
- name: Tauri build
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
uses: tauri-apps/tauri-action@v0.6.0
uses: tauri-apps/tauri-action@v0.6.1
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -281,7 +281,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -420,7 +420,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -448,7 +448,7 @@ jobs:
- name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0.6.0
uses: tauri-apps/tauri-action@v0.6.1
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -505,7 +505,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -531,7 +531,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -593,7 +593,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.12.0"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
+3 -1
View File
@@ -11,4 +11,6 @@ scripts/_env.sh
.idea
.old
.eslintcache
target
.changelog_backups
target
CLAUDE.md
+5 -42
View File
@@ -1,51 +1,14 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
if ! command -v "cargo-make" >/dev/null 2>&1; then
echo "❌ cargo-make is required for pre-commit checks."
cargo install --force cargo-make
fi
if ! command -v pnpm >/dev/null 2>&1; then
echo "❌ pnpm is required for pre-commit checks."
exit 1
fi
LOCALE_DIFF="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/locales/' || true)"
if [ -n "$LOCALE_DIFF" ]; then
echo "[pre-commit] Locale changes detected. Regenerating i18n types..."
pnpm i18n:types
if [ -d src/types/generated ]; then
echo "[pre-commit] Staging regenerated i18n type artifacts..."
git add src/types/generated
fi
fi
echo "[pre-commit] Running pnpm format before lint..."
pnpm format
echo "[pre-commit] Running lint-staged for JS/TS files..."
pnpm exec lint-staged
RUST_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src-tauri/.*\.rs$' || true)"
if [ -n "$RUST_FILES" ]; then
echo "[pre-commit] Formatting Rust changes with cargo fmt..."
cargo fmt
while IFS= read -r file; do
[ -n "$file" ] && git add "$file"
done <<< "$RUST_FILES"
echo "[pre-commit] Linting Rust changes with cargo clippy..."
cargo clippy-all
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
echo "[pre-commit] Installing clash-verge-logging-check..."
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
fi
clash-verge-logging-check
fi
TS_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx)$' || true)"
if [ -n "$TS_FILES" ]; then
echo "[pre-commit] Running TypeScript type check..."
pnpm typecheck
fi
echo "[pre-commit] All checks completed successfully."
cargo make pre-commit
+4 -31
View File
@@ -1,36 +1,9 @@
#!/bin/bash
set -euo pipefail
remote_name="${1:-origin}"
remote_url="${2:-unknown}"
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
if ! command -v pnpm >/dev/null 2>&1; then
echo "❌ pnpm is required for pre-push checks."
exit 1
if ! command -v "cargo-make" >/dev/null 2>&1; then
echo "❌ cargo-make is required for pre-push checks."
cargo install --force cargo-make
fi
echo "[pre-push] Preparing to push to '$remote_name' ($remote_url). Running full validation..."
echo "[pre-push] Checking Prettier formatting..."
pnpm format:check
echo "[pre-push] Running ESLint..."
pnpm lint
echo "[pre-push] Running TypeScript type checking..."
pnpm typecheck
if command -v cargo >/dev/null 2>&1; then
echo "[pre-push] Verifying Rust formatting..."
cargo fmt --check
echo "[pre-push] Running cargo clippy..."
cargo clippy-all
else
echo "[pre-push] ⚠️ cargo not found; skipping Rust checks."
fi
echo "[pre-push] All checks passed."
cargo make pre-push
+5
View File
@@ -0,0 +1,5 @@
queue_rules:
- name: LetMeMergeForYou
batch_size: 3
allow_queue_branch_edit: true
queue_conditions: []
+1
View File
@@ -2,6 +2,7 @@
# Changelog.md
# CONTRIBUTING.md
.changelog_backups
pnpm-lock.yaml
src-tauri/target/
Generated
+1071 -1198
View File
File diff suppressed because it is too large Load Diff
+29 -17
View File
@@ -1,11 +1,12 @@
[workspace]
members = [
"src-tauri",
"crates/clash-verge-draft",
"crates/clash-verge-logging",
"crates/clash-verge-signal",
"crates/tauri-plugin-clash-verge-sysinfo",
"crates/clash-verge-types",
"src-tauri",
"crates/clash-verge-draft",
"crates/clash-verge-logging",
"crates/clash-verge-signal",
"crates/tauri-plugin-clash-verge-sysinfo",
"crates/clash-verge-i18n",
"crates/clash-verge-limiter",
]
resolver = "2"
@@ -39,38 +40,49 @@ opt-level = 0
debug = true
strip = false
[profile.debug-release]
inherits = "fast-release"
codegen-units = 1
split-debuginfo = "unpacked"
[workspace.dependencies]
clash-verge-draft = { path = "crates/clash-verge-draft" }
clash-verge-logging = { path = "crates/clash-verge-logging" }
clash-verge-signal = { path = "crates/clash-verge-signal" }
clash-verge-types = { path = "crates/clash-verge-types" }
clash-verge-i18n = { path = "crates/clash-verge-i18n" }
clash-verge-limiter = { path = "crates/clash-verge-limiter" }
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tauri = { version = "2.9.5" }
tauri = { version = "2.10.2" }
tauri-plugin-clipboard-manager = "2.3.2"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.100"
criterion = { version = "0.7.0", features = ["async_tokio"] }
tokio = { version = "1.48.0", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
anyhow = "1.0.102"
criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.49.0", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
flexi_logger = "0.31.7"
flexi_logger = "0.31.8"
log = "0.4.29"
smartstring = { version = "1.0.1" }
compact_str = { version = "0.9.0", features = ["serde"] }
serde = { version = "1.0.228" }
serde_json = { version = "1.0.145" }
serde_json = { version = "1.0.149" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.11.0" }
# *** For Windows platform only ***
deelevate = "0.2.0"
# *********************************
[patch.crates-io]
# Patches until https://github.com/tauri-apps/tao/pull/1167 is merged.
tao = { git = "https://github.com/tauri-apps/tao" }
[workspace.lints.clippy]
correctness = { level = "deny", priority = -1 }
suspicious = { level = "deny", priority = -1 }
+5 -277
View File
@@ -1,291 +1,19 @@
## v2.4.4
- **Mihomo(Meta) 内核升级至 v1.19.17**
## v2.4.7
### 🐞 修复问题
- Linux 无法切换 TUN 堆栈
- macOS service 启动项显示名称(试验性修改)
- macOS 非预期 Tproxy 端口设置
- 流量图缩放异常
- PAC 自动代理脚本内容无法动态调整
- 兼容从旧版服务模式升级
- Monaco 编辑器的行数上限
- 已删除节点在手动分组中导致配置无法加载
- 仪表盘与托盘状态不同步
- 彻底修复 macOS 连接页面显示异常
- windows 端监听关机信号失败
- 修复代理按钮和高亮状态不同步
- 修复侧边栏可能的未能正确跳转
- 修复解锁测试部分地区图标编码不正确
- 修复 IP 检测切页后强制刷新,改为仅在必要时更新
- 修复在搜索框输入不完整正则直接崩溃
- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
- 修复更新时加载进度条异常
- 升级内核失败导致内核不可用问题
- 修复 macOS 在安装和卸载服务时提示与操作不匹配
- 修复菜单排序模式拖拽异常
- 修复托盘菜单代理组前的异常勾选状态
- 修复 Windows 下自定义标题栏按钮在最小化 / 关闭后 hover 状态残留
- 修复直接覆盖 `config.yaml` 使用时无法展开代理组
- 修复 macOS 下应用启动时系统托盘图标颜色闪烁
- 修复 Windows 管理员身份运行时开关 TUN 模式异常
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 支持连接页面各个项目的排序
- 实现可选的自动备份
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
- 日志页面支持按时间倒序
- 增加「重新激活订阅」的全局快捷键
- WebView2 Runtime 修复构建升级到 133.0.3065.92
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 网络请求改为使用 rustls,提升 TLS 兼容性
- rustls 避免因服务器证书链配置问题或较新 TLS 要求导致订阅无法导入
- 替换前端信息编辑组件,提供更好性能
- 优化后端内存和性能表现
- 防止退出时可能的禁用 TUN 失败
- 全新 i18n 支持方式
- 优化备份设置布局
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
- 性能优化系统状态获取
- 优化托盘菜单当前订阅检测逻辑
- 优化连接页面表格渲染
- 优化链式代理 UI 反馈
- 优化重启应用的资源清理逻辑
- 优化前端数据刷新
- 优化流量采样和数据处理
- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
- 优化前端 WebSocket 连接机制
- 改进旧版 Service 需要重新安装检测流程
- 优化 macOS, Linux 和 Windows 系统信号处理
- 链式代理仅显示 Selector 类型规则组
- 优化订阅错误通知,仅在手动触发时
- 隐藏日志中的订阅信息
- 优化部分界面文案文本
</details>
## v2.4.3
**发行代号:澜**
代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。
特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
- 修复前端无法及时刷新操作状态
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复轻量模式托盘状态不同步
- 修复一键导入订阅导致应用卡死崩溃的问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- **Mihomo(Meta) 内核升级至 v1.19.15**
- 支持前端修改日志(最大文件大小、最大保留数量)
- 新增链式代理图形化设置功能
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
- 监听关机事件,自动关闭系统代理
- 主界面“当前节点”卡片新增“延迟测试”按钮
- 新增批量选择配置文件功能
- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置
- 新增本地备份功能
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
- 允许独立控制订阅自动更新
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
- 重构日志系统,单个日志默认最大 10 MB
- 优化前端资源占用
- 改进 macos 下系统代理设置的方法
- 优化 TUN 模式可用性的判断
- 移除流媒体检测的系统级提示(使用软件内通知)
- 优化后端 i18n 资源占用
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
- 修改内核默认日志级别为 Info
- 支持通过桌面快捷方式重新打开应用
- 支持订阅界面输入链接后回车导入
- 选择按延迟排序时每次延迟测试自动刷新节点顺序
- 配置重载失败时自动重启核心
- 启用 TUN 前等待服务就绪
- 卸载 TUN 时会先关闭
- 优化应用启动页
- 优化首页当前节点对MATCH规则的支持
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
- 添加热键绑定错误的提示信息
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
</details>
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1
### 🏆 重大改进
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.13**
### 🚀 性能优化
- 优化热键响应速度,提升快捷键操作体验
- 改进服务管理响应性,减少系统服务操作等待时间
- 提升文件和配置处理性能
- 优化任务管理和日志记录效率
- 优化异步内存管理,减少内存占用并提升多任务处理效率
- 优化启动阶段初始化性能
### 🐞 修复问题
- 修复应用在某些操作中可能出现的响应延迟问题
- 修复任务管理中的潜在并发问题
- 修复通过托盘重启应用无法恢复
- 修复订阅在某些情况下无法导入
- 修复无法新建订阅时使用远程链接
- 修复卸载服务后的 tun 开关状态问题
- 修复页面快速切换订阅时导致崩溃
- 修复丢失工作目录时无法恢复环境
- 修复从轻量模式恢复导致崩溃
### 👙 界面样式
- 统一代理设置样式
### 🗑️ 移除内容
- 移除启动阶段自动清理过期订阅
## v2.4.0
**发行代号:融**
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
### 🏆 重大改进
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.12**
- 新增版本信息复制按钮
- 增强型流量监控,支持更详细的数据分析
- 新增流量图表多种显示模式
- 新增强制刷新配置和节点缓存功能
- 首页流量统计支持查看刻度线详情
### 🚀 性能优化
- 全面提升数据传输和处理效率
- 优化内存使用,减少系统资源消耗
- 改进流量图表渲染性能
- 优化配置和节点刷新策略,从5秒延长到60秒
- 改进数据缓存机制,减少重复请求
- 优化异步程序性能
### 🐞 修复问题
- 修复系统代理状态检测和显示不一致问题
- 修复系统主题窗口颜色不一致问题
- 修复特殊字符 URL 处理问题
- 修复配置修改后缓存不同步问题
- 修复 Windows 安装器自启设置问题
- 修复 macOS 下 Dock 图标恢复窗口问题
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
- 修复架构升级后节点测速功能异常
- 修复架构升级后流量统计功能异常
- 修复架构升级后日志功能异常
- 修复外部控制器跨域配置保存问题
- 修复首页端口显示不一致问题
- 修复首页流量统计刻度线显示问题
- 修复日志页面按钮功能混淆问题
- 修复日志等级设置保存问题
- 修复日志等级异常过滤
- 修复清理日志天数功能异常
- 修复偶发性启动卡死问题
- 修复首页虚拟网卡开关在管理模式下的状态问题
### 🔧 技术改进
- 统一使用新的内核通信方式
- 新增外部控制器配置界面
- 改进跨平台兼容性支持
+86
View File
@@ -0,0 +1,86 @@
[config]
skip_core_tasks = true
skip_git_env_info = true
skip_rust_env_info = true
skip_crate_env_info = true
# --- Backend ---
[tasks.rust-format]
install_crate = "rustfmt"
command = "cargo"
args = ["fmt", "--", "--emit=files"]
[tasks.rust-clippy]
description = "Run cargo clippy to lint the code"
command = "cargo"
args = ["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"]
# --- Frontend ---
[tasks.eslint]
description = "Run ESLint to lint the code"
command = "pnpm"
args = ["lint"]
[tasks.eslint.windows]
command = "pnpm.cmd"
[tasks.typecheck]
description = "Run type checks"
command = "pnpm"
args = ["typecheck"]
[tasks.typecheck.windows]
command = "pnpm.cmd"
[tasks.lint-staged]
description = "Run lint-staged for staged files"
command = "pnpm"
args = ["exec", "lint-staged"]
[tasks.lint-staged.windows]
command = "pnpm.cmd"
# --- Jobs ---
# Rust format (for pre-commit)
[tasks.rust-format-check]
description = "Check Rust code formatting"
dependencies = ["rust-format"]
[tasks.rust-format-check.condition]
files_modified.input = [
"./src-tauri/**/*.rs",
"./crates/**/*.rs",
"**/Cargo.toml",
]
files_modified.output = ["./target/debug/*", "./target/release/*"]
# Rust lint (for pre-push)
[tasks.rust-lint]
description = "Run Rust linting"
dependencies = ["rust-clippy"]
[tasks.rust-lint.condition]
files_modified.input = [
"./src-tauri/**/*.rs",
"./crates/**/*.rs",
"**/Cargo.toml",
]
files_modified.output = ["./target/debug/*", "./target/release/*"]
# Frontend format (for pre-commit)
[tasks.frontend-format]
description = "Frontend format checks"
dependencies = ["lint-staged"]
# Frontend lint (for pre-push)
[tasks.frontend-lint]
description = "Frontend linting and type checking"
dependencies = ["eslint", "typecheck"]
# --- Git Hooks ---
[tasks.pre-commit]
description = "Pre-commit checks: format only"
dependencies = ["rust-format-check", "frontend-format"]
[tasks.pre-push]
description = "Pre-push checks: lint and typecheck"
dependencies = ["rust-lint", "frontend-lint"]
+12 -19
View File
@@ -48,30 +48,23 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
## Promotion
#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
### ✈️ [狗狗加速 —— 技术流机场 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)
🚀 高性能海外技术流机场,支持免费试用与优惠套餐,全面解锁流媒体及 AI 服务,全球首家采用 **QUIC 协议**。
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
🎁 使用 **Clash Verge 专属邀请链接** 注册即送 **3 天免费试用**,每日 **1GB 流量**:👉 [点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
#### **核心优势:**
🧩 YXVM 独立服务器优势:
- 📱 自研 iOS 客户端(业内"唯一")技术经得起考验,极大**持续研发**投入
- 🧑‍💻 **12小时真人客服**(顺带解决 Clash Verge 使用问题)
- 💰 优惠套餐每月**仅需 21 元,160G 流量,年付 8 折**
- 🌍 海外团队,无跑路风险,高达 50% 返佣
- ⚙️ **集群负载均衡**设计,**负载监控和随时扩容**,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
- ⚡ 全球首家**Quic 协议机场**,现已上线更快的 Tuic 协议(Clash Verge 客户端最佳搭配)
- 🎬 解锁**流媒体及 主流 AI**
- 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源,非VPS可比,性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
- 💡 支持即开即用,多机房选择,CN2 / IEPL 可选
- 📦 本项目使用配置已在售,欢迎同款入手!
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
## Features
+1 -2
View File
@@ -104,8 +104,7 @@ pub fn bench_draft(c: &mut Criterion) {
let draft = black_box(make_draft());
let _: Result<(), anyhow::Error> = draft
.with_data_modify::<_, _, _>(|mut box_data| async move {
box_data.enable_auto_launch =
Some(!box_data.enable_auto_launch.unwrap_or(false));
box_data.enable_auto_launch = Some(!box_data.enable_auto_launch.unwrap_or(false));
Ok((box_data, ()))
})
.await;
+31 -33
View File
@@ -1,13 +1,13 @@
use parking_lot::RwLock;
use std::sync::Arc;
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
pub type SharedDraft<T> = Arc<T>;
type DraftInner<T> = (SharedDraft<T>, Option<SharedDraft<T>>);
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone> {
#[derive(Debug)]
pub struct Draft<T> {
inner: Arc<RwLock<DraftInner<T>>>,
}
@@ -15,12 +15,12 @@ impl<T: Clone> Draft<T> {
#[inline]
pub fn new(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
inner: Arc::new(RwLock::new((Arc::new(data), None))),
}
}
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
#[inline]
pub fn data_arc(&self) -> SharedBox<T> {
pub fn data_arc(&self) -> SharedDraft<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
@@ -28,7 +28,7 @@ impl<T: Clone> Draft<T> {
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc,不 clone T
#[inline]
pub fn latest_arc(&self) -> SharedBox<T> {
pub fn latest_arc(&self) -> SharedDraft<T> {
let guard = self.inner.read();
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
}
@@ -41,21 +41,11 @@ impl<T: Clone> Draft<T> {
where
F: FnOnce(&mut T) -> R,
{
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
let mut guard = self.inner.write();
let mut draft_arc = if guard.1.is_none() {
Arc::clone(&guard.0)
} else {
#[allow(clippy::unwrap_used)]
guard.1.take().unwrap()
};
drop(guard);
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &mut T
let result = f(&mut **boxed);
// 恢复修改后的草稿 Arc
self.inner.write().1 = Some(draft_arc);
let mut draft_arc = guard.1.take().unwrap_or_else(|| Arc::clone(&guard.0));
let data_mut = Arc::make_mut(&mut draft_arc);
let result = f(data_mut);
guard.1 = Some(draft_arc);
result
}
@@ -81,22 +71,30 @@ impl<T: Clone> Draft<T> {
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
F: FnOnce(T) -> Fut + Send,
Fut: std::future::Future<Output = Result<(T, R), anyhow::Error>> + Send,
{
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
let local: Box<T> = {
let (local, original_arc) = {
let guard = self.inner.read();
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
(*guard.0).clone()
let arc = Arc::clone(&guard.0);
((*arc).clone(), arc)
};
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 Arc)
self.inner.write().0 = Arc::new(new_local);
let mut guard = self.inner.write();
if !Arc::ptr_eq(&guard.0, &original_arc) {
return Err(anyhow::anyhow!(
"Optimistic lock failed: Committed data has changed during async operation"
));
}
guard.0 = Arc::from(new_local);
Ok(res)
}
}
impl<T: Clone> Clone for Draft<T> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
+5 -11
View File
@@ -133,10 +133,7 @@ mod tests {
let prev_draft_ptr = std::sync::Arc::as_ptr(&draft_after_first_edit);
draft.apply();
let committed_after_apply = draft.data_arc();
assert_eq!(
std::sync::Arc::as_ptr(&committed_after_apply),
prev_draft_ptr
);
assert_eq!(std::sync::Arc::as_ptr(&committed_after_apply), prev_draft_ptr);
// 第二次编辑:此时草稿唯一持有(无其它引用),不应再克隆
// 获取草稿 Arc 的指针并立即丢弃本地引用,避免增加 strong_count
@@ -198,7 +195,7 @@ mod tests {
// 使用 with_data_modify 异步(立即就绪)地更新 committed
let res = block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(true);
Ok((Box::new(*v), "done")) // Dereference v to get Box<T>
Ok((v, "done"))
}));
assert_eq!(
{
@@ -218,11 +215,8 @@ mod tests {
let draft = Draft::new(IVerge::default());
#[allow(clippy::unwrap_used)]
let err = block_on_ready(draft.with_data_modify(|v| async move {
drop(v);
Err::<(Box<IVerge>, ()), _>(anyhow!("boom"))
}))
.unwrap_err();
let err = block_on_ready(draft.with_data_modify(|_v| async move { Err::<(IVerge, ()), _>(anyhow!("boom")) }))
.unwrap_err();
assert_eq!(format!("{err}"), "boom");
}
@@ -246,7 +240,7 @@ mod tests {
#[allow(clippy::unwrap_used)]
block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(false); // 与草稿不同
Ok((Box::new(*v), ())) // Dereference v to get Box<T>
Ok((v, ()))
}))
.unwrap();
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "clash-verge-i18n"
version = "0.1.0"
edition = "2024"
[dependencies]
rust-i18n = "3.1.5"
sys-locale = "0.3.2"
[lints]
workspace = true
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: لوحة التحكم
body: تم تحديث حالة عرض لوحة التحكم.
clashModeChanged:
title: تبديل الوضع
body: تم التبديل إلى {mode}.
systemProxyToggled:
title: وكيل النظام
body: تم تحديث حالة وكيل النظام.
tunModeToggled:
title: وضع TUN
body: تم تحديث حالة وضع TUN.
lightweightModeEntered:
title: الوضع الخفيف
body: تم الدخول إلى الوضع الخفيف.
profilesReactivated:
title: الملفات التعريفية
body: تمت إعادة تفعيل الملف التعريفي.
appQuit:
title: على وشك الخروج
body: Clash Verge على وشك الخروج.
appHidden:
title: تم إخفاء التطبيق
body: Clash Verge يعمل في الخلفية.
service:
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
tray:
dashboard: لوحة التحكم
ruleMode: وضع القواعد
globalMode: الوضع العام
directMode: الوضع المباشر
outboundModes: أوضاع الخروج
rule: قاعدة
direct: مباشر
global: عام
profiles: الملفات التعريفية
proxies: وكلاء
systemProxy: وكيل النظام
tunMode: وضع TUN
closeAllConnections: إغلاق كل الاتصالات
lightweightMode: الوضع الخفيف
copyEnv: نسخ متغيرات البيئة
confDir: دليل الإعدادات
coreDir: دليل النواة
logsDir: دليل السجلات
openDir: فتح الدليل
appLog: سجل التطبيق
coreLog: سجل النواة
restartClash: إعادة تشغيل نواة Clash
restartApp: إعادة تشغيل التطبيق
vergeVersion: إصدار Verge
more: المزيد
exit: خروج
tooltip:
systemProxy: وكيل النظام
tun: TUN
profile: ملف تعريفي
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Übersicht
body: Die Sichtbarkeit der Übersicht wurde aktualisiert.
clashModeChanged:
title: Moduswechsel
body: Auf {mode} umgeschaltet.
systemProxyToggled:
title: Systemproxy
body: Der Status des Systemproxys wurde aktualisiert.
tunModeToggled:
title: TUN-Modus
body: Der Status des TUN-Modus wurde aktualisiert.
lightweightModeEntered:
title: Leichtmodus
body: Leichtmodus aktiviert.
profilesReactivated:
title: Profile
body: Profil reaktiviert.
appQuit:
title: Beenden steht bevor
body: Clash Verge wird gleich beendet.
appHidden:
title: Anwendung ausgeblendet
body: Clash Verge läuft im Hintergrund.
service:
adminInstallPrompt: Für die Installation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
adminUninstallPrompt: Für die Deinstallation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
tray:
dashboard: Übersicht
ruleMode: Regelmodus
globalMode: Globaler Modus
directMode: Direktmodus
outboundModes: Ausgangsmodi
rule: Regel
direct: Direkt
global: Global
profiles: Profile
proxies: Proxy
systemProxy: Systemproxy
tunMode: TUN-Modus
closeAllConnections: Alle Verbindungen schließen
lightweightMode: Leichtmodus
copyEnv: Umgebungsvariablen kopieren
confDir: Konfigurationsverzeichnis
coreDir: Core-Verzeichnis
logsDir: Log-Verzeichnis
openDir: Verzeichnis öffnen
appLog: Anwendungslog
coreLog: Core-Log
restartClash: Clash-Core neu starten
restartApp: Anwendung neu starten
vergeVersion: Verge-Version
more: Mehr
exit: Beenden
tooltip:
systemProxy: Systemproxy
tun: TUN
profile: Profil
@@ -25,9 +25,8 @@ notifications:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Installing the service requires administrator privileges.
adminUninstallPrompt: Uninstalling the service requires administrator privileges.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Panel
body: La visibilidad del panel se ha actualizado.
clashModeChanged:
title: Cambio de modo
body: Cambiado a {mode}.
systemProxyToggled:
title: Proxy del sistema
body: El estado del proxy del sistema se ha actualizado.
tunModeToggled:
title: Modo TUN
body: El estado del modo TUN se ha actualizado.
lightweightModeEntered:
title: Modo ligero
body: Se ha entrado en el modo ligero.
profilesReactivated:
title: Perfiles
body: Perfil reactivado.
appQuit:
title: A punto de salir
body: Clash Verge está a punto de salir.
appHidden:
title: Aplicación oculta
body: Clash Verge se está ejecutando en segundo plano.
service:
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
tray:
dashboard: Panel
ruleMode: Modo de reglas
globalMode: Modo global
directMode: Modo directo
outboundModes: Modos de salida
rule: Regla
direct: Directo
global: Global
profiles: Perfiles
proxies: Proxies
systemProxy: Proxy del sistema
tunMode: Modo TUN
closeAllConnections: Cerrar todas las conexiones
lightweightMode: Modo ligero
copyEnv: Copiar variables de entorno
confDir: Directorio de configuración
coreDir: Directorio del núcleo
logsDir: Directorio de registros
openDir: Abrir directorio
appLog: Registro de la aplicación
coreLog: Registro del núcleo
restartClash: Reiniciar el núcleo de Clash
restartApp: Reiniciar aplicación
vergeVersion: Versión de Verge
more: Más
exit: Salir
tooltip:
systemProxy: Proxy del sistema
tun: TUN
profile: Perfil
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: داشبورد
body: وضعیت نمایش داشبورد به‌روزرسانی شد.
clashModeChanged:
title: تغییر حالت
body: به {mode} تغییر کرد.
systemProxyToggled:
title: پروکسی سیستم
body: وضعیت پروکسی سیستم به‌روزرسانی شد.
tunModeToggled:
title: حالت TUN
body: وضعیت حالت TUN به‌روزرسانی شد.
lightweightModeEntered:
title: حالت سبک
body: به حالت سبک وارد شد.
profilesReactivated:
title: پروفایل‌ها
body: پروفایل دوباره فعال شد.
appQuit:
title: در آستانه خروج
body: Clash Verge در آستانه خروج است.
appHidden:
title: برنامه پنهان شد
body: Clash Verge در پس‌زمینه در حال اجراست.
service:
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
tray:
dashboard: داشبورد
ruleMode: حالت قوانین
globalMode: حالت سراسری
directMode: حالت مستقیم
outboundModes: حالت‌های خروجی
rule: قانون
direct: مستقیم
global: سراسری
profiles: پروفایل‌ها
proxies: پروکسی‌ها
systemProxy: پروکسی سیستم
tunMode: حالت TUN
closeAllConnections: بستن همه اتصال‌ها
lightweightMode: حالت سبک
copyEnv: کپی متغیرهای محیطی
confDir: پوشه پیکربندی
coreDir: پوشه هسته
logsDir: پوشه گزارش‌ها
openDir: باز کردن پوشه
appLog: گزارش برنامه
coreLog: گزارش هسته
restartClash: راه‌اندازی مجدد هسته Clash
restartApp: راه‌اندازی مجدد برنامه
vergeVersion: نسخه Verge
more: بیشتر
exit: خروج
tooltip:
systemProxy: پروکسی سیستم
tun: TUN
profile: پروفایل
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Dasbor
body: Visibilitas dasbor telah diperbarui.
clashModeChanged:
title: Peralihan Mode
body: Beralih ke {mode}.
systemProxyToggled:
title: Proksi Sistem
body: Status proksi sistem telah diperbarui.
tunModeToggled:
title: Mode TUN
body: Status mode TUN telah diperbarui.
lightweightModeEntered:
title: Mode Ringan
body: Masuk ke mode ringan.
profilesReactivated:
title: Profil
body: Profil diaktifkan kembali.
appQuit:
title: Akan Keluar
body: Clash Verge akan keluar.
appHidden:
title: Aplikasi Disembunyikan
body: Clash Verge berjalan di latar belakang.
service:
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
tray:
dashboard: Dasbor
ruleMode: Mode Aturan
globalMode: Mode Global
directMode: Mode Langsung
outboundModes: Mode Keluar
rule: Aturan
direct: Langsung
global: Global
profiles: Profil
proxies: Proksi
systemProxy: Proksi Sistem
tunMode: Mode TUN
closeAllConnections: Tutup Semua Koneksi
lightweightMode: Mode Ringan
copyEnv: Salin Variabel Lingkungan
confDir: Direktori Konfigurasi
coreDir: Direktori Core
logsDir: Direktori Log
openDir: Buka Direktori
appLog: Log Aplikasi
coreLog: Log Core
restartClash: Mulai Ulang Core Clash
restartApp: Mulai Ulang Aplikasi
vergeVersion: Versi Verge
more: Lainnya
exit: Keluar
tooltip:
systemProxy: Proksi Sistem
tun: TUN
profile: Profil
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: ダッシュボード
body: ダッシュボードの表示状態が更新されました。
clashModeChanged:
title: モード切り替え
body: "{mode} に切り替えました。"
systemProxyToggled:
title: システムプロキシ
body: システムプロキシの状態が更新されました。
tunModeToggled:
title: TUN モード
body: TUN モードの状態が更新されました。
lightweightModeEntered:
title: 軽量モード
body: 軽量モードに入りました。
profilesReactivated:
title: プロファイル
body: プロファイルが再有効化されました。
appQuit:
title: 終了間近
body: Clash Verge はまもなく終了します。
appHidden:
title: アプリが非表示
body: Clash Verge はバックグラウンドで実行中です。
service:
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
tray:
dashboard: ダッシュボード
ruleMode: ルールモード
globalMode: グローバルモード
directMode: ダイレクトモード
outboundModes: アウトバウンドモード
rule: ルール
direct: ダイレクト
global: グローバル
profiles: プロファイル
proxies: プロキシ
systemProxy: システムプロキシ
tunMode: TUN モード
closeAllConnections: すべての接続を閉じる
lightweightMode: 軽量モード
copyEnv: 環境変数をコピー
confDir: 設定ディレクトリ
coreDir: コアディレクトリ
logsDir: ログディレクトリ
openDir: ディレクトリを開く
appLog: アプリケーションログ
coreLog: コアログ
restartClash: Clash コアを再起動
restartApp: アプリケーションを再起動
vergeVersion: Verge バージョン
more: その他
exit: 終了
tooltip:
systemProxy: システムプロキシ
tun: TUN
profile: プロファイル
@@ -16,8 +16,8 @@ notifications:
title: 경량 모드
body: 경량 모드에 진입했습니다.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
title: 프로필
body: 프로필이 다시 활성화되었습니다.
appQuit:
title: 곧 종료
body: Clash Verge가 곧 종료됩니다.
@@ -25,13 +25,14 @@ notifications:
title: 앱이 숨겨짐
body: Clash Verge가 백그라운드에서 실행 중입니다.
service:
adminPrompt: 서비스를 설치하려면 관리자 권한이 필요합니다.
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
tray:
dashboard: 대시보드
ruleMode: 규칙 모드
globalMode: 전역 모드
directMode: 직접 모드
outboundModes: Outbound Modes
outboundModes: 아웃바운드 모드
rule: 규칙
direct: 직접
global: 글로벌
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Панель
body: Видимость панели обновлена.
clashModeChanged:
title: Смена режима
body: Переключено на {mode}.
systemProxyToggled:
title: Системный прокси
body: Статус системного прокси обновлен.
tunModeToggled:
title: Режим TUN
body: Статус режима TUN обновлен.
lightweightModeEntered:
title: Легкий режим
body: Включен легкий режим.
profilesReactivated:
title: Профили
body: Профиль повторно активирован.
appQuit:
title: Скорый выход
body: Clash Verge скоро завершит работу.
appHidden:
title: Приложение скрыто
body: Clash Verge работает в фоновом режиме.
service:
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
tray:
dashboard: Панель
ruleMode: Режим правил
globalMode: Глобальный режим
directMode: Прямой режим
outboundModes: Исходящие режимы
rule: Правило
direct: Прямой
global: Глобальный
profiles: Профили
proxies: Прокси
systemProxy: Системный прокси
tunMode: Режим TUN
closeAllConnections: Закрыть все соединения
lightweightMode: Легкий режим
copyEnv: Копировать переменные среды
confDir: Каталог конфигурации
coreDir: Каталог ядра
logsDir: Каталог журналов
openDir: Открыть каталог
appLog: Журнал приложения
coreLog: Журнал ядра
restartClash: Перезапустить ядро Clash
restartApp: Перезапустить приложение
vergeVersion: Версия Verge
more: Еще
exit: Выход
tooltip:
systemProxy: Системный прокси
tun: TUN
profile: Профиль
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Gösterge Paneli
body: Gösterge panelinin görünürlüğü güncellendi.
clashModeChanged:
title: Mod Değişimi
body: "{mode} moduna geçildi."
systemProxyToggled:
title: Sistem Vekil'i
body: Sistem vekil'i durumu güncellendi.
tunModeToggled:
title: TUN Modu
body: TUN modu durumu güncellendi.
lightweightModeEntered:
title: Hafif Mod
body: Hafif moda geçildi.
profilesReactivated:
title: Profiller
body: Profil yeniden etkinleştirildi.
appQuit:
title: Çıkış Yapılmak Üzere
body: Clash Verge kapanmak üzere.
appHidden:
title: Uygulama Gizlendi
body: Clash Verge arka planda çalışıyor.
service:
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
tray:
dashboard: Gösterge Paneli
ruleMode: Kural Modu
globalMode: Küresel Mod
directMode: Doğrudan Mod
outboundModes: Giden Modlar
rule: Kural
direct: Doğrudan
global: Küresel
profiles: Profiller
proxies: Vekil'ler
systemProxy: Sistem Vekil'i
tunMode: TUN Modu
closeAllConnections: Tüm Bağlantıları Kapat
lightweightMode: Hafif Mod
copyEnv: Ortam Değişkenlerini Kopyala
confDir: Yapılandırma Dizini
coreDir: Çekirdek Dizini
logsDir: Günlük Dizini
openDir: Dizini Aç
appLog: Uygulama Günlüğü
coreLog: Çekirdek Günlüğü
restartClash: Clash Çekirdeğini Yeniden Başlat
restartApp: Uygulamayı Yeniden Başlat
vergeVersion: Verge Sürümü
more: Daha Fazla
exit: Çıkış
tooltip:
systemProxy: Sistem Vekil'i
tun: TUN
profile: Profil
+60
View File
@@ -0,0 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Идарә панеле
body: Идарә панеленең күренеше яңартылды.
clashModeChanged:
title: Режим алыштыру
body: "{mode} режимына күчтел."
systemProxyToggled:
title: Системалы прокси
body: Системалы прокси хәле яңартылды.
tunModeToggled:
title: TUN режимы
body: TUN режимы хәле яңартылды.
lightweightModeEntered:
title: Җиңел режим
body: Җиңел режимга күчелде.
profilesReactivated:
title: Профильләр
body: Профиль яңадан активлаштырылды.
appQuit:
title: Чыгар алдыннан
body: Clash Verge чыгарга җыена.
appHidden:
title: Кушымта яшерелде
body: Clash Verge фон режимында эшли.
service:
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
tray:
dashboard: Идарә панеле
ruleMode: Кагыйдә режимы
globalMode: Глобаль режим
directMode: Турыдан-туры режим
outboundModes: Чыгыш режимнары
rule: Кагыйдә
direct: Турыдан-туры
global: Глобаль
profiles: Профильләр
proxies: Проксилар
systemProxy: Системалы прокси
tunMode: TUN режимы
closeAllConnections: Барлык тоташуларны ябу
lightweightMode: Җиңел режим
copyEnv: Мохит үзгәрүчәннәрен күчерү
confDir: Конфигурация каталогы
coreDir: Ядро каталогы
logsDir: Журнал каталогы
openDir: Каталогны ачу
appLog: Кушымта журналы
coreLog: Ядро журналы
restartClash: Clash ядрәсен кабат җибәрү
restartApp: Кушымтаны кабат җибәрү
vergeVersion: Verge версиясе
more: Күбрәк
exit: Чыгу
tooltip:
systemProxy: Системалы прокси
tun: TUN
profile: Профиль
@@ -25,8 +25,8 @@ notifications:
title: 應用已隱藏
body: Clash Verge 正在背景執行。
service:
adminInstallPrompt: 安裝服務需要管理員權限
adminUninstallPrompt: 卸载服務需要管理員權限
adminInstallPrompt: 安裝 Clash Verge 服務需要管理員權限
adminUninstallPrompt: 卸载 Clash Verge 服務需要管理員權限
tray:
dashboard: 儀表板
ruleMode: 規則模式
+103
View File
@@ -0,0 +1,103 @@
use rust_i18n::i18n;
const DEFAULT_LANGUAGE: &str = "zh";
i18n!("locales", fallback = "zh");
#[inline]
fn locale_alias(locale: &str) -> Option<&'static str> {
match locale {
"ja" | "ja-jp" | "jp" => Some("jp"),
"zh" | "zh-cn" | "zh-hans" | "zh-sg" | "zh-my" | "zh-chs" => Some("zh"),
"zh-tw" | "zh-hk" | "zh-hant" | "zh-mo" | "zh-cht" => Some("zhtw"),
_ => None,
}
}
#[inline]
fn resolve_supported_language(language: &str) -> Option<&'static str> {
if language.is_empty() {
return None;
}
let normalized = language.to_lowercase().replace('_', "-");
let segments: Vec<&str> = normalized.split('-').collect();
let supported = rust_i18n::available_locales!();
for i in (1..=segments.len()).rev() {
let prefix = segments[..i].join("-");
if let Some(alias) = locale_alias(&prefix)
&& let Some(&found) = supported.iter().find(|&&l| l.eq_ignore_ascii_case(alias))
{
return Some(found);
}
if let Some(&found) = supported.iter().find(|&&l| l.eq_ignore_ascii_case(&prefix)) {
return Some(found);
}
}
None
}
#[inline]
fn current_language(language: Option<&str>) -> &str {
language
.as_ref()
.filter(|lang| !lang.is_empty())
.and_then(|lang| resolve_supported_language(lang))
.unwrap_or_else(system_language)
}
#[inline]
pub fn system_language() -> &'static str {
sys_locale::get_locale()
.as_deref()
.and_then(resolve_supported_language)
.unwrap_or(DEFAULT_LANGUAGE)
}
#[inline]
pub fn sync_locale(language: Option<&str>) {
let language = current_language(language);
set_locale(language);
}
#[inline]
pub fn set_locale(language: &str) {
let lang = resolve_supported_language(language).unwrap_or(DEFAULT_LANGUAGE);
rust_i18n::set_locale(lang);
}
#[inline]
pub fn translate(key: &str) -> Cow<'_, str> {
rust_i18n::t!(key)
}
#[macro_export]
macro_rules! t {
($key:expr) => {
$crate::translate(&$key)
};
($key:expr, $($arg_name:ident = $arg_value:expr),*) => {
{
let mut _text = $crate::translate(&$key);
$(
_text = _text.replace(&format!("{{{}}}", stringify!($arg_name)), &$arg_value);
)*
_text
}
};
}
#[cfg(test)]
mod test {
use super::resolve_supported_language;
#[test]
fn test_resolve_supported_language() {
assert_eq!(resolve_supported_language("en"), Some("en"));
assert_eq!(resolve_supported_language("en-US"), Some("en"));
assert_eq!(resolve_supported_language("zh"), Some("zh"));
assert_eq!(resolve_supported_language("zh-CN"), Some("zh"));
assert_eq!(resolve_supported_language("zh-Hant"), Some("zhtw"));
assert_eq!(resolve_supported_language("jp"), Some("jp"));
assert_eq!(resolve_supported_language("ja-JP"), Some("jp"));
assert_eq!(resolve_supported_language("fr"), None);
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "clash-verge-limiter"
version = "0.1.0"
edition = "2024"
[dependencies]
[lints]
workspace = true
+165
View File
@@ -0,0 +1,165 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub type SystemLimiter = Limiter<SystemClock>;
pub trait Clock: Send + Sync {
fn now_ms(&self) -> u64;
}
impl<T: Clock + ?Sized> Clock for &T {
fn now_ms(&self) -> u64 {
(**self).now_ms()
}
}
impl<T: Clock + ?Sized> Clock for Arc<T> {
fn now_ms(&self) -> u64 {
(**self).now_ms()
}
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now_ms(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
}
pub struct Limiter<C: Clock = SystemClock> {
last_run_ms: AtomicU64,
period_ms: u64,
clock: C,
}
impl<C: Clock> Limiter<C> {
pub const fn new(period: Duration, clock: C) -> Self {
Self {
last_run_ms: AtomicU64::new(0),
period_ms: period.as_millis() as u64,
clock,
}
}
pub fn check(&self) -> bool {
let now = self.clock.now_ms();
let last = self.last_run_ms.load(Ordering::Relaxed);
if now < last + self.period_ms && now >= last {
return false;
}
self.last_run_ms
.compare_exchange(last, now, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
}
}
#[cfg(test)]
mod extra_tests {
use super::*;
use std::sync::Arc;
use std::thread;
struct MockClock(AtomicU64);
impl Clock for MockClock {
fn now_ms(&self) -> u64 {
self.0.load(Ordering::SeqCst)
}
}
#[test]
fn test_zero_period_always_passes() {
let mock = MockClock(AtomicU64::new(100));
let limiter = Limiter::new(Duration::from_millis(0), &mock);
assert!(limiter.check());
assert!(limiter.check());
}
#[test]
fn test_boundary_condition() {
let period_ms = 100;
let mock = MockClock(AtomicU64::new(1000));
let limiter = Limiter::new(Duration::from_millis(period_ms), &mock);
assert!(limiter.check());
mock.0.store(1099, Ordering::SeqCst);
assert!(!limiter.check());
mock.0.store(1100, Ordering::SeqCst);
assert!(limiter.check(), "Should pass exactly at period boundary");
}
#[test]
fn test_high_concurrency_consistency() {
let period = Duration::from_millis(1000);
let mock = Arc::new(MockClock(AtomicU64::new(1000)));
let limiter = Arc::new(Limiter::new(period, Arc::clone(&mock)));
assert!(limiter.check());
mock.0.store(2500, Ordering::SeqCst);
let mut handles = vec![];
for _ in 0..20 {
let l = Arc::clone(&limiter);
handles.push(thread::spawn(move || l.check()));
}
#[allow(clippy::unwrap_used)]
let results: Vec<bool> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let success_count = results.iter().filter(|&&x| x).count();
assert_eq!(success_count, 1);
assert_eq!(limiter.last_run_ms.load(Ordering::SeqCst), 2500);
}
#[test]
fn test_extreme_time_jump() {
let mock = MockClock(AtomicU64::new(100));
let limiter = Limiter::new(Duration::from_millis(100), &mock);
assert!(limiter.check());
mock.0.store(u64::MAX - 10, Ordering::SeqCst);
assert!(limiter.check());
}
#[test]
fn test_system_clock_real_path() {
let clock = SystemClock;
let start = clock.now_ms();
assert!(start > 0);
std::thread::sleep(Duration::from_millis(10));
assert!(clock.now_ms() >= start);
}
#[test]
fn test_limiter_with_system_clock_default() {
let limiter = Limiter::new(Duration::from_millis(100), SystemClock);
assert!(limiter.check());
}
#[test]
fn test_coverage_time_backward() {
let mock = MockClock(AtomicU64::new(5000));
let limiter = Limiter::new(Duration::from_millis(100), &mock);
assert!(limiter.check());
mock.0.store(4000, Ordering::SeqCst);
assert!(limiter.check(), "Should pass and reset when time moves backward");
assert_eq!(limiter.last_run_ms.load(Ordering::SeqCst), 4000);
}
}
-1
View File
@@ -11,4 +11,3 @@ flexi_logger = { workspace = true }
[features]
default = []
tauri-dev = []
+4 -14
View File
@@ -1,6 +1,5 @@
use compact_str::CompactString;
use flexi_logger::DeferredNow;
#[cfg(not(feature = "tauri-dev"))]
use flexi_logger::filter::LogLineFilter;
use flexi_logger::writers::FileLogWriter;
use flexi_logger::writers::LogWriter as _;
@@ -93,27 +92,19 @@ pub fn write_sidecar_log(
) {
let args = format_args!("{}", message);
let record = Record::builder()
.args(args)
.level(level)
.target("sidecar")
.build();
let record = Record::builder().args(args).level(level).target("sidecar").build();
let _ = writer.write(now, &record);
}
#[cfg(not(feature = "tauri-dev"))]
pub struct NoModuleFilter<'a>(pub &'a [&'a str]);
pub struct NoModuleFilter<'a>(pub Vec<&'a str>);
#[cfg(not(feature = "tauri-dev"))]
impl<'a> NoModuleFilter<'a> {
#[inline]
pub fn filter(&self, record: &Record) -> bool {
if let Some(module) = record.module_path() {
for blocked in self.0 {
if module.len() >= blocked.len()
&& module.as_bytes()[..blocked.len()] == blocked.as_bytes()[..]
{
for blocked in self.0.iter() {
if module.len() >= blocked.len() && module.as_bytes()[..blocked.len()] == blocked.as_bytes()[..] {
return false;
}
}
@@ -122,7 +113,6 @@ impl<'a> NoModuleFilter<'a> {
}
}
#[cfg(not(feature = "tauri-dev"))]
impl<'a> LogLineFilter for NoModuleFilter<'a> {
#[inline]
fn write(
+14 -12
View File
@@ -14,18 +14,20 @@ where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future + Send + 'static,
{
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
Ok(rt) => Some(rt),
Err(e) => {
logging!(
info,
Type::SystemSignal,
"register shutdown signal failed, create tokio runtime error: {}",
e
);
None
}
});
RUNTIME.get_or_init(
|| match tokio::runtime::Builder::new_current_thread().enable_all().build() {
Ok(rt) => Some(rt),
Err(e) => {
logging!(
info,
Type::SystemSignal,
"register shutdown signal failed, create tokio runtime error: {}",
e
);
None
}
},
);
#[cfg(unix)]
unix::register(f);
+3 -18
View File
@@ -17,36 +17,21 @@ where
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGTERM: {}",
e
);
logging!(error, Type::SystemSignal, "Failed to register SIGTERM: {}", e);
return;
}
};
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGINT: {}",
e
);
logging!(error, Type::SystemSignal, "Failed to register SIGINT: {}", e);
return;
}
};
let mut sighup = match signal(SignalKind::hangup()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGHUP: {}",
e
);
logging!(error, Type::SystemSignal, "Failed to register SIGHUP: {}", e);
return;
}
};
+5 -30
View File
@@ -17,12 +17,7 @@ where
let mut ctrl_c = match windows::ctrl_c() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+C: {}",
e
);
logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e);
return;
}
};
@@ -30,12 +25,7 @@ where
let mut ctrl_close = match windows::ctrl_close() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Close: {}",
e
);
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e);
return;
}
};
@@ -43,12 +33,7 @@ where
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Shutdown: {}",
e
);
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e);
return;
}
};
@@ -56,12 +41,7 @@ where
let mut ctrl_logoff = match windows::ctrl_logoff() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Logoff: {}",
e
);
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e);
return;
}
};
@@ -94,12 +74,7 @@ where
}
IS_CLEANING_UP.store(true, Ordering::SeqCst);
logging!(
info,
Type::SystemSignal,
"Caught Windows signal: {}",
signal_name
);
logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name);
f().await;
}
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "clash-verge-types"
version = "0.1.0"
edition = "2024"
rust-version = "1.91"
[dependencies]
serde = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true }
[lints]
workspace = true
-1
View File
@@ -1 +0,0 @@
pub mod runtime;
@@ -8,10 +8,12 @@ rust-version = "1.91"
tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
sysinfo = { version = "0.37.2", features = ["network", "system"] }
# sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.38", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.178"
libc = "0.2.182"
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }
@@ -13,13 +13,7 @@ pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<String, Err
/// 获取应用的运行时间(毫秒)
#[command]
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
Ok(state
.inner()
.read()
.appinfo
.app_startup_time
.elapsed()
.as_millis())
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
}
/// 检查应用是否以管理员身份运行
@@ -7,6 +7,8 @@ pub mod commands;
#[cfg(windows)]
use deelevate::{PrivilegeLevel, Token};
#[cfg(unix)]
pub use libc;
use parking_lot::RwLock;
use sysinfo::{Networks, System};
use tauri::{
@@ -118,6 +120,12 @@ fn is_binary_admin() -> bool {
.unwrap_or(false)
}
#[inline]
#[cfg(unix)]
pub fn current_gid() -> u32 {
unsafe { libc::getgid() }
}
#[inline]
pub fn list_network_interfaces() -> Vec<String> {
let mut networks = Networks::new();
@@ -132,6 +140,13 @@ pub fn set_app_core_mode<R: Runtime>(app: &tauri::AppHandle<R>, mode: impl Into<
spec.appinfo.app_core_mode = mode.into();
}
#[inline]
pub fn get_app_uptime<R: Runtime>(app: &tauri::AppHandle<R>) -> Instant {
let platform_spec = app.state::<RwLock<Platform>>();
let spec = platform_spec.read();
spec.appinfo.app_startup_time
}
#[inline]
pub fn is_current_app_handle_admin<R: Runtime>(app: &tauri::AppHandle<R>) -> bool {
let platform_spec = app.state::<RwLock<Platform>>();
View File
+10 -10
View File
@@ -5,8 +5,8 @@ Thanks for helping localize Clash Verge Rev. This guide reflects the current arc
## Quick workflow
- Update the language folder under `src/locales/<lang>/`; use `src/locales/en/` as the canonical reference for keys and intent.
- Run `pnpm format:i18n` to align structure and `pnpm i18n:types` to refresh generated typings.
- If you touch backend copy, edit the matching YAML file in `src-tauri/locales/<lang>.yml`.
- Run `pnpm i18n:format` to align structure (frontend JSON + backend YAML) and `pnpm i18n:types` to refresh generated typings.
- If you touch backend copy, edit the matching YAML file in `crates/clash-verge-i18n/locales/<lang>.yml`.
- Preview UI changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only).
- Keep PRs focused and add screenshots whenever layout could be affected by text length.
@@ -33,29 +33,29 @@ src/locales/
Because backend translations now live in their own directory, you no longer need to run `pnpm prebuild` just to sync locales—the frontend folder is the sole source of truth for web bundles.
## Tooling for frontend contributors
## Tooling for i18n contributors
- `pnpm format:i18n` → `node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English.
- `pnpm node scripts/cleanup-unused-i18n.mjs` (without flags) performs a dry-run audit. Use it to inspect missing or extra keys before committing.
- `pnpm i18n:format` → `node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English across both JSON and YAML bundles.
- `pnpm i18n:check` performs a dry-run audit of frontend and backend keys. It scans TS/TSX usage plus Rust `t!(...)` calls in `src-tauri/` and `crates/` to spot missing or extra entries.
- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`, ensuring TypeScript catches invalid key usage.
- For dynamic keys that the analyzer cannot statically detect, add explicit references in code or update the script whitelist to avoid false positives.
## Backend (Tauri) locale bundles
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `src-tauri/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `crates/clash-verge-i18n/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
- Keep `en.yml` semantically aligned with the Simplified Chinese baseline (`zh.yml`). Other locales may temporarily copy English if no translation is available yet.
- When a backend feature introduces new strings, update every YAML file to keep the key set consistent. Missing keys fall back to the default language (`zh`), so catching gaps early avoids mixed-language output.
- Rust code resolves the active language through `src-tauri/src/utils/i18n.rs`. No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
- The same `pnpm i18n:check` / `pnpm i18n:format` tooling now validates backend YAML keys against Rust usage, so run it after backend i18n edits.
- Rust code resolves the active language through the `clash-verge-i18n` crate (`crates/clash-verge-i18n/src/lib.rs`). No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
## Adding a new language
1. Duplicate `src/locales/en/` into `src/locales/<new-lang>/` and translate the JSON files while preserving key structure.
2. Update the locale’s `index.ts` to import every namespace. Matching the English file is the easiest way to avoid missing exports.
3. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
4. If the backend should expose the language, create `src-tauri/locales/<new-lang>.yml` and translate the keys used in existing YAML files.
5. Adjust `crowdin.yml` if the locale requires a special mapping for Crowdin.
6. Run `pnpm format:i18n`, `pnpm i18n:types`, and (optionally) `pnpm node scripts/cleanup-unused-i18n.mjs` in dry-run mode to confirm structure.
4. If the backend should expose the language, create `crates/clash-verge-i18n/<new-lang>.yml` and translate the keys used in existing YAML files.
5. Run `pnpm i18n:format`, `pnpm i18n:types`, and (optionally) `pnpm i18n:check` in dry-run mode to confirm structure.
## Authoring guidelines
+395
View File
@@ -1,3 +1,398 @@
## v(2.4.6)
> [!IMPORTANT]
> 历经多轮磨合与修正,这是自 2.0 以来我们最满意的里程碑版本。建议所有用户立即升级。
### 🐞 修复问题
- 修复首次启动时代理信息刷新缓慢
- 修复无网络时无限请求 IP 归属查询
- 修复 WebDAV 页面重试逻辑
- 修复 Linux 通过 GUI 安装服务模式权限不符合预期
- 修复 macOS 因网口顺序导致无法正确设置代理
- 修复恢复休眠后无法操作托盘
- 修复首页当前节点图标语义显示不一致
- 修复使用 URL scheme 导入订阅时没有及时重载配置
- 修复规则界面里的行号展示逻辑
- 修复 Windows 托盘打开日志失败
- 修复 KDE 首次启动报错
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 升级 Mihomo 内核到最新
- 支持订阅设置自动延时监测间隔
- 新增流量隧道管理界面,支持可视化添加/删除隧道配置
- Masque 协议的 GUI 支持
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 安装服务失败时报告更详细的错误
- 避免脏订阅地址无法 Scheme 导入订阅
- macOS TUN 覆盖 DNS 时使用 114.114.114.114
- 连通性测试替换为更快的 http://1.0.0.1
- 连接、规则、日志等页面的过滤搜索组件新增了清空输入框按钮
- 链式代理增加明显的入口出口与数据流向标识
- 优化 IP 信息卡
- 美化代理组图标样式
- 移除 Linux resources 文件夹下多余的服务二进制文件
</details>
## v2.4.5
- **Mihomo(Meta) 内核升级至 v1.19.19**
### 🐞 修复问题
- 修复 macOS 有线网络 DNS 劫持失败
- 修复 Monaco 编辑器内右键菜单显示异常
- 修复设置代理端口时检查端口占用
- 修复 Monaco 编辑器初始化卡 Loading
- 修复恢复备份时 `config.yaml` / `profiles.yaml` 文件内字段未正确恢复
- 修复 Windows 下系统主题同步问题
- 修复 URL Schemes 无法正常导入
- 修复 Linux 下无法安装 TUN 服务
- 修复可能的端口被占用误报
- 修复设置允许外部控制来源不能立即生效
- 修复前端性能回归问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 允许代理页面允许高级过滤搜索
- 备份设置页面新增导入备份按钮
- 允许修改通知弹窗位置
- 支持收起导航栏(导航栏右键菜单 / 界面设置)
- 允许将出站模式显示在托盘一级菜单
- 允许禁用在托盘中显示代理组
- 支持在「编辑节点」中直接导入 AnyTLS URI 配置
- 支持关闭「验证代理绕过格式」
- 新增系统代理绕过和 TUN 排除自定义网段的可视化编辑器
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 应用内更新日志支持解析并渲染 HTML 标签
- 性能优化前后端在渲染流量图时的资源
- 在 Linux NVIDIA 显卡环境下尝试禁用 WebKit DMABUF 渲染以规避潜在问题
- Windows 下自启动改为计划任务实现
- 改进托盘和窗口操作频率限制实现
- 使用「编辑节点」添加节点时,自动将节点添加到第一个 `select` 类型的代理组的第一位
- 隐藏侧边导航栏和悬浮跳转导航的滚动条
- 完善对 AnyTLS / Mieru / Sudoku 的 GUI 支持
- macOS 和 Linux 对服务 IPC 权限进一步限制
- 移除 Windows 自启动计划任务中冗余的 3 秒延时
- 右键错误通知可复制错误详情
- 保存 TUN 设置时优化执行流程,避免界面卡顿
- 补充 `deb` / `rpm` 依赖 `libayatana-appindicator`
- 「连接」表格标题的排序点击区域扩展到整列宽度
- 备份恢复时显示加载覆盖层,恢复过程无需再手动关闭对话框
</details>
## v2.4.4
- **Mihomo(Meta) 内核升级至 v1.19.17**
### 🐞 修复问题
- Linux 无法切换 TUN 堆栈
- macOS service 启动项显示名称(试验性修改)
- macOS 非预期 Tproxy 端口设置
- 流量图缩放异常
- PAC 自动代理脚本内容无法动态调整
- 兼容从旧版服务模式升级
- Monaco 编辑器的行数上限
- 已删除节点在手动分组中导致配置无法加载
- 仪表盘与托盘状态不同步
- 彻底修复 macOS 连接页面显示异常
- windows 端监听关机信号失败
- 修复代理按钮和高亮状态不同步
- 修复侧边栏可能的未能正确跳转
- 修复解锁测试部分地区图标编码不正确
- 修复 IP 检测切页后强制刷新,改为仅在必要时更新
- 修复在搜索框输入不完整正则直接崩溃
- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
- 修复更新时加载进度条异常
- 升级内核失败导致内核不可用问题
- 修复 macOS 在安装和卸载服务时提示与操作不匹配
- 修复菜单排序模式拖拽异常
- 修复托盘菜单代理组前的异常勾选状态
- 修复 Windows 下自定义标题栏按钮在最小化 / 关闭后 hover 状态残留
- 修复直接覆盖 `config.yaml` 使用时无法展开代理组
- 修复 macOS 下应用启动时系统托盘图标颜色闪烁
- 修复应用静默启动模式下非全局热键一直抢占其他应用按键问题
- 修复首页当前节点卡片按延迟排序时,打开节点列表后,`timeout` 节点被排在正常节点前的问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 支持连接页面各个项目的排序
- 实现可选的自动备份
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
- 日志页面支持按时间倒序
- 增加「重新激活订阅」的全局快捷键
- WebView2 Runtime 修复构建升级到 133.0.3065.92
- 侧边栏右键新增「恢复默认排序」
- Linux 下新增对 TUN 「自动重定向」(`auto-redirect` 字段)的配置支持,默认关闭
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 网络请求改为使用 rustls,提升 TLS 兼容性
- rustls 避免因服务器证书链配置问题或较新 TLS 要求导致订阅无法导入
- 替换前端信息编辑组件,提供更好性能
- 优化后端内存和性能表现
- 防止退出时可能的禁用 TUN 失败
- 全新 i18n 支持方式
- 优化备份设置布局
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
- 性能优化系统状态获取
- 优化托盘菜单当前订阅检测逻辑
- 优化连接页面表格渲染
- 优化链式代理 UI 反馈
- 优化重启应用的资源清理逻辑
- 优化前端数据刷新
- 优化流量采样和数据处理
- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
- 优化前端 WebSocket 连接机制
- 改进旧版 Service 需要重新安装检测流程
- 优化 macOS, Linux 和 Windows 系统信号处理
- 链式代理仅显示 Selector 类型规则组
- 优化 Windows 系统代理设置,不再依赖 `sysproxy.exe` 来设置代理
</details>
## v2.4.3
**发行代号:澜**
代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。
特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
- 修复前端无法及时刷新操作状态
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复轻量模式托盘状态不同步
- 修复一键导入订阅导致应用卡死崩溃的问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- **Mihomo(Meta) 内核升级至 v1.19.15**
- 支持前端修改日志(最大文件大小、最大保留数量)
- 新增链式代理图形化设置功能
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
- 监听关机事件,自动关闭系统代理
- 主界面“当前节点”卡片新增“延迟测试”按钮
- 新增批量选择配置文件功能
- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置
- 新增本地备份功能
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
- 允许独立控制订阅自动更新
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
- 重构日志系统,单个日志默认最大 10 MB
- 优化前端资源占用
- 改进 macos 下系统代理设置的方法
- 优化 TUN 模式可用性的判断
- 移除流媒体检测的系统级提示(使用软件内通知)
- 优化后端 i18n 资源占用
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
- 修改内核默认日志级别为 Info
- 支持通过桌面快捷方式重新打开应用
- 支持订阅界面输入链接后回车导入
- 选择按延迟排序时每次延迟测试自动刷新节点顺序
- 配置重载失败时自动重启核心
- 启用 TUN 前等待服务就绪
- 卸载 TUN 时会先关闭
- 优化应用启动页
- 优化首页当前节点对MATCH规则的支持
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
- 添加热键绑定错误的提示信息
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
</details>
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1
### 🏆 重大改进
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.13**
### 🚀 性能优化
- 优化热键响应速度,提升快捷键操作体验
- 改进服务管理响应性,减少系统服务操作等待时间
- 提升文件和配置处理性能
- 优化任务管理和日志记录效率
- 优化异步内存管理,减少内存占用并提升多任务处理效率
- 优化启动阶段初始化性能
### 🐞 修复问题
- 修复应用在某些操作中可能出现的响应延迟问题
- 修复任务管理中的潜在并发问题
- 修复通过托盘重启应用无法恢复
- 修复订阅在某些情况下无法导入
- 修复无法新建订阅时使用远程链接
- 修复卸载服务后的 tun 开关状态问题
- 修复页面快速切换订阅时导致崩溃
- 修复丢失工作目录时无法恢复环境
- 修复从轻量模式恢复导致崩溃
### 👙 界面样式
- 统一代理设置样式
### 🗑️ 移除内容
- 移除启动阶段自动清理过期订阅
## v2.4.0
**发行代号:融**
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
### 🏆 重大改进
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.12**
- 新增版本信息复制按钮
- 增强型流量监控,支持更详细的数据分析
- 新增流量图表多种显示模式
- 新增强制刷新配置和节点缓存功能
- 首页流量统计支持查看刻度线详情
### 🚀 性能优化
- 全面提升数据传输和处理效率
- 优化内存使用,减少系统资源消耗
- 改进流量图表渲染性能
- 优化配置和节点刷新策略,从5秒延长到60秒
- 改进数据缓存机制,减少重复请求
- 优化异步程序性能
### 🐞 修复问题
- 修复系统代理状态检测和显示不一致问题
- 修复系统主题窗口颜色不一致问题
- 修复特殊字符 URL 处理问题
- 修复配置修改后缓存不同步问题
- 修复 Windows 安装器自启设置问题
- 修复 macOS 下 Dock 图标恢复窗口问题
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
- 修复架构升级后节点测速功能异常
- 修复架构升级后流量统计功能异常
- 修复架构升级后日志功能异常
- 修复外部控制器跨域配置保存问题
- 修复首页端口显示不一致问题
- 修复首页流量统计刻度线显示问题
- 修复日志页面按钮功能混淆问题
- 修复日志等级设置保存问题
- 修复日志等级异常过滤
- 修复清理日志天数功能异常
- 修复偶发性启动卡死问题
- 修复首页虚拟网卡开关在管理模式下的状态问题
### 🔧 技术改进
- 统一使用新的内核通信方式
- 新增外部控制器配置界面
- 改进跨平台兼容性支持
## v2.3.2
### 🐞 修复问题
+12 -19
View File
@@ -51,30 +51,23 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
## Promotion
#### [Doggygo VPN — Performance-oriented global accelerator](https://verge.dginv.click/#/register?code=oaxsAGo6)
### ✈️ [Doggygo VPN — A Technical-Grade Proxy Service](https://verge.dginv.click/#/register?code=oaxsAGo6)
- High-performance overseas network service with free trials, discounted plans, streaming unlocks, and first-class Hysteria protocol support.
- Register through the exclusive Clash Verge link to get a 3-day trial with 1 GB of traffic per day: [Sign up](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Exclusive 20% off coupon for Clash Verge users: `verge20` (limited to 500 uses)
- Discounted bundle from ¥15.8 per month for 160 GB, plus an additional 20% off for yearly billing
- Operated by an overseas team with reliable service and up to 50% revenue share
- Load-balanced clusters with high-speed dedicated routes (compatible with legacy clients), exceptionally low latency, smooth 4K playback
- First global provider to support the `Hysteria2` protocol—perfect fit for the Clash Verge client
- Supports streaming services and ChatGPT access
- Official site: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
🚀 A high-performance, overseas, technical-grade proxy service offering free trials and discounted plans, fully unlocking streaming platforms and AI services. The world’s first provider to adopt the **QUIC protocol**.
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
🎁 Register via the **Clash Verge exclusive invitation link** to receive **3 days of free trial**, with **1GB traffic per day**: 👉 [Register here](https://verge.dginv.click/#/register?code=oaxsAGo6)
Our builds and releases run on YXVM dedicated servers that deliver premium resources, strong performance, and high-speed networking. If downloads feel fast and usage feels snappy, it is thanks to robust hardware.
#### **Core Advantages:**
🧩 Highlights of YXVM Dedicated Servers:
- 📱 Self-developed iOS client (the industry’s “only one”), with technology proven in production and **significant ongoing R&D investment**
- 🧑‍💻 **12-hour live customer support** (also assists with Clash Verge usage issues)
- 💰 Discounted plans at **only CNY 21 per month, 160GB traffic, 20% off with annual billing**
- 🌍 Overseas team, no risk of shutdown or exit scams, with up to **50% referral commission**
- ⚙️ **Cluster-based load balancing** architecture with **real-time load monitoring and elastic scaling**, high-speed dedicated lines (compatible with legacy clients), ultra-low latency, unaffected by peak hours, **4K streaming loads instantly**
- ⚡ The world’s first **QUIC-protocol-based proxy service**, now upgraded with the faster **Tuic protocol** (best paired with the Clash Verge client)
- 🎬 Unlocks **streaming platforms and mainstream AI services**
- 🌎 Optimized global routes for dramatically faster downloads
- 🔧 Bare-metal resources instead of shared VPS capacity for maximum performance
- 🧠 Great for proxy workloads, hosting web/CDN services, CI/CD pipelines, or any high-load tasks
- 💡 Ready to use instantly with multiple datacenter options, including CN2 and IEPL
- 📦 The configuration used by this project is on sale—feel free to get the same setup
- 🎯 Want the same build environment? [Order a YXVM server today](https://yxvm.com/aff.php?aff=827)
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
## Features
-13
View File
@@ -63,19 +63,6 @@ Consulta la [documentación del proyecto](https://clash-verge-rev.github.io/) pa
- Desbloquea servicios de streaming y acceso a ChatGPT
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Patrocinador de la infraestructura de compilación — [Servidores dedicados YXVM](https://yxvm.com/aff.php?aff=827)
Las compilaciones y lanzamientos del proyecto se ejecutan en servidores dedicados de YXVM, que proporcionan recursos premium, alto rendimiento y redes de alta velocidad. Si las descargas son rápidas y el uso es fluido, es gracias a este hardware robusto.
🧩 Ventajas de los servidores dedicados YXVM:
- 🌎 Rutas globales optimizadas para descargas significativamente más rápidas
- 🔧 Recursos bare-metal, en lugar de VPS compartidos, para obtener el máximo rendimiento
- 🧠 Ideales para proxys, alojamiento de sitios web/CDN, pipelines de CI/CD o cualquier carga elevada
- 💡 Listos para usar al instante, con múltiples centros de datos disponibles (incluidos CN2 e IEPL)
- 📦 La misma configuración utilizada por este proyecto está disponible para su compra
- 🎯 ¿Quieres el mismo entorno de compilación? [Solicita un servidor YXVM hoy](https://yxvm.com/aff.php?aff=827)
## Funciones
- Basado en Rust de alto rendimiento y en el framework Tauri 2
-12
View File
@@ -62,18 +62,6 @@
- پشتیبانی از سرویس‌های استریم و دسترسی به ChatGPT
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### حامی زیرساخت ساخت — [سرورهای اختصاصی YXVM](https://yxvm.com/aff.php?aff=827)
بیلدها و نسخه‌های ما روی سرورهای اختصاصی YXVM اجرا می‌شوند که منابع ممتاز، عملکرد قوی و شبکه پرسرعت را ارائه می‌دهند. اگر دانلودها سریع و استفاده از آن سریع به نظر می‌رسد، به لطف سخت‌افزار قوی است.
🧩 نکات برجسته سرورهای اختصاصی YXVM:
- 🌎 مسیرهای جهانی بهینه شده برای دانلودهای بسیار سریعتر
- 🔧 منابع فیزیکی به جای ظرفیت VPS مشترک برای حداکثر کارایی
- 🧠 عالی برای بارهای کاری پروکسی، میزبانی سرویس‌های وب/CDN، خطوط لوله CI/CD یا هرگونه کار با بار بالا
- 💡 آماده استفاده فوری با گزینه‌های متعدد مرکز داده، از جمله CN2 و IEPL
- 📦 پیکربندی مورد استفاده در این پروژه در حال فروش است - می‌توانید همان تنظیمات را تهیه کنید.
- 🎯 آیا محیط ساخت مشابهی می‌خواهید؟ [همین امروز یک سرور YXVM سفارش دهید](https://yxvm.com/aff.php?aff=827)
## ویژگی‌ها
- ساخته شده بر اساس Rust با کارایی بالا و فریم‌ورک Tauri 2
-13
View File
@@ -63,19 +63,6 @@ Windows (x64/x86)、Linux (x64/arm64)、macOS 10.15+ (Intel/Apple) をサポー
- ストリーミングおよび ChatGPT の利用にも対応
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### ビルド環境スポンサー — [YXVM 専用サーバー](https://yxvm.com/aff.php?aff=827)
本プロジェクトのビルドとリリースは、YXVM の専用サーバーによって支えられています。高速ダウンロードや快適な操作性は、強力なハードウェアがあってこそです。
🧩 YXVM 専用サーバーの特長:
- 🌎 最適化されたグローバル回線で圧倒的なダウンロード速度
- 🔧 VPS とは異なるベアメタル資源で最高性能を発揮
- 🧠 プロキシ運用、Web/CDN ホスティング、CI/CD など高負荷ワークロードに最適
- 💡 複数データセンターから即時利用可能。CN2 や IEPL も選択可
- 📦 本プロジェクトが使用している構成も販売中。同じ環境を入手できます
- 🎯 同じビルド体験をしたい方は [今すぐ YXVM サーバーを注文](https://yxvm.com/aff.php?aff=827)
## 機能
- 高性能な Rust と Tauri 2 フレームワークに基づくデスクトップアプリ
-13
View File
@@ -63,19 +63,6 @@ Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니
- 스트리밍 및 ChatGPT 접근 지원
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 빌드 인프라 스폰서 — [YXVM 전용 서버](https://yxvm.com/aff.php?aff=827)
본 프로젝트의 빌드 및 릴리스는 YXVM 전용 서버에서 구동됩니다. 빠른 다운로드와 경쾌한 사용감은 탄탄한 하드웨어 덕분입니다.
🧩 YXVM 전용 서버 하이라이트:
- 🌎 최적화된 글로벌 라우팅으로 대폭 빨라진 다운로드
- 🔧 공유 VPS가 아닌 베어메탈 자원으로 최대 성능 제공
- 🧠 프록시 워크로드, Web/CDN 호스팅, CI/CD, 고부하 작업에 적합
- 💡 CN2 / IEPL 등 다양한 데이터센터 옵션, 즉시 사용 가능
- 📦 본 프로젝트가 사용하는 구성도 판매 중 — 동일한 환경을 사용할 수 있습니다
- 🎯 동일한 빌드 환경이 필요하다면 [지금 YXVM 서버 주문](https://yxvm.com/aff.php?aff=827)
## 기능
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
-13
View File
@@ -59,19 +59,6 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
- Разблокировка потоковые сервисы и ChatGPT
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Среда сборки и публикации этого проекта полностью поддерживается выделенным сервером [YXVM](https://yxvm.com/aff.php?aff=827)
Благодарим вас за предоставление надежной бэкэнд-среды с эксклюзивными ресурсами, высокой производительностью и высокоскоростной сетью. Если вы считаете, что загрузка файлов происходит достаточно быстро, а использование — достаточно плавно, то это потому, что мы используем серверы высшего уровня!
🧩 Преимущества выделенного сервера YXVM:
- 🌎 Премиум-сеть с оптимизацией обратного пути для молниеносной скорости загрузки
- 🔧 Выделенные физические серверные ресурсы, не имеющие аналогов среди VPS, обеспечивающие максимальную производительность
- 🧠 Идеально подходит для прокси, хостинга веб-сайтов/CDN-сайтов, рабочих процессов CI/CD или любых приложений с высокой нагрузкой
- 💡 Поддержка использования сразу после включения, выбор нескольких дата-центров, CN2 / IEPL на выбор
- 📦 Эта конфигурация в настоящее время доступна для покупки — не стесняйтесь заказывать ту же модель!
- 🎯 Хотите попробовать такую же сборку? [Закажите выделенный сервер YXVM прямо сейчас!](https://yxvm.com/aff.php?aff=827)
## Фичи
- Основан на произвоительном Rust и фреймворке Tauri 2
+8
View File
@@ -5,6 +5,7 @@ import configPrettier from "eslint-config-prettier";
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
import pluginImportX from "eslint-plugin-import-x";
import pluginPrettier from "eslint-plugin-prettier";
import pluginReactCompiler from "eslint-plugin-react-compiler";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReactRefresh from "eslint-plugin-react-refresh";
import pluginUnusedImports from "eslint-plugin-unused-imports";
@@ -19,6 +20,7 @@ export default defineConfig([
js: eslintJS,
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
"react-hooks": pluginReactHooks,
"react-compiler": pluginReactCompiler,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
"import-x": pluginImportX,
"react-refresh": pluginReactRefresh,
@@ -35,6 +37,11 @@ export default defineConfig([
languageOptions: {
globals: globals.browser,
parserOptions: {
projectService: {
allowDefaultProject: ["src/polyfills/*.js"],
},
},
},
settings: {
@@ -52,6 +59,7 @@ export default defineConfig([
// React
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"react-compiler/react-compiler": "error",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
+50 -45
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.4.4-rc.1",
"version": "2.4.7",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
@@ -28,7 +28,8 @@
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
"i18n:check": "node scripts/cleanup-unused-i18n.mjs",
"i18n:format": "node scripts/cleanup-unused-i18n.mjs --align --apply",
"i18n:types": "node scripts/generate-i18n-keys.mjs",
"typecheck": "tsc --noEmit"
},
@@ -40,79 +41,83 @@
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.6",
"@mui/icons-material": "^7.3.8",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.6",
"@mui/material": "^7.3.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.13",
"@tauri-apps/api": "2.9.1",
"@tanstack/react-virtual": "^3.13.18",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-http": "~2.5.4",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-http": "~2.5.7",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "2.3.3",
"@tauri-apps/plugin-updater": "2.9.0",
"@tauri-apps/plugin-shell": "2.3.5",
"@tauri-apps/plugin-updater": "2.10.0",
"ahooks": "^3.9.6",
"axios": "^1.13.2",
"axios": "^1.13.5",
"dayjs": "1.11.19",
"foxact": "^0.2.49",
"i18next": "^25.7.3",
"foxact": "^0.2.52",
"foxts": "^5.2.1",
"i18next": "^25.8.13",
"ipaddr.js": "^2.3.0",
"js-yaml": "^4.1.1",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.23",
"monaco-editor": "^0.55.1",
"monaco-yaml": "^5.4.0",
"monaco-yaml": "^5.4.1",
"nanoid": "^5.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.68.0",
"react-i18next": "16.5.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-error-boundary": "6.1.1",
"react-hook-form": "^7.71.2",
"react-i18next": "16.5.4",
"react-markdown": "10.1.0",
"react-router": "^7.10.1",
"react-virtuoso": "^4.17.0",
"swr": "^2.3.8",
"react-router": "^7.13.0",
"react-virtuoso": "^4.18.1",
"rehype-raw": "^7.0.0",
"swr": "^2.4.0",
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
"types-pac": "^1.0.3"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.3.13",
"@eslint/js": "^9.39.2",
"@tauri-apps/cli": "2.9.6",
"@actions/github": "^9.0.0",
"@eslint-react/eslint-plugin": "^2.13.0",
"@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.10.0",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.4",
"@types/react": "19.2.7",
"@types/node": "^24.10.13",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react-swc": "^4.2.2",
"@vitejs/plugin-react-swc": "^4.2.3",
"adm-zip": "^0.5.16",
"cli-color": "^2.0.4",
"commander": "^14.0.2",
"commander": "^14.0.3",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint": "^10.0.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.25",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^13.0.0",
"globals": "^16.5.0",
"eslint-plugin-react-refresh": "^0.5.0",
"eslint-plugin-unused-imports": "^4.4.1",
"glob": "^13.0.6",
"globals": "^17.3.0",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"jiti": "^2.6.1",
"lint-staged": "^16.2.7",
"node-fetch": "^3.3.2",
"prettier": "^3.7.4",
"sass": "^1.96.0",
"tar": "^7.5.2",
"terser": "^5.44.1",
"prettier": "^3.8.1",
"sass": "^1.97.3",
"tar": "^7.5.9",
"terser": "^5.46.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0",
"vite": "^7.3.0",
"typescript-eslint": "^8.56.0",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0"
},
"lint-staged": {
@@ -125,7 +130,7 @@
]
},
"type": "module",
"packageManager": "pnpm@10.26.0",
"packageManager": "pnpm@10.29.2",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
+2182 -3149
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
{
"extends": ["config:recommended", ":disableDependencyDashboard"],
"baseBranches": ["dev"],
"enabledManagers": ["cargo", "npm", "github-actions"],
"labels": ["dependencies"],
"ignorePaths": [
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**",
"shared/**"
],
"rangeStrategy": "replace",
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"matchPackageNames": ["*"],
"semanticCommitType": "chore"
},
{
"description": "Disable node/pnpm version updates",
"matchPackageNames": ["node", "pnpm"],
"matchDepTypes": ["engines", "packageManager"],
"enabled": false
},
{
"description": "Group all cargo dependencies into a single PR",
"matchManagers": ["cargo"],
"groupName": "cargo dependencies"
},
{
"description": "Group all npm dependencies into a single PR",
"matchManagers": ["npm"],
"groupName": "npm dependencies"
},
{
"description": "Group all GitHub Actions updates into a single PR",
"matchManagers": ["github-actions"],
"groupName": "github actions"
}
],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["criterion"],
"lockFileMaintenance": {
"enabled": true,
"description": "Force update lockfile to track latest commits of git dependencies",
"schedule": ["before 5am on monday"]
}
}
-50
View File
@@ -1,50 +0,0 @@
{
extends: ["config:recommended", ":disableDependencyDashboard"],
baseBranches: ["dev"],
enabledManagers: ["cargo", "npm", "github-actions"],
labels: ["dependencies"],
ignorePaths: [
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**",
"shared/**",
],
rangeStrategy: "replace",
packageRules: [
{
matchUpdateTypes: ["patch"],
automerge: true,
},
{
semanticCommitType: "chore",
matchPackageNames: ["*"],
},
{
description: "Disable node/pnpm version updates",
matchPackageNames: ["node", "pnpm"],
matchDepTypes: ["engines", "packageManager"],
enabled: false,
},
{
description: "Group all cargo dependencies into a single PR",
matchManagers: ["cargo"],
groupName: "cargo dependencies",
},
{
description: "Group all npm dependencies into a single PR",
matchManagers: ["npm"],
groupName: "npm dependencies",
},
{
description: "Group all GitHub Actions updates into a single PR",
matchManagers: ["github-actions"],
groupName: "github actions",
},
],
postUpdateOptions: ["pnpmDedupe"],
ignoreDeps: ["criterion"],
}
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
# bump_changelog.sh
# - prepend ./Changelog.md to ./docs/Changelog.history.md
# - overwrite ./Changelog.md with ./template/Changelog.md
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"
CHANGELOG="Changelog.md"
HISTORY="docs/Changelog.history.md"
TEMPLATE="template/Changelog.md"
timestamp() { date +"%Y%m%d%H%M%S"; }
echo "Repo root: $ROOT_DIR"
if [ ! -f "$CHANGELOG" ]; then
echo "Error: $CHANGELOG not found" >&2
exit 2
fi
if [ ! -f "$TEMPLATE" ]; then
echo "Error: $TEMPLATE not found" >&2
exit 3
fi
BACKUP_DIR=".changelog_backups"
mkdir -p "$BACKUP_DIR"
bak_ts=$(timestamp)
cp "$CHANGELOG" "$BACKUP_DIR/Changelog.md.bak.$bak_ts"
echo "Backed up $CHANGELOG -> $BACKUP_DIR/Changelog.md.bak.$bak_ts"
if [ -f "$HISTORY" ]; then
cp "$HISTORY" "$BACKUP_DIR/Changelog.history.md.bak.$bak_ts"
echo "Backed up $HISTORY -> $BACKUP_DIR/Changelog.history.md.bak.$bak_ts"
fi
# Prepend current Changelog.md content to top of docs/Changelog.history.md
tmpfile=$(mktemp)
{
cat "$CHANGELOG"
echo
echo ""
if [ -f "$HISTORY" ]; then
cat "$HISTORY"
fi
} > "$tmpfile"
mv "$tmpfile" "$HISTORY"
echo "Prepended $CHANGELOG -> $HISTORY"
# Overwrite Changelog.md with template
cp "$TEMPLATE" "$CHANGELOG"
echo "Overwrote $CHANGELOG with $TEMPLATE"
echo "Done. Backups saved under $BACKUP_DIR"
exit 0
+248 -61
View File
@@ -4,18 +4,23 @@ import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import yaml from "js-yaml";
import ts from "typescript";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const TAURI_LOCALES_DIR = path.resolve(__dirname, "../src-tauri/locales");
const DEFAULT_SOURCE_DIRS = [
path.resolve(__dirname, "../src"),
const FRONTEND_LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const BACKEND_LOCALES_DIR = path.resolve(
__dirname,
"../crates/clash-verge-i18n/locales",
);
const DEFAULT_FRONTEND_SOURCE_DIRS = [path.resolve(__dirname, "../src")];
const DEFAULT_BACKEND_SOURCE_DIRS = [
path.resolve(__dirname, "../src-tauri"),
path.resolve(__dirname, "../crates"),
];
const EXCLUDE_USAGE_DIRS = [LOCALES_DIR, TAURI_LOCALES_DIR];
const EXCLUDE_USAGE_DIRS = [FRONTEND_LOCALES_DIR, BACKEND_LOCALES_DIR];
const DEFAULT_BASELINE_LANG = "en";
const IGNORE_DIR_NAMES = new Set([
".git",
@@ -36,7 +41,7 @@ const IGNORE_DIR_NAMES = new Set([
"logs",
"__pycache__",
]);
const SUPPORTED_EXTENSIONS = new Set([
const FRONTEND_EXTENSIONS = new Set([
".ts",
".tsx",
".js",
@@ -46,6 +51,7 @@ const SUPPORTED_EXTENSIONS = new Set([
".vue",
".json",
]);
const BACKEND_EXTENSIONS = new Set([".rs"]);
const TS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -86,20 +92,25 @@ const WHITELIST_KEYS = new Set([
"theme.light",
"theme.dark",
"theme.system",
"Already Using Latest Core Version",
"_version",
]);
const MAX_PREVIEW_ENTRIES = 40;
const dynamicKeyCache = new Map();
const fileUsageCache = new Map();
function resetUsageCaches() {
dynamicKeyCache.clear();
fileUsageCache.clear();
}
function printUsage() {
console.log(`Usage: pnpm node scripts/cleanup-unused-i18n.mjs [options]
Options:
--apply Write locale files with unused keys removed (default: report only)
--align Align locale structure/order using the baseline locale
--baseline <lang> Baseline locale file name (default: ${DEFAULT_BASELINE_LANG})
--baseline <lang> Baseline locale file name for frontend/backend (default: ${DEFAULT_BASELINE_LANG})
--keep-extra Preserve keys that exist only in non-baseline locales when aligning
--no-backup Skip creating \`.bak\` backups when applying changes
--report <path> Write a JSON report to the given path
@@ -148,7 +159,7 @@ function parseArgs(argv) {
if (!next) {
throw new Error("--baseline requires a locale name (e.g. en)");
}
options.baseline = next.replace(/\.json$/, "");
options.baseline = next.replace(/\.(json|ya?ml)$/i, "");
i += 1;
break;
}
@@ -211,14 +222,16 @@ function getAllFiles(start, predicate) {
return files;
}
function collectSourceFiles(sourceDirs) {
function collectSourceFiles(sourceDirs, options = {}) {
const supportedExtensions =
options.supportedExtensions ?? FRONTEND_EXTENSIONS;
const seen = new Set();
const files = [];
for (const dir of sourceDirs) {
const resolved = getAllFiles(dir, (filePath) => {
if (seen.has(filePath)) return false;
if (!SUPPORTED_EXTENSIONS.has(path.extname(filePath))) return false;
if (!supportedExtensions.has(path.extname(filePath))) return false;
if (
EXCLUDE_USAGE_DIRS.some((excluded) =>
filePath.startsWith(`${excluded}${path.sep}`),
@@ -673,6 +686,45 @@ function collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys) {
}
}
function readRustStringLiteral(source, startIndex) {
const slice = source.slice(startIndex);
if (slice.startsWith('"')) {
const match = slice.match(/^"(?:\\.|[^"\\])*"/);
if (!match) return null;
return match[0].slice(1, -1);
}
if (slice.startsWith("r")) {
const match = slice.match(/^r(#+)?"([\s\S]*?)"\1/);
if (!match) return null;
return match[2];
}
return null;
}
function collectUsedKeysFromRustFile(
file,
baselineNamespaces,
usedKeys,
_dynamicPrefixes,
) {
const pattern = /\b(?:[A-Za-z_][\w:]*::)?t!\s*\(/g;
let match;
while ((match = pattern.exec(file.content))) {
let index = match.index + match[0].length;
while (index < file.content.length && /\s/.test(file.content[index])) {
index += 1;
}
const key = readRustStringLiteral(file.content, index);
if (key) {
addKeyIfValid(key, usedKeys, baselineNamespaces, {
forceNamespace: true,
});
}
}
collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys);
}
function collectUsedI18nKeys(sourceFiles, baselineNamespaces) {
const usedKeys = new Set();
const dynamicPrefixes = new Set();
@@ -685,6 +737,13 @@ function collectUsedI18nKeys(sourceFiles, baselineNamespaces) {
usedKeys,
dynamicPrefixes,
);
} else if (file.extension === ".rs") {
collectUsedKeysFromRustFile(
file,
baselineNamespaces,
usedKeys,
dynamicPrefixes,
);
} else {
collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys);
}
@@ -864,12 +923,16 @@ function writeReport(reportPath, data) {
fs.writeFileSync(reportPath, `${payload}\n`, "utf8");
}
function loadLocales() {
if (!fs.existsSync(LOCALES_DIR)) {
throw new Error(`Locales directory not found: ${LOCALES_DIR}`);
function isPlainObject(value) {
return value && typeof value === "object" && !Array.isArray(value);
}
function loadFrontendLocales() {
if (!fs.existsSync(FRONTEND_LOCALES_DIR)) {
throw new Error(`Locales directory not found: ${FRONTEND_LOCALES_DIR}`);
}
const entries = fs.readdirSync(LOCALES_DIR, { withFileTypes: true });
const entries = fs.readdirSync(FRONTEND_LOCALES_DIR, { withFileTypes: true });
const locales = [];
for (const entry of entries) {
@@ -879,12 +942,12 @@ function loadLocales() {
!entry.name.endsWith(".bak") &&
!entry.name.endsWith(".old")
) {
const localePath = path.join(LOCALES_DIR, entry.name);
const localePath = path.join(FRONTEND_LOCALES_DIR, entry.name);
const name = path.basename(entry.name, ".json");
const raw = fs.readFileSync(localePath, "utf8");
locales.push({
name,
dir: LOCALES_DIR,
dir: FRONTEND_LOCALES_DIR,
format: "single-file",
files: [
{
@@ -901,7 +964,7 @@ function loadLocales() {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith(".")) continue;
const localeDir = path.join(LOCALES_DIR, entry.name);
const localeDir = path.join(FRONTEND_LOCALES_DIR, entry.name);
const namespaceEntries = fs
.readdirSync(localeDir, { withFileTypes: true })
.filter(
@@ -942,6 +1005,51 @@ function loadLocales() {
return locales;
}
function loadBackendLocales() {
if (!fs.existsSync(BACKEND_LOCALES_DIR)) {
return [];
}
const entries = fs.readdirSync(BACKEND_LOCALES_DIR, { withFileTypes: true });
const locales = [];
for (const entry of entries) {
if (!entry.isFile()) continue;
if (entry.name.endsWith(".bak") || entry.name.endsWith(".old")) {
continue;
}
if (!/\.(ya?ml)$/i.test(entry.name)) continue;
const localePath = path.join(BACKEND_LOCALES_DIR, entry.name);
const name = entry.name.replace(/\.(ya?ml)$/i, "");
const raw = fs.readFileSync(localePath, "utf8");
let data = {};
try {
const parsed = yaml.load(raw);
data = isPlainObject(parsed) ? parsed : {};
} catch (error) {
console.warn(`Warning: failed to parse ${localePath}: ${error.message}`);
data = {};
}
locales.push({
name,
dir: BACKEND_LOCALES_DIR,
format: "yaml-file",
files: [
{
namespace: "translation",
path: localePath,
},
],
data,
});
}
locales.sort((a, b) => a.name.localeCompare(b.name));
return locales;
}
function ensureBackup(localePath) {
const backupPath = `${localePath}.bak`;
if (fs.existsSync(backupPath)) {
@@ -1042,6 +1150,15 @@ function writeLocale(locale, data, options) {
let success = false;
try {
if (locale.format === "yaml-file") {
const target = locale.files[0].path;
backupIfNeeded(target, backups, options);
const serialized = yaml.dump(data ?? {}, { lineWidth: -1, noRefs: true });
fs.writeFileSync(target, `${serialized.trimEnd()}\n`, "utf8");
success = true;
return;
}
if (locale.format === "single-file") {
const target = locale.files[0].path;
backupIfNeeded(target, backups, options);
@@ -1097,6 +1214,8 @@ function processLocale(
sourceFiles,
missingFromSource,
options,
groupName,
baselineName,
) {
const data = JSON.parse(JSON.stringify(locale.data));
const flattened = flattenLocale(data);
@@ -1112,7 +1231,7 @@ function processLocale(
}
const sourceMissing =
locale.name === options.baseline
locale.name === baselineName
? missingFromSource.filter((key) => !flattened.has(key))
: [];
@@ -1165,8 +1284,9 @@ function processLocale(
}
return {
group: groupName,
locale: locale.name,
file: locale.format === "single-file" ? locale.files[0].path : locale.dir,
file: locale.format === "multi-file" ? locale.dir : locale.files[0].path,
totalKeys: flattened.size,
expectedKeys: expectedTotal,
unusedKeys: unused,
@@ -1178,34 +1298,42 @@ function processLocale(
};
}
function main() {
const argv = process.argv.slice(2);
let options;
try {
options = parseArgs(argv);
} catch (error) {
console.error(`Error: ${error.message}`);
console.log();
printUsage();
process.exit(1);
}
function summarizeResults(results) {
return results.reduce(
(totals, result) => {
totals.totalUnused += result.unusedKeys.length;
totals.totalMissing += result.missingKeys.length;
totals.totalExtra += result.extraKeys.length;
totals.totalSourceMissing += result.missingSourceKeys.length;
return totals;
},
{
totalUnused: 0,
totalMissing: 0,
totalExtra: 0,
totalSourceMissing: 0,
},
);
}
function processLocaleGroup(group, options) {
const sourceDirs = [
...new Set([...DEFAULT_SOURCE_DIRS, ...options.extraSources]),
...new Set([...group.sourceDirs, ...options.extraSources]),
];
console.log("Scanning source directories:");
console.log(`\n[${group.label}] Scanning source directories:`);
for (const dir of sourceDirs) {
console.log(` - ${dir}`);
}
const sourceFiles = collectSourceFiles(sourceDirs);
const locales = loadLocales();
const sourceFiles = collectSourceFiles(sourceDirs, {
supportedExtensions: group.supportedExtensions,
});
const locales = group.locales;
if (locales.length === 0) {
console.log("No locale files found.");
return;
console.log(`[${group.label}] No locale files found.`);
return null;
}
const baselineLocale = locales.find(
@@ -1215,7 +1343,7 @@ function main() {
if (!baselineLocale) {
const available = locales.map((item) => item.name).join(", ");
throw new Error(
`Baseline locale "${options.baseline}" not found. Available locales: ${available}`,
`[${group.label}] Baseline locale "${options.baseline}" not found. Available locales: ${available}`,
);
}
@@ -1235,8 +1363,11 @@ function main() {
return a.name.localeCompare(b.name);
});
console.log(`\nChecking ${locales.length} locale files...\n`);
console.log(
`\n[${group.label}] Checking ${locales.length} locale files...\n`,
);
resetUsageCaches();
const results = locales.map((locale) =>
processLocale(
locale,
@@ -1246,35 +1377,85 @@ function main() {
sourceFiles,
missingFromSource,
options,
group.label,
baselineLocale.name,
),
);
const totalUnused = results.reduce(
(count, result) => count + result.unusedKeys.length,
0,
);
const totalMissing = results.reduce(
(count, result) => count + result.missingKeys.length,
0,
);
const totalExtra = results.reduce(
(count, result) => count + result.extraKeys.length,
0,
);
const totalSourceMissing = results.reduce(
(count, result) => count + result.missingSourceKeys.length,
0,
);
const totals = summarizeResults(results);
console.log("\nSummary:");
console.log(`\n[${group.label}] Summary:`);
for (const result of results) {
console.log(
` • ${result.locale}: unused=${result.unusedKeys.length}, missing=${result.missingKeys.length}, extra=${result.extraKeys.length}, missingSource=${result.missingSourceKeys.length}, total=${result.totalKeys}, expected=${result.expectedKeys}`,
);
}
console.log(
`\nTotals → unused: ${totalUnused}, missing: ${totalMissing}, extra: ${totalExtra}, missingSource: ${totalSourceMissing}`,
`\n[${group.label}] Totals → unused: ${totals.totalUnused}, missing: ${totals.totalMissing}, extra: ${totals.totalExtra}, missingSource: ${totals.totalSourceMissing}`,
);
return {
group: group.label,
baseline: baselineLocale.name,
sourceDirs,
totals,
results,
};
}
function main() {
const argv = process.argv.slice(2);
let options;
try {
options = parseArgs(argv);
} catch (error) {
console.error(`Error: ${error.message}`);
console.log();
printUsage();
process.exit(1);
}
const localeGroups = [
{
label: "frontend",
locales: loadFrontendLocales(),
sourceDirs: DEFAULT_FRONTEND_SOURCE_DIRS,
supportedExtensions: FRONTEND_EXTENSIONS,
},
{
label: "backend",
locales: loadBackendLocales(),
sourceDirs: DEFAULT_BACKEND_SOURCE_DIRS,
supportedExtensions: BACKEND_EXTENSIONS,
},
].filter((group) => group.locales.length > 0);
if (localeGroups.length === 0) {
console.log("No locale files found.");
return;
}
const groupReports = [];
const allResults = [];
for (const group of localeGroups) {
const report = processLocaleGroup(group, options);
if (!report) continue;
groupReports.push(report);
allResults.push(...report.results);
}
if (groupReports.length > 1) {
const overallTotals = summarizeResults(allResults);
console.log(
`\nOverall totals → unused: ${overallTotals.totalUnused}, missing: ${overallTotals.totalMissing}, extra: ${overallTotals.totalExtra}, missingSource: ${overallTotals.totalSourceMissing}`,
);
}
if (allResults.length === 0) {
return;
}
if (options.apply) {
console.log(
"Files were updated in-place; review diffs before committing changes.",
@@ -1301,11 +1482,17 @@ function main() {
apply: options.apply,
backup: options.backup,
align: options.align,
baseline: baselineLocale.name,
baseline: options.baseline,
keepExtra: options.keepExtra,
sourceDirs,
},
results,
groups: groupReports.map((report) => ({
group: report.group,
baseline: report.baseline,
sourceDirs: report.sourceDirs,
totals: report.totals,
locales: report.results.map((result) => result.locale),
})),
results: allResults,
};
writeReport(options.reportPath, payload);
console.log(`Report written to ${options.reportPath}`);
+17 -22
View File
@@ -68,6 +68,11 @@ const SIDECAR_HOST = target
.toString()
.match(/(?<=host: ).+(?=\s*)/g)[0];
const RESOURCES_DIR = path.join(cwd, "src-tauri", "resources");
const SIDECAR_DIR = path.join(cwd, "src-tauri", "sidecar");
// Linux service binaries are bundled as externalBin sidecars (see tauri.linux.conf.json)
const SERVICE_DIR = platform === "linux" ? SIDECAR_DIR : RESOURCES_DIR;
// =======================
// Version Cache
// =======================
@@ -369,9 +374,8 @@ async function downloadFile(url, outPath) {
// =======================
async function resolveSidecar(binInfo) {
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
const sidecarPath = path.join(sidecarDir, targetFile);
await fsp.mkdir(sidecarDir, { recursive: true });
const sidecarPath = path.join(SIDECAR_DIR, targetFile);
await fsp.mkdir(SIDECAR_DIR, { recursive: true });
if (!FORCE && fs.existsSync(sidecarPath)) {
log_success(`"${name}" already exists, skipping download`);
@@ -460,9 +464,9 @@ async function resolveSidecar(binInfo) {
}
async function resolveResource(binInfo) {
const { file, downloadURL, localPath } = binInfo;
const resDir = path.join(cwd, "src-tauri/resources");
const targetPath = path.join(resDir, file);
const { file, downloadURL, localPath, dir } = binInfo;
const baseDir = dir ?? RESOURCES_DIR;
const targetPath = path.join(baseDir, file);
if (!FORCE && fs.existsSync(targetPath) && !downloadURL && !localPath) {
log_success(`"${file}" already exists, skipping`);
@@ -474,7 +478,7 @@ async function resolveResource(binInfo) {
log_success(`"${file}" already exists, skipping download`);
return;
}
await fsp.mkdir(resDir, { recursive: true });
await fsp.mkdir(baseDir, { recursive: true });
await downloadFile(downloadURL, targetPath);
await updateHashCache(targetPath);
}
@@ -483,7 +487,7 @@ async function resolveResource(binInfo) {
if (!(await hasFileChanged(localPath, targetPath))) {
return;
}
await fsp.mkdir(resDir, { recursive: true });
await fsp.mkdir(baseDir, { recursive: true });
await fsp.copyFile(localPath, targetPath);
await updateHashCache(targetPath);
log_success(`Copied file: ${file}`);
@@ -545,12 +549,11 @@ const resolveServicePermission = async () => {
"clash-verge-service-install*",
"clash-verge-service-uninstall*",
];
const resDir = path.join(cwd, "src-tauri/resources");
const hashCache = await loadHashCache();
let hasChanges = false;
for (const f of serviceExecutables) {
const files = glob.sync(path.join(resDir, f));
const files = glob.sync(path.join(SERVICE_DIR, f));
for (const filePath of files) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath);
@@ -576,7 +579,7 @@ const resolveServicePermission = async () => {
};
// =======================
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback)
// =======================
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
@@ -584,6 +587,7 @@ const resolveService = () => {
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service" + suffix + ext,
dir: SERVICE_DIR,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
@@ -592,6 +596,7 @@ const resolveInstall = () => {
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-install" + suffix + ext,
dir: SERVICE_DIR,
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
});
};
@@ -600,6 +605,7 @@ const resolveUninstall = () => {
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-uninstall" + suffix + ext,
dir: SERVICE_DIR,
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
});
};
@@ -624,11 +630,6 @@ const resolveEnableLoopback = () =>
file: "enableLoopback.exe",
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
});
const resolveWinSysproxy = () =>
resolveResource({
file: "sysproxy.exe",
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
});
const resolveSetDnsScript = () =>
resolveResource({
@@ -676,12 +677,6 @@ const tasks = [
retry: 5,
unixOnly: platform === "linux" || platform === "darwin",
},
{
name: "windows-sysproxy",
func: resolveWinSysproxy,
retry: 5,
winOnly: true,
},
{
name: "set_dns_script",
func: resolveSetDnsScript,
+4 -3
View File
@@ -39,9 +39,10 @@ function is_valid_ip() {
# 获取网络接口和硬件端口
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
/Hardware Port:/{port=$0; gsub("Hardware Port: ", "", port)}
/Device: /{if ($2 == dev) {print port; exit}}
# 从网络服务列表中获取硬件端口
hardware_port=$(networksetup -listnetworkserviceorder | awk -v dev="$nic" '
/^\([0-9]+\) /{port=$0; sub(/^\([0-9]+\) /, "", port)}
/\(Hardware Port:/{interface=$NF;sub(/\)/, "", interface); if (interface == dev) {print port; exit}}
')
# 获取当前DNS设置
+2 -1
View File
@@ -90,7 +90,8 @@ async function sendTelegramNotification() {
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
const encodedVersion = encodeURIComponent(version);
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
const releaseTag = isAutobuild ? "autobuild" : `v${version}`;
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/${releaseTag}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
// 发送到 Telegram
try {
+3 -10
View File
@@ -1,16 +1,9 @@
#!/bin/bash
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
/Hardware Port:/{
port=$0; gsub("Hardware Port: ", "", port)
}
/Device: /{
if ($2 == dev) {
print port;
exit
}
}
hardware_port=$(networksetup -listnetworkserviceorder | awk -v dev="$nic" '
/^\([0-9]+\) /{port=$0; sub(/^\([0-9]+\) /, "", port)}
/\(Hardware Port:/{interface=$NF;sub(/\)/, "", interface); if (interface == dev) {print port; exit}}
')
if [ -f .original_dns.txt ]; then
+33 -41
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.4.4-rc.1"
version = "2.4.7"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -18,23 +18,23 @@ crate-type = ["staticlib", "cdylib", "rlib"]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = ["clash_verge_logger/color"]
tauri-dev = ["clash-verge-logging/tauri-dev"]
tauri-dev = []
tokio-trace = ["console-subscriber"]
clippy = ["tauri/test"]
tracing = []
tracing-full = []
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.5.3", features = [] }
tauri-build = { version = "2.5.5", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
clash-verge-logging = { workspace = true }
clash-verge-signal = { workspace = true }
clash-verge-types = { workspace = true }
clash-verge-i18n = { workspace = true }
clash-verge-limiter = { workspace = true }
tauri-plugin-clash-verge-sysinfo = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
tauri = { workspace = true, features = [
@@ -54,76 +54,68 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
warp = { version = "0.4.2", features = ["server"] }
open = "5.3.3"
dunce = "1.0.5"
nanoid = "0.4"
chrono = "0.4.42"
chrono = "0.4.43"
boa_engine = "0.21.0"
once_cell = { version = "1.21.3", features = ["parking_lot"] }
port_scanner = "0.1.5"
delay_timer = "0.11.6"
percent-encoding = "2.3.2"
reqwest = { version = "0.12.24", features = ["json", "cookies", "rustls-tls"] }
regex = "1.12.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [
reqwest = { version = "0.13.2", features = [
"json",
"cookies",
"rustls",
"form",
] }
regex = "1.12.3"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "0.4.5", features = [
"guard",
] }
network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.3"
tauri-plugin-dialog = "2.4.2"
tauri-plugin-fs = "2.4.4"
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.3.5"
tauri-plugin-dialog = "2.6.0"
tauri-plugin-fs = "2.4.5"
tauri-plugin-process = "2.3.1"
tauri-plugin-deep-link = "2.4.5"
tauri-plugin-deep-link = "2.4.7"
tauri-plugin-window-state = "2.4.1"
zip = "6.0.0"
reqwest_dav = "0.2.2"
zip = "8.1.0"
reqwest_dav = "0.3.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.4"
futures = "0.3.31"
sys-locale = "0.3.2"
getrandom = "0.4.1"
futures = "0.3.32"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.3"
tokio-stream = "0.1.17"
tokio-stream = "0.1.18"
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2.5.4"
tauri-plugin-http = "2.5.7"
console-subscriber = { version = "0.5.0", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo"}
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
clash_verge_service_ipc = { version = "2.0.26", features = [
clash_verge_service_ipc = { version = "2.1.3", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.7.1"
rust-i18n = "3.1.5"
arc-swap = "1.8.2"
rust_iso3166 = "0.1.14"
dark-light = "2.0.0"
# Use the git repo until the next release after v2.0.0.
dark-light = { git = "https://github.com/rust-dark-light/dark-light" }
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }
runas = "=1.2.0"
winreg = "0.55.0"
winapi = { version = "0.3.9", features = [
"winbase",
"fileapi",
"winnt",
"handleapi",
"errhandlingapi",
"minwindef",
"winerror",
"tlhelp32",
"processthreadsapi",
"winhttp",
"winreg",
] }
windows = { version = "0.62.2", features = ["Win32_Globalization"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.1"
tauri-plugin-updater = "2.9.0"
tauri-plugin-updater = "2.10.0"
[dev-dependencies]
criterion = { workspace = true }
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Regel
direct: Direkt
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Regla
direct: Directo
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Aturan
direct: Langsung
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: ルール
direct: ダイレクト
global: グローバル
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Правило
direct: Прямой
global: Глобальный
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Kural
direct: Doğrudan
global: Küresel
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
+8 -2
View File
@@ -45,14 +45,20 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
/// 打开 Verge 最新日志
#[tauri::command]
pub async fn open_app_log() -> CmdResult<()> {
open::that(dirs::app_latest_log().stringify_err()?).stringify_err()
let log_path = dirs::app_latest_log().stringify_err()?;
#[cfg(target_os = "windows")]
let log_path = crate::utils::help::snapshot_path(&log_path).stringify_err()?;
open::that(log_path).stringify_err()
}
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
/// 打开 Clash 最新日志
#[tauri::command]
pub async fn open_core_log() -> CmdResult<()> {
open::that(dirs::clash_latest_log().stringify_err()?).stringify_err()
let log_path = dirs::clash_latest_log().stringify_err()?;
#[cfg(target_os = "windows")]
let log_path = crate::utils::help::snapshot_path(&log_path).stringify_err()?;
open::that(log_path).stringify_err()
}
/// 打开/关闭开发者工具
+6
View File
@@ -27,6 +27,12 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
feat::restore_local_backup(filename).await.stringify_err()
}
/// Import local backup into the app's backup directory
#[tauri::command]
pub async fn import_local_backup(source: String) -> CmdResult<String> {
feat::import_local_backup(source).await.stringify_err()
}
/// Export local backup to a user selected destination
#[tauri::command]
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
+6
View File
@@ -4,6 +4,7 @@ use clash_verge_logging::{Type, logging};
use gethostname::gethostname;
use network_interface::NetworkInterface;
use serde_yaml_ng::Mapping;
use std::net::TcpListener;
use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_clash_verge_sysinfo;
@@ -95,3 +96,8 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
Ok(result)
}
#[tauri::command]
pub fn is_port_in_use(port: u16) -> bool {
TcpListener::bind(("127.0.0.1", port)).is_err()
}
+16 -15
View File
@@ -1,5 +1,6 @@
use super::CmdResult;
use super::StringifyErr as _;
use crate::utils::window_manager::WindowManager;
use crate::{
config::{
Config, IProfiles, PrfItem, PrfOption,
@@ -13,10 +14,9 @@ use crate::{
feat,
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
process::AsyncHandler,
ret_err,
utils::{dirs, help},
};
use clash_verge_draft::SharedBox;
use clash_verge_draft::SharedDraft;
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use smartstring::alias::String;
@@ -26,7 +26,7 @@ use std::time::Duration;
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
#[tauri::command]
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
logging!(debug, Type::Cmd, "获取配置文件列表");
let draft = Config::profiles().await;
let data = draft.data_arc();
@@ -65,7 +65,7 @@ pub async fn enhance_profiles() -> CmdResult {
/// 导入配置文件
#[tauri::command]
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", help::mask_url(&url));
// 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
@@ -96,19 +96,18 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
if let Some(uid) = &item.uid {
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid.clone());
handle::Handle::notify_profile_changed(uid);
}
// 异步保存配置文件并发送全局通知
let uid_clone = item.uid.clone();
if let Some(uid) = uid_clone {
if let Some(uid) = &item.uid {
// 延迟发送,确保文件已完全写入
tokio::time::sleep(Duration::from_millis(100)).await;
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid);
}
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", help::mask_url(&url));
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
Ok(())
}
@@ -137,7 +136,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
// 发送配置变更通知
if let Some(uid) = item.uid.clone() {
if let Some(uid) = &item.uid {
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid);
}
@@ -158,7 +157,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
/// 更新配置文件
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
match feat::update_profile(&index, option.as_ref(), true, true).await {
match feat::update_profile(&index, option.as_ref(), true, true, true).await {
Ok(_) => {
let _: () = Config::profiles().await.apply();
Ok(())
@@ -184,7 +183,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
handle::Handle::refresh_clash();
// 发送配置变更通知
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
handle::Handle::notify_profile_changed(index);
handle::Handle::notify_profile_changed(&index);
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
}
Err(e) => {
@@ -311,9 +310,11 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
}
if let Some(current) = current_value {
if let Some(current) = current_value
&& WindowManager::get_main_window().is_some()
{
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
handle::Handle::notify_profile_changed(current.to_owned());
handle::Handle::notify_profile_changed(current);
}
Ok(true)
@@ -432,7 +433,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
crate::core::handle::Handle::notify_timer_updated(index);
crate::core::handle::Handle::notify_timer_updated(&index);
}
});
}
@@ -455,7 +456,7 @@ pub async fn view_profile(index: String) -> CmdResult {
let path = dirs::app_profiles_dir().stringify_err()?.join(file.as_str());
if !path.exists() {
ret_err!("the file not found");
return CmdResult::Err(format!("file not found \"{}\"", path.display()).into());
}
help::open_file(path).stringify_err()
+1 -1
View File
@@ -99,7 +99,7 @@ pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<ser
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
// 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply()
}
logging_error!(Type::Core, CoreManager::global().update_config().await);
logging_error!(Type::Core, CoreManager::global().apply_generate_config().await);
Ok(())
}
+2 -2
View File
@@ -1,10 +1,10 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, config::IVerge, feat};
use clash_verge_draft::SharedBox;
use clash_verge_draft::SharedDraft;
/// 获取Verge配置
#[tauri::command]
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
feat::fetch_verge_config().await.stringify_err()
}
+88 -7
View File
@@ -1,6 +1,6 @@
use super::{IClashTemp, IProfiles, IVerge};
use crate::{
config::{PrfItem, profiles_append_item_safe},
config::{PrfItem, profiles_append_item_safe, runtime::IRuntime},
constants::{files, timing},
core::{
CoreManager,
@@ -16,9 +16,9 @@ use anyhow::{Result, anyhow};
use backoff::{Error as BackoffError, ExponentialBackoff};
use clash_verge_draft::Draft;
use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_types::runtime::IRuntime;
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::path::PathBuf;
use std::{collections::HashSet, path::PathBuf};
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
use tokio::sync::OnceCell;
use tokio::time::sleep;
@@ -65,6 +65,9 @@ impl Config {
pub async fn init_config() -> Result<()> {
Self::ensure_default_profile_items().await?;
let verge = Self::verge().await.latest_arc();
clash_verge_i18n::sync_locale(verge.language.as_deref());
// init Tun mode
let handle = Handle::app_handle();
let is_admin = is_current_app_handle_admin(handle);
@@ -89,6 +92,12 @@ impl Config {
handle::Handle::notice_message(msg_type, msg_content);
}
{
let profiles = Self::profiles().await.data_arc();
// Logging error internally
let _ = profiles.cleanup_orphaned_files().await;
}
Ok(())
}
@@ -165,18 +174,23 @@ impl Config {
};
let runtime = Self::runtime().await;
let runtime_arc = runtime.latest_arc();
let config = runtime_arc
let runtime_lastest = runtime.latest_arc();
// Fall back to committed config if runtime config is missing
let runtime_data = runtime.data_arc();
let config = runtime_lastest
.config
.as_ref()
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
.or_else(|| runtime_data.config.as_ref())
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
Ok(path)
}
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
let (mut config, exists_keys, logs) = enhance::enhance().await;
sanitize_tunnels_proxy(&mut config);
Self::runtime().await.edit_draft(|d| {
*d = IRuntime {
@@ -238,6 +252,73 @@ impl Config {
}
}
fn sanitize_tunnels_proxy(config: &mut Mapping) {
// 检查是否存在 tunnels
if !config
.get("tunnels")
.and_then(|v| v.as_sequence())
.is_some_and(|t| tunnels_need_validation(t))
{
return;
}
// 在需要时,收集可用目标(proxies + proxy-groups + 内建)
let mut valid: HashSet<String> = HashSet::with_capacity(64);
collect_names(config, "proxies", &mut valid);
collect_names(config, "proxy-groups", &mut valid);
valid.insert("DIRECT".into());
valid.insert("REJECT".into());
let Some(tunnels) = config.get_mut("tunnels").and_then(|v| v.as_sequence_mut()) else {
return;
};
// 修改 tunnels:删除无效 proxy
for item in tunnels {
let Some(tunnel) = item.as_mapping_mut() else { continue };
let Some(proxy_name) = tunnel.get("proxy").and_then(|v| v.as_str()) else {
continue;
};
if proxy_name == "DIRECT" || proxy_name == "REJECT" {
continue;
}
if !valid.contains(proxy_name) {
tunnel.remove("proxy");
}
}
}
// tunnels 存在且至少有一条 tunnel 的 proxy 需要校验时才返回 true
fn tunnels_need_validation(tunnels: &[Value]) -> bool {
tunnels.iter().any(|item| {
item.as_mapping()
.and_then(|t| t.get("proxy"))
.and_then(|p| p.as_str())
.is_some_and(|name| name != "DIRECT" && name != "REJECT")
})
}
fn collect_names(config: &Mapping, list_key: &str, out: &mut HashSet<String>) {
let Some(Value::Sequence(seq)) = config.get(list_key) else {
return;
};
for item in seq {
let Value::Mapping(map) = item else {
continue;
};
if let Some(Value::String(n)) = map.get("name")
&& !n.is_empty()
{
out.insert(n.into());
}
}
}
#[derive(Debug)]
pub enum ConfigType {
Run,
+1
View File
@@ -4,6 +4,7 @@ mod config;
mod encrypt;
mod prfitem;
pub mod profiles;
pub mod runtime;
mod verge;
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, verge::*};
+45 -3
View File
@@ -12,6 +12,9 @@ use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::time::Duration;
use tokio::fs;
// TODO, use other re-export
use reqwest_dav::re_exports::url::form_urlencoded;
use tauri::Url;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem {
@@ -278,9 +281,17 @@ impl PrfItem {
ProxyType::None
};
let url = fix_dirty_url(url)?;
// 使用网络管理器发送请求
let resp = match NetworkManager::new()
.get_with_interrupt(url, proxy_type, Some(timeout), user_agent.clone(), accept_invalid_certs)
.get_with_interrupt(
url.as_str(),
proxy_type,
Some(timeout),
user_agent.clone(),
accept_invalid_certs,
)
.await
{
Ok(r) => r,
@@ -340,7 +351,9 @@ impl PrfItem {
},
}
}
None => Some(crate::utils::help::get_last_part_and_decode(url).unwrap_or_else(|| "Remote File".into())),
None => {
Some(crate::utils::help::get_last_part_and_decode(url.as_str()).unwrap_or_else(|| "Remote File".into()))
}
};
let update_interval = match update_interval {
Some(val) => Some(val),
@@ -410,7 +423,7 @@ impl PrfItem {
name: Some(name),
desc: desc.cloned(),
file: Some(file),
url: Some(url.into()),
url: Some(url.as_str().into()),
selected: None,
extra,
option: Some(PrfOption {
@@ -569,3 +582,32 @@ impl PrfItem {
const fn default_allow_auto_update() -> Option<bool> {
Some(true)
}
/// Fix URLs where query parameters are incorrectly appended to the path segment
///
/// Incorrect Example: https://example.com/path&param1=value1
fn fix_dirty_url(input: &str) -> Result<Url> {
let mut url = match Url::parse(input) {
Ok(u) => u,
Err(e) => {
return Err(anyhow::anyhow!(
"failed to parse deep link url: {:?}, input: {:?}",
e,
input
));
}
};
if url.query().is_none() && url.path().contains('&') {
let path = url.path().to_string();
if let Some((clean_path, dirty_params)) = path.split_once('&') {
url.set_path(clean_path);
url.query_pairs_mut()
.extend_pairs(form_urlencoded::parse(dirty_params.as_bytes()));
}
}
Ok(url)
}
+35 -47
View File
@@ -8,7 +8,7 @@ use clash_verge_logging::{Type, logging};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::{collections::HashSet, sync::Arc};
use std::collections::HashSet;
use tokio::fs;
/// Define the `profiles.yaml` schema
@@ -31,8 +31,8 @@ pub struct IProfilePreview<'a> {
#[derive(Debug, Clone)]
pub struct CleanupResult {
pub total_files: usize,
pub deleted_files: Vec<String>,
pub failed_deletions: Vec<String>,
pub deleted_files: usize,
pub failed_deletions: usize,
}
macro_rules! patch {
@@ -45,13 +45,9 @@ macro_rules! patch {
impl IProfiles {
// Helper to find and remove an item by uid from the items vec, returning its file name (if any).
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<String>) -> Option<String> {
for (i, _) in items.iter().enumerate() {
if items[i].uid == target_uid {
return items.remove(i).file;
}
}
None
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> {
let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?;
items.remove(index).file
}
pub async fn new() -> Self {
@@ -126,15 +122,6 @@ impl IProfiles {
bail!("failed to get the profile item \"uid:{}\"", uid_str);
}
pub fn get_item_arc(&self, uid: &str) -> Option<Arc<PrfItem>> {
self.items.as_ref().and_then(|items| {
items
.iter()
.find(|it| it.uid.as_deref() == Some(uid))
.map(|it| Arc::new(it.clone()))
})
}
/// append new item
/// if the file_data is some
/// then should save the data to file
@@ -276,33 +263,38 @@ impl IProfiles {
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(uid);
let current = current.clone();
let item = self.get_item(uid)?;
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
let (merge_uid, script_uid, rules_uid, proxies_uid, groups_uid) = {
let item = self.get_item(uid)?;
let option = item.option.as_ref();
(
option.and_then(|e| e.merge.clone()),
option.and_then(|e| e.script.clone()),
option.and_then(|e| e.rules.clone()),
option.and_then(|e| e.proxies.clone()),
option.and_then(|e| e.groups.clone()),
)
};
let mut items = self.items.take().unwrap_or_default();
// remove the main item (if exists) and delete its file
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) {
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.as_str())) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
// remove related extension items (merge, script, rules, proxies, groups)
if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) {
if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.as_deref()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) {
if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.as_deref()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) {
if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.as_deref()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) {
if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.as_deref()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) {
if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.as_deref()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
// delete the original uid
@@ -374,15 +366,11 @@ impl IProfiles {
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
pub async fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
pub async fn cleanup_orphaned_files(&self) -> Result<()> {
let profiles_dir = dirs::app_profiles_dir()?;
if !profiles_dir.exists() {
return Ok(CleanupResult {
total_files: 0,
deleted_files: vec![],
failed_deletions: vec![],
});
return Ok(());
}
// 获取所有 active profile 的文件名集合
@@ -393,11 +381,11 @@ impl IProfiles {
// 扫描 profiles 目录下的所有文件
let mut total_files = 0;
let mut deleted_files = vec![];
let mut failed_deletions = vec![];
let mut deleted_files = 0;
let mut failed_deletions = 0;
for entry in std::fs::read_dir(&profiles_dir)? {
let entry = entry?;
let mut dir_entries = tokio::fs::read_dir(&profiles_dir).await?;
while let Some(entry) = dir_entries.next_entry().await? {
let path = entry.path();
if !path.is_file() {
@@ -419,11 +407,11 @@ impl IProfiles {
if !active_files.contains(file_name) {
match path.to_path_buf().remove_if_exists().await {
Ok(_) => {
deleted_files.push(file_name.into());
logging!(info, Type::Config, "已清理冗余文件: {file_name}");
deleted_files += 1;
logging!(debug, Type::Config, "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}").into());
failed_deletions += 1;
logging!(warn, Type::Config, "Warning: 清理文件失败: {file_name} - {e}");
}
}
@@ -442,11 +430,11 @@ impl IProfiles {
Type::Config,
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
result.total_files,
result.deleted_files.len(),
result.failed_deletions.len()
result.deleted_files,
result.failed_deletions
);
Ok(result)
Ok(())
}
/// 不删除全局扩展配置
@@ -2,7 +2,9 @@ use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"];
use crate::enhance::field::use_keys;
const PATCH_CONFIG_INNER: [&str; 5] = ["allow-lan", "ipv6", "log-level", "unified-delay", "tunnels"];
#[derive(Default, Clone)]
pub struct IRuntime {
@@ -20,7 +22,7 @@ impl IRuntime {
Self::default()
}
// 这里只更改 allow-lan | ipv6 | log-level | tun
// 这里只更改 allow-lan | ipv6 | log-level | tun | tunnels
#[inline]
pub fn patch_config(&mut self, patch: &Mapping) {
let config = if let Some(config) = self.config.as_mut() {
@@ -125,9 +127,8 @@ impl IRuntime {
&& let Some(Value::Sequence(proxies)) = config.get_mut("proxies")
{
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
if let Some(Value::Mapping(proxy)) = proxies
.iter_mut()
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
if let Some(Value::Mapping(proxy)) =
proxies.iter_mut().find(|proxy| proxy.get("name") == Some(dialer_proxy))
&& i != 0
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
{
@@ -137,16 +138,3 @@ impl IRuntime {
}
}
}
// TODO 完整迁移 enhance 行为后移除
#[inline]
fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
config
.iter()
.filter_map(|(key, _)| key.as_str())
.map(|s: &str| {
let mut s: String = s.into();
s.make_ascii_lowercase();
s
})
}
+30 -18
View File
@@ -1,7 +1,7 @@
use crate::config::Config;
use crate::{
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
utils::{dirs, help, i18n},
utils::{dirs, help},
};
use anyhow::Result;
use clash_verge_logging::{Type, logging};
@@ -66,6 +66,13 @@ pub struct IVerge {
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_order: Option<Vec<String>>,
/// toast / notice position on screen
#[serde(skip_serializing_if = "Option::is_none")]
pub notice_position: Option<String>,
/// collapse navigation bar
pub collapse_navbar: Option<bool>,
/// sysproxy tray icon
pub sysproxy_tray_icon: Option<bool>,
@@ -87,6 +94,9 @@ pub struct IVerge {
/// enable proxy guard
pub enable_proxy_guard: Option<bool>,
/// enable bypass format check
pub enable_bypass_check: Option<bool>,
/// enable dns settings - this controls whether dns_config.yaml is applied
pub enable_dns_settings: Option<bool>,
@@ -145,6 +155,9 @@ pub struct IVerge {
/// 是否自动检测当前节点延迟
pub enable_auto_delay_detection: Option<bool>,
/// 自动检测当前节点延迟的间隔(分钟)
pub auto_delay_detection_interval_minutes: Option<u64>,
/// 是否使用内部的脚本支持,默认为真
pub enable_builtin_enhanced: Option<bool>,
@@ -222,7 +235,10 @@ pub struct IVerge {
// pub enable_tray_icon: Option<bool>,
/// show proxy groups directly on tray root menu
pub tray_inline_proxy_groups: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_proxy_groups_display_mode: Option<String>,
/// show outbound modes directly on tray root menu
pub tray_inline_outbound_modes: Option<bool>,
/// 自动进入轻量模式
pub enable_auto_light_weight_mode: Option<bool>,
@@ -336,19 +352,6 @@ impl IVerge {
self.clash_core.clone().unwrap_or_else(|| "verge-mihomo".into())
}
fn get_system_language() -> String {
let sys_lang = sys_locale::get_locale().unwrap_or_else(|| "en".into()).to_lowercase();
let lang_code = sys_lang.split(['_', '-']).next().unwrap_or("en");
let supported_languages = i18n::get_supported_languages();
if supported_languages.contains(&lang_code.into()) {
lang_code.into()
} else {
String::from("en")
}
}
pub async fn new() -> Self {
match dirs::verge_path() {
Ok(path) => match help::read_yaml::<Self>(&path).await {
@@ -378,7 +381,7 @@ impl IVerge {
app_log_max_size: Some(128),
app_log_max_count: Some(8),
clash_core: Some("verge-mihomo".into()),
language: Some(Self::get_system_language()),
language: Some(clash_verge_i18n::system_language().into()),
theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))]
env_type: Some("bash".into()),
@@ -391,6 +394,8 @@ impl IVerge {
#[cfg(target_os = "macos")]
tray_icon: Some("monochrome".into()),
menu_icon: Some("monochrome".into()),
notice_position: Some("top-right".into()),
collapse_navbar: Some(false),
common_tray_icon: Some(false),
sysproxy_tray_icon: Some(false),
tun_tray_icon: Some(false),
@@ -416,6 +421,7 @@ impl IVerge {
verge_port: Some(7899),
verge_http_enabled: Some(false),
enable_proxy_guard: Some(false),
enable_bypass_check: Some(true),
use_default_bypass: Some(true),
proxy_guard_duration: Some(30),
auto_close_connection: Some(true),
@@ -430,7 +436,8 @@ impl IVerge {
webdav_password: None,
enable_tray_speed: Some(false),
// enable_tray_icon: Some(true),
tray_inline_proxy_groups: Some(true),
tray_proxy_groups_display_mode: Some("default".into()),
tray_inline_outbound_modes: Some(false),
enable_global_hotkey: Some(true),
enable_auto_light_weight_mode: Some(false),
auto_light_weight_minutes: Some(10),
@@ -475,6 +482,8 @@ impl IVerge {
patch!(tray_icon);
patch!(menu_icon);
patch!(menu_order);
patch!(notice_position);
patch!(collapse_navbar);
patch!(common_tray_icon);
patch!(sysproxy_tray_icon);
patch!(tun_tray_icon);
@@ -499,6 +508,7 @@ impl IVerge {
patch!(verge_http_enabled);
patch!(enable_system_proxy);
patch!(enable_proxy_guard);
patch!(enable_bypass_check);
patch!(use_default_bypass);
patch!(system_proxy_bypass);
patch!(proxy_guard_duration);
@@ -516,6 +526,7 @@ impl IVerge {
patch!(default_latency_test);
patch!(default_latency_timeout);
patch!(enable_auto_delay_detection);
patch!(auto_delay_detection_interval_minutes);
patch!(enable_builtin_enhanced);
patch!(proxy_layout_column);
patch!(test_list);
@@ -529,7 +540,8 @@ impl IVerge {
patch!(webdav_password);
patch!(enable_tray_speed);
// patch!(enable_tray_icon);
patch!(tray_inline_proxy_groups);
patch!(tray_proxy_groups_display_mode);
patch!(tray_inline_outbound_modes);
patch!(enable_auto_light_weight_mode);
patch!(auto_light_weight_minutes);
patch!(enable_dns_settings);
-6
View File
@@ -23,9 +23,7 @@ pub mod timing {
use super::Duration;
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(300);
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300);
#[cfg(target_os = "windows")]
pub const SERVICE_WAIT_MAX: Duration = Duration::from_millis(3000);
@@ -33,10 +31,6 @@ pub mod timing {
pub const SERVICE_WAIT_INTERVAL: Duration = Duration::from_millis(200);
}
pub mod retry {
pub const EVENT_EMIT_THRESHOLD: u64 = 10;
}
pub mod files {
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
+9 -8
View File
@@ -52,16 +52,16 @@ impl Operation {
}
pub struct WebDavClient {
config: Arc<ArcSwapOption<WebDavConfig>>,
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
config: ArcSwapOption<WebDavConfig>,
clients: ArcSwap<HashMap<Operation, reqwest_dav::Client>>,
}
impl WebDavClient {
pub fn global() -> &'static Self {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| Self {
config: Arc::new(ArcSwapOption::new(None)),
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
config: ArcSwapOption::new(None),
clients: ArcSwap::new(Arc::new(HashMap::new())),
})
}
@@ -146,11 +146,12 @@ impl WebDavClient {
}
}
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
{
let mut map = (**self.clients.load()).clone();
map.insert(op, client.clone());
self.clients.store(map.into());
self.clients.rcu(|clients_map| {
let mut new_map = (**clients_map).clone();
new_map.insert(op, client.clone());
Arc::new(new_map)
});
}
Ok(client)
+21 -132
View File
@@ -1,34 +1,21 @@
use crate::{APP_HANDLE, constants::timing, singleton};
use parking_lot::RwLock;
use crate::{APP_HANDLE, singleton};
use smartstring::alias::String;
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
};
use tauri::{AppHandle, Manager as _, WebviewWindow};
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::AppHandle;
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
use tokio::sync::RwLockReadGuard;
use super::notification::{ErrorMessage, FrontendEvent, NotificationSystem};
use super::notification::{FrontendEvent, NotificationSystem};
#[derive(Debug)]
pub struct Handle {
is_exiting: AtomicBool,
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
startup_completed: AtomicBool,
pub(crate) notification_system: Arc<RwLock<Option<NotificationSystem>>>,
}
impl Default for Handle {
fn default() -> Self {
Self {
is_exiting: AtomicBool::new(false),
startup_errors: Arc::new(RwLock::new(Vec::new())),
startup_completed: AtomicBool::new(false),
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
}
}
}
@@ -40,19 +27,6 @@ impl Handle {
Self::default()
}
pub fn init(&self) {
if self.is_exiting() {
return;
}
let mut system_opt = self.notification_system.write();
if let Some(system) = system_opt.as_mut()
&& !system.is_running
{
system.start();
}
}
pub fn app_handle() -> &'static AppHandle {
#[allow(clippy::expect_used)]
APP_HANDLE.get().expect("App handle not initialized")
@@ -62,142 +36,57 @@ impl Handle {
Self::app_handle().mihomo().read().await
}
pub fn get_window() -> Option<WebviewWindow> {
Self::app_handle().get_webview_window("main")
}
pub fn refresh_clash() {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::RefreshClash);
}
Self::send_event(FrontendEvent::RefreshClash);
}
pub fn refresh_verge() {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::RefreshVerge);
}
Self::send_event(FrontendEvent::RefreshVerge);
}
pub fn notify_profile_changed(profile_id: String) {
pub fn notify_profile_changed(profile_id: &String) {
Self::send_event(FrontendEvent::ProfileChanged {
current_profile_id: profile_id,
});
}
pub fn notify_timer_updated(profile_index: String) {
pub fn notify_timer_updated(profile_index: &String) {
Self::send_event(FrontendEvent::TimerUpdated { profile_index });
}
pub fn notify_profile_update_started(uid: String) {
pub fn notify_profile_update_started(uid: &String) {
Self::send_event(FrontendEvent::ProfileUpdateStarted { uid });
}
pub fn notify_profile_update_completed(uid: String) {
pub fn notify_profile_update_completed(uid: &String) {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
}
// TODO 利用 &str 等缩短 Clone
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
let handle = Self::global();
let status_str = status.into();
pub fn notice_message<S: AsRef<str>, M: Into<String>>(status: S, msg: M) {
let status_str = status.as_ref();
let msg_str = msg.into();
if !handle.startup_completed.load(Ordering::Acquire) {
handle.startup_errors.write().push(ErrorMessage {
status: status_str,
message: msg_str,
});
return;
}
if handle.is_exiting() {
return;
}
Self::send_event(FrontendEvent::NoticeMessage {
status: status_str,
message: msg_str,
});
}
pub fn set_is_exiting(&self) {
self.is_exiting.store(true, Ordering::Release);
}
pub fn is_exiting(&self) -> bool {
self.is_exiting.load(Ordering::Acquire)
}
fn send_event(event: FrontendEvent) {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(event);
}
}
pub fn mark_startup_completed(&self) {
self.startup_completed.store(true, Ordering::Release);
self.send_startup_errors();
}
fn send_startup_errors(&self) {
let errors = {
let mut errors = self.startup_errors.write();
std::mem::take(&mut *errors)
};
if errors.is_empty() {
return;
}
let _ = thread::Builder::new()
.name("startup-errors-sender".into())
.spawn(move || {
thread::sleep(timing::STARTUP_ERROR_DELAY);
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
for error in errors {
if handle.is_exiting() {
break;
}
system.send_event(FrontendEvent::NoticeMessage {
status: error.status,
message: error.message,
});
thread::sleep(timing::ERROR_BATCH_DELAY);
}
}
});
}
pub fn set_is_exiting(&self) {
self.is_exiting.store(true, Ordering::Release);
let mut system_opt = self.notification_system.write();
if let Some(system) = system_opt.as_mut() {
system.shutdown();
}
}
pub fn is_exiting(&self) -> bool {
self.is_exiting.load(Ordering::Acquire)
NotificationSystem::send_event(event);
}
}
+11 -5
View File
@@ -1,6 +1,7 @@
use crate::process::AsyncHandler;
use crate::singleton;
use crate::utils::notification::{NotificationEvent, notify_event};
use crate::utils::window_manager::WindowManager;
use crate::{config::Config, core::handle, feat, module::lightweight::entry_lightweight_mode};
use anyhow::{Result, bail};
use arc_swap::ArcSwap;
@@ -243,7 +244,7 @@ impl Hotkey {
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event);
let hotkey = hotkey_event.key;
if hotkey == Code::KeyQ && is_quit {
if let Some(window) = handle::Handle::get_window()
if let Some(window) = WindowManager::get_main_window()
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
@@ -260,8 +261,9 @@ impl Hotkey {
Self::execute_function(function);
} else {
use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
let window = WindowManager::get_main_window();
let is_visible = WindowManager::is_main_window_visible(window.as_ref());
let is_focused = WindowManager::is_main_window_focused(window.as_ref());
if is_focused && is_visible {
Self::execute_function(function);
@@ -287,8 +289,12 @@ singleton!(Hotkey, INSTANCE);
impl Hotkey {
pub async fn init(&self, skip: bool) -> Result<()> {
if skip {
logging!(debug, Type::Hotkey, "skip register all hotkeys");
return Ok(());
}
let verge = Config::verge().await;
let enable_global_hotkey = !skip && verge.data_arc().enable_global_hotkey.unwrap_or(true);
let enable_global_hotkey = verge.latest_arc().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -298,7 +304,7 @@ impl Hotkey {
);
// Extract hotkeys data before async operations
let hotkeys = verge.data_arc().hotkeys.clone();
let hotkeys = verge.latest_arc().hotkeys.clone();
if let Some(hotkeys) = hotkeys {
logging!(debug, Type::Hotkey, "Has {} hotkeys need to register", hotkeys.len());
+221 -1
View File
@@ -1 +1,221 @@
// TODO: global logger to record verge log message
use std::{
str::FromStr as _,
sync::{
Arc,
atomic::{AtomicU64, AtomicUsize, Ordering},
},
};
use anyhow::{Result, bail};
use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::WriterConfig;
use compact_str::CompactString;
use flexi_logger::{
Cleanup, Criterion, DeferredNow, FileSpec, LogSpecBuilder, LogSpecification, LoggerHandle,
writers::{FileLogWriter, FileLogWriterBuilder, LogWriter as _},
};
use log::{Level, LevelFilter, Record};
use parking_lot::{Mutex, RwLock};
use crate::{
core::service,
singleton,
utils::dirs::{self, service_log_dir, sidecar_log_dir},
};
pub struct Logger {
handle: Arc<Mutex<Option<LoggerHandle>>>,
sidecar_file_writer: Arc<RwLock<Option<FileLogWriter>>>,
log_level: Arc<RwLock<LevelFilter>>,
log_max_size: AtomicU64,
log_max_count: AtomicUsize,
}
impl Default for Logger {
fn default() -> Self {
Self {
handle: Arc::new(Mutex::new(None)),
sidecar_file_writer: Arc::new(RwLock::new(None)),
log_level: Arc::new(RwLock::new(LevelFilter::Info)),
log_max_size: AtomicU64::new(128),
log_max_count: AtomicUsize::new(8),
}
}
}
singleton!(Logger, LOGGER);
impl Logger {
fn new() -> Self {
Self::default()
}
pub async fn init(&self) -> Result<()> {
let (log_level, log_max_size, log_max_count) = {
let verge_guard = crate::config::Config::verge().await;
let verge = verge_guard.latest_arc();
(
verge.get_log_level(),
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let log_level = std::env::var("RUST_LOG")
.ok()
.and_then(|v| log::LevelFilter::from_str(&v).ok())
.unwrap_or(log_level);
*self.log_level.write() = log_level;
self.log_max_size.store(log_max_size, Ordering::SeqCst);
self.log_max_count.store(log_max_count, Ordering::SeqCst);
#[cfg(not(any(feature = "tauri-dev", feature = "tokio-trace")))]
{
let log_spec = Self::generate_log_spec(log_level);
let log_dir = dirs::app_logs_dir()?;
let logger = flexi_logger::Logger::with(log_spec)
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(log_level.into())
.format(clash_verge_logger::console_format)
.format_for_files(clash_verge_logger::file_format_with_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
);
let mut filter_modules = vec!["wry", "tokio_tungstenite", "tungstenite"];
#[cfg(not(feature = "tracing"))]
filter_modules.push("tauri::ipc");
#[cfg(feature = "tracing")]
filter_modules.extend(["kode_bridge"]);
let logger = logger.filter(Box::new(clash_verge_logging::NoModuleFilter(filter_modules)));
let handle = logger.start()?;
*self.handle.lock() = Some(handle);
}
let sidecar_file_writer = self.generate_sidecar_writer()?;
*self.sidecar_file_writer.write() = Some(sidecar_file_writer);
Ok(())
}
fn generate_log_spec(log_level: LevelFilter) -> LogSpecification {
let mut spec = LogSpecBuilder::new();
let log_level = std::env::var("RUST_LOG")
.ok()
.and_then(|v| log::LevelFilter::from_str(&v).ok())
.unwrap_or(log_level);
spec.default(log_level);
#[cfg(feature = "tracing")]
spec.module("tauri", log::LevelFilter::Debug)
.module("wry", log::LevelFilter::Off)
.module("tauri_plugin_mihomo", log::LevelFilter::Off);
spec.build()
}
fn generate_file_log_writer(&self) -> Result<FileLogWriterBuilder> {
let log_dir = dirs::app_logs_dir()?;
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
let flwb = FileLogWriter::builder(FileSpec::default().directory(log_dir).basename("")).rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
);
Ok(flwb)
}
/// only update app log level
pub fn update_log_level(&self, level: LevelFilter) -> Result<()> {
*self.log_level.write() = level;
let log_level = self.log_level.read().to_owned();
if let Some(handle) = self.handle.lock().as_mut() {
let log_spec = Self::generate_log_spec(log_level);
handle.set_new_spec(log_spec);
handle.adapt_duplication_to_stdout(log_level.into())?;
} else {
bail!("failed to get logger handle, make sure it init");
};
Ok(())
}
/// update app and mihomo core log config
pub async fn update_log_config(&self, log_max_size: u64, log_max_count: usize) -> Result<()> {
self.log_max_size.store(log_max_size, Ordering::SeqCst);
self.log_max_count.store(log_max_count, Ordering::SeqCst);
if let Some(handle) = self.handle.lock().as_ref() {
let log_file_writer = self.generate_file_log_writer()?;
handle.reset_flw(&log_file_writer)?;
} else {
bail!("failed to get logger handle, make sure it init");
};
let sidecar_writer = self.generate_sidecar_writer()?;
*self.sidecar_file_writer.write() = Some(sidecar_writer);
// update service writer config
if service::is_service_ipc_path_exists() && service::is_service_available().await.is_ok() {
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into();
clash_verge_service_ipc::update_writer(&WriterConfig {
directory: service_log_dir,
max_log_size: log_max_size * 1024,
max_log_files: log_max_count,
})
.await?;
}
Ok(())
}
fn generate_sidecar_writer(&self) -> Result<FileLogWriter> {
let sidecar_log_dir = sidecar_log_dir()?;
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
Ok(FileLogWriter::builder(
FileSpec::default()
.directory(sidecar_log_dir)
.basename("sidecar")
.suppress_timestamp(),
)
.format(clash_verge_logger::file_format_without_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
)
.try_build()?)
}
pub fn writer_sidecar_log(&self, level: Level, message: &CompactString) {
if let Some(writer) = self.sidecar_file_writer.read().as_ref() {
let mut now = DeferredNow::default();
let args = format_args!("{}", message);
let record = Record::builder().args(args).level(level).target("sidecar").build();
let _ = writer.write(&mut now, &record);
} else {
logging!(error, Type::System, "failed to get sidecar file log writer");
}
}
pub fn service_writer_config(&self) -> Result<WriterConfig> {
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into();
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
let writer_config = WriterConfig {
directory: service_log_dir,
max_log_size: log_max_size * 1024,
max_log_files: log_max_count,
};
Ok(writer_config)
}
}
+4 -2
View File
@@ -1,13 +1,12 @@
use super::CoreManager;
use crate::{
config::{Config, ConfigType},
config::{Config, ConfigType, runtime::IRuntime},
constants::timing,
core::{handle, validate::CoreConfigValidator},
utils::{dirs, help},
};
use anyhow::{Result, anyhow};
use clash_verge_logging::{Type, logging};
use clash_verge_types::runtime::IRuntime;
use smartstring::alias::String;
use std::{collections::HashSet, path::PathBuf, time::Instant};
use tauri_plugin_mihomo::Error as MihomoError;
@@ -60,7 +59,10 @@ impl CoreManager {
async fn perform_config_update(&self) -> Result<(bool, String)> {
Config::generate().await?;
self.apply_generate_config().await
}
pub async fn apply_generate_config(&self) -> Result<(bool, String)> {
match CoreConfigValidator::global().validate_config().await {
Ok((true, _)) => {
let run_path = Config::generate_file(ConfigType::Run).await?;
+7 -1
View File
@@ -83,7 +83,7 @@ impl CoreManager {
#[cfg(target_os = "windows")]
async fn wait_for_service_if_needed(&self) {
use crate::{config::Config, constants::timing};
use crate::{config::Config, constants::timing, core::service};
use backoff::{Error as BackoffError, ExponentialBackoff};
let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
@@ -108,6 +108,12 @@ impl CoreManager {
return Ok(());
}
// If the service IPC path is not ready yet, treat it as transient and retry.
// Running init/refresh too early can mark service state unavailable and break later config reloads.
if !service::is_service_ipc_path_exists() {
return Err(BackoffError::transient(anyhow::anyhow!("Service IPC not ready")));
}
manager.init().await.map_err(BackoffError::transient)?;
let _ = manager.refresh().await;
+12 -11
View File
@@ -2,14 +2,13 @@ use super::{CoreManager, RunningMode};
use crate::{
AsyncHandler,
config::{Config, IClashTemp},
core::{handle, manager::CLASH_LOGGER, service},
core::{handle, logger::Logger, manager::CLASH_LOGGER, service},
logging,
utils::{dirs, init::sidecar_writer},
utils::dirs,
};
use anyhow::Result;
use clash_verge_logging::{SharedWriter, Type, write_sidecar_log};
use clash_verge_logging::Type;
use compact_str::CompactString;
use flexi_logger::DeferredNow;
use log::Level;
use scopeguard::defer;
use tauri_plugin_shell::ShellExt as _;
@@ -31,6 +30,8 @@ impl CoreManager {
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
#[cfg(unix)]
let previous_mask = unsafe { tauri_plugin_clash_verge_sysinfo::libc::umask(0o007) };
let (mut rx, child) = app_handle
.shell()
.sidecar(clash_core.as_str())?
@@ -47,6 +48,10 @@ impl CoreManager {
&IClashTemp::guard_external_controller_ipc(),
])
.spawn()?;
#[cfg(unix)]
unsafe {
tauri_plugin_clash_verge_sysinfo::libc::umask(previous_mask)
};
let pid = child.pid();
logging!(trace, Type::Core, "Sidecar started with PID: {}", pid);
@@ -54,20 +59,16 @@ impl CoreManager {
self.set_running_child_sidecar(child);
self.set_running_mode(RunningMode::Sidecar);
let shared_writer: SharedWriter = std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?));
AsyncHandler::spawn(|| async move {
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let mut now = DeferredNow::default();
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
write_sidecar_log(shared_writer.lock().await, &mut now, Level::Error, &message);
let message = CompactString::from(&*String::from_utf8_lossy(&line));
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message).await;
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
let mut now = DeferredNow::default();
let message = if let Some(code) = term.code {
CompactString::from(format!("Process terminated with code: {}", code))
} else if let Some(signal) = term.signal {
@@ -75,7 +76,7 @@ impl CoreManager {
} else {
CompactString::from("Process terminated")
};
write_sidecar_log(shared_writer.lock().await, &mut now, Level::Info, &message);
Logger::global().writer_sidecar_log(Level::Info, &message);
CLASH_LOGGER.clear_logs().await;
break;
}

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