Compare commits

...
287 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
Tunglies b35d0ac16f chore: prerelease 2.4.4-rc.1 2025-12-16 16:05:48 +08:00
Slinetrac 3198c3f023 chore(deps): bump npm deps 2025-12-16 11:48:18 +08:00
Slinetrac a3df3fc2a6 refactor(uri-parser): centralize scheme dispatch and harden parsing 2025-12-15 14:46:21 +08:00
Tunglies 507d85ac2f fix: update dependencies in Cargo.lock and Cargo.toml for compatibility 2025-12-15 14:26:54 +08:00
Tunglies 236e5c56c3 fix: update windows-sys and socket2 dependencies in Cargo.lock 2025-12-15 14:23:42 +08:00
Tunglies 5b63f350ed fix: update edition and rust-version fields in Cargo.toml files 2025-12-15 14:17:56 +08:00
Tunglies 5f885bbe32 fix: update clash_verge_service_ipc to version 2.0.26 and kode-bridge to version 0.3.5 2025-12-15 14:09:33 +08:00
oomeow 6e170993dc docs: update Changelog.md 2025-12-14 13:39:54 +08:00
oomeow ee6b857336 perf: cache listen functions to prevent always repeatedly remove and re-add listener on useLayoutEvents hook 2025-12-14 13:06:34 +08:00
Slinetrac 195f470bca docs: Changelog.md 2025-12-14 10:07:39 +08:00
xuvjso 37c2428f49 fix: head state not updated when using config.yaml as current profile (#5827) 2025-12-14 10:01:46 +08:00
Slinetrac 34793f9880 chore(husky): support cargo workspace 2025-12-13 20:28:25 +08:00
Slinetrac 04e50824f1 chore(issue): adjust issue template 2025-12-13 20:00:18 +08:00
oomeow da7faf815f Revert "perf: improve check tun mode available when update tray menu"
This reverts commit af7b33d35b.
Closes: https://github.com/clash-verge-rev/clash-verge-rev/issues/5806
2025-12-13 18:26:27 +08:00
oomeow af9b67186e perf: handle tray icon event to reduce always spawn thread 2025-12-13 16:49:55 +08:00
oomeow c5fa64a213 perf: use blocking recv method reduce unnecessary polling overhead. 2025-12-13 16:10:15 +08:00
oomeow 5c42658a2e fix: init work config and resources twice 2025-12-13 15:21:22 +08:00
oomeow 3c1bcd7d72 fix: macOS tray icon color flickers during app launch 2025-12-13 13:46:49 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 328803b894 chore(deps): update dependency react-i18next to v16.5.0 (#5808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 12:08:51 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> fbe44cf9c1 chore(deps): update actions/upload-artifact action to v6 (#5812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 12:08:29 +08:00
Slinetrac 6db559f346 chore(i18n): optimize proxy strategy names and GeoSite capitalization #5811
Closes #5811
2025-12-13 11:19:59 +08:00
Slinetrac f0c7aca3ff fix(window): hover effect of the minimize and close button does not disappear
`5bb4539e3f2d04288bf52164fdbf47bcaf949aea` was accidentally reverted in `8316c75c7836af254257d6be338d99c5c4c5923c`, so we reapply it here.
2025-12-12 17:27:05 +08:00
Slinetrac 8316c75c78 Revert "refactor(window): improve WindowProvider implementation"
May break frontend input on macOS
This reverts commit d8e386e3
2025-12-12 16:45:37 +08:00
Tunglies e56a989944 fix: improve service availability checks #5788 2025-12-12 16:07:36 +08:00
Tunglies cea78289c1 Revert "fix: service unexpected status after restart app in unix (#5768)"
This reverts commit 2995f61087.
2025-12-12 12:46:51 +08:00
Slinetrac 1a2d45555d docs: unify language list separators 2025-12-12 09:31:00 +08:00
Mehdi BahramiandSlinetrac 11879e224c docs(i18n): add README_fa.md (#5792)
* Create README_fa.md

in Persian

* Update README_fa.md

Readme file in Persian

* Add links for Persian and Korean in README_es

Added links for Persian and Korean languages in the README.

* Add Persian link to language options in README

Added Persian language link to the README.

* Add Persian link to Japanese README

* Add Persian link to Korean README

* Add links for Korean and Persian in README_ru

Added links to Korean and Persian translations in the README.

* update

fix error

* Add Persian language link to README

update

* Update link to Persian README in multiple languages section

fix

* Update README_fa.md

* style: prettier

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2025-12-12 09:23:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> c8628f32df chore(deps): update npm dependencies to v19.2.3 (#5802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 08:41:19 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 1bb1cb3511 chore(deps): update actions/cache action to v5 (#5801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 08:38:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 711aaf05d3 chore(deps): update npm dependencies to v19.2.2 (#5800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 08:38:14 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 430c946b61 chore(deps): update dependency node to v24.12.0 (#5799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 08:37:49 +08:00
hankandTunglies 2995f61087 fix: service unexpected status after restart app in unix (#5768)
* fix: 修复macos重启应用后需要重设服务器模式问题

* chore: remove package-lock.json (using pnpm)

* Delete test.sh

* refactor(lifecycle): remove unnecessary conditional compilation for macOS and Windows

* refactor(timing): remove conditional compilation for service wait durations on Windows and macOS

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-12-11 20:09:27 +08:00
Sline 2aaa91f4cb perf(connections): improve scrolling performance on macOS (#5787) 2025-12-11 17:54:04 +08:00
Mehdi BahramiandSline 104fd11b04 Update settings.json (#5791)
* Update settings.json

Add translation to Persian

* Update src/locales/fa/settings.json

---------

Co-authored-by: Sline <realakayuki@gmail.com>
2025-12-11 17:39:04 +08:00
Mehdi Bahrami c7dd93004e chore(i18n): fa tunMode notifications (#5790)
translate some
2025-12-11 16:43:07 +08:00
Slinetrac 86dcf0bdcf fix: proxy chain #5776
Closes #5776
2025-12-11 16:23:43 +08:00
Slinetrac 09a4e7e083 docs: Changelog.md 2025-12-11 12:12:33 +08:00
UndoandSline 5bb4539e3f fix(window): hover effect of the minimize and close button does not disappear (#5786)
* fix(window): hover effect of the minimize and close button does not disappear

* docs: add necessary comments

---------

Co-authored-by: Sline <realakayuki@gmail.com>
2025-12-11 12:02:05 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> cad1cf2895 chore(deps): update dependency react-i18next to v16.4.1 (#5780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 02:42:42 +00:00
Tunglies 623ed69a37 fix(logging): adjust conditional compilation for NoModuleFilter based on tracing-full feature 2025-12-11 08:03:32 +08:00
Tunglies 7838fa1e75 refactor(tray): remove timer for updating tray in plan, use update manually to prevent needless refresh to cause visuual flash 2025-12-10 23:40:17 +08:00
Tunglies dd34ced070 fix(service): await for service operations, prevent too early to check service available 2025-12-10 23:29:11 +08:00
Tunglies 66483d0ef6 docs: add section on signing commits to CONTRIBUTING.md 2025-12-10 22:22:53 +08:00
Slinetrac 54858ce1fb chore: move onlyBuiltDependencies to package.json 2025-12-10 17:56:12 +08:00
Tunglies dfa5cff1b7 feat(proxy-groups): filter available groups to show only Selector types in chain mode 2025-12-10 10:04:20 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2b552d6ddb chore(deps): update dependency @tauri-apps/cli to v2.9.6 (#5769)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 19:48:05 +00:00
oomeow af7b33d35b perf: improve check tun mode available when update tray menu 2025-12-09 19:06:57 +08:00
Slinetrac d8e386e394 refactor(window): improve WindowProvider implementation 2025-12-09 16:42:29 +08:00
Slinetrac bae65a523a chore: rename files to kebab-case and update imports 2025-12-09 13:52:42 +08:00
Slinetrac aa767695af chore(types): consolidate traffic monitor types into shared globals 2025-12-09 13:27:46 +08:00
wonfen 494e888b52 fix: typo 2025-12-08 11:26:20 +08:00
Tunglies 5292067284 chore: bump pnpm version to 10.24.0 2025-12-07 21:36:35 +08:00
Sline 9286e921a6 chore(linux): remove WebKit DMABUF env setup logic (#5758) 2025-12-07 16:06:54 +08:00
oomeowandTunglies 3ab985aacc fix: failed to restart core (#5754)
* fix: failed to restart core after install service

* chore: update tauri-plugin-mihomo deps

* fix: update dependencies for tauri-plugin-mihomo and related packages

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-12-07 12:42:32 +08:00
Slinetrac 418fc1c702 chore(vite): add semantic chunk naming 2025-12-07 12:35:39 +08:00
Slinetrac b5da9c9629 chore: update Cargo.lock for sysproxy-rs 2025-12-06 22:53:17 +08:00
oomeow 86936d5322 fix: wrong service status when handle service status error 2025-12-06 20:34:41 +08:00
oomeow c1489647d1 fix: update service status when service is ready 2025-12-06 20:22:45 +08:00
Tunglies af0a3be66d chore: update browserlist-db 2025-12-06 20:15:55 +08:00
361 changed files with 15697 additions and 12926 deletions
+3 -3
View File
@@ -11,11 +11,11 @@ body:
## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
3. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 查看 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本更新日志
5. 请 **务必** 尝试 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本,确定问题是否仍然存在
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 AutoBuild 版本,否则 issue 将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
+2 -2
View File
@@ -10,10 +10,10 @@ body:
value: |
## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
+3 -3
View File
@@ -307,7 +307,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
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.11.1"
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.11.1"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
+26 -20
View File
@@ -46,7 +46,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
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,11 +196,11 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -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,11 +294,11 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -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,11 +439,11 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -535,7 +541,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
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.11.1"
node-version: "24.13.1"
- uses: pnpm/action-setup@v4
name: Install pnpm
+5 -5
View File
@@ -103,11 +103,11 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.13.1"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -153,7 +153,7 @@ jobs:
- name: Upload Artifacts (macOS)
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/dmg/*.dmg
@@ -161,7 +161,7 @@ jobs:
- name: Upload Artifacts (Windows)
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/nsis/*.exe
@@ -169,7 +169,7 @@ jobs:
- name: Upload Artifacts (Linux)
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
+2 -2
View File
@@ -47,12 +47,12 @@ jobs:
- uses: actions/setup-node@v6
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: "24.11.1"
node-version: "24.13.1"
cache: "pnpm"
- name: Restore pnpm cache
if: steps.check_frontend.outputs.frontend == 'true'
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}"
+8 -8
View File
@@ -197,7 +197,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
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.11.1"
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.11.1"
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.11.1"
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.11.1"
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.11.1"
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.11.1"
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.11.1"
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 -48
View File
@@ -1,57 +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..."
(
cd src-tauri
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..."
(
cd src-tauri
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 -37
View File
@@ -1,42 +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..."
(
cd src-tauri
cargo fmt --check
)
echo "[pre-push] Running cargo clippy..."
(
cd src-tauri
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/
+5 -1
View File
@@ -119,13 +119,17 @@ cargo fmt
pnpm format
```
### Signing your commit
Signed commits are required to verify authorship and ensure your contributions can be merged. Reference signing-commits [here](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
### Submitting Your Changes
1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Commit your changes with clear messages.
3. Commit your changes with clear messages and make sure it's signed.
4. Push your branch and submit a pull request.
Generated
+1095 -1238
View File
File diff suppressed because it is too large Load Diff
+29 -20
View File
@@ -1,17 +1,15 @@
[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"
[workspace.package]
edition = "2024"
rust-version = "1.91"
[profile.release]
panic = "abort"
@@ -42,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.4" }
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 -273
View File
@@ -1,287 +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 管理员身份运行时开关 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 系统信号处理
- 优化订阅错误通知,仅在手动触发时
- 隐藏日志中的订阅信息
- 优化部分界面文案文本
</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"]
+14 -20
View File
@@ -16,7 +16,8 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
<a href="./docs/README_es.md">Español</a> ·
<a href="./docs/README_ru.md">Русский</a> ·
<a href="./docs/README_ja.md">日本語</a> ·
<a href="./docs/README_ko.md">한국어</a>
<a href="./docs/README_ko.md">한국어</a> ·
<a href="./docs/README_fa.md">فارسی</a>
</p>
## Preview
@@ -47,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: 规则模式
@@ -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(
+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "clash-verge-signal"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
edition = "2024"
rust-version = "1.91"
[dependencies]
clash-verge-logging = { workspace = true }
+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.workspace = true
rust-version.workspace = true
[dependencies]
serde = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true }
[lints]
workspace = true
-1
View File
@@ -1 +0,0 @@
pub mod runtime;
@@ -1,17 +1,19 @@
[package]
name = "tauri-plugin-clash-verge-sysinfo"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
edition = "2024"
rust-version = "1.91"
[dependencies]
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
### 🐞 修复问题
+14 -20
View File
@@ -16,7 +16,8 @@ A Clash Meta GUI built with <a href="https://github.com/tauri-apps/tauri">Tauri<
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## Preview
@@ -50,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
+2 -14
View File
@@ -16,7 +16,8 @@ Una interfaz gráfica para Clash Meta construida con <a href="https://github.com
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## Vista previa
@@ -62,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
+112
View File
@@ -0,0 +1,112 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
<br>
</h1>
<h3 align="center">
یک رابط کاربری گرافیکی Clash Meta که با <a href="https://github.com/tauri-apps/tauri">Tauri</a> ساخته شده است.
</h3>
<p align="center">
زبان‌ها:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## پیش‌نمایش
| تاریک | روشن |
| ----------------------------------- | ------------------------------------- |
| ![Dark Preview](./preview_dark.png) | ![Light Preview](./preview_light.png) |
## نصب
برای دانلود فایل نصبی متناسب با پلتفرم خود، به [صفحه انتشار](https://github.com/clash-verge-rev/clash-verge-rev/releases) مراجعه کنید.<br> ما بسته‌هایی برای ویندوز (x64/x86)، لینوکس (x64/arm64) و macOS 10.15+ (اینتل/اپل) ارائه می‌دهیم.
#### انتخاب کانال انتشار
| Channel | توضیحات | Link |
| :---------- | :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- |
| Stable | ساخت رسمی با قابلیت اطمینان بالا، ایده‌آل برای استفاده روزانه. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha (EOL) | نسخه‌های قدیمی (Legacy builds) برای اعتبارسنجی خط لوله انتشار (publish pipeline) استفاده می‌شوند. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | نسخه‌های آزمایشی برای آزمایش و دریافت بازخورد. منتظر تغییرات آزمایشی باشید. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### راهنماهای نصب و سوالات متداول
برای مراحل نصب، عیب‌یابی و سوالات متداول، [مستندات پروژه](https://clash-verge-rev.github.io/) را مطالعه کنید.
---
### کانال تلگرام
برای اطلاع از آخرین اخبار به [@clash_verge_rev](https://t.me/clash_verge_re) بپیوندید.
## تبلیغات
#### [Doggygo VPN — شتاب‌دهنده جهانی عملکردگرا](https://verge.dginv.click/#/register?code=oaxsAGo6)
- سرویس شبکه برون مرزی با عملکرد بالا به همراه دوره‌های آزمایشی رایگان، طرح‌های تخفیف‌دار، امکان باز کردن قفل استریم و پشتیبانی درجه یک از پروتکل هیستریا.
- از طریق لینک اختصاصی Clash Verge ثبت نام کنید تا یک دوره آزمایشی ۳ روزه با ۱ گیگابایت ترافیک در روز دریافت کنید: [ثبت نام](https://verge.dginv.click/#/register?code=oaxsAGo6)
- کوپن تخفیف ۲۰٪ ویژه کاربران Clash Verge: `verge20` (محدود به ۵۰۰ بار استفاده)
- بسته تخفیف‌دار از ۱۵.۸ ین در ماه برای ۱۶۰ گیگابایت، به علاوه ۲۰٪ تخفیف اضافی برای صورتحساب سالانه
- توسط یک تیم خارجی با خدمات قابل اعتماد و تا 50٪ سهم درآمد اداره می‌شود
- کلاسترهای متعادل بار با مسیرهای اختصاصی پرسرعت (سازگار با کلاینت‌های قدیمی)، تأخیر فوق‌العاده کم، پخش روان 4K
- اولین ارائه‌دهنده جهانی که از پروتکل «Hysteria2» پشتیبانی می‌کند - کاملاً مناسب برای کلاینت Clash Verge
- پشتیبانی از سرویس‌های استریم و دسترسی به ChatGPT
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
## ویژگی‌ها
- ساخته شده بر اساس Rust با کارایی بالا و فریم‌ورک Tauri 2
- با هسته جاسازی‌شده [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) ارائه می‌شود و از تغییر به کانال «آلفا» پشتیبانی می‌کند.
- رابط کاربری تمیز و مرتب با کنترل‌های رنگ تم، آیکون‌های گروه/سینی پروکسی و `تزریق CSS`
- مدیریت پروفایل پیشرفته (ادغام و کمک‌کننده‌های اسکریپت) با نکات مربوط به سینتکس پیکربندی
- کنترل‌های پروکسی سیستم، حالت محافظت و پشتیبانی از `TUN` (آداپتور شبکه مجازی)
- ویرایشگرهای بصری برای گره‌ها و قوانین
- پشتیبان‌گیری و همگام‌سازی مبتنی بر WebDAV برای تنظیمات
### سوالات متداول
برای راهنمایی‌های مربوط به هر پلتفرم، به [صفحه سوالات متداول](https://clash-verge-rev.github.io/faq/windows.html) مراجعه کنید.
### اهدا
[پشتیبانی از توسعه Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
## توسعه
برای دستورالعمل‌های دقیق مشارکت، به [CONTRIBUTING.md](../CONTRIBUTING.md) مراجعه کنید.
پس از نصب تمام پیش‌نیازهای **Tauri**، پوسته توسعه را با دستور زیر اجرا کنید:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## مشارکت‌ها
مشکلات و درخواست‌های pull مورد استقبال قرار می‌گیرند!
## تقدیر و تشکر
Clash Verge Rev بر اساس این پروژه‌ها ساخته شده یا از آنها الهام گرفته است:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): یک رابط کاربری گرافیکی Clash مبتنی بر Tauri برای ویندوز، macOS و لینوکس..
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): ساخت برنامه‌های دسکتاپ کوچک‌تر، سریع‌تر و امن‌تر با رابط کاربری وب.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): یک تونل مبتنی بر قانون که با زبان Go نوشته شده است.
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): یک تونل مبتنی بر قانون که با زبان Go نوشته شده است.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): رابط کاربری گرافیکی Clash برای ویندوز و macOS.
- [vitejs/vite](https://github.com/vitejs/vite): ابزارهای فرانت‌اند نسل بعدی با DX فوق‌العاده سریع.
## مجوز
مجوز GPL-3.0. برای جزئیات بیشتر به [فایل مجوز](../LICENSE) مراجعه کنید.
+2 -14
View File
@@ -16,7 +16,8 @@
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## プレビュー
@@ -62,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 フレームワークに基づくデスクトップアプリ
+2 -14
View File
@@ -16,7 +16,8 @@
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## 미리보기
@@ -62,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 프레임워크 기반 데스크톱 앱
+2 -14
View File
@@ -16,7 +16,8 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## Предпросмотр
@@ -58,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 },
+61 -46
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.4.4-rc",
"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.12",
"@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.1",
"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.1",
"react-dom": "19.2.1",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.67.0",
"react-i18next": "16.4.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.0",
"react-virtuoso": "^4.16.1",
"swr": "^2.3.7",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main",
"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.12",
"@eslint/js": "^9.39.1",
"@tauri-apps/cli": "2.9.5",
"@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.1",
"@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.1",
"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.24",
"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.94.2",
"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.48.1",
"vite": "^7.2.6",
"typescript-eslint": "^8.56.0",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0"
},
"lint-staged": {
@@ -125,5 +130,15 @@
]
},
"type": "module",
"packageManager": "pnpm@10.22.0"
"packageManager": "pnpm@10.29.2",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@swc/core",
"core-js",
"es5-ext",
"esbuild",
"unrs-resolver"
]
}
}
+2391 -3334
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
onlyBuiltDependencies:
- "@parcel/watcher"
- "@swc/core"
- core-js
- es5-ext
- esbuild
- unrs-resolver
+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", "updateCargoLock"],
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
+251 -64
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"]);
@@ -77,29 +83,34 @@ const IGNORED_KEY_PREFIXES = new Set([
const NOTICE_METHOD_NAMES = new Set(["success", "error", "info", "warning"]);
const NOTICE_SERVICE_IDENTIFIERS = new Set([
"@/services/noticeService",
"./noticeService",
"../services/noticeService",
"@/services/notice-service",
"./notice-service",
"../services/notice-service",
]);
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
+35 -42
View File
@@ -1,14 +1,14 @@
[package]
name = "clash-verge"
version = "2.4.4-rc"
version = "2.4.7"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
build = "build.rs"
edition = { workspace = true }
rust-version = { workspace = true }
edition = "2024"
rust-version = "1.91"
[lib]
name = "app_lib"
@@ -18,7 +18,7 @@ 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 = []
@@ -27,13 +27,14 @@ tracing = []
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 = [
@@ -53,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.24", 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);
}
}

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