Compare commits

...
111 Commits
Author SHA1 Message Date
wonfen a9464ff776 chore: update release log & fix workflow2 2025-08-22 16:30:07 +08:00
Tunglies 335ca817d2 refactor: restrict AsyncHandler usage to Windows platform only 2025-08-22 04:18:21 +08:00
Tunglies 6d112c387d refactor: replace tokio::task::spawn_blocking with AsyncHandler::spawn_blocking for improved task management 2025-08-22 04:05:35 +08:00
Tunglies e4c243de2d refactor: Replace tokio::spawn with AsyncHandler::spawn for better task management
- Replace direct tokio::spawn calls with AsyncHandler::spawn across multiple modules
- Improves task lifecycle management and error handling consistency
- Affected files:
  - src-tauri/src/cmd/network.rs
  - src-tauri/src/core/core.rs
  - src-tauri/src/core/event_driven_proxy.rs
  - src-tauri/src/enhance/tun.rs
  - src-tauri/src/ipc/logs.rs
  - src-tauri/src/ipc/memory.rs
  - src-tauri/src/ipc/monitor.rs
  - src-tauri/src/ipc/traffic.rs
  - src-tauri/src/utils/network.rs
  - src-tauri/src/utils/resolve.rs

This change provides better control over async task spawning and helps prevent
potential issues with unmanaged background tasks.
2025-08-22 03:41:14 +08:00
Tunglies 02f67961a9 feat: add tokio-stream dependency and refactor event loop handling in EventDrivenProxyManager 2025-08-22 03:16:59 +08:00
Tunglies 7d5fd295ed feat: add dev:trace script for enhanced debugging in development 2025-08-22 02:28:44 +08:00
Tunglies daa0b1592d chore: bump version to 2.4.1 2025-08-22 00:29:42 +08:00
Tunglies b411783bbe fix: enhance tag version check for consistency in release workflow 2025-08-22 00:15:56 +08:00
Tunglies 40a59bbc1a fix: disable automatic generation of release notes in workflow 2025-08-21 23:56:02 +08:00
wonfen 93fc4932ee Release 2.4.0 2025-08-21 22:45:55 +08:00
Tunglies 2277d7232e refactor: improve code formatting and readability in autobuild and telegram scripts 2025-08-21 21:23:47 +08:00
Tunglies 435318cf1d fix: simplify return statements in updateProxy method for clarity 2025-08-21 21:23:46 +08:00
wonfen a9a9d8a78f fix(workflow): file version & format 2025-08-21 21:21:52 +08:00
❤是纱雾酱哟~ a2544d237e ci: improve commit checking and update release version script (#4471)
- Increase `fetch-depth` to 50 for more accurate commit history in CI
- Update `release-version.mjs` to use `bash` explicitly for improved compatibility
  - Also avoid errors when invoking the script

Signed-off-by: Dragon1573 <49941141+Dragon1573@users.noreply.github.com>
2025-08-21 21:16:15 +08:00
wonfen 9397ac0174 fix(workflow): download files name & release note 2025-08-21 19:49:17 +08:00
wonfen 4c719da096 chore(workflow): add release info & telegram notification 2025-08-21 18:33:54 +08:00
Tunglies 7613417c33 feat: enhance Tauri build steps with clearer naming and include updater JSON 2025-08-20 23:06:55 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a2a65cade7 chore(deps): update npm dependencies (#4461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:54:47 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 3a07402aa2 chore(deps): update npm dependencies (#4454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:49:45 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
59b67f5d3f chore(deps): update cargo dependencies (#4451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-08-20 22:49:37 +08:00
Tunglies e30cfc3a2f chore: update dependencies and improve IPC request handling 2025-08-20 22:48:08 +08:00
Tunglies 52655d9702 feat: add configuration options to IpcManager for improved client setup 2025-08-20 18:05:31 +08:00
Tunglies e93846ddc1 refactor: improve log management by introducing a constant for max logs and simplifying log level handling 2025-08-19 22:43:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4cf2f6b1e6 chore(deps): update cargo dependencies (#4443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 16:36:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 43a3cb74ac chore(deps): update npm dependencies (#4430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 16:36:24 +08:00
TianHua Liu 3360339c08 docs: move Windows ARM devices note to CONTRIBUTING.md (#4446)
* docs: how to develop locally

* docs: move Windows ARM devices note to CONTRIBUTING.md
2025-08-19 16:28:42 +08:00
Tunglies f0dbe9fa60 feat: add console-subscriber for improved logging and tracing support 2025-08-18 23:40:25 +08:00
Tunglies 756751b765 refactor: simplify app restart logic and improve error handling 2025-08-18 22:33:29 +08:00
Tunglies 85a9f6c8d4 fix: correct log cleanup day mapping and update logging level #4434 2025-08-18 19:14:08 +08:00
AhaoandTunglies 7fe0381850 renew: remove whether to enable_random_port (#4401)
* refactor: streamline clean old assets job by using reusable workflow

* refactor: update clean old assets job to include steps section

* refactor: add checkout step in clean_old_assets job for improved repository access

* fix: correct path to clean old assets workflow in autobuild.yml

* fix: update path to clean old assets workflow in autobuild.yml

* refactor: simplify clean_old_assets job by removing unnecessary steps

* refactor: enhance clean_old_assets job dependencies for improved execution flow

* Revert "refactor: enhance clean_old_assets job dependencies for improved execution flow"

This reverts commit 1a5108b5ad.

* feat: implement get_latest_tauri_commit script and update release versioning logic

* renew: remove whether to enable_random_port

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-08-18 02:03:06 +08:00
Tunglies 537d27d10b fix: clippy errors with new config (#4428)
* refactor: improve code quality with clippy fixes and standardized logging

- Replace dangerous unwrap()/expect() calls with proper error handling
- Standardize logging from log:: to logging\! macro with Type:: classifications
- Fix app handle panics with graceful fallback patterns
- Improve error resilience across 35+ modules without breaking functionality
- Reduce clippy warnings from 300+ to 0 in main library code

* chore: update Cargo.toml configuration

* refactor: resolve all clippy warnings
- Fix Arc clone warnings using explicit Arc::clone syntax across 9 files
- Add #[allow(clippy::expect_used)] to test functions for appropriate expect usage
- Remove no-effect statements from debug code cleanup
- Apply clippy auto-fixes for dbg\! macro removals and path statements
- Achieve zero clippy warnings on all targets with -D warnings flag

* chore: update Cargo.toml clippy configuration

* refactor: simplify macOS job configuration and improve caching

* refactor: remove unnecessary async/await from service and proxy functions

* refactor: streamline pnpm installation in CI configuration

* refactor: simplify error handling and remove unnecessary else statements

* refactor: replace async/await with synchronous locks for core management

* refactor: add workflow_dispatch trigger to clippy job

* refactor: convert async functions to synchronous for service management

* refactor: convert async functions to synchronous for UWP tool invocation

* fix: change wrong logging

* refactor: convert proxy restoration functions to async

* Revert "refactor: convert proxy restoration functions to async"

This reverts commit b82f5d250b.

* refactor: update proxy restoration functions to return Result types

* fix: handle errors during proxy restoration and update async function signatures

* fix: handle errors during proxy restoration and update async function signatures

* refactor: update restore_pac_proxy and restore_sys_proxy functions to async

* fix: convert restore_pac_proxy and restore_sys_proxy functions to async

* fix: await restore_sys_proxy calls in proxy restoration logic

* fix: suppress clippy warnings for unused async functions in proxy restoration

* fix: suppress clippy warnings for unused async functions in proxy restoration
2025-08-18 02:02:25 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a5fdd3f1a2 chore(deps): update npm dependencies (#4400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 15:43:15 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> be8a632a09 chore(deps): update rust crate async-trait to 0.1.89 (#4404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 15:42:31 +08:00
ONEUI8andONEUI-7 95c34f1df5 Update profile.rs (#4408)
Co-authored-by: ONEUI-7 <150009830+ONEUI-7@users.noreply.github.com>
2025-08-16 15:42:21 +08:00
TianHua Liu 8b53a7bd99 docs: how to develop locally (#4409) 2025-08-16 15:38:32 +08:00
Tunglies b169ee8149 Revert "refactor: replace tokio Mutex with parking_lot Mutex for improved performance"
This reverts commit 9cc6dde999.
2025-08-16 04:24:03 +00:00
Tunglies 9cc6dde999 refactor: replace tokio Mutex with parking_lot Mutex for improved performance 2025-08-16 02:44:00 +08:00
Tunglies cf1fbb63c4 refactor: replace log macros with logging utility for IPC monitoring 2025-08-15 20:41:45 +08:00
Tunglies 2aa629ff5d feat: refactor check_commit job to use new workflow inputs and streamline build checks
fix: update tag_name in check_commit job to use a static value

refactor: streamline latest.json handling and improve commit hash comparison logic

fix: update curl command to follow redirects when fetching latest.json
2025-08-14 20:42:26 +08:00
Tunglies 1e2b453c24 refactor: streamline clean old assets job by using reusable workflow
refactor: update clean old assets job to include steps section

refactor: add checkout step in clean_old_assets job for improved repository access

fix: correct path to clean old assets workflow in autobuild.yml

fix: update path to clean old assets workflow in autobuild.yml

refactor: simplify clean_old_assets job by removing unnecessary steps

refactor: enhance clean_old_assets job dependencies for improved execution flow

Revert "refactor: enhance clean_old_assets job dependencies for improved execution flow"

This reverts commit 1a5108b5ad.

feat: implement get_latest_tauri_commit script and update release versioning logic
2025-08-14 20:42:24 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 331e4a4970 chore(deps): update cargo dependencies (#4360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 16:27:14 +08:00
Tunglies ee3ffaef1d refactor: simplify log warning message formatting and remove unused LogLevel enum 2025-08-13 16:25:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7cc3bc83a0 chore(deps): update npm dependencies (#4367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 16:09:56 +08:00
Tunglies 558e28ddaf refactor: simplify log retrieval by removing level parameter and relying on server-side filtering #4293 2025-08-13 01:15:33 +08:00
Sergey KharenkoandTunglies 45e69543b3 fix: auto-detect KDE/Plasma and disable GTK CSD to fix titlebar button freeze (#4380)
* fix: auto-detect KDE/Plasma and disable GTK CSD to fix titlebar button freeze

* chore: update UPDATELOG

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-08-12 19:27:46 +08:00
wonfen 67ee41c5ea fix: ensure TUN is disabled before core exit 2025-08-11 23:19:49 +08:00
wonfen 160ed05178 fix: unify TUN status detection logic 2025-08-11 20:45:23 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5ecfe121b3 chore(deps): update dependency swr to ^2.3.5 (#4359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-09 17:42:09 +08:00
Tunglies a654137af9 fix: adjust cleanup function and reduce refresh interval to 1 second 2025-08-09 10:04:38 +08:00
Tunglies cb591f19fb feat: add workflow to clean old release assets with versioning and dry run support 2025-08-08 23:50:52 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4823a348be chore(deps): update rust crate warp to 0.4.1 (#4342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 23:49:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 32da6ae808 chore(deps): update npm dependencies (#4347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 23:48:55 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7eb70b0f0d chore(deps): update dependency @vitejs/plugin-react to v5 (#4348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 23:47:18 +08:00
Tunglies 52e8e45daf fix: correct log level hierarchy and enhance log filtering logic 2025-08-08 23:30:24 +08:00
Tunglies 319c5b84fa Revert "feat: enhance log filtering by adding support for debug level and updating log hierarchy #4293"
This reverts commit a9cfb2cfaa.
2025-08-08 15:17:46 +08:00
Tunglies 6a93ff1fc1 fix: streamline autobuild versioning by utilizing outputs from check_commit job 2025-08-08 15:15:27 +08:00
Tunglies 6069b654d1 fix: ensure app quit notification is sent before quitting 2025-08-08 14:57:47 +08:00
Tunglies 2af8c32497 Revert "add: home card drag (#4215)"
This reverts commit 84989e0ea3.
2025-08-07 20:05:36 +08:00
Tunglies a3957289c8 fix: enhance asset cleanup and versioning logic in autobuild workflow 2025-08-07 19:47:31 +08:00
Tunglies 3f5cd6c26a fix: resolve system proxy status detection and display inconsistency
- Fixed getSystemProxyActualState logic to properly check actual system status
- Unified system proxy state display across all components
- Replaced systemProxyIndicator with actualState for consistent UI display
- Updated components: setting-system, ProxyControlSwitches, proxy-tun-card
- Added entry to v2.4.0 changelog
2025-08-07 02:18:50 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8046dad56d chore(deps): update npm dependencies (#4324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 22:47:01 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>TungliesTunglies
cdc1fd2d87 chore(deps): update cargo dependencies (#4329)
* chore(deps): update cargo dependencies

* fix: update warp dependency to include server feature

* fix: update return type of scheme_handler to String for consistency

* fix: add hyper-util dependency and update warp path handlers for response status

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-08-06 22:46:46 +08:00
Tunglies 499626b946 fix: resolve intermittent startup deadlock issues
- Optimize configuration access locks to prevent race conditions
- Enhance UI monitoring thread with non-blocking lock operations
- Improve window creation timing and synchronization
- Add comprehensive deadlock detection and debugging logs
- Simplify code structure with better error handling patterns
- Update changelog with user-friendly descriptions
2025-08-06 22:12:00 +08:00
Tunglies a9cfb2cfaa feat: enhance log filtering by adding support for debug level and updating log hierarchy #4293 2025-08-06 20:49:54 +08:00
Tunglies 7b976c16eb feat: enhance autobuild logic to check for Tauri-related changes and manage versioning 2025-08-06 20:34:57 +08:00
Tunglies 44e8a035aa fix: improve profile import validation and handle async lock correctly
fix: refactor import_profile function for improved readability and maintainability
2025-08-05 23:23:11 +08:00
Tunglies 6b57607926 fix: update autobuild versioning logic and improve asset checking regex 2025-08-05 23:12:15 +08:00
Tunglies c3675e48fd fix: update tauri.conf.json version to use full version information including build metadata 2025-08-05 22:56:34 +08:00
Tunglies a66393c609 feat: enhance profile import functionality with timeout and robust refresh strategy 2025-08-05 20:29:36 +08:00
Tunglies 776abaf56d fix: update service messages to use PRODUCTNAME variable for consistency 2025-08-05 20:09:35 +08:00
Tunglies 18808004f4 fix: update Cargo.toml version handling to use the provided version directly 2025-08-05 20:02:47 +08:00
Tunglies db8761946d feat: add autobuild check logic workflow for version and source changes 2025-08-05 06:31:49 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2194a96145 chore(deps): update npm dependencies (#4294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 14:57:24 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> ecd396d70f chore(deps): update cargo dependencies (#4297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 14:46:54 +08:00
Tunglies 5ab0438397 fix: duplicated build version checks 2025-08-04 14:44:51 +08:00
Tunglies bcde047695 refactor: enhance logging in autobuild workflow with emojis for better visibility 2025-08-03 18:52:26 +08:00
Tunglies 109f5f9648 fix: resolve macOS window management issues and improve logging during app reopen events 2025-08-03 18:40:19 +08:00
Tunglies d16c691c0f fix: can not filiter log level as expected 2025-08-03 09:52:27 +08:00
Tunglies 3eb2a5b3ef refactor: optimize timer and network management with atomic operations 2025-08-01 23:02:11 +08:00
Tunglies 569e2d5192 refactor: enhance traffic monitoring system with unified data management
✨ New Features:
- Implement unified traffic monitoring hook with reference counting
- Add intelligent data sampling and compression for better performance
- Introduce enhanced canvas traffic graph with mouse hover tooltips
- Add Y-axis labels and improved time axis display strategies
- Support multiple time ranges (1, 5, 10 minutes) with adaptive formatting

🚀 Performance Improvements:
- Smart data compression reduces memory usage by 80%
- Reference counting prevents unnecessary data collection when no components need it
- Debounced data updates reduce UI thrashing
- Optimized canvas rendering with controlled frame rates

🔧 Technical Improvements:
- Consolidate traffic monitoring logic into single hook (use-traffic-monitor.ts)
- Remove duplicate hook implementations
- Improve error handling with fallback to last valid data
- Add comprehensive traffic statistics and monitoring diagnostics
- Enhance tooltip system with precise data point highlighting

🐞 Bug Fixes:
- Fix connection speed display issues after IPC migration
- Improve data freshness indicators
- Better handling of network errors and stale data
- Consistent traffic parsing across all components

📝 Code Quality:
- Add TypeScript interfaces for better type safety
- Implement proper cleanup for animation frames and references
- Add error boundaries for traffic components
- Improve component naming and organization

This refactoring provides a more robust, performant, and feature-rich traffic monitoring system while maintaining backward compatibility.
2025-07-31 20:38:11 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 0077157d28 chore(deps): update cargo dependencies (#4275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 14:43:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 9e19bab5a7 chore(deps): update dependency zustand to ^5.0.7 (#4286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 14:42:45 +08:00
Tunglies b1e2940db6 refactor: optimize singleton macro usage with Default trait implementations (#4279)
* refactor: implement DRY principle improvements across backend

Major DRY violations identified and addressed:

1. **IPC Stream Monitor Pattern**:
   - Created `utils/ipc_monitor.rs` with generic `IpcStreamMonitor` trait
   - Added `IpcMonitorManager` for common async task management patterns
   - Eliminates duplication across traffic.rs, memory.rs, and logs.rs

2. **Singleton Pattern Duplication**:
   - Created `utils/singleton.rs` with `singleton\!` and `singleton_with_logging\!` macros
   - Replaces 16+ duplicate singleton implementations across codebase
   - Provides consistent, tested patterns for global instances

3. **macOS Activation Policy Refactoring**:
   - Consolidated 3 duplicate methods into single parameterized `set_activation_policy()`
   - Eliminated code duplication while maintaining backward compatibility
   - Reduced maintenance burden for macOS-specific functionality

These improvements enhance maintainability, reduce bug potential, and ensure consistent patterns across the backend codebase.

* fix: resolve test failures and clippy warnings

- Fix doctest in singleton.rs by using rust,ignore syntax and proper code examples
- Remove unused time::Instant import from ipc_monitor.rs
- Add #[allow(dead_code)] attributes to future-use utility modules
- All 11 unit tests now pass successfully
- All clippy checks pass with -D warnings strict mode
- Documentation tests properly ignore example code that requires full context

* refactor: migrate code to use new utility tools (partial)

Progress on systematic migration to use created utility tools:

1. **Reorganized IPC Monitor**:
   - Moved ipc_monitor.rs to src-tauri/src/ipc/monitor.rs for better organization
   - Updated module structure to emphasize IPC relationship

2. **IpcManager Singleton Migration**:
   - Replaced manual OnceLock singleton pattern with singleton_with_logging\! macro
   - Simplified initialization code and added consistent logging
   - Removed unused imports (OnceLock, logging::Type)

3. **ProxyRequestCache Singleton Migration**:
   - Migrated from once_cell::sync::OnceCell to singleton\! macro
   - Cleaner, more maintainable singleton pattern
   - Consistent with project-wide singleton approach

These migrations demonstrate the utility and effectiveness of the created tools:
- Less boilerplate code
- Consistent patterns across codebase
- Easier maintenance and debugging

* feat: complete migration to new utility tools - phase 1

Successfully migrated core components to use the created utility tools:

- Moved `ipc_monitor.rs` to `src-tauri/src/ipc/monitor.rs`
- Better organization emphasizing IPC relationship
- Updated module exports and imports

- **IpcManager**: Migrated to `singleton_with_logging\!` macro
- **ProxyRequestCache**: Migrated to `singleton\!` macro
- Eliminated ~30 lines of boilerplate singleton code
- Consistent logging and initialization patterns

- Removed unused imports (OnceLock, once_cell, logging::Type)
- Cleaner, more maintainable code structure
- All 11 unit tests pass successfully
- Zero compilation warnings

- **Lines of code reduced**: ~50+ lines of boilerplate
- **Consistency improved**: Unified singleton patterns
- **Maintainability enhanced**: Centralized utility functions
- **Test coverage maintained**: 100% test pass rate

Remaining complex monitors (traffic, memory, logs) will be migrated to use the shared IPC monitoring patterns in the next phase, which requires careful refactoring of their streaming logic.

* refactor: complete singleton pattern migration to utility macros

Migrate remaining singleton patterns across the backend to use standardized
utility macros, achieving significant code reduction and consistency improvements.

- **LogsMonitor** (ipc/logs.rs): `OnceLock` → `singleton_with_logging\!`
- **Sysopt** (core/sysopt.rs): `OnceCell` → `singleton_lazy\!`
- **Tray** (core/tray/mod.rs): Complex `OnceCell` → `singleton_lazy\!`
- **Handle** (core/handle.rs): `OnceCell` → `singleton\!`
- **CoreManager** (core/core.rs): `OnceCell` → `singleton_lazy\!`
- **TrafficMonitor** (ipc/traffic.rs): `OnceLock` → `singleton_lazy_with_logging\!`
- **MemoryMonitor** (ipc/memory.rs): `OnceLock` → `singleton_lazy_with_logging\!`

- `singleton_lazy\!` - For complex initialization patterns
- `singleton_lazy_with_logging\!` - For complex initialization with logging

- **Code Reduction**: -33 lines of boilerplate singleton code
- **DRY Compliance**: Eliminated duplicate initialization patterns
- **Consistency**: Unified singleton approach across codebase
- **Maintainability**: Centralized singleton logic in utility macros
- **Zero Breaking Changes**: All existing APIs remain compatible

All tests pass and clippy warnings resolved.

* refactor: optimize singleton macros using Default trait implementation

Simplify singleton macro usage by implementing Default trait for complex
initialization patterns, significantly improving code readability and maintainability.

- **MemoryMonitor**: Move IPC client initialization to Default impl
- **TrafficMonitor**: Move IPC client initialization to Default impl
- **Sysopt**: Move Arc<Mutex> initialization to Default impl
- **Tray**: Move struct field initialization to Default impl
- **CoreManager**: Move Arc<Mutex> initialization to Default impl

```rust
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", || {
    let ipc_path_buf = ipc_path().unwrap();
    let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
    let client = IpcStreamClient::new(ipc_path).unwrap();
    MemoryMonitor::new(client)
});
```

```rust
impl Default for MemoryMonitor { /* initialization logic */ }
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", MemoryMonitor::default);
```

- **Code Reduction**: -17 lines of macro closure code (80%+ simplification)
- **Separation of Concerns**: Initialization logic moved to proper Default impl
- **Readability**: Single-line macro calls vs multi-line closures
- **Testability**: Default implementations can be tested independently
- **Rust Idioms**: Using standard Default trait pattern
- **Performance**: Function calls more efficient than closures

All tests pass and clippy warnings resolved.

* refactor: implement MonitorData and StreamingParser traits for IPC monitors

* refactor: add timeout and retry_interval fields to IpcStreamMonitor; update TrafficMonitorState to derive Default

* refactor: migrate AppHandleManager to unified singleton control

- Replace manual singleton implementation with singleton_with_logging\! macro
- Remove std::sync::Once dependency in favor of OnceLock-based pattern
- Improve error handling for macOS activation policy methods
- Maintain thread safety with parking_lot::Mutex for AppHandle storage
- Add proper initialization check to prevent duplicate handle assignment
- Enhance logging consistency across AppHandleManager operations

* refactor: improve hotkey management with enum-based operations

- Add HotkeyFunction enum for type-safe function selection
- Add SystemHotkey enum for predefined system shortcuts
- Implement Display and FromStr traits for type conversions
- Replace string-based hotkey registration with enum methods
- Add register_system_hotkey() and unregister_system_hotkey() methods
- Maintain backward compatibility with string-based register() method
- Migrate singleton pattern to use singleton_with_logging\! macro
- Extract hotkey function execution logic into centralized execute_function()
- Update lib.rs to use new enum-based SystemHotkey operations
- Improve type safety and reduce string manipulation errors

Benefits:
- Type safety prevents invalid hotkey function names
- Centralized function execution reduces code duplication
- Enum-based API provides better IDE autocomplete support
- Maintains full backward compatibility with existing configurations

* fix: resolve LightWeightState initialization order panic

- Modify with_lightweight_status() to safely handle unmanaged state using try_state()
- Return Option<R> instead of R to gracefully handle state unavailability
- Update is_in_lightweight_mode() to use unwrap_or(false) for safe defaults
- Add state availability check in auto_lightweight_mode_init() before access
- Maintain singleton check priority while preventing early state access panics
- Fix clippy warnings for redundant pattern matching

Resolves runtime panic: "state() called before manage() for LightWeightState"

* refactor: add unreachable patterns for non-macOS in hotkey handling

* refactor: simplify SystemHotkey enum by removing redundant cfg attributes

* refactor: add macOS conditional compilation for system hotkey registration methods

* refactor: streamline hotkey unregistration and error logging for macOS
2025-07-31 14:35:13 +08:00
Tunglies 4113cd619c feat: add devcontainer configuration for Docker and Codespaces support 2025-07-31 11:39:14 +08:00
Tunglies 1f78d576a3 feat: migrate logs API from REST to IPC streaming (#4277)
* feat: migrate logs API from REST to IPC streaming

- Replace REST API `/logs` calls with IPC streaming implementation
- Add new `src-tauri/src/ipc/logs.rs` with `LogsMonitor` for real-time log streaming
- Implement duplicate stream prevention with level tracking
- Add frontend-backend communication via Tauri commands for log management
- Remove WebSocket compatibility, maintain IPC-only mode
- Fix duplicate monitoring task startup when toggling log service
- Add proper task lifecycle management with JoinHandle cleanup

* refactor: remove dead code from logs.rs to fix clippy warnings

- Remove unused `timestamp` field from LogItem struct
- Remove unused `client` field from LogsMonitor struct
- Remove unused methods: `is_fresh`, `get_current_monitoring_level`, `get_current_logs`
- Simplify LogsMonitor initialization by removing client dependency
- All clippy warnings with -D warnings now resolved

* refactor: extract duplicate fmt_bytes function to utils module

- Create new utils/format.rs module with fmt_bytes function
- Remove duplicate fmt_bytes implementations from traffic.rs and memory.rs
- Update imports to use shared utils::format::fmt_bytes
- Add comprehensive unit tests for fmt_bytes function
- Ensure DRY principle compliance and code maintainability
2025-07-30 23:11:21 +08:00
Tunglies e2a548f6a5 fix: update bzip2 and sysinfo versions, remove unused libbz2-rs-sys package 2025-07-30 23:00:46 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
2956725e66 chore(deps): update cargo dependencies (#4231)
* chore(deps): update cargo dependencies

* chore(deps): update cargo dependencies

* fix: sysinfo crate use limit features

* chore(deps): update npm dependencies (#4254)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore: update sysinfo and zip dependency versions in Cargo.toml; add ignored dependencies in renovate.json

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-07-30 21:22:33 +08:00
Dyna 5131d37d58 fix: the issue of home page port being out of sync (#4271)
* synchronize port 7897

* Update UPDATELOG.md
2025-07-30 19:59:11 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 1dfba159e0 chore(deps): update npm dependencies (#4254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 20:12:54 +08:00
Tunglies 631718a138 feat: enhance autobuild workflow to check for existing assets before running 2025-07-29 20:09:55 +08:00
Tunglies 3172ab2d1c chore: update Mihomo version references in prebuild script 2025-07-29 19:55:25 +08:00
Dyna 6a6bde3764 refactor: external-controller-cors to always show (#4243)
* refactor: external-controller-cors to always show

* i18n: update translation

* Update UPDATELOG.md
2025-07-28 11:24:16 +08:00
wonfen f756b37f97 i18n: add missing trans 2025-07-28 09:12:40 +08:00
Tunglies a26c28517a chore: update dependencies and versions in Cargo files 2025-07-27 22:51:51 +08:00
Tunglies 894428642b fix: add sysinfo to ignored dependencies in renovate configuration 2025-07-27 08:42:44 +08:00
Tunglies 36d58d05b3 chore: downgrade sysinfo version 2025-07-27 08:30:23 +08:00
Tunglies 80de055fc2 fix: resolve connection speed display issues after IPC migration #4208 (#4229)
🐞 Bug Fixes:
- Fix missing upload/download rate display in connections page after IPC migration
- Implement real-time connection speed calculation based on data differences
- Add connection speed data types and calculation logic

🔧 Technical Improvements:
- Add connection speed calculation function in AppDataProvider
- Use useRef to store previous connection data for speed difference calculation
- Add curUpload and curDownload fields to connection data
- Optimize connection data processing flow for accurate speed calculations

📝 Changelog:
- Update relevant issue descriptions in UPDATELOG.md
2025-07-27 03:00:08 +08:00
Tungliesandrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4905b44c8a fix: resolve speed test functionality issue after IPC migration #4221, #4218 (#4228)
* chore(deps): update cargo dependencies

* fix: sysinfo crate use limit features

* fix: update headers-core dependency and kode-bridge version; enhance system monitor status validation

* fix: extend overall_status type in ISystemMonitorOverview to include 'healthy'

* refactor: update URL encoding strategy in IpcManager and cmdGetProxyDelay function

* fix: resolve speed test functionality issue after IPC migration

* fix: resolve speed test functionality issue after IPC migration #4221, #4218

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-27 02:07:00 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 02e19bb132 chore(deps): update dependency cross-env to v10 (#4214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-26 22:55:16 +08:00
Dyna 84989e0ea3 add: home card drag (#4215)
* add home page control buttons

* Update UPDATELOG.md

* update zh en

* Revert "add home page control buttons"

This reverts commit e184843855.

* Update UPDATELOG.md

* Revert "update zh en"

This reverts commit cbaddf59fb.

* remove unnecessary code

* fix: home.tsx

* add  react-beautiful-dnd script

* add home page drag

* fix: react-beautiful-dndA setup problem was encountered

* Revert "fix: react-beautiful-dndA setup problem was encountered"

This reverts commit 81c34dd472.

* fix: react-beautiful-dndA setup problem was encountered

* Update types.d.ts

* Revert "Update types.d.ts"

This reverts commit 854046cf2f.

* update @types/react-beautiful-dnd

* Update home.tsx

* Update UPDATELOG.md

* remove unnecessary components

* Revert "add  react-beautiful-dnd script"

This reverts commit e84d569225.

* Reapply "add  react-beautiful-dnd script"

This reverts commit 2379fd27c4.

* fix: home page error
2025-07-26 22:13:44 +08:00
Dyna 9661c5fd82 fix: Windows installer parameter problem (#4213)
* repair and delete the startup item

* fix: parameter usage issues

* Update UPDATELOG.md

* Update UPDATELOG.md

* fix: install
2025-07-26 07:52:21 +08:00
Tunglies c8dfdb7a5a feat: add shared cache key for autobuild workflows 2025-07-25 18:36:27 +08:00
Tunglies df5897c908 Squashed commit of the following:
commit 8928e6438277995f7167e400d4d77657a0ab0113
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Fri Jul 25 18:25:13 2025 +0800

    feat: add release step to development workflow for versioning

commit 14085c4f7c8943669fdacae3bd2b6a07c0c0389a
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Fri Jul 25 18:19:36 2025 +0800

    feat: add release commands for autobuild and deploytest to package.json and update version script
2025-07-25 18:25:40 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8d0af75145 chore(deps): update npm dependencies (#4098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 17:58:20 +08:00
Mimi 36c6f0ca8d feat: unify switch component styles and BaseDialog behavior in the UI (#4183)
* feat: unify switch component styles in the UI

* fix: prettier

* feat: unify Item styles
2025-07-25 17:57:33 +08:00
Tunglies 3048a2ae08 feat: Implement configuration caching mechanism and force-refresh feature**
✨ **New Features**:

* Added API and frontend support for forcibly refreshing Clash configuration cache
* Implemented a configuration cache TTL mechanism (60 seconds) to reduce redundant requests
* Introduced `ProxyRequestCache` system to manage backend data caching
* Automatically refresh frontend state after core operations to enhance user experience

🚀 **Performance Optimizations**:

* Increased Clash configuration refresh interval from 5 seconds to 60 seconds
* Force refresh cache after configuration updates to resolve data inconsistency
* Automatically trigger state refresh after core switch, start, stop, and restart actions

🔧 **Technical Improvements**:

* Removed unused dependencies: `ab_glyph`, `owned_ttf_parser`, `ttf-parser`
* Simplified WebSocket dependency management, unified `tungstenite` version
* Refactored configuration save validation process, improved merge file handling
* Improved error handling and overall user experience
2025-07-24 01:59:25 +08:00
Tunglies 27535c7bb7 refactor: remove unused dependencies from Cargo.toml and Cargo.lock (#4179) 2025-07-24 00:51:36 +08:00
Tunglies 15a1770ee9 feat: migrate mihomo to use kode-bridge IPC on Windows and Unix (#4051)
* Refactor Mihomo API integration and remove crate_mihomo_api

- Removed the `mihomo_api` crate and its dependencies from the project.
- Introduced `IpcManager` for handling IPC communication with Mihomo.
- Implemented IPC methods for managing proxies, connections, and configurations.
- Updated `MihomoManager` to utilize `IpcManager` instead of the removed crate.
- Added platform-specific IPC socket path handling for macOS, Linux, and Windows.
- Cleaned up related tests and configuration files.

* fix: remove duplicate permission entry in desktop capabilities

* refactor: replace MihomoManager with IpcManager and remove Mihomo module

* fix: restore tempfile dependency in dev-dependencies

* fix: update kode-bridge dependency to use git source from the dev branch

* feat: migrate mihomo to use kode-bridge IPC on Windows

This commit implements a comprehensive migration from legacy service IPC to the kode-bridge library for Windows IPC communication. Key changes include:

Replace service_ipc with kode-bridge IpcManager for all mihomo communications
Simplify proxy commands using new caching mechanism with ProxyRequestCache
Add Windows named pipe (\.\pipe\mihomo) and Unix socket IPC endpoint configuration
Update Tauri permissions and dependencies (dashmap, tauri-plugin-notification)
Add IPC logging support and improve error handling
Fix Windows IPC path handling in directory utilities
This migration enables better cross-platform IPC support and improved performance for mihomo proxy core communication.

* doc: add IPC communication with Mihomo kernel, removing Restful API dependency

* fix: standardize logging type naming from IPC to Ipc for consistency

* refactor: clean up and optimize code structure across multiple components and services

- Removed unnecessary comments and whitespace in various files.
- Improved code readability and maintainability by restructuring functions and components.
- Updated localization files for consistency and accuracy.
- Enhanced performance by optimizing hooks and utility functions.
- General code cleanup in settings, pages, and services to adhere to best practices.

* fix: simplify URL formatting in test_proxy_delay method

* fix: update kode-bridge dependency to version 0.1.3 and change source to crates.io

* fix: update macOS target versions in development workflow

* Revert "fix: update macOS target versions in development workflow"

This reverts commit b9831357e4.

* feat: enhance IPC path handling for Unix systems and improve directory safety checks

* feat: add conditional compilation for Unix-specific IPC path handling

* chore: update cagro.lock

* feat: add external controller configuration and UI support

* Refactor proxy and connection management to use IPC-based commands

- Updated `get_proxies` function in `proxy.rs` to call the new IPC command.
- Renamed `get_refresh_proxies` to `get_proxies` in `ipc/general.rs` for consistency.
- Added new IPC commands for managing proxies, connections, and configurations in `cmds.ts`.
- Refactored API calls in various components to use the new IPC commands instead of HTTP requests.
- Improved error handling and response management in the new IPC functions.
- Cleaned up unused API functions in `api.ts` and redirected relevant calls to `cmds.ts`.
- Enhanced connection management features including health checks and updates for proxy providers.

* chore: update dependencies and improve error handling in IPC manager

* fix: downgrade zip dependency from 4.3.0 to 4.2.0

* feat: Implement traffic and memory data monitoring service

- Added `TrafficService` and `TrafficManager` to manage traffic and memory data collection.
- Introduced commands to get traffic and memory data, start and stop the traffic service.
- Integrated IPC calls for traffic and memory data retrieval in the frontend.
- Updated `AppDataProvider` and `EnhancedTrafficStats` components to utilize new data fetching methods.
- Removed WebSocket connections for traffic and memory data, replaced with IPC polling.
- Added logging for better traceability of data fetching and service status.

* refactor: unify external controller handling and improve IPC path resolution

* fix: replace direct IPC path retrieval with guard function for external controller

* fix: convert external controller IPC path to string for proper insertion in config map

* fix: update dependencies and improve IPC response handling

* fix: remove unnecessary unix conditional for ipc path import

* Refactor traffic and memory monitoring to use IPC stream; remove TrafficService and TrafficManager. Introduce new IPC-based data retrieval methods for traffic and memory, including formatted data and system overview. Update frontend components to utilize new APIs for enhanced data display and management.

* chore: bump crate rand version to 0.9.2

* feat: Implement enhanced traffic monitoring system with data compression and sampling

- Introduced `useTrafficMonitorEnhanced` hook for advanced traffic data management.
- Added `TrafficDataSampler` class for handling raw and compressed traffic data.
- Implemented reference counting to manage data collection based on component usage.
- Enhanced data validation with `SystemMonitorValidator` for API responses.
- Created diagnostic tools for monitoring performance and error tracking.
- Updated existing hooks to utilize the new enhanced monitoring features.
- Added utility functions for generating and formatting diagnostic reports.

* feat(ipc): improve URL encoding and error handling for IPC requests

- Add percent-encoding for URL paths to handle special characters properly
- Enhance error handling in update_proxy with proper logging
- Remove excessive debug logging to reduce noise
- Update kode-bridge dependency to v0.1.5
- Fix JSON parsing error handling in PUT requests

Changes include:
- Proper URL encoding for connection IDs, proxy names, and test URLs
- Enhanced error handling with fallback responses in updateProxy
- Comment out verbose debug logs in traffic monitoring and data validation
- Update dependency version for improved IPC functionality

* feat: major improvements in architecture, traffic monitoring, and data validation

* Refactor traffic graph components: Replace EnhancedTrafficGraph with EnhancedCanvasTrafficGraph, improve rendering performance, and enhance visual elements. Remove deprecated code and ensure compatibility with global data management.

* chore: update UPDATELOG.md for v2.4.0 release, refine traffic monitoring system details, and enhance IPC functionality

* chore: update UPDATELOG.md to reflect removal of deprecated MihomoManager and unify IPC control

* refactor: remove global traffic service testing method from cmds.ts

* Update src/components/home/enhanced-canvas-traffic-graph.tsx

* Update src/hooks/use-traffic-monitor-enhanced.ts

* Update src/components/layout/layout-traffic.tsx

* refactor: remove debug state management from LayoutTraffic component

---------
2025-07-24 00:49:55 +08:00
Dyna f580409ade add: Verge Version copy button (#4164) 2025-07-23 11:11:34 +08:00
Tunglies f209d17e3c chore: bump clash-verge-rev version to 2.4.0 2025-07-22 18:42:25 +08:00
Dyna 5047b0f614 fix: the problem of inconsistent color of system theme window (#4133)
* fix-title

* Update UPDATELOG.md
2025-07-22 11:01:23 +08:00
137 changed files with 10587 additions and 5196 deletions
+101
View File
@@ -0,0 +1,101 @@
{
"name": "Clash Verge Rev Development Environment",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "latest",
"profile": "default"
},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"customizations": {
"vscode": {
"extensions": [
"rust-lang.rust-analyzer",
"tauri-apps.tauri-vscode",
"ms-vscode.vscode-typescript-next",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-json",
"redhat.vscode-yaml",
"formulahendry.auto-rename-tag",
"ms-vscode.hexeditor",
"christian-kohler.path-intellisense",
"yzhang.markdown-all-in-one",
"streetsidesoftware.code-spell-checker",
"ms-vscode.vscode-eslint"
],
"settings": {
"rust-analyzer.cargo.features": ["verge-dev"],
"rust-analyzer.check.command": "clippy",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "redhat.vscode-yaml"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}
},
"forwardPorts": [1420, 3000, 8080, 9090, 7890, 7891],
"portsAttributes": {
"1420": {
"label": "Tauri Dev Server",
"onAutoForward": "notify"
},
"3000": {
"label": "Vite Dev Server",
"onAutoForward": "notify"
},
"7890": {
"label": "Clash HTTP Proxy",
"onAutoForward": "silent"
},
"7891": {
"label": "Clash SOCKS Proxy",
"onAutoForward": "silent"
},
"9090": {
"label": "Clash API",
"onAutoForward": "silent"
}
},
"postCreateCommand": "bash .devcontainer/post-create.sh",
"mounts": [
"source=clash-verge-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
"source=clash-verge-cargo-registry,target=/usr/local/cargo/registry,type=volume",
"source=clash-verge-cargo-git,target=/usr/local/cargo/git,type=volume"
],
"containerEnv": {
"RUST_BACKTRACE": "1",
"NODE_OPTIONS": "--max-old-space-size=4096",
"TAURI_DEV_WATCHER_IGNORE_FILE": ".taurignore"
},
"remoteUser": "vscode",
"workspaceFolder": "/workspaces/clash-verge-rev",
"shutdownAction": "stopContainer"
}
+133
View File
@@ -0,0 +1,133 @@
name: Autobuild Check Logic Test
on:
workflow_dispatch:
jobs:
check_autobuild_logic:
name: Check Autobuild Should Run Logic
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version or source changed, or assets already exist
id: check
run: |
# # 仅用于测试逻辑,手动触发自动跳过
# if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# echo "should_run=skip" >> $GITHUB_OUTPUT
# echo "🟡 手动触发,跳过 should_run 检查"
# exit 0
# fi
# 确保有 HEAD~1
if ! git rev-parse HEAD~1 > /dev/null 2>&1; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 没有前一个提交,默认需要构建"
exit 0
fi
# 版本号变更判断
CURRENT_VERSION=$(jq -r '.version' package.json)
PREVIOUS_VERSION=$(git show HEAD~1:package.json | jq -r '.version' 2>/dev/null || echo "")
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 版本号变更: $PREVIOUS_VERSION → $CURRENT_VERSION"
exit 0
fi
# 检查 src 变更(排除常见产物与缓存)
SRC_DIFF=$(git diff --name-only HEAD~1 HEAD -- src/ | grep -Ev '^src/(dist|build|node_modules|\.next|\.cache)' || true)
TAURI_DIFF=$(git diff --name-only HEAD~1 HEAD -- src-tauri/ | grep -Ev '^src-tauri/(target|node_modules|dist|\.cache)' || true)
if [ -n "$SRC_DIFF" ] || [ -n "$TAURI_DIFF" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 源码变更 detected"
exit 0
fi
# 找到最后一个修改 Tauri 相关文件的 commit
echo "🔍 查找最后一个 Tauri 相关变更的 commit..."
LAST_TAURI_COMMIT=""
for commit in $(git rev-list HEAD --max-count=50); do
# 检查此 commit 是否修改了 Tauri 相关文件
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
HAS_TAURI_CHANGES=false
# 检查各个模式
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
HAS_TAURI_CHANGES=true
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
HAS_TAURI_CHANGES=true
fi
if [ "$HAS_TAURI_CHANGES" = true ]; then
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
break
fi
done
if [ -z "$LAST_TAURI_COMMIT" ]; then
echo "⚠️ 最近的 commits 中未找到 Tauri 相关变更,使用当前 commit"
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
fi
CURRENT_COMMIT=$(git rev-parse --short HEAD)
echo "📝 最后 Tauri 相关 commit: $LAST_TAURI_COMMIT"
echo "📝 当前 commit: $CURRENT_COMMIT"
# 检查 autobuild release 是否存在
AUTOBUILD_RELEASE_EXISTS=$(gh release view "autobuild" --json id -q '.id' 2>/dev/null || echo "")
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 没有 autobuild release,需构建"
else
# 检查 latest.json 是否存在
LATEST_JSON_EXISTS=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
if [ -z "$LATEST_JSON_EXISTS" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 没有 latest.json,需构建"
else
# 下载并解析 latest.json 检查版本和 commit hash
echo "📥 下载 latest.json 检查版本..."
LATEST_JSON_URL=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .browser_download_url' 2>/dev/null)
if [ -n "$LATEST_JSON_URL" ]; then
LATEST_JSON_CONTENT=$(curl -s "$LATEST_JSON_URL" 2>/dev/null || echo "")
if [ -n "$LATEST_JSON_CONTENT" ]; then
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
echo "📦 最新 autobuild 版本: $LATEST_VERSION"
# 从版本字符串中提取 commit hash (格式: X.Y.Z+autobuild.MMDD.commit)
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
echo "📝 最新 autobuild commit: $LATEST_COMMIT"
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 Tauri commit hash 不匹配 ($LAST_TAURI_COMMIT != $LATEST_COMMIT),需构建"
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "🔴 相同 Tauri commit hash ($LAST_TAURI_COMMIT),不需构建"
fi
else
echo "should_run=true" >> $GITHUB_OUTPUT
echo "⚠️ 无法下载或解析 latest.json,需构建"
fi
else
echo "should_run=true" >> $GITHUB_OUTPUT
echo "⚠️ 无法获取 latest.json 下载 URL,需构建"
fi
fi
fi
- name: Output should_run result
run: |
echo "Result: ${{ steps.check.outputs.should_run }}"
+165 -92
View File
@@ -18,55 +18,10 @@ concurrency:
jobs:
check_commit:
name: Check Commit Needs Build
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version changed or src changed
id: check
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "Current version: $CURRENT_VERSION"
git checkout HEAD~1 package.json
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
echo "Previous version: $PREVIOUS_VERSION"
git checkout HEAD package.json
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
echo "Current src hash: $CURRENT_SRC_HASH"
echo "Previous src hash: $PREVIOUS_SRC_HASH"
echo "Current tauri hash: $CURRENT_TAURI_HASH"
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
echo "Source directories changed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "Version and source directories unchanged"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
uses: clash-verge-rev/clash-verge-rev/.github/workflows/check-commit-needs-build.yml@dev
with:
tag_name: autobuild
force_build: ${{ github.event_name == 'workflow_dispatch' }}
update_tag:
name: Update tag
@@ -95,9 +50,28 @@ jobs:
fi
shell: bash
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Release AutoBuild Version
run: pnpm release-version autobuild-latest
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
VERSION=$(jq -r .version package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
shell: bash
- run: |
@@ -111,25 +85,24 @@ jobs:
cat > release.txt << EOF
$UPDATE_LOGS
## 我应该下载哪个版本?
### MacOS
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg
### Linux
- Linux 64位: amd64.deb/amd64.rpm
- Linux arm64 architecture: arm64.deb/aarch64.rpm
- Linux armv7架构: armhf.deb/armhfp.rpm
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- 64位: x64-setup.exe
- arm64架构: arm64-setup.exe
#### 便携版问题很多不再提供
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- 64位: x64_fixed_webview2-setup.exe
- arm64架构: arm64_fixed_webview2-setup.exe
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -152,29 +125,18 @@ jobs:
clean_old_assets:
name: Clean Old Release Assets
runs-on: ubuntu-latest
needs: update_tag
if: ${{ needs.update_tag.result == 'success' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove old assets from release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ env.TAG_NAME }}
run: |
VERSION=$(cat package.json | jq -r '.version')
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
for asset in $assets; do
if [[ "$asset" != *"$VERSION"* ]]; then
echo "Deleting old asset: $asset"
gh release delete-asset "$TAG_NAME" "$asset" -y
fi
done
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' && needs.update_tag.result == 'success' }}
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
with:
tag_name: autobuild
dry_run: false
autobuild-x86-windows-macos-linux:
name: Autobuild x86 Windows, MacOS and Linux
needs: update_tag
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
strategy:
fail-fast: false
matrix:
@@ -206,6 +168,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -230,9 +193,9 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
run: pnpm release-version autobuild-latest
- name: Tauri build
- name: Tauri build for Windows-macOS-Linux
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
@@ -253,10 +216,12 @@ jobs:
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
includeUpdaterJson: true
autobuild-arm-linux:
name: Autobuild ARM Linux
needs: update_tag
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
strategy:
fail-fast: false
matrix:
@@ -284,6 +249,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -302,7 +268,7 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
run: pnpm release-version autobuild-latest
- name: Setup for linux
run: |
@@ -353,7 +319,7 @@ jobs:
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
- name: Tauri Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
@@ -389,7 +355,8 @@ jobs:
autobuild-x86-arm-windows_webview2:
name: Autobuild x86 and ARM Windows with WebView2
needs: update_tag
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
strategy:
fail-fast: false
matrix:
@@ -414,6 +381,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -432,7 +400,7 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
run: pnpm release-version autobuild-latest
- name: Download WebView2 Runtime
run: |
@@ -441,7 +409,7 @@ jobs:
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Tauri build
- name: Tauri build for Windows
id: build
uses: tauri-apps/tauri-action@v0
env:
@@ -452,6 +420,7 @@ jobs:
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
includeUpdaterJson: true
- name: Rename
run: |
@@ -486,3 +455,107 @@ jobs:
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify-telegram:
name: Notify Telegram
runs-on: ubuntu-latest
needs:
[
update_tag,
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Release AutoBuild Version
run: pnpm release-version autobuild-latest
- name: Get Version and Release Info
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Generate release.txt
run: |
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Send Telegram Notification
run: node scripts/telegram.mjs
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
BUILD_TYPE: autobuild
VERSION: ${{ env.VERSION }}
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
@@ -0,0 +1,159 @@
name: Check Commit Needs Build
on:
workflow_dispatch:
inputs:
tag_name:
description: "Release tag name to check against (default: autobuild)"
required: false
default: "autobuild"
type: string
force_build:
description: "Force build regardless of checks"
required: false
default: false
type: boolean
workflow_call:
inputs:
tag_name:
description: "Release tag name to check against (default: autobuild)"
required: false
default: "autobuild"
type: string
force_build:
description: "Force build regardless of checks"
required: false
default: false
type: boolean
outputs:
should_run:
description: "Whether the build should run"
value: ${{ jobs.check_commit.outputs.should_run }}
last_tauri_commit:
description: "The last commit hash with Tauri-related changes"
value: ${{ jobs.check_commit.outputs.last_tauri_commit }}
autobuild_version:
description: "The generated autobuild version string"
value: ${{ jobs.check_commit.outputs.autobuild_version }}
permissions:
contents: read
actions: read
env:
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
jobs:
check_commit:
name: Check Commit Needs Build
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Check if version changed or src changed
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Force build if requested
if [ "${{ inputs.force_build }}" == "true" ]; then
echo "🚀 Force build requested"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "📦 Current version: $CURRENT_VERSION"
git checkout HEAD~1 package.json
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
echo "📦 Previous version: $PREVIOUS_VERSION"
git checkout HEAD package.json
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "✅ Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
# Use get_latest_tauri_commit.bash to find the latest Tauri-related commit
echo "🔍 Finding last commit with Tauri-related changes using script..."
# Make script executable
chmod +x scripts-workflow/get_latest_tauri_commit.bash
# Get the latest Tauri-related commit hash (full hash)
LAST_TAURI_COMMIT_FULL=$(./scripts-workflow/get_latest_tauri_commit.bash)
if [[ $? -ne 0 ]] || [[ -z "$LAST_TAURI_COMMIT_FULL" ]]; then
echo "❌ Failed to get Tauri-related commit, using current commit"
LAST_TAURI_COMMIT_FULL=$(git rev-parse HEAD)
fi
# Get short hash for display and version tagging
LAST_TAURI_COMMIT=$(git rev-parse --short "$LAST_TAURI_COMMIT_FULL")
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
# Generate autobuild version using autobuild-latest format
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
MONTH=$(date +%m)
DAY=$(date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
# Set outputs for other jobs to use
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
# Check if autobuild release exists
echo "🔍 Checking autobuild release and latest.json..."
AUTOBUILD_RELEASE_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json id -q '.id' 2>/dev/null || echo "")
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
echo "✅ No autobuild release exists, build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
# Check if latest.json exists in the release
LATEST_JSON_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
if [ -z "$LATEST_JSON_EXISTS" ]; then
echo "✅ No latest.json found in autobuild release, build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
# Download and parse latest.json to check version and commit hash
echo "📥 Downloading latest.json to check version..."
LATEST_JSON_URL="https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild/latest.json"
LATEST_JSON_CONTENT=$(curl -sL "$LATEST_JSON_URL" 2>/dev/null || echo "")
if [ -n "$LATEST_JSON_CONTENT" ]; then
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
echo "📦 Latest autobuild version: $LATEST_VERSION"
# Extract commit hash from version string (format: X.Y.Z+autobuild.MMDD.commit)
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
echo "📝 Latest autobuild commit: $LATEST_COMMIT"
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
echo "✅ Tauri commit hash mismatch ($LAST_TAURI_COMMIT != $LATEST_COMMIT), build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "❌ Same Tauri commit hash ($LAST_TAURI_COMMIT), no build needed"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
else
echo "⚠️ Failed to download or parse latest.json, build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
fi
fi
fi
+220
View File
@@ -0,0 +1,220 @@
name: Clean Old Assets
on:
workflow_dispatch:
inputs:
tag_name:
description: "Release tag name to clean (default: autobuild)"
required: false
default: "autobuild"
type: string
dry_run:
description: "Dry run mode (only show what would be deleted)"
required: false
default: false
type: boolean
workflow_call:
inputs:
tag_name:
description: "Release tag name to clean (default: autobuild)"
required: false
default: "autobuild"
type: string
dry_run:
description: "Dry run mode (only show what would be deleted)"
required: false
default: false
type: boolean
permissions: write-all
env:
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
TAG_CHANNEL: AutoBuild
jobs:
check_current_version:
name: Check Current Version and Commit
runs-on: ubuntu-latest
outputs:
current_version: ${{ steps.check.outputs.current_version }}
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Get current version and find last Tauri commit
id: check
run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "📦 Current version: $CURRENT_VERSION"
# Find the last commit that changed Tauri-related files
echo "🔍 Finding last commit with Tauri-related changes..."
# Define patterns for Tauri-related files
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
# Get the last commit that changed any of these patterns (excluding build artifacts)
LAST_TAURI_COMMIT=""
for commit in $(git rev-list HEAD --max-count=50); do
# Check if this commit changed any Tauri-related files
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
HAS_TAURI_CHANGES=false
# Check each pattern
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
HAS_TAURI_CHANGES=true
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
HAS_TAURI_CHANGES=true
fi
if [ "$HAS_TAURI_CHANGES" = true ]; then
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
break
fi
done
if [ -z "$LAST_TAURI_COMMIT" ]; then
echo "⚠️ No Tauri-related changes found in recent commits, using current commit"
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
fi
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
echo "📝 Current commit: $(git rev-parse --short HEAD)"
# Generate autobuild version for consistency
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
MONTH=$(date +%m)
DAY=$(date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"
# Set outputs for other jobs to use
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
clean_old_assets:
name: Clean Old Release Assets
runs-on: ubuntu-latest
needs: check_current_version
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Clean old assets from release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ env.TAG_NAME }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
# Use values from check_current_version job
CURRENT_AUTOBUILD_VERSION="${{ needs.check_current_version.outputs.autobuild_version }}"
LAST_TAURI_COMMIT="${{ needs.check_current_version.outputs.last_tauri_commit }}"
CURRENT_VERSION="${{ needs.check_current_version.outputs.current_version }}"
echo "📦 Current version: $CURRENT_VERSION"
echo "📦 Current autobuild version: $CURRENT_AUTOBUILD_VERSION"
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
echo "🏷️ Target tag: $TAG_NAME"
echo "🔍 Dry run mode: $DRY_RUN"
# Check if release exists
RELEASE_EXISTS=$(gh release view "$TAG_NAME" --json id -q '.id' 2>/dev/null || echo "")
if [ -z "$RELEASE_EXISTS" ]; then
echo "❌ Release '$TAG_NAME' not found"
exit 1
fi
echo "✅ Found release '$TAG_NAME'"
# Get all assets
echo "📋 Getting list of all assets..."
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
if [ -z "$assets" ]; then
echo "ℹ️ No assets found in release '$TAG_NAME'"
exit 0
fi
echo "📋 Found assets:"
echo "$assets" | sed 's/^/ - /'
# Count assets to keep and delete
ASSETS_TO_KEEP=""
ASSETS_TO_DELETE=""
for asset in $assets; do
# Keep assets that match current autobuild version or are non-versioned files (like latest.json)
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
ASSETS_TO_KEEP="$ASSETS_TO_KEEP$asset\n"
else
ASSETS_TO_DELETE="$ASSETS_TO_DELETE$asset\n"
fi
done
echo ""
echo "🔒 Assets to keep (current version: $CURRENT_AUTOBUILD_VERSION):"
if [ -n "$ASSETS_TO_KEEP" ]; then
echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | sed 's/^/ - /'
else
echo " - None"
fi
echo ""
echo "🗑️ Assets to delete:"
if [ -n "$ASSETS_TO_DELETE" ]; then
echo -e "$ASSETS_TO_DELETE" | grep -v '^$' | sed 's/^/ - /'
else
echo " - None"
echo "ℹ️ No old assets to clean"
exit 0
fi
if [ "$DRY_RUN" = "true" ]; then
echo ""
echo "🔍 DRY RUN MODE: No assets will actually be deleted"
echo " To actually delete these assets, run this workflow again with dry_run=false"
else
echo ""
echo "🗑️ Deleting old assets..."
DELETED_COUNT=0
FAILED_COUNT=0
for asset in $assets; do
# Skip assets that should be kept
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
continue
fi
echo " Deleting: $asset"
if gh release delete-asset "$TAG_NAME" "$asset" -y 2>/dev/null; then
DELETED_COUNT=$((DELETED_COUNT + 1))
else
echo " ⚠️ Failed to delete $asset"
FAILED_COUNT=$((FAILED_COUNT + 1))
fi
done
echo ""
echo "📊 Cleanup summary:"
echo " - Deleted: $DELETED_COUNT assets"
if [ $FAILED_COUNT -gt 0 ]; then
echo " - Failed: $FAILED_COUNT assets"
fi
echo " - Kept: $(echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | wc -l) assets"
if [ $FAILED_COUNT -gt 0 ]; then
echo "⚠️ Some assets failed to delete. Please check the logs above."
exit 1
else
echo "✅ Cleanup completed successfully!"
fi
fi
+3
View File
@@ -2,6 +2,7 @@ name: Clippy Lint
on:
pull_request:
workflow_dispatch:
jobs:
clippy:
@@ -32,6 +33,8 @@ jobs:
with:
workspaces: src-tauri
save-if: false
cache-all-crates: false
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
+19 -16
View File
@@ -13,14 +13,11 @@ on:
required: false
type: boolean
default: true
run_macos_x86_64:
description: "运行 macOS x86_64"
required: false
type: boolean
default: true
permissions: write-all
env:
TAG_NAME: deploytest
TAG_CHANNEL: DeployTest
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
@@ -43,14 +40,13 @@ jobs:
bundle: dmg
id: macos-aarch64
input: run_macos_aarch64
- os: macos-latest
target: x86_64-apple-darwin
bundle: dmg
id: macos-x86_64
input: run_macos_x86_64
runs-on: ${{ matrix.os }}
steps:
- name: Skip job if not selected
if: github.event.inputs[matrix.input] != 'true'
run: echo "Job ${{ matrix.id }} skipped as requested"
- name: Checkout Repository
if: github.event.inputs[matrix.input] == 'true'
uses: actions/checkout@v4
@@ -69,12 +65,8 @@ jobs:
with:
workspaces: src-tauri
save-if: false
- name: Install Node
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"
cache-all-crates: true
shared-key: autobuild-shared
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -82,12 +74,23 @@ jobs:
with:
run_install: false
- name: Install Node
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- name: Pnpm install and check
if: github.event.inputs[matrix.input] == 'true'
run: |
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
if: github.event.inputs[matrix.input] == 'true'
run: pnpm release-version ${{ env.TAG_NAME }}
- name: Tauri build
if: github.event.inputs[matrix.input] == 'true'
uses: tauri-apps/tauri-action@v0
+221 -15
View File
@@ -5,10 +5,6 @@ on:
# ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch:
push:
# 应当限制在 main 分支上触发发布。
branches:
- main
# 应当限制 v*.*.* 的 tag 触发发布。
tags:
- "v*.*.*"
permissions: write-all
@@ -27,22 +23,131 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if tag is from main branch
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Checking if tag $TAG_REF is from main branch..."
TAG_COMMIT=$(git rev-list -n 1 $TAG_REF)
MAIN_COMMITS=$(git rev-list origin/main)
if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then
echo "✅ Tag $TAG_REF is from main branch"
else
echo "❌ Tag $TAG_REF is not from main branch"
echo "This release workflow only accepts tags from main branch."
exit 1
fi
- name: Check tag and package.json version
run: |
TAG_REF="${GITHUB_REF##*/}"
TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
echo "Current tag: $TAG_REF"
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
EXPECTED_TAG="v$PKG_VERSION"
if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then
echo "❌ Version mismatch:"
echo " Git tag : $TAG_REF"
echo " package.json : $EXPECTED_TAG"
exit 1
fi
echo "Tag and package.json version are consistent."
echo "✅ Tag and package.json version are consistent."
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [release, release-for-linux-arm, release-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
TAG_REF="${GITHUB_REF##*/}"
echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV
VERSION=$(echo "$TAG_REF" | sed 's/^v//')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/$TAG_REF" >> $GITHUB_ENV
shell: bash
- run: |
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_NAME }}"
body_path: release.txt
prerelease: false
token: ${{ secrets.GITHUB_TOKEN }}
# generate_release_notes: true
release:
name: Release Build
needs: check_tag_version
needs: [check_tag_version]
strategy:
fail-fast: false
matrix:
@@ -110,14 +215,20 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev v__VERSION__"
releaseBody: "More new features are now supported."
tagName: ${{ github.ref_name }}
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
releaseBody: "Draft release, will be updated later."
releaseDraft: true
prerelease: false
tauriScript: pnpm
args: --target ${{ matrix.target }}
includeUpdaterJson: true
release-for-linux-arm:
name: Release Build for Linux ARM
needs: [check_tag_version]
strategy:
fail-fast: false
matrix:
@@ -232,7 +343,7 @@ jobs:
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
@@ -240,6 +351,7 @@ jobs:
release-for-fixed-webview2:
name: Release Build for Fixed WebView2
needs: [check_tag_version]
strategy:
fail-fast: false
matrix:
@@ -323,7 +435,7 @@ jobs:
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
body: "More new features are now supported."
body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
@@ -386,7 +498,7 @@ jobs:
submit-to-winget:
name: Submit to Winget
runs-on: ubuntu-latest
needs: [release-update]
needs: [update_tag, release-update]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -405,3 +517,97 @@ jobs:
release-tag: v${{env.VERSION}}
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
notify-telegram:
name: Notify Telegram
runs-on: ubuntu-latest
needs: [update_tag, release-update, release-update-for-fixed-webview2, submit-to-winget]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Get Version and Release Info
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Generate release.txt
run: |
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Send Telegram Notification
run: node scripts/telegram.mjs
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
BUILD_TYPE: release
VERSION: ${{ env.VERSION }}
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
+7
View File
@@ -12,6 +12,11 @@ Before you start contributing to the project, you need to set up your developmen
### Setup for Windows Users
> [!NOTE]
> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.**
>
> Because the `ring` crate is compiled based on `clang` under Windows ARM.
If you're a Windows user, you may need to perform some additional steps:
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
@@ -51,11 +56,13 @@ apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev
You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run prebuild
# Use '--force' to force update to the latest version
# pnpm run prebuild --force
```
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
### Run the Development Server
+1 -1
View File
@@ -88,7 +88,7 @@ To run the development server, execute the following commands after all prerequi
```shell
pnpm i
pnpm run check
pnpm run prebuild
pnpm dev
```
+64 -7
View File
@@ -1,3 +1,60 @@
## v2.4.0
**发行代号:融**
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
### 🏆 重大改进
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.12**
- 新增版本信息复制按钮
- 增强型流量监控,支持更详细的数据分析
- 新增流量图表多种显示模式
- 新增强制刷新配置和节点缓存功能
- 首页流量统计支持查看刻度线详情
### 🚀 性能优化
- 全面提升数据传输和处理效率
- 优化内存使用,减少系统资源消耗
- 改进流量图表渲染性能
- 优化配置和节点刷新策略,从5秒延长到60秒
- 改进数据缓存机制,减少重复请求
- 优化异步程序性能
### 🐞 修复问题
- 修复系统代理状态检测和显示不一致问题
- 修复系统主题窗口颜色不一致问题
- 修复特殊字符 URL 处理问题
- 修复配置修改后缓存不同步问题
- 修复 Windows 安装器自启设置问题
- 修复 macOS 下 Dock 图标恢复窗口问题
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
- 修复架构升级后节点测速功能异常
- 修复架构升级后流量统计功能异常
- 修复架构升级后日志功能异常
- 修复外部控制器跨域配置保存问题
- 修复首页端口显示不一致问题
- 修复首页流量统计刻度线显示问题
- 修复日志页面按钮功能混淆问题
- 修复日志等级设置保存问题
- 修复日志等级异常过滤
- 修复清理日志天数功能异常
- 修复偶发性启动卡死问题
- 修复首页虚拟网卡开关在管理模式下的状态问题
### 🔧 技术改进
- 统一使用新的内核通信方式
- 新增外部控制器配置界面
- 改进跨平台兼容性支持
## v2.3.2
### 🐞 修复问题
@@ -390,7 +447,7 @@
- 新增窗口状态实时监控与自动保存功能
- 增强核心配置变更时的验证与错误处理机制
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
- 支持通过环境变量 `CLASH_VERGE_REV_IP`自定义复制IP地址
- 添加连接表列宽持久化设置与进程过滤功能
- 新增代理组首字母导航与动态滚动定位功能
- 实现连接追踪暂停/恢复功能
@@ -711,7 +768,7 @@
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
- 各种细节优化和界面性能优化
@@ -751,7 +808,7 @@
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
- 各种细节优化和界面性能优化
@@ -787,7 +844,7 @@
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
### Bugs Fixes
@@ -961,7 +1018,7 @@
### Features
- 缓存代理组图标
- 使用`boa_engine` 代替 `rquickjs`
- 使用 `boa_engine` 代替 `rquickjs`
- 支持 Linux armv7
### Bugs Fixes
@@ -1026,7 +1083,7 @@
- 支持自定义托盘图标
- 支持禁用代理组图标
- 代理组显示当前代理
- 修改 `打开面板` 快捷键为`打开/关闭面板`
- 修改 `打开面板` 快捷键为 `打开/关闭面板`
---
@@ -1190,7 +1247,7 @@
### Bugs Fixes
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
- Windows 下更新时无法覆盖 `clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
- 窗口最大化按钮变化问题
- 窗口尺寸保存错误问题
- 复制环境变量类型无法切换问题
+37 -32
View File
@@ -1,10 +1,11 @@
{
"name": "clash-verge",
"version": "2.3.2",
"version": "2.4.0",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=1 RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
"tauri": "tauri",
@@ -18,6 +19,8 @@
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
"release-version": "node scripts/release-version.mjs",
"release:autobuild": "pnpm release-version autobuild",
"release:deploytest": "pnpm release-version deploytest",
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
@@ -31,72 +34,74 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.2.0",
"@mui/lab": "7.0.0-beta.14",
"@mui/material": "^7.2.0",
"@mui/x-data-grid": "^8.8.0",
"@tauri-apps/api": "2.6.0",
"@mui/icons-material": "^7.3.1",
"@mui/lab": "7.0.0-beta.16",
"@mui/material": "^7.3.1",
"@mui/x-data-grid": "^8.10.2",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.3.0",
"@tauri-apps/plugin-fs": "^2.4.0",
"@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
"@tauri-apps/plugin-notification": "^2.3.0",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.0",
"@tauri-apps/plugin-updater": "2.9.0",
"@tauri-apps/plugin-window-state": "^2.3.0",
"@tauri-apps/plugin-window-state": "^2.4.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.0",
"axios": "^1.10.0",
"ahooks": "^3.9.3",
"axios": "^1.11.0",
"chart.js": "^4.5.0",
"cli-color": "^2.0.4",
"dayjs": "1.11.13",
"foxact": "^0.2.49",
"glob": "^11.0.3",
"i18next": "^25.3.2",
"i18next": "^25.4.0",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5",
"react": "19.1.0",
"react": "19.1.1",
"react-beautiful-dnd": "^13.1.1",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0",
"react-dom": "19.1.1",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.60.0",
"react-i18next": "15.6.0",
"react-hook-form": "^7.62.0",
"react-i18next": "15.7.0",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.3",
"react-virtuoso": "^4.13.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.8.1",
"react-virtuoso": "^4.14.0",
"sockette": "^2.0.6",
"swr": "^2.3.4",
"swr": "^2.3.6",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.6"
"zustand": "^5.0.8"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@tauri-apps/cli": "2.6.2",
"@tauri-apps/cli": "2.8.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@vitejs/plugin-legacy": "^7.0.0",
"@vitejs/plugin-react": "4.6.0",
"@types/react": "19.1.10",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "19.1.7",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.1",
"adm-zip": "^0.5.16",
"commander": "^14.0.0",
"cross-env": "^7.0.3",
"cross-env": "^10.0.0",
"https-proxy-agent": "^7.0.6",
"meta-json-schema": "^1.19.11",
"meta-json-schema": "^1.19.12",
"node-fetch": "^3.3.2",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.1.0",
"sass": "^1.89.2",
"prettier-plugin-organize-imports": "^4.2.0",
"sass": "^1.90.0",
"terser": "^5.43.1",
"typescript": "^5.8.3",
"vite": "^7.0.4",
"typescript": "^5.9.2",
"vite": "^7.1.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0"
},
+1119 -956
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -38,5 +38,5 @@
}
],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["serde_yaml"]
"ignoreDeps": ["serde_yaml", "sysinfo", "zip", "criterion"]
}
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
# 获取最近一个和 Tauri 相关的改动的 commit hash
# This script finds the latest commit that modified Tauri-related files
# Tauri 相关文件的模式
TAURI_PATTERNS=(
"src-tauri/"
"Cargo.toml"
"Cargo.lock"
"tauri.*.conf.json"
"package.json"
"pnpm-lock.yaml"
"src/"
)
# 排除的文件模式(build artifacts 等)
EXCLUDE_PATTERNS=(
"src-tauri/target/"
"src-tauri/gen/"
"*.log"
"*.tmp"
"node_modules/"
".git/"
)
# 构建 git log 的路径过滤参数
PATHS=""
for pattern in "${TAURI_PATTERNS[@]}"; do
if [[ -e "$pattern" ]]; then
PATHS="$PATHS $pattern"
fi
done
# 如果没有找到相关路径,返回错误
if [[ -z "$PATHS" ]]; then
echo "Error: No Tauri-related paths found in current directory" >&2
exit 1
fi
# 获取最新的 commit hash
# 使用 git log 查找最近修改了 Tauri 相关文件的提交
LATEST_COMMIT=$(git log --format="%H" -n 1 -- $PATHS)
# 验证是否找到了 commit
if [[ -z "$LATEST_COMMIT" ]]; then
echo "Error: No commits found for Tauri-related files" >&2
exit 1
fi
# 输出结果
echo "$LATEST_COMMIT"
# 如果需要更多信息,可以取消注释以下行
# echo "Latest Tauri-related commit: $LATEST_COMMIT"
# git show --stat --oneline "$LATEST_COMMIT"
+6 -6
View File
@@ -61,12 +61,12 @@ const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/down
let META_ALPHA_VERSION;
const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-compatible",
"darwin-x64": "mihomo-darwin-amd64-v2",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
@@ -108,12 +108,12 @@ const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION;
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-compatible",
"darwin-x64": "mihomo-darwin-amd64-v2",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
+49 -8
View File
@@ -6,15 +6,19 @@
*
* <version> can be:
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
* - A tag: "alpha", "beta", "rc", or "autobuild"
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
*
* Examples:
* pnpm release-version 1.2.3
* pnpm release-version v1.2.3-beta
* pnpm release-version beta
* pnpm release-version autobuild
* pnpm release-version autobuild-latest
* pnpm release-version deploytest
*
* The script will:
* - Validate and normalize the version argument
@@ -43,6 +47,26 @@ function getGitShortCommit() {
}
}
/**
* 获取最新 Tauri 相关提交的短 hash
* @returns {string}
*/
function getLatestTauriCommit() {
try {
const fullHash = execSync(
"bash ./scripts-workflow/get_latest_tauri_commit.bash",
)
.toString()
.trim();
return execSync(`git rev-parse --short ${fullHash}`).toString().trim();
} catch (e) {
console.warn(
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
);
return getGitShortCommit();
}
}
/**
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
* @param {boolean} withCommit 是否带 commit
@@ -135,20 +159,19 @@ async function updateCargoVersion(newVersion) {
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) {
return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${baseVersion}"`,
`version = "${versionWithoutV}"`,
);
}
return line;
});
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`);
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
} catch (error) {
console.error("Error updating Cargo.toml version:", error);
throw error;
@@ -168,19 +191,23 @@ async function updateTauriConfigVersion(newVersion) {
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
tauriConfig.version = baseVersion;
// 使用完整版本信息,包含build metadata
tauriConfig.version = versionWithoutV;
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
console.log(
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
);
} catch (error) {
console.error("Error updating tauri.conf.json version:", error);
throw error;
@@ -214,7 +241,14 @@ async function main(versionArg) {
try {
let newVersion;
const validTags = ["alpha", "beta", "rc", "autobuild"];
const validTags = [
"alpha",
"beta",
"rc",
"autobuild",
"autobuild-latest",
"deploytest",
];
if (validTags.includes(versionArg.toLowerCase())) {
const currentVersion = await getCurrentVersion();
@@ -223,6 +257,13 @@ async function main(versionArg) {
if (versionArg.toLowerCase() === "autobuild") {
// 格式: 2.3.0+autobuild.250613.cc39b27
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
} else if (versionArg.toLowerCase() === "autobuild-latest") {
// 格式: 2.3.0+autobuild.0614.a1b2c3d (使用最新 Tauri 提交)
const latestTauriCommit = getLatestTauriCommit();
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
} else if (versionArg.toLowerCase() === "deploytest") {
// 格式: 2.3.0+deploytest.250613.cc39b27
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true)}`;
} else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
}
+105
View File
@@ -0,0 +1,105 @@
import axios from "axios";
import { readFileSync } from "fs";
import { log_success, log_error, log_info } from "./utils.mjs";
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
const CHAT_ID_TEST = "@vergetest"; // 测试频道
async function sendTelegramNotification() {
if (!process.env.TELEGRAM_BOT_TOKEN) {
throw new Error("TELEGRAM_BOT_TOKEN is required");
}
const version =
process.env.VERSION ||
(() => {
const pkg = readFileSync("package.json", "utf-8");
return JSON.parse(pkg).version;
})();
const downloadUrl =
process.env.DOWNLOAD_URL ||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}`;
const isAutobuild =
process.env.BUILD_TYPE === "autobuild" || version.includes("autobuild");
const chatId = isAutobuild ? CHAT_ID_TEST : CHAT_ID_RELEASE;
const buildType = isAutobuild ? "滚动更新版" : "正式版";
log_info(`Preparing Telegram notification for ${buildType} ${version}`);
log_info(`Target channel: ${chatId}`);
log_info(`Download URL: ${downloadUrl}`);
// 读取发布说明和下载地址
let releaseContent = "";
try {
releaseContent = readFileSync("release.txt", "utf-8");
log_info("成功读取 release.txt 文件");
} catch (error) {
log_error("无法读取 release.txt,使用默认发布说明");
releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。";
}
// Markdown 转换为 HTML
function convertMarkdownToTelegramHTML(content) {
return content
.split("\n")
.map((line) => {
if (line.trim().length === 0) {
return "";
} else if (line.startsWith("## ")) {
return `<b>${line.replace("## ", "")}</b>`;
} else if (line.startsWith("### ")) {
return `<b>${line.replace("### ", "")}</b>`;
} else if (line.startsWith("#### ")) {
return `<b>${line.replace("#### ", "")}</b>`;
} else {
let processedLine = line.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2">$1</a>',
);
processedLine = processedLine.replace(
/\*\*([^*]+)\*\*/g,
"<b>$1</b>",
);
return processedLine;
}
})
.join("\n");
}
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
// 发送到 Telegram
try {
await axios.post(
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
{
chat_id: chatId,
text: content,
link_preview_options: {
is_disabled: false,
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}`,
prefer_large_media: true,
},
parse_mode: "HTML",
},
);
log_success(`✅ Telegram 通知发送成功到 ${chatId}`);
} catch (error) {
log_error(
`❌ Telegram 通知发送失败到 ${chatId}:`,
error.response?.data || error.message,
);
process.exit(1);
}
}
// 执行函数
sendTelegramNotification().catch((error) => {
log_error("脚本执行失败:", error.message);
process.exit(1);
});
+424 -588
View File
File diff suppressed because it is too large Load Diff
+72 -29
View File
@@ -1,8 +1,8 @@
[package]
name = "clash-verge"
version = "2.3.2"
version = "2.4.0"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
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"
@@ -13,11 +13,11 @@ build = "build.rs"
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.3.0", features = [] }
tauri-build = { version = "2.4.0", features = [] }
[dependencies]
warp = "0.3.7"
anyhow = "1.0.98"
warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.99"
dirs = "6.0"
open = "5.3.2"
log = "0.4.27"
@@ -25,43 +25,41 @@ dunce = "1.0.5"
log4rs = "1.3.0"
nanoid = "0.4"
chrono = "0.4.41"
sysinfo = "=0.35.2"
sysinfo = { version = "=0.35.2", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.140"
serde_json = "1.0.143"
serde_yaml = "0.9.34"
once_cell = "1.21.3"
lazy_static = "1.5.0"
port_scanner = "0.1.5"
delay_timer = "0.11.6"
parking_lot = "0.12.4"
percent-encoding = "2.3.1"
tokio = { version = "1.46.1", features = [
tokio = { version = "1.47.1", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.22", features = ["json", "rustls-tls", "cookies"] }
reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "cookies"] }
regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6"
tauri = { version = "2.6.2", features = [
tauri = { version = "2.8.2", features = [
"protocol-asset",
"devtools",
"tray-icon",
"image-ico",
"image-png",
] }
network-interface = { version = "2.0.1", features = ["serde"] }
network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.0"
tauri-plugin-dialog = "2.3.0"
tauri-plugin-fs = "2.4.0"
tauri-plugin-dialog = "2.3.3"
tauri-plugin-fs = "2.4.2"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.0"
tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.3.0"
tauri-plugin-deep-link = "2.4.1"
tauri-plugin-devtools = "2.0.1"
tauri-plugin-window-state = "2.4.0"
zip = "=4.2.0"
reqwest_dav = "0.2.1"
aes-gcm = { version = "0.10.3", features = ["std"] }
@@ -69,16 +67,17 @@ base64 = "0.22.1"
getrandom = "0.3.3"
futures = "0.3.31"
sys-locale = "0.3.2"
async-trait = "0.1.88"
mihomo_api = { path = "src_crates/crate_mihomo_api" }
libc = "0.2.174"
libc = "0.2.175"
gethostname = "1.0.2"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
kode-bridge = "0.2.1-rc1"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.0"
dashmap = "7.0.0-rc2"
console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
@@ -110,6 +109,7 @@ tauri-plugin-updater = "2.9.0"
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = []
tokio-trace = ["console-subscriber"]
[profile.release]
panic = "abort"
@@ -139,12 +139,55 @@ name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies]
criterion = "0.6.0"
tempfile = "3.20.0"
criterion = "0.7.0"
[workspace]
members = ["src_crates/crate_mihomo_api"]
[lints.clippy]
# Core categories - most important for code safety and correctness
correctness = { level = "deny", priority = -1 }
suspicious = { level = "deny", priority = -1 }
[[bench]]
name = "draft_benchmark"
harness = false
# Critical safety lints - warn for now due to extensive existing usage
unwrap_used = "warn"
expect_used = "warn"
panic = "deny"
unimplemented = "deny"
# Development quality lints
todo = "warn"
dbg_macro = "warn"
#print_stdout = "warn"
#print_stderr = "warn"
# Performance lints for proxy application
clone_on_ref_ptr = "warn"
rc_clone_in_vec_init = "warn"
large_stack_arrays = "warn"
large_const_arrays = "warn"
# Security lints
#integer_division = "warn"
#lossy_float_literal = "warn"
#default_numeric_fallback = "warn"
# Mutex and async lints - strict control
async_yields_async = "deny" # Prevents missing await in async blocks
mutex_atomic = "deny" # Use atomics instead of Mutex<bool/int>
mutex_integer = "deny" # Use AtomicInt instead of Mutex<int>
rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong
unused_async = "deny" # Too many false positives in Tauri/framework code
await_holding_lock = "deny"
large_futures = "deny"
future_not_send = "deny"
# Common style improvements
redundant_else = "deny" # Too many in existing code
needless_continue = "deny" # Too many in existing code
needless_raw_string_hashes = "deny" # Too many in existing code
# Disable noisy categories for existing codebase but keep them available
#style = { level = "allow", priority = -1 }
#complexity = { level = "allow", priority = -1 }
#perf = { level = "allow", priority = -1 }
#pedantic = { level = "allow", priority = -1 }
#nursery = { level = "allow", priority = -1 }
#restriction = { level = "allow", priority = -1 }
+30 -18
View File
@@ -523,7 +523,7 @@ FunctionEnd
${If} $0 == 0
Push $0
${If} $1 == 0
DetailPrint "Restart Clash Verge Service..."
DetailPrint "Restart ${PRODUCTNAME} Service..."
SimpleSC::StartService "clash_verge_service" "" 30
${EndIf}
${ElseIf} $0 != 0
@@ -549,20 +549,20 @@ FunctionEnd
${If} $0 == 0
Push $0
${If} $1 == 1
DetailPrint "Stop Clash Verge Service..."
DetailPrint "Stop ${PRODUCTNAME} Service..."
SimpleSC::StopService "clash_verge_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0
DetailPrint "Removing Clash Verge Service..."
DetailPrint "Removing ${PRODUCTNAME} Service..."
SimpleSC::RemoveService "clash_verge_service"
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
MessageBox MB_OK|MB_ICONSTOP "${PRODUCTNAME} Service Stop Error ($0)"
${EndIf}
${ElseIf} $1 == 0
DetailPrint "Removing Clash Verge Service..."
DetailPrint "Removing ${PRODUCTNAME} Service..."
SimpleSC::RemoveService "clash_verge_service"
${EndIf}
${ElseIf} $0 != 0
@@ -661,7 +661,7 @@ SectionEnd
Pop $R0
${If} $R0 = 0
IfSilent kill 0
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "${PRODUCTNAME} 正在运行 $\n点击确定以终止运行" IDOK kill IDCANCEL cancel ${|}
kill:
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
@@ -762,8 +762,18 @@ Section Install
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; 修复系统级启动文件夹
CreateDirectory "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"
DetailPrint "系统级启动文件夹已确保存在"
; 修复用户级启动文件夹
SetShellVarContext current
StrCpy $0 "$SMPROGRAMS\Startup"
CreateDirectory "$0"
DetailPrint "已确保用户级启动文件夹存在: $0"
; 删除 window-state.json 文件 .window-state.json 文件
DetailPrint "开始删除删除 window-state.json or .window-state.json"
DetailPrint "开始删除 window-state.json or .window-state.json"
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
@@ -924,7 +934,7 @@ Section Uninstall
!insertmacro RemoveVergeService
; 删除 window-state.json 文件 .window-state.json 文件
DetailPrint "开始删除删除 window-state.json or .window-state.json"
DetailPrint "开始删除 window-state.json or .window-state.json"
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
@@ -991,7 +1001,7 @@ Section Uninstall
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
; 删除所有用户的桌面快捷方式
DetailPrint "开始删除所有用户桌面的 Clash Verge 快捷方式..."
DetailPrint "开始删除所有用户桌面的 ${PRODUCTNAME} 快捷方式..."
; 删除公共桌面快捷方式
Delete "C:\Users\Public\Desktop\Clash Verge.lnk"
@@ -1019,7 +1029,7 @@ Section Uninstall
Delete "$R4\Clash Verge.lnk"
Delete "$R4\clash-verge.lnk"
DetailPrint "尝试删除用户 '$R3' 桌面的 Clash Verge 快捷方式"
DetailPrint "尝试删除用户 '$R3' 桌面的 ${PRODUCTNAME} 快捷方式"
${EndIf}
; 递增循环计数器
@@ -1029,18 +1039,20 @@ Section Uninstall
DetailPrint "所有用户桌面快捷方式删除完成"
; 删除用户开始菜单文件夹
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
RMDir /r /REBOOTOK "$SMPROGRAMS\$AppStartMenuFolder"
; 删除用户级开始菜单中的应用程序文件夹和快捷方式
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式..."
RMDir /r /REBOOTOK "$SMPROGRAMS\Clash Verge"
RMDir /r /REBOOTOK "$SMPROGRAMS\clash-verge"
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式完成"
; 删除系统级开始菜单中的 Clash Verge
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\Clash Verge.lnk"
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\clash-verge.lnk"
; 删除系统级开始菜单中的应用程序文件夹和快捷方式
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式..."
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge"
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\clash-verge"
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式完成"
; 删除所有带 Clash Verge 或 clash-verge 的注册表项
DetailPrint "开始清理所有 Clash Verge 相关的注册表项..."
DetailPrint "开始清理所有 ${PRODUCTNAME} 相关的注册表项..."
; 设置注册表查看模式 (64位)
SetRegView 64
+1 -2
View File
@@ -121,9 +121,8 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
Err(_) => {
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().to_string());
} else {
return Err("Failed to create temporary file".into());
}
return Err("Failed to create temporary file".into());
}
};
+386 -26
View File
@@ -1,8 +1,19 @@
use super::CmdResult;
use crate::{
config::*, core::*, feat, module::mihomo::MihomoManager, process::AsyncHandler, wrap_err,
config::*,
core::*,
feat,
ipc::{self, IpcManager},
logging,
process::AsyncHandler,
state::proxy::ProxyRequestCache,
utils::logging::Type,
wrap_err,
};
use serde_yaml::Mapping;
use std::time::Duration;
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
/// 复制Clash环境变量
#[tauri::command]
@@ -33,7 +44,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
/// 切换Clash核心
#[tauri::command]
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
log::info!(target: "app", "changing core to {clash_core}");
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global()
.change_core(Some(clash_core.clone()))
@@ -43,14 +54,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
// 切换内核后重启内核
match CoreManager::global().restart_core().await {
Ok(_) => {
log::info!(target: "app", "core changed and restarted to {clash_core}");
logging!(
info,
Type::Core,
"core changed and restarted to {clash_core}"
);
handle::Handle::notice_message("config_core::change_success", &clash_core);
handle::Handle::refresh_clash();
Ok(None)
}
Err(err) => {
let error_msg = format!("Core changed but failed to restart: {err}");
log::error!(target: "app", "{error_msg}");
logging!(error, Type::Core, "{error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
@@ -58,7 +73,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
}
Err(err) => {
let error_msg = err.to_string();
log::error!(target: "app", "failed to change core: {error_msg}");
logging!(error, Type::Core, "failed to change core: {error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
@@ -68,19 +83,31 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
/// 启动核心
#[tauri::command]
pub async fn start_core() -> CmdResult {
wrap_err!(CoreManager::global().start_core().await)
let result = wrap_err!(CoreManager::global().start_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// 关闭核心
#[tauri::command]
pub async fn stop_core() -> CmdResult {
wrap_err!(CoreManager::global().stop_core().await)
let result = wrap_err!(CoreManager::global().stop_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// 重启核心
#[tauri::command]
pub async fn restart_core() -> CmdResult {
wrap_err!(CoreManager::global().restart_core().await)
let result = wrap_err!(CoreManager::global().restart_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// 获取代理延迟
@@ -90,9 +117,11 @@ pub async fn clash_api_get_proxy_delay(
url: Option<String>,
timeout: i32,
) -> CmdResult<serde_json::Value> {
MihomoManager::global()
.test_proxy_delay(&name, url, timeout)
.await
wrap_err!(
IpcManager::global()
.test_proxy_delay(&name, url, timeout)
.await
)
}
/// 测试URL延迟
@@ -116,7 +145,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
// 保存DNS配置到文件
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
log::info!(target: "app", "DNS config saved to {dns_path:?}");
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
Ok(())
}
@@ -137,20 +166,20 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
let dns_path = match dirs::app_home_dir() {
Ok(path) => path.join("dns_config.yaml"),
Err(e) => {
log::error!(target: "app", "Failed to get home dir: {e}");
logging!(error, Type::Config, "Failed to get home dir: {e}");
return;
}
};
if !dns_path.exists() {
log::warn!(target: "app", "DNS config file not found");
logging!(warn, Type::Config, "DNS config file not found");
return;
}
let dns_yaml = match std::fs::read_to_string(&dns_path) {
Ok(content) => content,
Err(e) => {
log::error!(target: "app", "Failed to read DNS config: {e}");
logging!(error, Type::Config, "Failed to read DNS config: {e}");
return;
}
};
@@ -163,12 +192,12 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
patch
}
Err(e) => {
log::error!(target: "app", "Failed to parse DNS config: {e}");
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
return;
}
};
log::info!(target: "app", "Applying DNS config from file");
logging!(info, Type::Config, "Applying DNS config from file");
// 重新生成配置,确保DNS配置被正确应用
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
@@ -177,37 +206,53 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
.patch_config(patch_config.clone());
// 首先重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config with DNS: {err}");
if let Err(err) = Config::generate() {
logging!(
error,
Type::Config,
"Failed to regenerate config with DNS: {err}"
);
return;
}
// 然后应用新配置
if let Err(err) = CoreManager::global().update_config().await {
log::error!(target: "app", "Failed to apply config with DNS: {err}");
logging!(
error,
Type::Config,
"Failed to apply config with DNS: {err}"
);
} else {
log::info!(target: "app", "DNS config successfully applied");
logging!(info, Type::Config, "DNS config successfully applied");
handle::Handle::refresh_clash();
}
} else {
// 当关闭DNS设置时,不需要对配置进行任何修改
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
log::info!(target: "app", "DNS settings disabled, regenerating config");
logging!(
info,
Type::Config,
"DNS settings disabled, regenerating config"
);
// 重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config: {err}");
if let Err(err) = Config::generate() {
logging!(error, Type::Config, "Failed to regenerate config: {err}");
return;
}
// 应用新配置
match CoreManager::global().update_config().await {
Ok(_) => {
log::info!(target: "app", "Config regenerated successfully");
logging!(info, Type::Config, "Config regenerated successfully");
handle::Handle::refresh_clash();
}
Err(err) => {
log::error!(target: "app", "Failed to apply regenerated config: {err}");
logging!(
error,
Type::Config,
"Failed to apply regenerated config: {err}"
);
}
}
}
@@ -267,3 +312,318 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
Err(e) => Err(e.to_string()),
}
}
/// 获取Clash版本信息
#[tauri::command]
pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_version().await)
}
/// 获取Clash配置
#[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
/// 强制刷新Clash配置缓存
#[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
cache.map.remove(&key);
get_clash_config().await
}
/// 更新地理数据
#[tauri::command]
pub async fn update_geo_data() -> CmdResult {
wrap_err!(IpcManager::global().update_geo_data().await)
}
/// 升级Clash核心
#[tauri::command]
pub async fn upgrade_clash_core() -> CmdResult {
wrap_err!(IpcManager::global().upgrade_core().await)
}
/// 获取规则
#[tauri::command]
pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rules().await)
}
/// 更新代理选择
#[tauri::command]
pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
}
/// 获取代理提供者
#[tauri::command]
pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_providers_proxies().await)
}
/// 获取规则提供者
#[tauri::command]
pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rule_providers().await)
}
/// 代理提供者健康检查
#[tauri::command]
pub async fn proxy_provider_health_check(name: String) -> CmdResult {
wrap_err!(
IpcManager::global()
.proxy_provider_health_check(&name)
.await
)
}
/// 更新代理提供者
#[tauri::command]
pub async fn update_proxy_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
}
/// 更新规则提供者
#[tauri::command]
pub async fn update_rule_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_rule_provider(&name).await)
}
/// 获取连接
#[tauri::command]
pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_connections().await)
}
/// 删除连接
#[tauri::command]
pub async fn delete_clash_connection(id: String) -> CmdResult {
wrap_err!(IpcManager::global().delete_connection(&id).await)
}
/// 关闭所有连接
#[tauri::command]
pub async fn close_all_clash_connections() -> CmdResult {
wrap_err!(IpcManager::global().close_all_connections().await)
}
/// 获取流量数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
let traffic = crate::ipc::get_current_traffic().await;
let result = serde_json::json!({
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate,
"last_updated": traffic.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 获取内存数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
let memory = crate::ipc::get_current_memory().await;
let usage_percent = if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
};
let result = serde_json::json!({
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": usage_percent,
"last_updated": memory.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
#[tauri::command]
pub async fn start_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
// 新的IPC监控在首次访问时自动启动
// 触发一次访问以确保监控器已初始化
let _ = crate::ipc::get_current_traffic().await;
let _ = crate::ipc::get_current_memory().await;
logging!(info, Type::Ipc, "IPC流式监控已激活");
Ok(())
}
/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
#[tauri::command]
pub async fn stop_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
// 新的IPC监控是持久的,无需显式停止
logging!(info, Type::Ipc, "IPC流式监控继续运行");
Ok(())
}
/// 获取格式化的流量数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
logging!(trace, Type::Ipc, "获取格式化流量数据");
let (up_rate, down_rate, total_up, total_down, is_fresh) =
crate::ipc::get_formatted_traffic().await;
let result = serde_json::json!({
"up_rate_formatted": up_rate,
"down_rate_formatted": down_rate,
"total_up_formatted": total_up,
"total_down_formatted": total_down,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
);
Ok(result)
}
/// 获取格式化的内存数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
logging!(info, Type::Ipc, "获取格式化内存数据");
let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
let result = serde_json::json!({
"inuse_formatted": inuse,
"oslimit_formatted": oslimit,
"usage_percent": usage_percent,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
);
Ok(result)
}
/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
#[tauri::command]
pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
logging!(debug, Type::Ipc, "获取系统监控概览");
// 并发获取流量和内存数据
let (traffic, memory) = tokio::join!(
crate::ipc::get_current_traffic(),
crate::ipc::get_current_memory()
);
let (traffic_formatted, memory_formatted) = tokio::join!(
crate::ipc::get_formatted_traffic(),
crate::ipc::get_formatted_memory()
);
let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
let result = serde_json::json!({
"traffic": {
"raw": {
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate
},
"formatted": {
"up_rate": traffic_formatted.0,
"down_rate": traffic_formatted.1,
"total_up": traffic_formatted.2,
"total_down": traffic_formatted.3
},
"is_fresh": traffic_is_fresh
},
"memory": {
"raw": {
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
}
},
"formatted": {
"inuse": memory_formatted.0,
"oslimit": memory_formatted.1,
"usage_percent": memory_formatted.2
},
"is_fresh": memory_is_fresh
},
"overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
});
Ok(result)
}
/// 获取代理组延迟
#[tauri::command]
pub async fn get_group_proxy_delays(
group_name: String,
url: Option<String>,
timeout: Option<i32>,
) -> CmdResult<serde_json::Value> {
wrap_err!(
IpcManager::global()
.get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
.await
)
}
/// 检查调试是否启用
#[tauri::command]
pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
match IpcManager::global().is_debug_enabled().await {
Ok(enabled) => Ok(enabled),
Err(_) => Ok(false),
}
}
/// 垃圾回收
#[tauri::command]
pub async fn clash_gc() -> CmdResult {
wrap_err!(IpcManager::global().gc().await)
}
/// 获取日志 (使用新的流式实现)
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
Ok(ipc::get_logs_json().await)
}
/// 启动日志监控
#[tauri::command]
pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
ipc::start_logs_monitoring(level).await;
Ok(())
}
/// 停止日志监控
#[tauri::command]
pub async fn stop_logs_monitoring() -> CmdResult {
ipc::stop_logs_monitoring().await;
Ok(())
}
/// 清除日志
#[tauri::command]
pub async fn clear_logs() -> CmdResult {
ipc::clear_logs().await;
Ok(())
}
+357 -72
View File
@@ -1,3 +1,4 @@
use crate::{logging, utils::logging::Type};
use chrono::Local;
use regex::Regex;
use reqwest::Client;
@@ -250,7 +251,23 @@ async fn check_gemini(client: &Client) -> UnlockItem {
let status = if is_ok { "Yes" } else { "No" };
// 尝试提取国家代码
let re = Regex::new(r#",2,1,200,"([A-Z]{3})""#).unwrap();
let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Gemini regex: {}",
e
);
return UnlockItem {
name: "Gemini".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let region = re.captures(&body).and_then(|caps| {
caps.get(1).map(|m| {
let country_code = m.as_str();
@@ -303,7 +320,23 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
}
} else if body_lower.contains("ad-free") {
// 尝试解析国家代码
let re = Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#).unwrap();
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile YouTube Premium regex: {}",
e
);
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let region = re.captures(&body).and_then(|caps| {
caps.get(1).map(|m| {
let country_code = m.as_str().trim();
@@ -350,11 +383,16 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
let cookie_store = Arc::new(reqwest::cookie::Jar::default());
// 使用带Cookie的客户端
let client_with_cookies = reqwest::Client::builder()
let client_with_cookies = match reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
.cookie_provider(Arc::clone(&cookie_store))
.build()
.unwrap_or_else(|_| client.clone());
.build() {
Ok(client) => client,
Err(e) => {
logging!(error, Type::Network, "Failed to create client with cookies for Bahamut Anime: {}", e);
client.clone()
}
};
// 第一步:获取设备ID (会自动保存Cookie)
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
@@ -363,10 +401,21 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
match response.text().await {
Ok(text) => {
// 使用正则提取deviceid
let re = Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#).unwrap();
re.captures(&text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
.unwrap_or_default()
match Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#) {
Ok(re) => re
.captures(&text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
.unwrap_or_default(),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile deviceid regex for Bahamut Anime: {}",
e
);
String::new()
}
}
}
Err(_) => String::new(),
}
@@ -421,17 +470,25 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
.await
{
Ok(response) => match response.text().await {
Ok(body) => {
let region_re = Regex::new(r#"data-geo="([^"]+)"#).unwrap();
region_re
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
Ok(region_re) => region_re
.captures(&body)
.and_then(|caps| caps.get(1))
.map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{emoji}{country_code}")
})
}
}),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile region regex for Bahamut Anime: {}",
e
);
None
}
},
Err(_) => None,
},
Err(_) => None,
@@ -495,8 +552,40 @@ async fn check_netflix(client: &Client) -> UnlockItem {
}
// 获取状态码
let status1 = result1.unwrap().status().as_u16();
let status2 = result2.unwrap().status().as_u16();
let status1 = match result1 {
Ok(response) => response.status().as_u16(),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Netflix response 1: {}",
e
);
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let status2 = match result2 {
Ok(response) => response.status().as_u16(),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Netflix response 2: {}",
e
);
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
// 根据状态码判断解锁状况
if status1 == 404 && status2 == 404 {
@@ -685,7 +774,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
}
let device_response = device_result.unwrap();
let device_response = match device_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ device response: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
// 检查是否 403 错误
if device_response.status().as_u16() == 403 {
@@ -710,7 +815,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
// 提取 assertion
let re = Regex::new(r#""assertion"\s*:\s*"([^"]+)"#).unwrap();
let re = match Regex::new(r#""assertion"\s*:\s*"([^"]+)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile assertion regex for Disney+: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let assertion = match re.captures(&device_body) {
Some(caps) => caps.get(1).map(|m| m.as_str().to_string()),
None => None,
@@ -729,7 +850,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
let token_url = "https://disney.api.edge.bamgrid.com/token";
// 构建请求体 - 使用表单数据格式而非 JSON
let assertion_str = assertion.unwrap();
let assertion_str = match assertion {
Some(assertion) => assertion,
None => {
logging!(error, Type::Network, "No assertion found for Disney+");
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (No Assertion)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let token_body = [
(
"grant_type",
@@ -762,7 +894,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
}
let token_response = token_result.unwrap();
let token_response = match token_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ token response: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let token_status = token_response.status();
// 保存原始响应用于调试
@@ -798,10 +946,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
.map(|s| s.to_string()),
Err(_) => {
// 如果 JSON 解析失败,尝试使用正则表达式
let refresh_token_re = Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#).unwrap();
refresh_token_re
.captures(&token_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
match Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#) {
Ok(refresh_token_re) => refresh_token_re
.captures(&token_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile refresh_token regex for Disney+: {}",
e
);
None
}
}
}
};
@@ -825,7 +983,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
// GraphQL API 通常接受 JSON 格式
let graphql_payload = format!(
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
refresh_token.unwrap()
refresh_token.unwrap_or_default()
);
let graphql_result = client
@@ -857,21 +1015,56 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
// 解析 GraphQL 响应获取区域信息
let graphql_response = graphql_result.unwrap();
let graphql_response = match graphql_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ GraphQL response: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let graphql_status = graphql_response.status();
let graphql_body_text = (graphql_response.text().await).unwrap_or_default();
let graphql_body_text = match graphql_response.text().await {
Ok(text) => text,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to read Disney+ GraphQL response text: {}",
e
);
String::new()
}
};
// 如果 GraphQL 响应为空或明显错误,尝试直接获取区域信息
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
// 尝试直接从主页获取区域信息
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
Ok(response) => match response.text().await {
Ok(body) => {
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
region_re
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
Ok(region_re) => region_re
.captures(&body)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ main page region regex: {}",
e
);
None
}
},
Err(_) => None,
},
Err(_) => None,
@@ -898,28 +1091,59 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
region: None,
check_time: Some(get_local_date_string()),
};
} else {
}
return UnlockItem {
name: "Disney+".to_string(),
status: format!(
"Failed (GraphQL error: {}, status: {})",
graphql_body_text.chars().take(50).collect::<String>() + "...",
graphql_status.as_u16()
),
region: None,
check_time: Some(get_local_date_string()),
};
}
// 提取国家代码
let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ countryCode regex: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: format!(
"Failed (GraphQL error: {}, status: {})",
graphql_body_text.chars().take(50).collect::<String>() + "...",
graphql_status.as_u16()
),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
}
// 提取国家代码
let region_re = Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#).unwrap();
};
let region_code = region_re
.captures(&graphql_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
// 提取支持状态
let supported_re = Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#).unwrap();
let supported_re = match Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ supported location regex: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let in_supported_location = supported_re
.captures(&graphql_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
@@ -929,12 +1153,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
// 尝试直接从主页获取区域信息
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
Ok(response) => match response.text().await {
Ok(body) => {
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
region_re
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
Ok(region_re) => region_re
.captures(&body)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ main page region regex: {}",
e
);
None
}
},
Err(_) => None,
},
Err(_) => None,
@@ -958,7 +1190,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
}
let region = region_code.unwrap();
let region = match region_code {
Some(code) => code,
None => {
logging!(error, Type::Network, "No region code found for Disney+");
return UnlockItem {
name: "Disney+".to_string(),
status: "No".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
// 判断日本地区
if region == "JP" {
@@ -1028,13 +1271,47 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
}
// 解析响应内容
match result.unwrap().text().await {
let response = match result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Prime Video response: {}",
e
);
return UnlockItem {
name: "Prime Video".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
match response.text().await {
Ok(body) => {
// 检查是否被地区限制
let is_blocked = body.contains("isServiceRestricted");
// 提取地区信息
let region_re = Regex::new(r#""currentTerritory":"([^"]+)"#).unwrap();
let region_re = match Regex::new(r#""currentTerritory":"([^"]+)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Prime Video region regex: {}",
e
);
return UnlockItem {
name: "Prime Video".to_string(),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let region_code = region_re
.captures(&body)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
@@ -1182,8 +1459,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加哔哩哔哩大陆检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_bilibili_china_mainland(&client).await;
let mut results = results.lock().await;
@@ -1193,8 +1470,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加哔哩哔哩港澳台检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_bilibili_hk_mc_tw(&client).await;
let mut results = results.lock().await;
@@ -1204,8 +1481,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加合并的ChatGPT检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let chatgpt_results = check_chatgpt_combined(&client).await;
let mut results = results.lock().await;
@@ -1215,8 +1492,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加Gemini检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_gemini(&client).await;
let mut results = results.lock().await;
@@ -1226,8 +1503,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加YouTube Premium检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_youtube_premium(&client).await;
let mut results = results.lock().await;
@@ -1237,8 +1514,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加动画疯检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_bahamut_anime(&client).await;
let mut results = results.lock().await;
@@ -1248,8 +1525,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加 Netflix 检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_netflix(&client).await;
let mut results = results.lock().await;
@@ -1259,8 +1536,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加 Disney+ 检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_disney_plus(&client).await;
let mut results = results.lock().await;
@@ -1270,8 +1547,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加 Prime Video 检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_prime_video(&client).await;
let mut results = results.lock().await;
@@ -1287,9 +1564,17 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
}
// 获取所有结果
let results = Arc::try_unwrap(results)
.expect("无法获取结果,可能仍有引用存在")
.into_inner();
let results = match Arc::try_unwrap(results) {
Ok(mutex) => mutex.into_inner(),
Err(_) => {
logging!(
error,
Type::Network,
"Failed to unwrap results Arc, references still exist"
);
return Err("Failed to collect results".to_string());
}
};
Ok(results)
}
-1
View File
@@ -1,6 +1,5 @@
use anyhow::Result;
// Common result type used by command functions
pub type CmdResult<T = ()> = Result<T, String>;
// Command modules
+2 -1
View File
@@ -1,5 +1,6 @@
use super::CmdResult;
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
use crate::process::AsyncHandler;
use crate::wrap_err;
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
@@ -32,7 +33,7 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
let current = proxy_manager.get_auto_proxy_cached();
// 异步请求更新,立即返回缓存数据
tokio::spawn(async move {
AsyncHandler::spawn(move || async move {
let _ = proxy_manager.get_auto_proxy_async().await;
});
+101 -7
View File
@@ -2,7 +2,9 @@ use super::CmdResult;
use crate::{
config::{Config, IProfiles, PrfItem, PrfOption},
core::{handle, timer::Timer, tray::Tray, CoreManager},
feat, logging, ret_err,
feat, logging,
process::AsyncHandler,
ret_err,
utils::{dirs, help, logging::Type},
wrap_err,
};
@@ -37,7 +39,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
// 策略1: 尝试快速获取latest数据
let latest_result = tokio::time::timeout(
Duration::from_millis(500),
tokio::task::spawn_blocking(move || {
AsyncHandler::spawn_blocking(move || {
let profiles = Config::profiles();
let latest = profiles.latest_ref();
IProfiles {
@@ -64,7 +66,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
// 策略2: 如果快速获取失败,尝试获取data()
let data_result = tokio::time::timeout(
Duration::from_secs(2),
tokio::task::spawn_blocking(move || {
AsyncHandler::spawn_blocking(move || {
let profiles = Config::profiles();
let data = profiles.latest_ref();
IProfiles {
@@ -102,7 +104,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
"所有获取配置策略都失败,尝试fallback"
);
match tokio::task::spawn_blocking(IProfiles::new).await {
match AsyncHandler::spawn_blocking(IProfiles::new).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用fallback配置成功");
Ok(profiles)
@@ -129,8 +131,100 @@ pub async fn enhance_profiles() -> CmdResult {
/// 导入配置文件
#[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
wrap_err!(Config::profiles().data_mut().append_item(item))
logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
// 使用超时保护避免长时间阻塞
let import_result = tokio::time::timeout(
Duration::from_secs(60), // 60秒超时
async {
let item = PrfItem::from_url(&url, None, None, option).await?;
logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
// 获取导入前的配置数量用于验证
let pre_count = Config::profiles()
.latest_ref()
.items
.as_ref()
.map_or(0, |items| items.len());
Config::profiles().data_mut().append_item(item.clone())?;
// 验证导入是否成功
let post_count = Config::profiles()
.latest_ref()
.items
.as_ref()
.map_or(0, |items| items.len());
if post_count <= pre_count {
logging!(
error,
Type::Cmd,
true,
"[导入订阅] 配置未增加,导入可能失败"
);
return Err(anyhow::anyhow!("配置导入后数量未增加"));
}
logging!(
info,
Type::Cmd,
true,
"[导入订阅] 配置保存成功,数量: {} -> {}",
pre_count,
post_count
);
// 立即发送配置变更通知
if let Some(uid) = &item.uid {
logging!(
info,
Type::Cmd,
true,
"[导入订阅] 发送配置变更通知: {}",
uid
);
handle::Handle::notify_profile_changed(uid.clone());
}
// 异步保存配置文件并发送全局通知
let uid_clone = item.uid.clone();
crate::process::AsyncHandler::spawn(move || async move {
// 在异步块中重新获取锁,避免跨await问题
let save_result = { Config::profiles().data_mut().save_file() };
if let Err(e) = save_result {
logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e);
} else {
logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功");
// 发送全局配置更新通知
if let Some(uid) = uid_clone {
// 延迟发送,确保文件已完全写入
tokio::time::sleep(Duration::from_millis(100)).await;
handle::Handle::notify_profile_changed(uid);
}
}
});
Ok(())
},
)
.await;
match import_result {
Ok(Ok(())) => {
logging!(info, Type::Cmd, true, "[导入订阅] 导入完成: {}", url);
Ok(())
}
Ok(Err(e)) => {
logging!(error, Type::Cmd, true, "[导入订阅] 导入失败: {}", e);
Err(format!("导入订阅失败: {e}"))
}
Err(_) => {
logging!(error, Type::Cmd, true, "[导入订阅] 导入超时(60秒): {}", url);
Err("导入订阅超时,请检查网络连接".into())
}
}
}
/// 重新排序配置文件
@@ -280,7 +374,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
match file_read_result {
Ok(Ok(content)) => {
let yaml_parse_result = tokio::task::spawn_blocking(move || {
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
serde_yaml::from_str::<serde_yaml::Value>(&content)
})
.await;
+11 -7
View File
@@ -1,20 +1,21 @@
use super::CmdResult;
use crate::module::mihomo::MihomoManager;
use crate::{ipc::IpcManager, logging, state::proxy::ProxyRequestCache, utils::logging::Type};
use std::time::Duration;
use crate::state::proxy::ProxyRequestCache;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
manager.get_refresh_proxies().await.expect("fetch failed")
manager.get_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
@@ -31,12 +32,15 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("providers", "default");
let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
manager.get_providers_proxies().await.expect("fetch failed")
manager.get_providers_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
+31 -12
View File
@@ -29,7 +29,8 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
};
// 保存新的配置文件
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_data = file_data.ok_or("file_data is None")?;
wrap_err!(fs::write(&file_path, &file_data))?;
let file_path_str = file_path.to_string_lossy().to_string();
logging!(
@@ -61,14 +62,20 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
"[cmd配置save] merge文件语法验证通过"
);
// 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await {
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
match CoreManager::global().update_config().await {
Ok(_) => {
// 配置更新成功,刷新前端
handle::Handle::refresh_clash();
}
Err(e) => {
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
}
}
return Ok(());
}
@@ -133,17 +140,29 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|| (!file_path_str.ends_with(".js") && !is_script_error)
{
// 普通YAML错误使用YAML通知处理
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
logging!(
info,
Type::Config,
"[cmd配置save] YAML配置文件验证失败,发送通知"
);
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
logging!(
info,
Type::Config,
"[cmd配置save] 脚本文件验证失败,发送通知"
);
let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
logging!(
info,
Type::Config,
"[cmd配置save] 其他类型验证失败,发送一般通知"
);
handle::Handle::notice_message("config_validate::error", &error_msg);
}
+12 -10
View File
@@ -3,13 +3,15 @@ use crate::{
core::{service, CoreManager},
utils::i18n::t,
};
use anyhow::Result;
async fn execute_service_operation(
service_op: impl std::future::Future<Output = Result<(), impl ToString + std::fmt::Debug>>,
op_type: &str,
) -> CmdResult {
if service_op.await.is_err() {
let emsg = format!("{} {} failed", op_type, "Service");
async fn execute_service_operation_sync<F, E>(service_op: F, op_type: &str) -> CmdResult
where
F: FnOnce() -> Result<(), E>,
E: ToString + std::fmt::Debug,
{
if let Err(e) = service_op() {
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string());
return Err(t(emsg.as_str()));
}
if CoreManager::global().restart_core().await.is_err() {
@@ -21,22 +23,22 @@ async fn execute_service_operation(
#[tauri::command]
pub async fn install_service() -> CmdResult {
execute_service_operation(service::install_service(), "Install").await
execute_service_operation_sync(service::install_service, "Install").await
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
execute_service_operation(service::uninstall_service(), "Uninstall").await
execute_service_operation_sync(service::uninstall_service, "Uninstall").await
}
#[tauri::command]
pub async fn reinstall_service() -> CmdResult {
execute_service_operation(service::reinstall_service(), "Reinstall").await
execute_service_operation_sync(service::reinstall_service, "Reinstall").await
}
#[tauri::command]
pub async fn repair_service() -> CmdResult {
execute_service_operation(service::force_reinstall_service(), "Repair").await
execute_service_operation_sync(service::force_reinstall_service, "Repair").await
}
#[tauri::command]
+9 -5
View File
@@ -1,7 +1,9 @@
use super::CmdResult;
use crate::{
core::{handle, CoreManager},
logging,
module::sysinfo::PlatformSpecification,
utils::logging::Type,
};
use once_cell::sync::Lazy;
use std::{
@@ -23,20 +25,22 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
#[tauri::command]
pub async fn export_diagnostic_info() -> CmdResult<()> {
let sysinfo = PlatformSpecification::new_async().await;
let sysinfo = PlatformSpecification::new_sync();
let info = format!("{sysinfo:?}");
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or("Failed to get app handle")?;
let cliboard = app_handle.clipboard();
if cliboard.write_text(info).is_err() {
log::error!(target: "app", "Failed to write to clipboard");
logging!(error, Type::System, "Failed to write to clipboard");
}
Ok(())
}
#[tauri::command]
pub async fn get_system_info() -> CmdResult<String> {
let sysinfo = PlatformSpecification::new_async().await;
let sysinfo = PlatformSpecification::new_sync();
let info = format!("{sysinfo:?}");
Ok(info)
}
@@ -44,7 +48,7 @@ pub async fn get_system_info() -> CmdResult<String> {
/// 获取当前内核运行模式
#[tauri::command]
pub async fn get_running_mode() -> Result<String, String> {
Ok(CoreManager::global().get_running_mode().await.to_string())
Ok(CoreManager::global().get_running_mode().to_string())
}
/// 获取应用的运行时间(毫秒)
+4 -4
View File
@@ -6,8 +6,8 @@ mod platform {
use super::CmdResult;
use crate::{core::win_uwp, wrap_err};
pub async fn invoke_uwp_tool() -> CmdResult {
wrap_err!(win_uwp::invoke_uwptools().await)
pub fn invoke_uwp_tool() -> CmdResult {
wrap_err!(win_uwp::invoke_uwptools())
}
}
@@ -16,7 +16,7 @@ mod platform {
mod platform {
use super::CmdResult;
pub async fn invoke_uwp_tool() -> CmdResult {
pub fn invoke_uwp_tool() -> CmdResult {
Ok(())
}
}
@@ -24,5 +24,5 @@ mod platform {
/// Command exposed to Tauri
#[tauri::command]
pub async fn invoke_uwp_tool() -> CmdResult {
platform::invoke_uwp_tool().await
platform::invoke_uwp_tool()
}
+62 -2
View File
@@ -1,3 +1,5 @@
use crate::config::Config;
use crate::utils::dirs::{ipc_path, path_to_str};
use crate::utils::{dirs, help};
use anyhow::Result;
use serde::{Deserialize, Serialize};
@@ -17,7 +19,9 @@ impl IClashTemp {
Ok(mut map) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
map.insert(key.clone(), template.0.get(key).unwrap().clone());
if let Some(value) = template.0.get(key) {
map.insert(key.clone(), value.clone());
}
}
});
// 确保 secret 字段存在且不为空
@@ -57,6 +61,16 @@ impl IClashTemp {
map.insert("ipv6".into(), true.into());
map.insert("mode".into(), "rule".into());
map.insert("external-controller".into(), "127.0.0.1:9097".into());
#[cfg(unix)]
map.insert(
"external-controller-unix".into(),
Self::guard_external_controller_ipc().into(),
);
#[cfg(windows)]
map.insert(
"external-controller-pipe".into(),
Self::guard_external_controller_ipc().into(),
);
cors_map.insert("allow-private-network".into(), true.into());
cors_map.insert(
"allow-origins".into(),
@@ -87,7 +101,12 @@ impl IClashTemp {
let mixed_port = Self::guard_mixed_port(&config);
let socks_port = Self::guard_socks_port(&config);
let port = Self::guard_port(&config);
let ctrl = Self::guard_server_ctrl(&config);
let ctrl = Self::guard_external_controller(&config);
#[cfg(unix)]
let external_controller_unix = Self::guard_external_controller_ipc();
#[cfg(windows)]
let external_controller_pipe = Self::guard_external_controller_ipc();
#[cfg(not(target_os = "windows"))]
config.insert("redir-port".into(), redir_port.into());
#[cfg(target_os = "linux")]
@@ -97,6 +116,16 @@ impl IClashTemp {
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.into());
#[cfg(unix)]
config.insert(
"external-controller-unix".into(),
external_controller_unix.into(),
);
#[cfg(windows)]
config.insert(
"external-controller-pipe".into(),
external_controller_pipe.into(),
);
config
}
@@ -245,6 +274,26 @@ impl IClashTemp {
.unwrap_or("127.0.0.1:9097".into())
}
pub fn guard_external_controller(config: &Mapping) -> String {
// 在初始化阶段,直接返回配置中的值,不进行额外检查
// 这样可以避免在配置加载期间的循环依赖
Self::guard_server_ctrl(config)
}
pub fn guard_external_controller_with_setting(config: &Mapping) -> String {
// 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge()
.latest_ref()
.enable_external_controller
.unwrap_or(false);
if enable_external_controller {
Self::guard_server_ctrl(config)
} else {
"".into()
}
}
pub fn guard_client_ctrl(config: &Mapping) -> String {
let value = Self::guard_server_ctrl(config);
match SocketAddr::from_str(value.as_str()) {
@@ -257,6 +306,17 @@ impl IClashTemp {
Err(_) => "127.0.0.1:9097".into(),
}
}
pub fn guard_external_controller_ipc() -> String {
// 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致
ipc_path()
.ok()
.and_then(|path| path_to_str(&path).ok().map(|s| s.to_string()))
.unwrap_or_else(|| {
log::error!(target: "app", "Failed to get IPC path, using default");
"127.0.0.1:9090".to_string()
})
}
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+12 -16
View File
@@ -72,7 +72,7 @@ impl Config {
.append_item(script_item.clone())?;
}
// 生成运行时配置
if let Err(err) = Self::generate().await {
if let Err(err) = Self::generate() {
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
} else {
logging!(info, Type::Config, true, "生成运行时配置成功");
@@ -96,8 +96,7 @@ impl Config {
error_msg
);
CoreManager::global()
.use_default_config("config_validate::boot_error", &error_msg)
.await?;
.use_default_config("config_validate::boot_error", &error_msg)?;
Some(("config_validate::boot_error", error_msg))
} else {
logging!(info, Type::Config, true, "配置验证成功");
@@ -107,16 +106,13 @@ impl Config {
Err(err) => {
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
CoreManager::global()
.use_default_config("config_validate::process_terminated", "")
.await?;
.use_default_config("config_validate::process_terminated", "")?;
Some(("config_validate::process_terminated", String::new()))
}
}
} else {
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
CoreManager::global()
.use_default_config("config_validate::error", "")
.await?;
CoreManager::global().use_default_config("config_validate::error", "")?;
Some(("config_validate::error", String::new()))
};
@@ -150,8 +146,8 @@ impl Config {
}
/// 生成订阅存好
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
pub fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance();
*Config::runtime().draft_mut() = Box::new(IRuntime {
config: Some(config),
@@ -174,33 +170,33 @@ mod tests {
use std::mem;
#[test]
#[allow(unused_variables)]
#[allow(clippy::expect_used)]
fn test_prfitem_from_merge_size() {
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
dbg!(&merge_item);
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))
.expect("Failed to create merge item in test");
let prfitem_size = mem::size_of_val(&merge_item);
dbg!(prfitem_size);
// Boxed version
let boxed_merge_item = Box::new(merge_item);
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
dbg!(box_prfitem_size);
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
assert!(box_prfitem_size < prfitem_size);
}
#[test]
#[allow(unused_variables)]
fn test_draft_size_non_boxed() {
let draft = Draft::from(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
dbg!(iruntime_size);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
#[test]
#[allow(unused_variables)]
fn test_draft_size_boxed() {
let draft = Draft::from(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
dbg!(box_iruntime_size);
assert_eq!(
box_iruntime_size,
std::mem::size_of::<Draft<Box<IRuntime>>>()
+9 -2
View File
@@ -46,11 +46,18 @@ impl<T: Clone + ToOwned> Draft<Box<T>> {
if guard.1.is_none() {
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
guard.1 = Some(guard.0.clone());
return RwLockWriteGuard::map(guard, |inner| inner.1.as_mut().unwrap());
return RwLockWriteGuard::map(guard, |inner| {
inner.1.as_mut().unwrap_or_else(|| {
unreachable!("Draft was just created above, this should never fail")
})
});
}
// 已存在草稿,升级为写锁映射
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
inner.1.as_mut().unwrap()
inner
.1
.as_mut()
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
})
}
+16 -15
View File
@@ -147,12 +147,15 @@ impl PrfItem {
bail!("type should not be null");
}
match item.itype.unwrap().as_str() {
let itype = item
.itype
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
match itype.as_str() {
"remote" => {
if item.url.is_none() {
bail!("url should not be null");
}
let url = item.url.as_ref().unwrap().as_str();
let url = item
.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
let name = item.name;
let desc = item.desc;
PrfItem::from_url(url, name, desc, item.option).await
@@ -549,22 +552,20 @@ impl PrfItem {
/// get the file data
pub fn read_file(&self) -> Result<String> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let file = self
.file
.clone()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file);
fs::read_to_string(path).context("failed to read the file")
}
/// save the file data
pub fn save_file(&self, data: String) -> Result<()> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let file = self
.file
.clone()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file);
fs::write(path, data.as_bytes()).context("failed to save the file")
}
+14 -10
View File
@@ -77,10 +77,11 @@ impl IProfiles {
}
if let Some(current) = patch.current {
let items = self.items.as_ref().unwrap();
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
if let Some(items) = self.items.as_ref() {
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
}
}
@@ -127,7 +128,9 @@ impl IProfiles {
bail!("the file should not be null");
}
let file = item.file.clone().unwrap();
let file = item.file.clone().ok_or_else(|| {
anyhow::anyhow!("file field is required when file_data is provided")
})?;
let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path)
@@ -168,11 +171,12 @@ impl IProfiles {
}
}
if old_index.is_none() || new_index.is_none() {
return Ok(());
}
let item = items.remove(old_index.unwrap());
items.insert(new_index.unwrap(), item);
let (old_idx, new_idx) = match (old_index, new_index) {
(Some(old), Some(new)) => (old, new),
_ => return Ok(()),
};
let item = items.remove(old_idx);
items.insert(new_idx, item);
self.items = Some(items);
self.save_file()
}
+8 -8
View File
@@ -138,9 +138,6 @@ pub struct IVerge {
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
pub auto_log_clean: Option<i32>,
/// 是否启用随机端口
pub enable_random_port: Option<bool>,
/// verge 的各种 port 用于覆盖 clash 的各种 port
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
@@ -204,6 +201,9 @@ pub struct IVerge {
/// 启用代理页面自动滚动
pub enable_hover_jump_navigator: Option<bool>,
/// 启用外部控制器
pub enable_external_controller: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
@@ -371,7 +371,6 @@ impl IVerge {
proxy_auto_config: Some(false),
pac_file_content: Some(DEFAULT_PAC.into()),
proxy_host: Some("127.0.0.1".into()),
enable_random_port: Some(false),
#[cfg(not(target_os = "windows"))]
verge_redir_port: Some(7895),
#[cfg(not(target_os = "windows"))]
@@ -391,7 +390,7 @@ impl IVerge {
auto_close_connection: Some(true),
auto_check_update: Some(true),
enable_builtin_enhanced: Some(true),
auto_log_clean: Some(2),
auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天
webdav_url: None,
webdav_username: None,
webdav_password: None,
@@ -403,6 +402,7 @@ impl IVerge {
enable_dns_settings: Some(false),
home_cards: None,
service_state: None,
enable_external_controller: Some(false),
..Self::default()
}
}
@@ -444,7 +444,6 @@ impl IVerge {
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_hover_jump_navigator);
patch!(enable_random_port);
#[cfg(not(target_os = "windows"))]
patch!(verge_redir_port);
#[cfg(not(target_os = "windows"))]
@@ -491,6 +490,7 @@ impl IVerge {
patch!(enable_dns_settings);
patch!(home_cards);
patch!(service_state);
patch!(enable_external_controller);
}
/// 在初始化前尝试拿到单例端口的值
@@ -562,7 +562,6 @@ pub struct IVergeResponse {
pub proxy_layout_column: Option<i32>,
pub test_list: Option<Vec<IVergeTestItem>>,
pub auto_log_clean: Option<i32>,
pub enable_random_port: Option<bool>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
#[cfg(not(target_os = "windows"))]
@@ -586,6 +585,7 @@ pub struct IVergeResponse {
pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub enable_external_controller: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
}
@@ -634,7 +634,6 @@ impl From<IVerge> for IVergeResponse {
proxy_layout_column: verge.proxy_layout_column,
test_list: verge.test_list,
auto_log_clean: verge.auto_log_clean,
enable_random_port: verge.enable_random_port,
#[cfg(not(target_os = "windows"))]
verge_redir_port: verge.verge_redir_port,
#[cfg(not(target_os = "windows"))]
@@ -658,6 +657,7 @@ impl From<IVerge> for IVergeResponse {
enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
enable_external_controller: verge.enable_external_controller,
service_state: verge.service_state,
}
}
+5 -3
View File
@@ -1,3 +1,5 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::time::{timeout, Duration};
@@ -26,7 +28,7 @@ impl Default for AsyncSysproxy {
Self {
enable: false,
host: "127.0.0.1".to_string(),
port: 7890,
port: 7897,
bypass: String::new(),
}
}
@@ -74,7 +76,7 @@ impl AsyncProxyQuery {
#[cfg(target_os = "windows")]
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
// Windows: 从注册表读取PAC配置
tokio::task::spawn_blocking(move || -> Result<AsyncAutoproxy> {
AsyncHandler::spawn_blocking(move || -> Result<AsyncAutoproxy> {
Self::get_pac_config_from_registry()
})
.await?
@@ -258,7 +260,7 @@ impl AsyncProxyQuery {
#[cfg(target_os = "windows")]
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
// Windows: 使用注册表直接读取代理设置
tokio::task::spawn_blocking(move || -> Result<AsyncSysproxy> {
AsyncHandler::spawn_blocking(move || -> Result<AsyncSysproxy> {
Self::get_system_proxy_from_registry()
})
.await?
+9 -5
View File
@@ -117,8 +117,7 @@ impl WebDavClient {
attempt.follow()
}
}))
.build()
.unwrap(),
.build()?,
)
.set_host(config.url)
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
@@ -243,12 +242,17 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
for entry in entries {
let entry = entry.unwrap();
let entry = entry?;
let path = entry.path();
if path.is_file() {
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
let file_name_os = entry.file_name();
let file_name = file_name_os
.to_str()
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
let backup_path = format!("profiles/{}", file_name);
zip.start_file(backup_path, options)?;
zip.write_all(fs::read(path).unwrap().as_slice())?;
let file_content = fs::read(&path)?;
zip.write_all(&file_content)?;
}
}
}
+61 -50
View File
@@ -4,8 +4,10 @@ use crate::{
handle,
service::{self},
},
ipc::IpcManager,
logging, logging_error,
module::mihomo::MihomoManager,
process::AsyncHandler,
singleton_lazy,
utils::{
dirs,
help::{self},
@@ -14,7 +16,7 @@ use crate::{
};
use anyhow::Result;
use chrono::Local;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{
fmt,
fs::{create_dir_all, File},
@@ -23,7 +25,6 @@ use std::{
sync::Arc,
};
use tauri_plugin_shell::{process::CommandChild, ShellExt};
use tokio::sync::Mutex;
#[derive(Debug)]
pub struct CoreManager {
@@ -136,7 +137,7 @@ impl CoreManager {
Ok(false)
}
/// 使用默认配置
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
pub fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
*Config::runtime().draft_mut() = Box::new(IRuntime {
config: Some(Config::clash().latest_ref().0.clone()),
@@ -186,7 +187,7 @@ impl CoreManager {
"检测到Merge文件,仅进行语法检查: {}",
config_path
);
return self.validate_file_syntax(config_path).await;
return self.validate_file_syntax(config_path);
}
// 检查是否为脚本文件
@@ -218,7 +219,7 @@ impl CoreManager {
"检测到脚本文件,使用JavaScript验证: {}",
config_path
);
return self.validate_script_file(config_path).await;
return self.validate_script_file(config_path);
}
// 对YAML配置文件使用Clash内核验证
@@ -250,7 +251,11 @@ impl CoreManager {
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
let msg = "Failed to get app handle";
logging!(error, Type::Core, true, "{}", msg);
anyhow::anyhow!(msg)
})?;
let app_dir = dirs::app_home_dir()?;
let app_dir_str = dirs::path_to_str(&app_dir)?;
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
@@ -298,7 +303,7 @@ impl CoreManager {
}
}
/// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
// 读取文件内容
@@ -326,7 +331,7 @@ impl CoreManager {
}
}
/// 验证脚本文件语法
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
// 读取脚本内容
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
@@ -383,7 +388,7 @@ impl CoreManager {
// 1. 先生成新的配置内容
logging!(info, Type::Config, true, "生成新的配置内容");
Config::generate().await?;
Config::generate()?;
// 2. 验证配置
match self.validate_config().await {
@@ -413,10 +418,7 @@ impl CoreManager {
logging_error!(Type::Core, true, "{}", msg);
msg
});
match MihomoManager::global()
.put_configs_force(run_path_str?)
.await
{
match IpcManager::global().put_configs_force(run_path_str?).await {
Ok(_) => {
Config::runtime().apply();
logging!(info, Type::Core, true, "Configuration updated successfully");
@@ -439,7 +441,7 @@ impl CoreManager {
// 获取当前管理的进程 PID
let current_pid = {
let child_guard = self.child_sidecar.lock().await;
let child_guard = self.child_sidecar.lock();
child_guard.as_ref().map(|child| child.pid())
};
@@ -531,7 +533,7 @@ impl CoreManager {
use winapi::um::winnt::HANDLE;
let process_name_clone = process_name.clone();
let pids = tokio::task::spawn_blocking(move || -> Result<Vec<u32>> {
let pids = AsyncHandler::spawn_blocking(move || -> Result<Vec<u32>> {
let mut pids = Vec::new();
unsafe {
@@ -626,7 +628,7 @@ impl CoreManager {
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
tokio::task::spawn_blocking(move || -> bool {
AsyncHandler::spawn_blocking(move || -> bool {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
if process_handle.is_null() {
@@ -701,7 +703,7 @@ impl CoreManager {
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
let result = tokio::task::spawn_blocking(move || -> Result<bool> {
let result = AsyncHandler::spawn_blocking(move || -> Result<bool> {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if process_handle.is_null() {
@@ -733,7 +735,7 @@ impl CoreManager {
}
}
async fn start_core_by_sidecar(&self) -> Result<()> {
fn start_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by sidecar");
let config_file = &Config::generate_file(ConfigType::Run)?;
let app_handle = handle::Handle::global()
@@ -763,7 +765,7 @@ impl CoreManager {
])
.spawn()?;
tokio::spawn(async move {
AsyncHandler::spawn(move || async move {
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
@@ -787,14 +789,14 @@ impl CoreManager {
"Started core by sidecar pid: {}",
pid
);
*self.child_sidecar.lock().await = Some(child);
self.set_running_mode(RunningMode::Sidecar).await;
*self.child_sidecar.lock() = Some(child);
self.set_running_mode(RunningMode::Sidecar);
Ok(())
}
async fn stop_core_by_sidecar(&self) -> Result<()> {
fn stop_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by sidecar");
if let Some(child) = self.child_sidecar.lock().await.take() {
if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid();
child.kill()?;
logging!(
@@ -805,7 +807,7 @@ impl CoreManager {
pid
);
}
self.set_running_mode(RunningMode::NotRunning).await;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
}
}
@@ -815,30 +817,35 @@ impl CoreManager {
logging!(trace, Type::Core, true, "Running core by service");
let config_file = &Config::generate_file(ConfigType::Run)?;
service::run_core_by_service(config_file).await?;
self.set_running_mode(RunningMode::Service).await;
self.set_running_mode(RunningMode::Service);
Ok(())
}
async fn stop_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by service");
service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning).await;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
}
}
impl CoreManager {
pub fn global() -> &'static CoreManager {
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
CORE_MANAGER.get_or_init(|| CoreManager {
impl Default for CoreManager {
fn default() -> Self {
CoreManager {
running: Arc::new(Mutex::new(RunningMode::NotRunning)),
child_sidecar: Arc::new(Mutex::new(None)),
})
}
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
impl CoreManager {
// 当服务安装失败时的回退逻辑
async fn attempt_service_init(&self) -> Result<()> {
if service::check_service_needs_reinstall().await {
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
if let Err(e) = service::reinstall_service().await {
if let Err(e) = service::reinstall_service() {
logging!(
warn,
Type::Core,
@@ -943,7 +950,7 @@ impl CoreManager {
true,
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
);
self.start_core_by_sidecar().await?;
self.start_core_by_sidecar()?;
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
// 后续的 Tray::global().subscribe_traffic().await 仍然会执行
} else {
@@ -955,7 +962,7 @@ impl CoreManager {
true,
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
);
match service::install_service().await {
match service::install_service() {
Ok(_) => {
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
let mut new_state = service::ServiceState::default();
@@ -979,7 +986,7 @@ impl CoreManager {
final_state.last_error =
Some("Newly installed service failed to start".to_string());
final_state.save()?;
self.start_core_by_sidecar().await?;
self.start_core_by_sidecar()?;
}
} else {
logging!(
@@ -995,7 +1002,7 @@ impl CoreManager {
.to_string(),
);
final_state.save()?;
self.start_core_by_sidecar().await?;
self.start_core_by_sidecar()?;
}
}
Err(err) => {
@@ -1006,7 +1013,7 @@ impl CoreManager {
..Default::default()
};
new_state.save()?;
self.start_core_by_sidecar().await?;
self.start_core_by_sidecar()?;
}
}
} else {
@@ -1035,7 +1042,7 @@ impl CoreManager {
}));
final_state.save()?;
}
self.start_core_by_sidecar().await?;
self.start_core_by_sidecar()?;
}
}
}
@@ -1046,13 +1053,13 @@ impl CoreManager {
Ok(())
}
pub async fn set_running_mode(&self, mode: RunningMode) {
let mut guard = self.running.lock().await;
pub fn set_running_mode(&self, mode: RunningMode) {
let mut guard = self.running.lock();
*guard = mode;
}
pub async fn get_running_mode(&self) -> RunningMode {
let guard = self.running.lock().await;
pub fn get_running_mode(&self) -> RunningMode {
let guard = self.running.lock();
(*guard).clone()
}
@@ -1060,7 +1067,7 @@ impl CoreManager {
pub async fn start_core(&self) -> Result<()> {
if service::is_service_available().await.is_ok() {
if service::check_service_needs_reinstall().await {
service::reinstall_service().await?;
service::reinstall_service()?;
}
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
self.start_core_by_service().await?;
@@ -1074,10 +1081,10 @@ impl CoreManager {
true,
"服务不可用,根据用户偏好使用Sidecar模式"
);
self.start_core_by_sidecar().await?;
self.start_core_by_sidecar()?;
} else {
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
self.start_core_by_sidecar().await?;
self.start_core_by_sidecar()?;
}
}
Ok(())
@@ -1085,9 +1092,9 @@ impl CoreManager {
/// 停止核心运行
pub async fn stop_core(&self) -> Result<()> {
match self.get_running_mode().await {
match self.get_running_mode() {
RunningMode::Service => self.stop_core_by_service().await,
RunningMode::Sidecar => self.stop_core_by_sidecar().await,
RunningMode::Sidecar => self.stop_core_by_sidecar(),
RunningMode::NotRunning => Ok(()),
}
}
@@ -1107,8 +1114,12 @@ impl CoreManager {
logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message.to_string());
}
let core: &str = &clash_core.clone().unwrap();
if !IVerge::VALID_CLASH_CORES.contains(&core) {
let core = clash_core.as_ref().ok_or_else(|| {
let msg = "Clash core should not be None";
logging!(error, Type::Core, true, "{}", msg);
msg.to_string()
})?;
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
let error_message = format!("Clash core invalid name: {core}");
logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message);
+79 -79
View File
@@ -2,10 +2,12 @@ use parking_lot::RwLock;
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep, timeout, Duration};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery;
use crate::logging_error;
use crate::process::AsyncHandler;
use crate::utils::logging::Type;
use once_cell::sync::Lazy;
use sysproxy::{Autoproxy, Sysproxy};
@@ -58,7 +60,7 @@ impl Default for ProxyState {
sys_proxy: Sysproxy {
enable: false,
host: "127.0.0.1".to_string(),
port: 7890,
port: 7897,
bypass: "".to_string(),
},
last_updated: std::time::Instant::now(),
@@ -74,7 +76,7 @@ pub struct EventDrivenProxyManager {
}
#[derive(Debug)]
struct QueryRequest {
pub struct QueryRequest {
response_tx: oneshot::Sender<Autoproxy>,
}
@@ -97,7 +99,7 @@ impl EventDrivenProxyManager {
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (query_tx, query_rx) = mpsc::unbounded_channel();
Self::start_event_loop(state.clone(), event_rx, query_rx);
Self::start_event_loop(Arc::clone(&state), event_rx, query_rx);
Self {
state,
@@ -170,39 +172,32 @@ impl EventDrivenProxyManager {
}
}
fn start_event_loop(
pub fn start_event_loop(
state: Arc<RwLock<ProxyState>>,
mut event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) {
tokio::spawn(async move {
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "事件驱动代理管理器启动");
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
let mut event_stream = UnboundedReceiverStream::new(event_rx);
let mut query_stream = UnboundedReceiverStream::new(query_rx);
loop {
tokio::select! {
event = event_rx.recv() => {
match event {
Some(event) => {
log::debug!(target: "app", "处理代理事件: {event:?}");
Self::handle_event(&state, event).await;
}
None => {
log::info!(target: "app", "事件通道关闭,代理管理器停止");
break;
}
}
Some(event) = event_stream.next() => {
log::debug!(target: "app", "处理代理事件: {event:?}");
Self::handle_event(&state, event).await;
}
query = query_rx.recv() => {
match query {
Some(query) => {
let result = Self::handle_query(&state).await;
let _ = query.response_tx.send(result);
}
None => {
log::info!(target: "app", "查询通道关闭");
break;
}
}
Some(query) = query_stream.next() => {
let result = Self::handle_query(&state).await;
let _ = query.response_tx.send(result);
}
else => {
// 两个通道都关闭时退出
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
break;
}
}
}
@@ -218,7 +213,7 @@ impl EventDrivenProxyManager {
Self::enable_system_proxy(state).await;
}
ProxyEvent::DisableProxy => {
Self::disable_system_proxy(state).await;
Self::disable_system_proxy(state);
}
ProxyEvent::SwitchToPac => {
Self::switch_proxy_mode(state, true).await;
@@ -307,7 +302,9 @@ impl EventDrivenProxyManager {
if !current.enable || current.url != expected.url {
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
Self::restore_pac_proxy(&expected.url).await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "恢复PAC代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
let restored = Self::get_auto_proxy_with_timeout().await;
@@ -329,7 +326,9 @@ impl EventDrivenProxyManager {
if !current.enable || current.host != expected.host || current.port != expected.port {
log::info!(target: "app", "系统代理设置异常,正在恢复...");
Self::restore_sys_proxy(&expected).await;
if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "恢复系统代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
let restored = Self::get_sys_proxy_with_timeout().await;
@@ -350,16 +349,20 @@ impl EventDrivenProxyManager {
if pac_enabled {
let expected = Self::get_expected_pac_config();
Self::restore_pac_proxy(&expected.url).await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "启用PAC代理失败: {}", e);
}
} else {
let expected = Self::get_expected_sys_proxy();
Self::restore_sys_proxy(&expected).await;
if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "启用系统代理失败: {}", e);
}
}
Self::check_and_restore_proxy(state).await;
}
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "禁用系统代理");
#[cfg(not(target_os = "windows"))]
@@ -380,13 +383,17 @@ impl EventDrivenProxyManager {
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
let expected = Self::get_expected_pac_config();
Self::restore_pac_proxy(&expected.url).await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "切换到PAC模式失败: {}", e);
}
} else {
let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
let expected = Self::get_expected_sys_proxy();
Self::restore_sys_proxy(&expected).await;
if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "切换到HTTP代理模式失败: {}", e);
}
}
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
@@ -506,37 +513,45 @@ impl EventDrivenProxyManager {
}
}
async fn restore_pac_proxy(expected_url: &str) {
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "windows")]
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
Self::execute_sysproxy_command(&["pac", expected_url]).await
}
#[allow(clippy::unused_async)]
#[cfg(not(target_os = "windows"))]
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
{
let new_autoproxy = Autoproxy {
enable: true,
url: expected_url.to_string(),
};
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
}
#[cfg(target_os = "windows")]
{
Self::execute_sysproxy_command(&["pac", expected_url]).await;
}
}
async fn restore_sys_proxy(expected: &Sysproxy) {
#[cfg(not(target_os = "windows"))]
{
logging_error!(Type::System, true, expected.set_system_proxy());
}
#[cfg(target_os = "windows")]
{
let address = format!("{}:{}", expected.host, expected.port);
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await;
// logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
new_autoproxy
.set_auto_proxy()
.map_err(|e| anyhow::anyhow!("Failed to set auto proxy: {}", e))
}
}
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: &[&str]) {
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
let address = format!("{}:{}", expected.host, expected.port);
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await
}
#[allow(clippy::unused_async)]
#[cfg(not(target_os = "windows"))]
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
{
// logging_error!(Type::System, true, expected.set_system_proxy());
expected
.set_system_proxy()
.map_err(|e| anyhow::anyhow!("Failed to set system proxy: {}", e))
}
}
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
use crate::utils::dirs;
#[allow(unused_imports)] // creation_flags必须
use std::os::windows::process::CommandExt;
@@ -546,37 +561,22 @@ impl EventDrivenProxyManager {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "获取服务路径失败: {e}");
return;
return Err(e);
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe 不存在");
return;
}
anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist");
let output = Command::new(sysproxy_exe)
let _output = Command::new(sysproxy_exe)
.args(args)
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
.output()
.await;
.await?;
match output {
Ok(output) => {
if !output.status.success() {
log::error!(target: "app", "执行sysproxy命令失败: {args:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
log::error!(target: "app", "sysproxy错误输出: {stderr}");
}
} else {
log::debug!(target: "app", "成功执行sysproxy命令: {args:?}");
}
}
Err(e) => {
log::error!(target: "app", "执行sysproxy命令出错: {e}");
}
}
Ok(())
}
}
+105 -101
View File
@@ -1,4 +1,4 @@
use once_cell::sync::OnceCell;
use crate::singleton;
use parking_lot::RwLock;
use std::{
sync::{
@@ -82,121 +82,123 @@ impl NotificationSystem {
*self.last_emit_time.write() = Instant::now();
self.worker_handle = Some(
thread::Builder::new()
.name("frontend-notifier".into())
.spawn(move || {
let handle = Handle::global();
match thread::Builder::new()
.name("frontend-notifier".into())
.spawn(move || {
let handle = Handle::global();
while !handle.is_exiting() {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(event) => {
let system_guard = handle.notification_system.read();
if system_guard.as_ref().is_none() {
log::warn!("NotificationSystem not found in handle while processing event.");
continue;
}
let system = system_guard.as_ref().unwrap();
while !handle.is_exiting() {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(event) => {
let system_guard = handle.notification_system.read();
let Some(system) = system_guard.as_ref() else {
log::warn!("NotificationSystem not found in handle while processing event.");
continue;
};
let is_emergency = *system.emergency_mode.read();
let is_emergency = *system.emergency_mode.read();
if is_emergency {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
log::warn!(
"Emergency mode active, skipping info message"
);
continue;
}
if is_emergency {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
log::warn!(
"Emergency mode active, skipping info message"
);
continue;
}
}
}
if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now();
if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now();
let (event_name_str, payload_result) = match event {
FrontendEvent::RefreshClash => {
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::RefreshVerge => {
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::NoticeMessage { status, message } => {
match serde_json::to_value((status, message)) {
Ok(p) => ("verge://notice-message", Ok(p)),
Err(e) => {
log::error!("Failed to serialize NoticeMessage payload: {e}");
("verge://notice-message", Err(e))
}
}
}
FrontendEvent::ProfileChanged { current_profile_id } => {
("profile-changed", Ok(serde_json::json!(current_profile_id)))
}
FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
}
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
}
FrontendEvent::ProfileUpdateCompleted { uid } => {
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
}
};
if let Ok(payload) = payload_result {
match window.emit(event_name_str, payload) {
Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {event_name_str}");
}
}
let (event_name_str, payload_result) = match event {
FrontendEvent::RefreshClash => {
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::RefreshVerge => {
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::NoticeMessage { status, message } => {
match serde_json::to_value((status, message)) {
Ok(p) => ("verge://notice-message", Ok(p)),
Err(e) => {
log::warn!("Failed to emit event {event_name_str}: {e}");
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
log::error!("Failed to serialize NoticeMessage payload: {e}");
("verge://notice-message", Err(e))
}
}
}
FrontendEvent::ProfileChanged { current_profile_id } => {
("profile-changed", Ok(serde_json::json!(current_profile_id)))
}
FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
}
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
}
FrontendEvent::ProfileUpdateCompleted { uid } => {
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
}
};
let errors = system.stats.total_errors.load(Ordering::SeqCst);
const EMIT_ERROR_THRESHOLD: u64 = 10;
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
log::warn!(
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
);
*system.emergency_mode.write() = true;
}
if let Ok(payload) = payload_result {
match window.emit(event_name_str, payload) {
Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {event_name_str}");
}
}
Err(e) => {
log::warn!("Failed to emit event {event_name_str}: {e}");
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
let errors = system.stats.total_errors.load(Ordering::SeqCst);
const EMIT_ERROR_THRESHOLD: u64 = 10;
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
log::warn!(
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
);
*system.emergency_mode.write() = true;
}
}
} else {
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
}
} else {
log::warn!("No window found, skipping event emit.");
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
}
thread::sleep(Duration::from_millis(20));
}
Err(mpsc::RecvTimeoutError::Timeout) => {
continue;
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
log::info!(
"Notification channel disconnected, exiting worker thread"
);
break;
} else {
log::warn!("No window found, skipping event emit.");
}
thread::sleep(Duration::from_millis(20));
}
Err(mpsc::RecvTimeoutError::Timeout) => {
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
log::info!(
"Notification channel disconnected, exiting worker thread"
);
break;
}
}
}
log::info!("Notification worker thread exiting");
})
.expect("Failed to start notification worker thread"),
);
log::info!("Notification worker thread exiting");
}) {
Ok(handle) => {
self.worker_handle = Some(handle);
}
Err(e) => {
log::error!("Failed to start notification worker thread: {e}");
}
}
}
/// 发送事件到队列
@@ -272,10 +274,12 @@ impl Default for Handle {
}
}
// Use singleton macro
singleton!(Handle, HANDLE);
impl Handle {
pub fn global() -> &'static Handle {
static HANDLE: OnceCell<Handle> = OnceCell::new();
HANDLE.get_or_init(Handle::default)
pub fn new() -> Self {
Self::default()
}
pub fn init(&self, app_handle: &AppHandle) {
+275 -177
View File
@@ -1,28 +1,275 @@
use crate::utils::notification::{notify_event, NotificationEvent};
use crate::{
config::Config, core::handle, feat, logging, logging_error,
module::lightweight::entry_lightweight_mode, utils::logging::Type,
module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type,
};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
/// Enum representing all available hotkey functions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HotkeyFunction {
OpenOrCloseDashboard,
ClashModeRule,
ClashModeGlobal,
ClashModeDirect,
ToggleSystemProxy,
ToggleTunMode,
EntryLightweightMode,
Quit,
#[cfg(target_os = "macos")]
Hide,
}
impl fmt::Display for HotkeyFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
HotkeyFunction::ClashModeRule => "clash_mode_rule",
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
HotkeyFunction::Quit => "quit",
#[cfg(target_os = "macos")]
HotkeyFunction::Hide => "hide",
};
write!(f, "{s}")
}
}
impl FromStr for HotkeyFunction {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
"quit" => Ok(HotkeyFunction::Quit),
#[cfg(target_os = "macos")]
"hide" => Ok(HotkeyFunction::Hide),
_ => bail!("invalid hotkey function: {}", s),
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Enum representing predefined system hotkeys
pub enum SystemHotkey {
CmdQ,
CmdW,
}
#[cfg(target_os = "macos")]
impl fmt::Display for SystemHotkey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
SystemHotkey::CmdQ => "CMD+Q",
SystemHotkey::CmdW => "CMD+W",
};
write!(f, "{s}")
}
}
#[cfg(target_os = "macos")]
impl SystemHotkey {
pub fn function(self) -> HotkeyFunction {
match self {
SystemHotkey::CmdQ => HotkeyFunction::Quit,
SystemHotkey::CmdW => HotkeyFunction::Hide,
}
}
}
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>,
}
impl Hotkey {
pub fn global() -> &'static Hotkey {
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
HOTKEY.get_or_init(|| Hotkey {
fn new() -> Self {
Self {
current: Arc::new(Mutex::new(Vec::new())),
})
}
}
/// Execute the function associated with a hotkey function enum
fn execute_function(function: HotkeyFunction, app_handle: &tauri::AppHandle) {
match function {
HotkeyFunction::OpenOrCloseDashboard => {
logging!(
debug,
Type::Hotkey,
true,
"=== Hotkey Dashboard Window Operation Start ==="
);
logging!(
info,
Type::Hotkey,
true,
"Using unified WindowManager for hotkey operation (bypass debounce)"
);
crate::feat::open_or_close_dashboard_hotkey();
logging!(
debug,
Type::Hotkey,
"=== Hotkey Dashboard Window Operation End ==="
);
notify_event(app_handle, NotificationEvent::DashboardToggled);
}
HotkeyFunction::ClashModeRule => {
feat::change_clash_mode("rule".into());
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Rule" },
);
}
HotkeyFunction::ClashModeGlobal => {
feat::change_clash_mode("global".into());
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Global" },
);
}
HotkeyFunction::ClashModeDirect => {
feat::change_clash_mode("direct".into());
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Direct" },
);
}
HotkeyFunction::ToggleSystemProxy => {
feat::toggle_system_proxy();
notify_event(app_handle, NotificationEvent::SystemProxyToggled);
}
HotkeyFunction::ToggleTunMode => {
feat::toggle_tun_mode(None);
notify_event(app_handle, NotificationEvent::TunModeToggled);
}
HotkeyFunction::EntryLightweightMode => {
entry_lightweight_mode();
notify_event(app_handle, NotificationEvent::LightweightModeEntered);
}
HotkeyFunction::Quit => {
notify_event(app_handle, NotificationEvent::AppQuit);
feat::quit();
}
#[cfg(target_os = "macos")]
HotkeyFunction::Hide => {
feat::hide();
notify_event(app_handle, NotificationEvent::AppHidden);
}
}
}
#[cfg(target_os = "macos")]
/// Register a system hotkey using enum
pub fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
let hotkey_str = hotkey.to_string();
let function = hotkey.function();
self.register_hotkey_with_function(&hotkey_str, function)
}
#[cfg(target_os = "macos")]
/// Unregister a system hotkey using enum
pub fn unregister_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
let hotkey_str = hotkey.to_string();
self.unregister(&hotkey_str)
}
/// Register a hotkey with function enum
pub fn register_hotkey_with_function(
&self,
hotkey: &str,
function: HotkeyFunction,
) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let manager = app_handle.global_shortcut();
logging!(
debug,
Type::Hotkey,
"Attempting to register hotkey: {} for function: {}",
hotkey,
function
);
if manager.is_registered(hotkey) {
logging!(
debug,
Type::Hotkey,
"Hotkey {} was already registered, unregistering first",
hotkey
);
manager.unregister(hotkey)?;
}
let app_handle_clone = app_handle.clone();
let is_quit = matches!(function, HotkeyFunction::Quit);
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
if event.state == ShortcutState::Pressed {
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event);
if hotkey_event.key == Code::KeyQ && is_quit {
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function, &app_handle_clone);
}
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
let is_enable_global_hotkey = Config::verge()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if is_enable_global_hotkey {
Self::execute_function(function, &app_handle_clone);
} else {
use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible {
Self::execute_function(function, &app_handle_clone);
}
}
}
}
});
logging!(
debug,
Type::Hotkey,
"Successfully registered hotkey {} for {}",
hotkey,
function
);
Ok(())
}
}
// Use unified singleton macro
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey {
pub fn init(&self) -> Result<()> {
let verge = Config::verge();
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
@@ -106,183 +353,24 @@ impl Hotkey {
}
pub fn reset(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let manager = app_handle.global_shortcut();
manager.unregister_all()?;
Ok(())
}
/// Register a hotkey with string-based function (backward compatibility)
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
logging!(
debug,
Type::Hotkey,
"Attempting to register hotkey: {} for function: {}",
hotkey,
func
);
if manager.is_registered(hotkey) {
logging!(
debug,
Type::Hotkey,
"Hotkey {} was already registered, unregistering first",
hotkey
);
manager.unregister(hotkey)?;
}
let app_handle_clone = app_handle.clone();
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
"open_or_close_dashboard" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
logging!(
debug,
Type::Hotkey,
true,
"=== Hotkey Dashboard Window Operation Start ==="
);
logging!(
info,
Type::Hotkey,
true,
"Using unified WindowManager for hotkey operation (bypass debounce)"
);
crate::feat::open_or_close_dashboard_hotkey();
logging!(
debug,
Type::Hotkey,
"=== Hotkey Dashboard Window Operation End ==="
);
notify_event(&app_handle, NotificationEvent::DashboardToggled);
})
}
"clash_mode_rule" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::change_clash_mode("rule".into());
notify_event(
&app_handle,
NotificationEvent::ClashModeChanged { mode: "Rule" },
);
})
}
"clash_mode_global" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::change_clash_mode("global".into());
notify_event(
&app_handle,
NotificationEvent::ClashModeChanged { mode: "Global" },
);
})
}
"clash_mode_direct" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::change_clash_mode("direct".into());
notify_event(
&app_handle,
NotificationEvent::ClashModeChanged { mode: "Direct" },
);
})
}
"toggle_system_proxy" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::toggle_system_proxy();
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
})
}
"toggle_tun_mode" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::toggle_tun_mode(None);
notify_event(&app_handle, NotificationEvent::TunModeToggled);
})
}
"entry_lightweight_mode" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
entry_lightweight_mode();
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
})
}
"quit" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::quit();
notify_event(&app_handle, NotificationEvent::AppQuit);
})
}
#[cfg(target_os = "macos")]
"hide" => {
let app_handle = app_handle_clone.clone();
Box::new(move || {
feat::hide();
notify_event(&app_handle, NotificationEvent::AppHidden);
})
}
_ => {
logging!(error, Type::Hotkey, "Invalid function: {}", func);
bail!("invalid function \"{func}\"");
}
};
let is_quit = func.trim() == "quit";
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
if event.state == ShortcutState::Pressed {
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey);
if hotkey.key == Code::KeyQ && is_quit {
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
logging!(debug, Type::Hotkey, "Executing quit function");
f();
}
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
let is_enable_global_hotkey = Config::verge()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if is_enable_global_hotkey {
f();
} else {
use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible {
f();
}
}
}
}
});
logging!(
debug,
Type::Hotkey,
"Successfully registered hotkey {} for {}",
hotkey,
func
);
Ok(())
let function = HotkeyFunction::from_str(func)?;
self.register_hotkey_with_function(hotkey, function)
}
pub fn unregister(&self, hotkey: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?;
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
@@ -356,7 +444,17 @@ impl Hotkey {
impl Drop for Hotkey {
fn drop(&mut self) {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(
error,
Type::Hotkey,
"Failed to get app handle during hotkey cleanup"
);
return;
}
};
if let Err(e) = app_handle.global_shortcut().unregister_all() {
logging!(
error,
+29 -32
View File
@@ -112,7 +112,7 @@ pub struct JsonResponse {
}
#[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> {
pub fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, true, "uninstall service");
use deelevate::{PrivilegeLevel, Token};
@@ -138,7 +138,7 @@ pub async fn uninstall_service() -> Result<()> {
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -146,7 +146,7 @@ pub async fn uninstall_service() -> Result<()> {
}
#[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> {
pub fn install_service() -> Result<()> {
logging!(info, Type::Service, true, "install service");
use deelevate::{PrivilegeLevel, Token};
@@ -172,7 +172,7 @@ pub async fn install_service() -> Result<()> {
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -180,7 +180,7 @@ pub async fn install_service() -> Result<()> {
}
#[cfg(target_os = "windows")]
pub async fn reinstall_service() -> Result<()> {
pub fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
// 获取当前服务状态
@@ -198,7 +198,7 @@ pub async fn reinstall_service() -> Result<()> {
}
// 先卸载服务
if let Err(err) = uninstall_service().await {
if let Err(err) = uninstall_service() {
logging!(
warn,
Type::Service,
@@ -209,7 +209,7 @@ pub async fn reinstall_service() -> Result<()> {
}
// 再安装服务
match install_service().await {
match install_service() {
Ok(_) => {
// 记录安装信息并保存
service_state.record_install();
@@ -228,7 +228,7 @@ pub async fn reinstall_service() -> Result<()> {
}
#[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> {
pub fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, true, "uninstall service");
use users::get_effective_uid;
@@ -254,13 +254,13 @@ pub async fn uninstall_service() -> Result<()> {
Type::Service,
true,
"uninstall status code:{}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -268,7 +268,7 @@ pub async fn uninstall_service() -> Result<()> {
}
#[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> {
pub fn install_service() -> Result<()> {
logging!(info, Type::Service, true, "install service");
use users::get_effective_uid;
@@ -294,13 +294,13 @@ pub async fn install_service() -> Result<()> {
Type::Service,
true,
"install status code:{}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -308,7 +308,7 @@ pub async fn install_service() -> Result<()> {
}
#[cfg(target_os = "linux")]
pub async fn reinstall_service() -> Result<()> {
pub fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
// 获取当前服务状态
@@ -326,7 +326,7 @@ pub async fn reinstall_service() -> Result<()> {
}
// 先卸载服务
if let Err(err) = uninstall_service().await {
if let Err(err) = uninstall_service() {
logging!(
warn,
Type::Service,
@@ -337,7 +337,7 @@ pub async fn reinstall_service() -> Result<()> {
}
// 再安装服务
match install_service().await {
match install_service() {
Ok(_) => {
// 记录安装信息并保存
service_state.record_install();
@@ -356,7 +356,7 @@ pub async fn reinstall_service() -> Result<()> {
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> {
pub fn uninstall_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, true, "uninstall service");
@@ -384,7 +384,7 @@ pub async fn uninstall_service() -> Result<()> {
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -392,7 +392,7 @@ pub async fn uninstall_service() -> Result<()> {
}
#[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> {
pub fn install_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, true, "install service");
@@ -420,7 +420,7 @@ pub async fn install_service() -> Result<()> {
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -428,7 +428,7 @@ pub async fn install_service() -> Result<()> {
}
#[cfg(target_os = "macos")]
pub async fn reinstall_service() -> Result<()> {
pub fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
// 获取当前服务状态
@@ -446,7 +446,7 @@ pub async fn reinstall_service() -> Result<()> {
}
// 先卸载服务
if let Err(err) = uninstall_service().await {
if let Err(err) = uninstall_service() {
logging!(
warn,
Type::Service,
@@ -457,7 +457,7 @@ pub async fn reinstall_service() -> Result<()> {
}
// 再安装服务
match install_service().await {
match install_service() {
Ok(_) => {
// 记录安装信息并保存
service_state.record_install();
@@ -856,17 +856,14 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
if let Ok(()) = start_with_existing_service(config_file).await {
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
return Ok(());
} else {
bail!("服务版本不匹配且无法重装,启动失败");
}
bail!("服务版本不匹配且无法重装,启动失败");
}
log::info!(target: "app", "开始重装服务");
if let Err(err) = reinstall_service().await {
if let Err(err) = reinstall_service() {
log::warn!(target: "app", "服务重装失败: {err}");
log::info!(target: "app", "尝试使用现有服务");
return start_with_existing_service(config_file).await;
bail!("Failed to reinstall service: {}", err);
}
log::info!(target: "app", "服务重装成功,尝试启动");
@@ -890,7 +887,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
if check_service_needs_reinstall().await {
log::info!(target: "app", "服务需要重装");
if let Err(err) = reinstall_service().await {
if let Err(err) = reinstall_service() {
log::warn!(target: "app", "服务重装失败: {err}");
bail!("Failed to reinstall service: {}", err);
}
@@ -970,7 +967,7 @@ pub async fn is_service_available() -> Result<()> {
}
/// 强制重装服务(UI修复按钮)
pub async fn force_reinstall_service() -> Result<()> {
pub fn force_reinstall_service() -> Result<()> {
log::info!(target: "app", "用户请求强制重装服务");
let service_state = ServiceState::default();
@@ -978,7 +975,7 @@ pub async fn force_reinstall_service() -> Result<()> {
log::info!(target: "app", "已重置服务状态,开始执行重装");
match reinstall_service().await {
match reinstall_service() {
Ok(()) => {
log::info!(target: "app", "服务重装成功");
Ok(())
+3 -1
View File
@@ -1,3 +1,5 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use anyhow::{bail, Context, Result};
use hmac::{Hmac, Mac};
@@ -143,7 +145,7 @@ pub async fn send_ipc_request(
let request_json = serde_json::to_string(&request)?;
let result = tokio::task::spawn_blocking(move || -> Result<IpcResponse> {
let result = AsyncHandler::spawn_blocking(move || -> Result<IpcResponse> {
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
Ok(name) => name,
Err(e) => {
+43 -24
View File
@@ -3,11 +3,10 @@ use crate::utils::autostart as startup_shortcut;
use crate::{
config::{Config, IVerge},
core::{handle::Handle, EventDrivenProxyManager},
logging, logging_error,
logging, logging_error, singleton_lazy,
utils::logging::Type,
};
use anyhow::Result;
use once_cell::sync::OnceCell;
use std::sync::Arc;
#[cfg(not(target_os = "windows"))]
use sysproxy::{Autoproxy, Sysproxy};
@@ -52,15 +51,19 @@ fn get_bypass() -> String {
}
}
impl Sysopt {
pub fn global() -> &'static Sysopt {
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
SYSOPT.get_or_init(|| Sysopt {
impl Default for Sysopt {
fn default() -> Self {
Sysopt {
update_sysproxy: Arc::new(TokioMutex::new(false)),
reset_sysproxy: Arc::new(TokioMutex::new(false)),
})
}
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(Sysopt, SYSOPT, Sysopt::default);
impl Sysopt {
pub fn init_guard_sysproxy(&self) -> Result<()> {
// 使用事件驱动代理管理器
let proxy_manager = EventDrivenProxyManager::global();
@@ -146,7 +149,9 @@ impl Sysopt {
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
@@ -157,23 +162,27 @@ impl Sysopt {
let shell = app_handle.shell();
let output = if pac_enable {
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
shell
.command(sysproxy_str)
.args(["pac", address.as_str()])
.output()
.await
.unwrap();
output
.await?
} else {
let address = format!("{proxy_host}:{port}");
let bypass = get_bypass();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
shell
.command(sysproxy_str)
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap();
output
.await?
};
if !output.status.success() {
@@ -215,7 +224,9 @@ impl Sysopt {
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
@@ -225,12 +236,15 @@ impl Sysopt {
}
let shell = app_handle.shell();
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.command(sysproxy_str)
.args(["set", "1"])
.output()
.await
.unwrap();
.await?;
if !output.status.success() {
bail!("sysproxy exe run failed");
@@ -276,7 +290,10 @@ impl Sysopt {
/// 尝试使用原来的自启动方法
fn try_original_autostart_method(&self, is_enable: bool) {
let app_handle = Handle::global().app_handle().unwrap();
let Some(app_handle) = Handle::global().app_handle() else {
log::error!(target: "app", "App handle not available for autostart");
return;
};
let autostart_manager = app_handle.autolaunch();
if is_enable {
@@ -303,7 +320,9 @@ impl Sysopt {
}
// 回退到原来的方法
let app_handle = Handle::global().app_handle().unwrap();
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let autostart_manager = app_handle.autolaunch();
match autostart_manager.is_enabled() {
+26 -25
View File
@@ -1,9 +1,14 @@
use crate::{config::Config, feat, logging, logging_error, utils::logging::Type};
use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type};
use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, sync::Arc};
use parking_lot::RwLock;
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
};
type TaskID = u64;
@@ -22,23 +27,24 @@ pub struct Timer {
/// save the current state - using RwLock for better read concurrency
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
/// increment id - kept as mutex since it's just a counter
pub timer_count: Arc<Mutex<TaskID>>,
/// increment id - atomic counter for better performance
pub timer_count: AtomicU64,
/// Flag to mark if timer is initialized - atomic for better performance
pub initialized: Arc<std::sync::atomic::AtomicBool>,
pub initialized: AtomicBool,
}
impl Timer {
pub fn global() -> &'static Timer {
static TIMER: OnceCell<Timer> = OnceCell::new();
// Use singleton macro
singleton!(Timer, TIMER_INSTANCE);
TIMER.get_or_init(|| Timer {
impl Timer {
fn new() -> Self {
Timer {
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(RwLock::new(HashMap::new())),
timer_count: Arc::new(Mutex::new(1)),
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
})
timer_count: AtomicU64::new(1),
initialized: AtomicBool::new(false),
}
}
/// Initialize timer with better error handling and atomic operations
@@ -46,12 +52,7 @@ impl Timer {
// Use compare_exchange for thread-safe initialization check
if self
.initialized
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
@@ -63,8 +64,7 @@ impl Timer {
// Initialize timer tasks
if let Err(e) = self.refresh() {
// Reset initialization flag on error
self.initialized
.store(false, std::sync::atomic::Ordering::SeqCst);
self.initialized.store(false, Ordering::SeqCst);
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
return Err(e);
}
@@ -299,7 +299,8 @@ impl Timer {
}
// Find new tasks to add
let mut next_id = *self.timer_count.lock();
let mut next_id = self.timer_count.load(Ordering::Relaxed);
let original_id = next_id;
for (uid, &interval) in new_map.iter() {
if !timer_map.contains_key(uid) {
@@ -316,8 +317,8 @@ impl Timer {
}
// Update counter only if we added new tasks
if next_id > *self.timer_count.lock() {
*self.timer_count.lock() = next_id;
if next_id > original_id {
self.timer_count.store(next_id, Ordering::Relaxed);
}
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
+57 -68
View File
@@ -2,11 +2,13 @@ use once_cell::sync::OnceCell;
use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::ipc::Rate;
use crate::{
cmd,
config::Config,
feat, logging,
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
module::lightweight::is_in_lightweight_mode,
singleton_lazy,
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
Type,
};
@@ -69,9 +71,10 @@ impl TrayState {
let verge = Config::verge().latest_ref().clone();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon {
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
let icon_data = fs::read(common_icon_path).unwrap();
return (true, icon_data);
if let Ok(Some(common_icon_path)) = find_target_icons("common") {
if let Ok(icon_data) = fs::read(common_icon_path) {
return (true, icon_data);
}
}
}
#[cfg(target_os = "macos")]
@@ -103,9 +106,10 @@ impl TrayState {
let verge = Config::verge().latest_ref().clone();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon {
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
let icon_data = fs::read(sysproxy_icon_path).unwrap();
return (true, icon_data);
if let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") {
if let Ok(icon_data) = fs::read(sysproxy_icon_path) {
return (true, icon_data);
}
}
}
#[cfg(target_os = "macos")]
@@ -137,9 +141,10 @@ impl TrayState {
let verge = Config::verge().latest_ref().clone();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon {
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
let icon_data = fs::read(tun_icon_path).unwrap();
return (true, icon_data);
if let Ok(Some(tun_icon_path)) = find_target_icons("tun") {
if let Ok(icon_data) = fs::read(tun_icon_path) {
return (true, icon_data);
}
}
}
#[cfg(target_os = "macos")]
@@ -167,33 +172,33 @@ impl TrayState {
}
}
impl Tray {
pub fn global() -> &'static Tray {
static TRAY: OnceCell<Tray> = OnceCell::new();
#[cfg(target_os = "macos")]
return TRAY.get_or_init(|| Tray {
impl Default for Tray {
fn default() -> Self {
Tray {
last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false),
});
#[cfg(not(target_os = "macos"))]
return TRAY.get_or_init(|| Tray {
last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false),
});
}
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(Tray, TRAY, Tray::default);
impl Tray {
pub fn init(&self) -> Result<()> {
Ok(())
}
/// 更新托盘点击行为
pub fn update_click_behavior(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
let tray = app_handle.tray_by_id("main").unwrap();
let tray = app_handle
.tray_by_id("main")
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
match tray_event.as_str() {
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
_ => tray.set_show_menu_on_left_click(false)?,
@@ -362,8 +367,12 @@ impl Tray {
/// 更新托盘显示状态的函数
pub fn update_tray_display(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let _tray = app_handle.tray_by_id("main").unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
let _tray = app_handle
.tray_by_id("main")
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
// 更新菜单
self.update_menu()?;
@@ -564,9 +573,8 @@ fn create_tray_menu(
is_current_profile,
None::<&str>,
)
.unwrap()
})
.collect();
.collect::<Result<Vec<_>, _>>()?;
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
.iter()
.map(|item| item as &dyn IsMenuItem<Wry>)
@@ -578,8 +586,7 @@ fn create_tray_menu(
t("Dashboard"),
true,
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
)
.unwrap();
)?;
let rule_mode = &CheckMenuItem::with_id(
app_handle,
@@ -588,8 +595,7 @@ fn create_tray_menu(
true,
mode == "rule",
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
)
.unwrap();
)?;
let global_mode = &CheckMenuItem::with_id(
app_handle,
@@ -598,8 +604,7 @@ fn create_tray_menu(
true,
mode == "global",
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
)
.unwrap();
)?;
let direct_mode = &CheckMenuItem::with_id(
app_handle,
@@ -608,8 +613,7 @@ fn create_tray_menu(
true,
mode == "direct",
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
)
.unwrap();
)?;
let profiles = &Submenu::with_id_and_items(
app_handle,
@@ -617,8 +621,7 @@ fn create_tray_menu(
t("Profiles"),
true,
&profile_menu_items,
)
.unwrap();
)?;
let system_proxy = &CheckMenuItem::with_id(
app_handle,
@@ -627,8 +630,7 @@ fn create_tray_menu(
true,
system_proxy_enabled,
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
)
.unwrap();
)?;
let tun_mode = &CheckMenuItem::with_id(
app_handle,
@@ -637,8 +639,7 @@ fn create_tray_menu(
true,
tun_mode_enabled,
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
)
.unwrap();
)?;
let lighteweight_mode = &CheckMenuItem::with_id(
app_handle,
@@ -647,11 +648,9 @@ fn create_tray_menu(
true,
is_lightweight_mode,
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
)
.unwrap();
)?;
let copy_env =
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
let copy_env = &MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>)?;
let open_app_dir = &MenuItem::with_id(
app_handle,
@@ -659,8 +658,7 @@ fn create_tray_menu(
t("Conf Dir"),
true,
None::<&str>,
)
.unwrap();
)?;
let open_core_dir = &MenuItem::with_id(
app_handle,
@@ -668,8 +666,7 @@ fn create_tray_menu(
t("Core Dir"),
true,
None::<&str>,
)
.unwrap();
)?;
let open_logs_dir = &MenuItem::with_id(
app_handle,
@@ -677,8 +674,7 @@ fn create_tray_menu(
t("Logs Dir"),
true,
None::<&str>,
)
.unwrap();
)?;
let open_dir = &Submenu::with_id_and_items(
app_handle,
@@ -686,8 +682,7 @@ fn create_tray_menu(
t("Open Dir"),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
)?;
let restart_clash = &MenuItem::with_id(
app_handle,
@@ -695,8 +690,7 @@ fn create_tray_menu(
t("Restart Clash Core"),
true,
None::<&str>,
)
.unwrap();
)?;
let restart_app = &MenuItem::with_id(
app_handle,
@@ -704,8 +698,7 @@ fn create_tray_menu(
t("Restart App"),
true,
None::<&str>,
)
.unwrap();
)?;
let app_version = &MenuItem::with_id(
app_handle,
@@ -713,8 +706,7 @@ fn create_tray_menu(
format!("{} {version}", t("Verge Version")),
true,
None::<&str>,
)
.unwrap();
)?;
let more = &Submenu::with_id_and_items(
app_handle,
@@ -722,13 +714,11 @@ fn create_tray_menu(
t("More"),
true,
&[restart_clash, restart_app, app_version],
)
.unwrap();
)?;
let quit =
&MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q")).unwrap();
let quit = &MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q"))?;
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
let separator = &PredefinedMenuItem::separator(app_handle)?;
let menu = tauri::menu::MenuBuilder::new(app_handle)
.items(&[
@@ -750,8 +740,7 @@ fn create_tray_menu(
separator,
quit,
])
.build()
.unwrap();
.build()?;
Ok(menu)
}
+1 -1
View File
@@ -6,7 +6,7 @@ use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
pub async fn invoke_uwptools() -> Result<()> {
pub fn invoke_uwptools() -> Result<()> {
let resource_dir = dirs::app_resources_dir()?;
let tool_path = resource_dir.join("enableLoopback.exe");
+4 -1
View File
@@ -18,7 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
deep_merge(&mut config, &Value::from(merge));
let config = config.as_mapping().unwrap().clone();
let config = config.as_mapping().cloned().unwrap_or_else(|| {
log::error!("Failed to convert merged config to mapping, using empty mapping");
Mapping::new()
});
config
}
+18 -3
View File
@@ -14,7 +14,7 @@ type ResultLog = Vec<(String, String)>;
/// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
let clash_config = { Config::clash().latest_ref().0.clone() };
@@ -234,7 +234,22 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
continue;
}
}
config.insert(key, value);
// 处理 external-controller 键的开关逻辑
if key.as_str() == Some("external-controller") {
let enable_external_controller = Config::verge()
.latest_ref()
.enable_external_controller
.unwrap_or(false);
if enable_external_controller {
config.insert(key, value);
} else {
// 如果禁用了外部控制器,设置为空字符串
config.insert(key, "".into());
}
} else {
config.insert(key, value);
}
}
}
@@ -259,7 +274,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
});
}
config = use_tun(config, enable_tun).await;
config = use_tun(config, enable_tun);
config = use_sort(config);
// 应用独立的DNS配置(如果启用)
+49 -25
View File
@@ -7,25 +7,43 @@ pub fn use_script(
config: Mapping,
name: String,
) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use parking_lot::Mutex;
use std::sync::Arc;
use boa_engine::{native_function::NativeFunction, Context, JsString, JsValue, Source};
use std::{cell::RefCell, rc::Rc};
let mut context = Context::default();
let outputs = Arc::new(Mutex::new(vec![]));
let outputs = Rc::new(RefCell::new(vec![]));
let copy_outputs = outputs.clone();
let copy_outputs = Rc::clone(&outputs);
unsafe {
let _ = context.register_global_builtin_callable(
"__verge_log__".into(),
2,
NativeFunction::from_closure(
move |_: &JsValue, args: &[JsValue], context: &mut Context| {
let level = args.first().unwrap().to_string(context)?;
let level = level.to_std_string().unwrap();
let data = args.get(1).unwrap().to_string(context)?;
let data = data.to_std_string().unwrap();
let mut out = copy_outputs.lock();
let level = args.first().ok_or_else(|| {
boa_engine::JsError::from_opaque(
JsString::from("Missing level argument").into(),
)
})?;
let level = level.to_string(context)?;
let level = level.to_std_string().map_err(|_| {
boa_engine::JsError::from_opaque(
JsString::from("Failed to convert level to string").into(),
)
})?;
let data = args.get(1).ok_or_else(|| {
boa_engine::JsError::from_opaque(
JsString::from("Missing data argument").into(),
)
})?;
let data = data.to_string(context)?;
let data = data.to_std_string().map_err(|_| {
boa_engine::JsError::from_opaque(
JsString::from("Failed to convert data to string").into(),
)
})?;
let mut out = copy_outputs.borrow_mut();
out.push((level, data));
Ok(JsValue::undefined())
},
@@ -50,25 +68,29 @@ pub fn use_script(
let safe_name = escape_js_string_for_single_quote(&name);
let code = format!(
r#"try{{
r"try{{
{script};
JSON.stringify(main({config_str},'{safe_name}')||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
}}"
);
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
if !result.is_string() {
anyhow::bail!("main function should return object");
}
let result = result.to_string(&mut context).unwrap();
let result = result.to_std_string().unwrap();
let result = result
.to_string(&mut context)
.map_err(|e| anyhow::anyhow!("Failed to convert JS result to string: {}", e))?;
let result = result
.to_std_string()
.map_err(|_| anyhow::anyhow!("Failed to convert JS string to std string"))?;
// 直接解析JSON结果,不做其他解析
let res: Result<Mapping, Error> = parse_json_safely(&result);
let mut out = outputs.lock();
let mut out = outputs.borrow_mut();
match res {
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
Err(err) => {
@@ -103,6 +125,8 @@ fn escape_js_string_for_single_quote(s: &str) -> String {
}
#[test]
#[allow(unused_variables)]
#[allow(clippy::expect_used)]
fn test_script() {
let script = r#"
function main(config) {
@@ -115,7 +139,7 @@ fn test_script() {
}
"#;
let config = r#"
let config = r"
rules:
- 111
- 222
@@ -123,22 +147,21 @@ fn test_script() {
enable: false
dns:
enable: false
"#;
";
let config = serde_yaml::from_str(config).unwrap();
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let config = serde_yaml::from_str(config).expect("Failed to parse test config YAML");
let (config, results) = use_script(script.into(), config, "".to_string())
.expect("Script execution should succeed in test");
let _ = serde_yaml::to_string(&config).unwrap();
let _ = serde_yaml::to_string(&config).expect("Failed to serialize config to YAML");
let yaml_config_size = std::mem::size_of_val(&config);
dbg!(yaml_config_size);
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
dbg!(box_yaml_config_size);
dbg!(results);
assert!(box_yaml_config_size < yaml_config_size);
}
// 测试特殊字符转义功能
#[test]
#[allow(clippy::expect_used)]
fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string_for_single_quote(test_string);
@@ -146,13 +169,14 @@ fn test_escape_unescape() {
println!("Escaped: {escaped}");
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
let parsed = parse_json_safely(json_str).unwrap();
let parsed = parse_json_safely(json_str).expect("Failed to parse test JSON safely");
assert!(parsed.contains_key("key"));
assert!(parsed.contains_key("nested"));
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
let parsed_quoted = parse_json_safely(quoted_json_str).unwrap();
let parsed_quoted =
parse_json_safely(quoted_json_str).expect("Failed to parse quoted test JSON safely");
assert!(parsed_quoted.contains_key("key"));
assert!(parsed_quoted.contains_key("nested"));
+29 -13
View File
@@ -89,6 +89,8 @@ mod tests {
use serde_yaml::Value;
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_delete_proxy_and_references() {
let config_str = r#"
proxies:
@@ -107,7 +109,8 @@ proxy-groups:
proxies:
- "proxy1"
"#;
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
let mut config: Mapping =
serde_yaml::from_str(config_str).expect("Failed to parse test config YAML");
let seq = SeqMap {
prepend: Sequence::new(),
@@ -118,38 +121,51 @@ proxy-groups:
config = use_seq(seq, config, "proxies");
// Check if proxy1 is removed from proxies
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
let proxies = config
.get("proxies")
.expect("proxies field should exist")
.as_sequence()
.expect("proxies should be a sequence");
assert_eq!(proxies.len(), 1);
assert_eq!(
proxies[0]
.as_mapping()
.unwrap()
.expect("proxy should be a mapping")
.get("name")
.unwrap()
.expect("proxy should have name")
.as_str()
.unwrap(),
.expect("name should be string"),
"proxy2"
);
// Check if proxy1 is removed from all groups
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
let groups = config
.get("proxy-groups")
.expect("proxy-groups field should exist")
.as_sequence()
.expect("proxy-groups should be a sequence");
let group1_proxies = groups[0]
.as_mapping()
.unwrap()
.expect("group should be a mapping")
.get("proxies")
.unwrap()
.expect("group should have proxies")
.as_sequence()
.unwrap();
.expect("group proxies should be a sequence");
let group2_proxies = groups[1]
.as_mapping()
.unwrap()
.expect("group should be a mapping")
.get("proxies")
.unwrap()
.expect("group should have proxies")
.as_sequence()
.unwrap();
.expect("group proxies should be a sequence");
assert_eq!(group1_proxies.len(), 1);
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
assert_eq!(
group1_proxies[0]
.as_str()
.expect("proxy name should be string"),
"proxy2"
);
assert_eq!(group2_proxies.len(), 0);
}
}
+11 -4
View File
@@ -1,5 +1,8 @@
use serde_yaml::{Mapping, Value};
#[cfg(target_os = "macos")]
use crate::process::AsyncHandler;
macro_rules! revise {
($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into());
@@ -18,7 +21,7 @@ macro_rules! append {
};
}
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
@@ -59,8 +62,10 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
AsyncHandler::spawn(move || async move {
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
});
}
}
@@ -69,7 +74,9 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
} else {
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
#[cfg(target_os = "macos")]
crate::utils::resolve::restore_public_dns().await;
AsyncHandler::spawn(move || async move {
crate::utils::resolve::restore_public_dns().await;
});
}
// 更新TUN配置
+3 -1
View File
@@ -57,7 +57,9 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let backup_storage_path = app_home_dir().unwrap().join(&filename);
let backup_storage_path = app_home_dir()
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
.join(&filename);
backup::WebDavClient::global()
.download(filename, backup_storage_path.clone())
.await
+19 -8
View File
@@ -1,8 +1,8 @@
use crate::{
config::Config,
core::{handle, tray, CoreManager},
ipc::IpcManager,
logging_error,
module::mihomo::MihomoManager,
process::AsyncHandler,
utils::{logging::Type, resolve},
};
@@ -28,22 +28,33 @@ pub fn restart_clash_core() {
/// Restart the application
pub fn restart_app() {
AsyncHandler::spawn(move || async move {
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
resolve::resolve_reset_async().await;
let app_handle = handle::Handle::global().app_handle().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
tauri::process::restart(&app_handle.env());
handle::Handle::global()
.app_handle()
.map(|app_handle| {
tauri::process::restart(&app_handle.env());
})
.unwrap_or_else(|| {
logging_error!(
Type::System,
false,
"{}",
"Failed to get app handle for restart"
);
});
});
}
fn after_change_clash_mode() {
AsyncHandler::spawn(move || async {
match MihomoManager::global().get_connections().await {
match IpcManager::global().get_connections().await {
Ok(connections) => {
if let Some(connections_array) = connections["connections"].as_array() {
for connection in connections_array {
if let Some(id) = connection["id"].as_str() {
let _ = MihomoManager::global().delete_connection(id).await;
let _ = IpcManager::global().delete_connection(id).await;
}
}
}
@@ -65,7 +76,7 @@ pub fn change_clash_mode(mode: String) {
});
AsyncHandler::spawn(move || async move {
log::debug!(target: "app", "change clash mode to {mode}");
match MihomoManager::global().patch_configs(json_value).await {
match IpcManager::global().patch_configs(json_value).await {
Ok(_) => {
// 更新订阅
Config::clash().data_mut().patch_config(mapping);
+12 -4
View File
@@ -15,7 +15,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
let res = {
// 激活订阅
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
Config::generate().await?;
Config::generate()?;
CoreManager::global().restart_core().await?;
} else {
if patch.get("mode").is_some() {
@@ -95,6 +95,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
let tray_event = patch.tray_event;
let home_cards = patch.home_cards.clone();
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
let enable_external_controller = patch.enable_external_controller;
let res: std::result::Result<(), anyhow::Error> = {
// Initialize with no flags set
let mut update_flags: i32 = UpdateFlags::None as i32;
@@ -165,9 +166,14 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
update_flags |= UpdateFlags::LighteWeight as i32;
}
// 处理 external-controller 的开关
if enable_external_controller.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
// Process updates based on flags
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
Config::generate().await?;
Config::generate()?;
CoreManager::global().restart_core().await?;
}
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
@@ -185,7 +191,9 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys)?;
}
}
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
tray::Tray::global().update_menu()?;
@@ -200,7 +208,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
tray::Tray::global().update_click_behavior()?;
}
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
if enable_auto_light_weight.unwrap() {
if enable_auto_light_weight.unwrap_or(false) {
lightweight::enable_auto_light_weight_mode();
} else {
lightweight::disable_auto_light_weight_mode();
+24 -3
View File
@@ -11,7 +11,14 @@ use anyhow::{bail, Result};
/// Toggle proxy profile
pub fn toggle_proxy_profile(profile_index: String) {
AsyncHandler::spawn(|| async move {
let app_handle = handle::Handle::global().app_handle().unwrap();
let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!(
error,
Type::Config,
"Failed to get app handle for profile toggle"
);
return;
};
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
Ok(_) => {
let _ = tray::Tray::global().update_menu();
@@ -50,9 +57,14 @@ pub async fn update_profile(
log::info!(target: "app",
"[订阅更新] {} 是远程订阅,URL: {}",
uid,
item.url.clone().unwrap()
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
);
Some((item.url.clone().unwrap(), item.option.clone()))
Some((
item.url
.clone()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
item.option.clone(),
))
}
};
@@ -136,6 +148,15 @@ pub async fn update_profile(
Ok(_) => {
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
if let Err(err) = cmd::proxy::force_refresh_proxies().await {
logging!(
error,
Type::Config,
true,
"[订阅更新] 代理组刷新失败: {}",
err
);
}
}
Err(err) => {
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
+12 -5
View File
@@ -1,7 +1,10 @@
use crate::{
config::{Config, IVerge},
core::handle,
ipc::IpcManager,
logging,
process::AsyncHandler,
utils::logging::Type,
};
use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt;
@@ -18,10 +21,7 @@ pub fn toggle_system_proxy() {
AsyncHandler::spawn(move || async move {
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable && auto_close_connection {
if let Err(err) = crate::module::mihomo::MihomoManager::global()
.close_all_connections()
.await
{
if let Err(err) = IpcManager::global().close_all_connections().await {
log::error!(target: "app", "Failed to close all connections: {err}");
}
}
@@ -73,7 +73,14 @@ pub fn copy_clash_env() {
.unwrap_or_else(|| "127.0.0.1".to_string())
});
let app_handle = handle::Handle::global().app_handle().unwrap();
let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!(
error,
Type::System,
"Failed to get app handle for proxy operation"
);
return;
};
let port = {
Config::verge()
.latest_ref()
+36 -30
View File
@@ -3,8 +3,8 @@ use crate::AppHandleManager;
use crate::{
config::Config,
core::{handle, sysopt, CoreManager},
ipc::IpcManager,
logging,
module::mihomo::MihomoManager,
utils::logging::Type,
};
@@ -67,7 +67,14 @@ pub fn quit() {
logging!(debug, Type::System, true, "启动退出流程");
// 获取应用句柄并设置退出标志
let app_handle = handle::Handle::global().app_handle().unwrap();
let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!(
error,
Type::System,
"Failed to get app handle for quit operation"
);
return;
};
handle::Handle::global().set_is_exiting();
// 优先关闭窗口,提供立即反馈
@@ -98,31 +105,30 @@ async fn clean_async() -> bool {
logging!(info, Type::System, true, "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_task = async {
if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) {
let disable_tun = serde_json::json!({
"tun": {
"enable": false
}
});
match timeout(
Duration::from_secs(2),
MihomoManager::global().patch_configs(disable_tun),
)
.await
{
Ok(_) => {
log::info!(target: "app", "TUN模式已禁用");
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
}
let tun_success = if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) {
let disable_tun = serde_json::json!({"tun": {"enable": false}});
match timeout(
Duration::from_secs(3),
IpcManager::global().patch_configs(disable_tun),
)
.await
{
Ok(Ok(_)) => {
log::info!(target: "app", "TUN模式已禁用");
tokio::time::sleep(Duration::from_millis(300)).await;
true
}
Ok(Err(e)) => {
log::warn!(target: "app", "禁用TUN模式失败: {e}");
false
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
}
} else {
true
}
} else {
true
};
// 2. 系统代理重置
@@ -178,8 +184,8 @@ async fn clean_async() -> bool {
}
};
// 并行执行所有清理任务
let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task);
// 并行执行剩余清理任务
let (proxy_success, core_success) = tokio::join!(proxy_task, core_task);
#[cfg(target_os = "macos")]
let dns_success = dns_task.await;
@@ -192,7 +198,7 @@ async fn clean_async() -> bool {
info,
Type::System,
true,
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
"异步关闭操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
tun_success,
proxy_success,
core_success,
@@ -209,7 +215,7 @@ pub fn clean() -> bool {
let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始执行清理操作...");
logging!(info, Type::System, true, "开始执行关闭操作...");
// 使用已有的异步清理函数
let cleanup_result = clean_async().await;
@@ -220,7 +226,7 @@ pub fn clean() -> bool {
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => {
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
logging!(info, Type::System, true, "关闭操作完成,结果: {}", result);
result
}
Err(_) => {
+386
View File
@@ -0,0 +1,386 @@
use std::time::Duration;
use kode_bridge::{
errors::{AnyError, AnyResult},
pool::PoolConfig,
ClientConfig, IpcHttpClient, LegacyResponse,
};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
// 定义用于URL路径的编码集合,只编码真正必要的字符
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ') // 空格
.add(b'/') // 斜杠
.add(b'?') // 问号
.add(b'#') // 井号
.add(b'&') // 和号
.add(b'%'); // 百分号
use crate::{logging, singleton_with_logging, utils::dirs::ipc_path};
// Helper function to create AnyError from string
fn create_error(msg: impl Into<String>) -> AnyError {
Box::new(std::io::Error::other(msg.into()))
}
pub struct IpcManager {
ipc_path: String,
config: ClientConfig,
}
impl IpcManager {
fn new() -> Self {
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!(
error,
crate::utils::logging::Type::Ipc,
true,
"Failed to get IPC path: {}",
e
);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
});
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
Self {
ipc_path: ipc_path.to_string(),
config: ClientConfig {
default_timeout: Duration::from_secs(5),
enable_pooling: true,
max_retries: 3,
max_concurrent_requests: 32,
max_requests_per_second: Some(5.0),
pool_config: PoolConfig {
max_size: 32,
min_idle: 2,
max_idle_time_ms: 10_000,
max_retries: 3,
max_concurrent_requests: 32,
max_requests_per_second: Some(5.0),
..Default::default()
},
..Default::default()
},
}
}
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
impl IpcManager {
pub async fn request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<LegacyResponse> {
let client = IpcHttpClient::with_config(&self.ipc_path, self.config.clone())?;
client.request(method, path, body).await
}
}
impl IpcManager {
pub async fn send_request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<serde_json::Value> {
let response = IpcManager::global().request(method, path, body).await?;
match method {
"GET" => Ok(response.json()?),
"PATCH" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
Ok(response.json()?)
}
}
"PUT" | "DELETE" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
match response.json() {
Ok(json) => Ok(json),
Err(_) => Ok(serde_json::json!({
"code": response.status,
"message": response.body,
"error": "failed to parse response as JSON"
})),
}
}
}
_ => match response.json() {
Ok(json) => Ok(json),
Err(_) => Ok(serde_json::json!({
"code": response.status,
"message": response.body,
"error": "failed to parse response as JSON"
})),
},
}
}
// 基础代理信息获取
pub async fn get_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/proxies";
self.send_request("GET", url, None).await
}
// 代理提供者信息获取
pub async fn get_providers_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/proxies";
self.send_request("GET", url, None).await
}
// 连接管理
pub async fn get_connections(&self) -> AnyResult<serde_json::Value> {
let url = "/connections";
self.send_request("GET", url, None).await
}
pub async fn delete_connection(&self, id: &str) -> AnyResult<()> {
let encoded_id = utf8_percent_encode(id, URL_PATH_ENCODE_SET).to_string();
let url = format!("/connections/{encoded_id}");
let response = self.send_request("DELETE", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"].as_str().unwrap_or("unknown error"),
))
}
}
pub async fn close_all_connections(&self) -> AnyResult<()> {
let url = "/connections";
let response = self.send_request("DELETE", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
}
impl IpcManager {
#[allow(dead_code)]
pub async fn is_mihomo_running(&self) -> AnyResult<()> {
let url = "/version";
let _response = self.send_request("GET", url, None).await?;
Ok(())
}
pub async fn put_configs_force(&self, clash_config_path: &str) -> AnyResult<()> {
let url = "/configs?force=true";
let payload = serde_json::json!({
"path": clash_config_path,
});
let _response = self.send_request("PUT", url, Some(&payload)).await?;
Ok(())
}
pub async fn patch_configs(&self, config: serde_json::Value) -> AnyResult<()> {
let url = "/configs";
let response = self.send_request("PATCH", url, Some(&config)).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
pub async fn test_proxy_delay(
&self,
name: &str,
test_url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url =
test_url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码,直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
}
// 版本和配置相关
pub async fn get_version(&self) -> AnyResult<serde_json::Value> {
let url = "/version";
self.send_request("GET", url, None).await
}
pub async fn get_config(&self) -> AnyResult<serde_json::Value> {
let url = "/configs";
self.send_request("GET", url, None).await
}
pub async fn update_geo_data(&self) -> AnyResult<()> {
let url = "/configs/geo";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn upgrade_core(&self) -> AnyResult<()> {
let url = "/upgrade";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 规则相关
pub async fn get_rules(&self) -> AnyResult<serde_json::Value> {
let url = "/rules";
self.send_request("GET", url, None).await
}
pub async fn get_rule_providers(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/rules";
self.send_request("GET", url, None).await
}
pub async fn update_rule_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/rules/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 代理相关
pub async fn update_proxy(&self, group: &str, proxy: &str) -> AnyResult<()> {
// 使用 percent-encoding 进行正确的 URL 编码
let encoded_group = utf8_percent_encode(group, URL_PATH_ENCODE_SET).to_string();
let url = format!("/proxies/{encoded_group}");
let payload = serde_json::json!({
"name": proxy
});
match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => Ok(()),
Err(e) => {
logging!(
error,
crate::utils::logging::Type::Ipc,
true,
"IPC: updateProxy encountered error: {} (ignored, always returning true)",
e
);
Ok(())
}
}
}
pub async fn proxy_provider_health_check(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}/healthcheck");
let response = self.send_request("GET", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn update_proxy_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 延迟测试相关
pub async fn get_group_proxy_delays(
&self,
group_name: &str,
url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url = url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_group_name = utf8_percent_encode(group_name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码,直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
}
// 调试相关
pub async fn is_debug_enabled(&self) -> AnyResult<bool> {
let url = "/debug/pprof";
match self.send_request("GET", url, None).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub async fn gc(&self) -> AnyResult<()> {
let url = "/debug/gc";
let response = self.send_request("PUT", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
}
+330
View File
@@ -0,0 +1,330 @@
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, sync::Arc, time::Instant};
use tauri::async_runtime::JoinHandle;
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::MonitorData,
logging,
process::AsyncHandler,
singleton_with_logging,
utils::{dirs::ipc_path, logging::Type},
};
const MAX_LOGS: usize = 1000; // Maximum number of logs to keep in memory
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LogData {
#[serde(rename = "type")]
pub log_type: String,
pub payload: String,
}
#[derive(Debug, Clone)]
pub struct LogItem {
pub log_type: String,
pub payload: String,
pub time: String,
}
impl LogItem {
fn new(log_type: String, payload: String) -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs();
// Simple time formatting (HH:MM:SS)
let hours = (now / 3600) % 24;
let minutes = (now / 60) % 60;
let seconds = now % 60;
let time_str = format!("{hours:02}:{minutes:02}:{seconds:02}");
Self {
log_type,
payload,
time: time_str,
}
}
}
#[derive(Debug, Clone)]
pub struct CurrentLogs {
pub logs: VecDeque<LogItem>,
// pub level: String,
pub last_updated: Instant,
}
impl Default for CurrentLogs {
fn default() -> Self {
Self {
logs: VecDeque::with_capacity(MAX_LOGS),
// level: "info".to_string(),
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentLogs {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
// Logs monitor with streaming support
pub struct LogsMonitor {
current: Arc<RwLock<CurrentLogs>>,
task_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
current_monitoring_level: Arc<RwLock<Option<String>>>,
}
// Use singleton_with_logging macro
singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor");
impl LogsMonitor {
fn new() -> Self {
let current = Arc::new(RwLock::new(CurrentLogs::default()));
Self {
current,
task_handle: Arc::new(RwLock::new(None)),
current_monitoring_level: Arc::new(RwLock::new(None)),
}
}
pub async fn start_monitoring(&self, level: Option<String>) {
let filter_level = level.clone().unwrap_or_else(|| "info".to_string());
// Check if we're already monitoring the same level
// let level_changed = {
// let current_level = self.current_monitoring_level.read().await;
// if let Some(existing_level) = current_level.as_ref() {
// if existing_level == &filter_level {
// logging!(
// info,
// Type::Ipc,
// true,
// "LogsMonitor: Already monitoring level '{}', skipping duplicate request",
// filter_level
// );
// return;
// }
// true // Level changed
// } else {
// true // First time or was stopped
// }
// };
// Stop existing monitoring task if level changed or first time
{
let mut handle = self.task_handle.write().await;
if let Some(task) = handle.take() {
task.abort();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Stopped previous monitoring task (level changed)"
);
}
}
// We want to keep the logs cache even if the level changes,
// so we don't clear it here. The cache will be cleared only when the level changes
// and a new task is started. This allows us to keep logs from previous levels
// even if the level changes during monitoring.
// Clear logs cache when level changes to ensure fresh data
// if level_changed {
// let mut current = self.current.write().await;
// current.logs.clear();
// current.level = filter_level.clone();
// current.mark_fresh();
// logging!(
// info,
// Type::Ipc,
// true,
// "LogsMonitor: Cleared logs cache due to level change to '{}'",
// filter_level
// );
// }
// Update current monitoring level
{
let mut current_level = self.current_monitoring_level.write().await;
*current_level = Some(filter_level.clone());
}
let monitor_current = Arc::clone(&self.current);
let task = AsyncHandler::spawn(move || async move {
loop {
// Get fresh IPC path and client for each connection attempt
let (_ipc_path_buf, client) = match Self::create_ipc_client() {
Ok((path, client)) => (path, client),
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
};
let url = if filter_level == "all" {
"/logs".to_string()
} else {
format!("/logs?level={filter_level}")
};
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Starting stream for {}",
url
);
let _ = client
.get(&url)
.timeout(Duration::from_secs(30))
.process_lines(|line| {
Self::process_log_line(line, Arc::clone(&monitor_current))
})
.await;
// Wait before retrying
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
// Store the task handle
{
let mut handle = self.task_handle.write().await;
*handle = Some(task);
}
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Started new monitoring task for level: {:?}",
level
);
}
pub async fn stop_monitoring(&self) {
// Stop monitoring task but keep logs
{
let mut handle = self.task_handle.write().await;
if let Some(task) = handle.take() {
task.abort();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Stopped monitoring task"
);
}
}
// Reset monitoring level
{
let mut monitoring_level = self.current_monitoring_level.write().await;
*monitoring_level = None;
}
}
fn create_ipc_client() -> Result<
(std::path::PathBuf, kode_bridge::IpcStreamClient),
Box<dyn std::error::Error + Send + Sync>,
> {
use kode_bridge::IpcStreamClient;
let ipc_path_buf = ipc_path()?;
let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?;
let client = IpcStreamClient::new(ipc_path)?;
Ok((ipc_path_buf, client))
}
fn process_log_line(
line: &str,
current: Arc<RwLock<CurrentLogs>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(log_data) = serde_json::from_str::<LogData>(line.trim()) {
// Server-side filtering via query parameters handles the level filtering
// We only need to accept all logs since filtering is done at the endpoint level
let log_item = LogItem::new(log_data.log_type, log_data.payload);
AsyncHandler::spawn(move || async move {
let mut logs = current.write().await;
// Add new log
logs.logs.push_back(log_item);
// Keep only the last 1000 logs
if logs.logs.len() > 1000 {
logs.logs.pop_front();
}
logs.mark_fresh();
});
}
Ok(())
}
pub async fn current(&self) -> CurrentLogs {
self.current.read().await.clone()
}
pub async fn clear_logs(&self) {
let mut current = self.current.write().await;
current.logs.clear();
current.mark_fresh();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Cleared frontend logs (monitoring continues)"
);
}
pub async fn get_logs_as_json(&self) -> serde_json::Value {
let current = self.current().await;
// Simply return all cached logs since filtering is handled by start_monitoring
// and the cache is cleared when level changes
let logs: Vec<serde_json::Value> = current
.logs
.iter()
.map(|log| {
serde_json::json!({
"type": log.log_type,
"payload": log.payload,
"time": log.time
})
})
.collect();
serde_json::Value::Array(logs)
}
}
pub async fn start_logs_monitoring(level: Option<String>) {
LogsMonitor::global().start_monitoring(level).await;
}
pub async fn stop_logs_monitoring() {
LogsMonitor::global().stop_monitoring().await;
}
pub async fn clear_logs() {
LogsMonitor::global().clear_logs().await;
}
pub async fn get_logs_json() -> serde_json::Value {
LogsMonitor::global().get_logs_as_json().await
}
+119
View File
@@ -0,0 +1,119 @@
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
process::AsyncHandler,
singleton_lazy_with_logging,
utils::format::fmt_bytes,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MemoryData {
pub inuse: u64,
pub oslimit: u64,
}
#[derive(Debug, Clone)]
pub struct CurrentMemory {
pub inuse: u64,
pub oslimit: u64,
pub last_updated: Instant,
}
impl Default for CurrentMemory {
fn default() -> Self {
Self {
inuse: 0,
oslimit: 0,
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentMemory {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
impl StreamingParser for CurrentMemory {
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(memory) = serde_json::from_str::<MemoryData>(line.trim()) {
AsyncHandler::spawn(move || async move {
let mut current_guard = current.write().await;
current_guard.inuse = memory.inuse;
current_guard.oslimit = memory.oslimit;
current_guard.mark_fresh();
});
}
Ok(())
}
}
// Minimal memory monitor using the new architecture
pub struct MemoryMonitor {
monitor: IpcStreamMonitor<CurrentMemory>,
}
impl Default for MemoryMonitor {
fn default() -> Self {
MemoryMonitor {
monitor: IpcStreamMonitor::new(
"/memory".to_string(),
Duration::from_secs(10),
Duration::from_secs(2),
Duration::from_secs(10),
),
}
}
}
// Use simplified singleton_lazy_with_logging macro
singleton_lazy_with_logging!(
MemoryMonitor,
INSTANCE,
"MemoryMonitor",
MemoryMonitor::default
);
impl MemoryMonitor {
pub async fn current(&self) -> CurrentMemory {
self.monitor.current().await
}
pub async fn is_fresh(&self) -> bool {
self.monitor.is_fresh().await
}
}
pub async fn get_current_memory() -> CurrentMemory {
MemoryMonitor::global().current().await
}
pub async fn get_formatted_memory() -> (String, String, f64, bool) {
let monitor = MemoryMonitor::global();
let memory = monitor.current().await;
let is_fresh = monitor.is_fresh().await;
let usage_percent = if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
};
(
fmt_bytes(memory.inuse),
fmt_bytes(memory.oslimit),
usage_percent,
is_fresh,
)
}
+15
View File
@@ -0,0 +1,15 @@
pub mod general;
pub mod logs;
pub mod memory;
pub mod monitor;
pub mod traffic;
pub use general::IpcManager;
pub use logs::{clear_logs, get_logs_json, start_logs_monitoring, stop_logs_monitoring};
pub use memory::{get_current_memory, get_formatted_memory};
pub use traffic::{get_current_traffic, get_formatted_traffic};
pub struct Rate {
// pub up: usize,
// pub down: usize,
}
+120
View File
@@ -0,0 +1,120 @@
use kode_bridge::IpcStreamClient;
use std::sync::Arc;
use tokio::{sync::RwLock, time::Duration};
use crate::{
logging,
process::AsyncHandler,
utils::{dirs::ipc_path, logging::Type},
};
/// Generic base structure for IPC monitoring data with freshness tracking
pub trait MonitorData: Clone + Send + Sync + 'static {
/// Update the last_updated timestamp to now
fn mark_fresh(&mut self);
/// Check if data is fresh based on the given duration
fn is_fresh_within(&self, duration: Duration) -> bool;
}
/// Trait for parsing streaming data and updating monitor state
pub trait StreamingParser: MonitorData {
/// Parse a line of streaming data and update the current state
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
/// Generic IPC stream monitor that handles the common streaming pattern
pub struct IpcStreamMonitor<T>
where
T: MonitorData + StreamingParser + Default,
{
current: Arc<RwLock<T>>,
#[allow(dead_code)]
endpoint: String,
#[allow(dead_code)]
timeout: Duration,
#[allow(dead_code)]
retry_interval: Duration,
freshness_duration: Duration,
}
impl<T> IpcStreamMonitor<T>
where
T: MonitorData + StreamingParser + Default,
{
pub fn new(
endpoint: String,
timeout: Duration,
retry_interval: Duration,
freshness_duration: Duration,
) -> Self {
let current = Arc::new(RwLock::new(T::default()));
let monitor_current = Arc::clone(&current);
let endpoint_clone = endpoint.clone();
// Start the monitoring task
AsyncHandler::spawn(move || async move {
Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await;
});
Self {
current,
endpoint,
timeout,
retry_interval,
freshness_duration,
}
}
pub async fn current(&self) -> T {
self.current.read().await.clone()
}
pub async fn is_fresh(&self) -> bool {
self.current
.read()
.await
.is_fresh_within(self.freshness_duration)
}
/// The core streaming task that can be specialized per monitor type
async fn streaming_task(
current: Arc<RwLock<T>>,
endpoint: String,
timeout: Duration,
retry_interval: Duration,
) {
loop {
let ipc_path_buf = match ipc_path() {
Ok(path) => path,
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
tokio::time::sleep(retry_interval).await;
continue;
}
};
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
let client = match IpcStreamClient::new(ipc_path) {
Ok(client) => client,
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
tokio::time::sleep(retry_interval).await;
continue;
}
};
let _ = client
.get(&endpoint)
.timeout(timeout)
.process_lines(|line| T::parse_and_update(line, Arc::clone(&current)))
.await;
tokio::time::sleep(retry_interval).await;
}
}
}
+153
View File
@@ -0,0 +1,153 @@
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
process::AsyncHandler,
singleton_lazy_with_logging,
utils::format::fmt_bytes,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrafficData {
pub up: u64,
pub down: u64,
}
#[derive(Debug, Clone)]
pub struct CurrentTraffic {
pub up_rate: u64,
pub down_rate: u64,
pub total_up: u64,
pub total_down: u64,
pub last_updated: Instant,
}
impl Default for CurrentTraffic {
fn default() -> Self {
Self {
up_rate: 0,
down_rate: 0,
total_up: 0,
total_down: 0,
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentTraffic {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
// Traffic monitoring state for calculating rates
#[derive(Debug, Clone, Default)]
pub struct TrafficMonitorState {
pub current: CurrentTraffic,
pub last_traffic: Option<TrafficData>,
}
impl MonitorData for TrafficMonitorState {
fn mark_fresh(&mut self) {
self.current.mark_fresh();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.current.is_fresh_within(duration)
}
}
impl StreamingParser for TrafficMonitorState {
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(traffic) = serde_json::from_str::<TrafficData>(line.trim()) {
AsyncHandler::spawn(move || async move {
let mut state_guard = current.write().await;
let (up_rate, down_rate) = state_guard
.last_traffic
.as_ref()
.map(|l| {
(
traffic.up.saturating_sub(l.up),
traffic.down.saturating_sub(l.down),
)
})
.unwrap_or((0, 0));
state_guard.current = CurrentTraffic {
up_rate,
down_rate,
total_up: traffic.up,
total_down: traffic.down,
last_updated: Instant::now(),
};
state_guard.last_traffic = Some(traffic);
});
}
Ok(())
}
}
// Minimal traffic monitor using the new architecture
pub struct TrafficMonitor {
monitor: IpcStreamMonitor<TrafficMonitorState>,
}
impl Default for TrafficMonitor {
fn default() -> Self {
TrafficMonitor {
monitor: IpcStreamMonitor::new(
"/traffic".to_string(),
Duration::from_secs(10),
Duration::from_secs(1),
Duration::from_secs(5),
),
}
}
}
// Use simplified singleton_lazy_with_logging macro
singleton_lazy_with_logging!(
TrafficMonitor,
INSTANCE,
"TrafficMonitor",
TrafficMonitor::default
);
impl TrafficMonitor {
pub async fn current(&self) -> CurrentTraffic {
self.monitor.current().await.current
}
pub async fn is_fresh(&self) -> bool {
self.monitor.is_fresh().await
}
}
pub async fn get_current_traffic() -> CurrentTraffic {
TrafficMonitor::global().current().await
}
pub async fn get_formatted_traffic() -> (String, String, String, String, bool) {
let monitor = TrafficMonitor::global();
let traffic = monitor.current().await;
let is_fresh = monitor.is_fresh().await;
(
fmt_bytes(traffic.up_rate),
fmt_bytes(traffic.down_rate),
fmt_bytes(traffic.total_up),
fmt_bytes(traffic.total_down),
is_fresh,
)
}
+520 -249
View File
@@ -3,10 +3,13 @@ pub mod config;
mod core;
mod enhance;
mod feat;
mod ipc;
mod module;
mod process;
mod state;
mod utils;
#[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager;
use crate::{
core::hotkey,
process::AsyncHandler,
@@ -14,7 +17,6 @@ use crate::{
};
use config::Config;
use parking_lot::Mutex;
use std::sync::Once;
use tauri::AppHandle;
#[cfg(target_os = "macos")]
use tauri::Manager;
@@ -26,201 +28,271 @@ use utils::logging::Type;
/// A global singleton handle to the application.
pub struct AppHandleManager {
inner: Mutex<Option<AppHandle>>,
init: Once,
handle: Mutex<Option<AppHandle>>,
}
impl AppHandleManager {
/// Get the global instance of the app handle manager.
pub fn global() -> &'static Self {
static INSTANCE: AppHandleManager = AppHandleManager {
inner: Mutex::new(None),
init: Once::new(),
};
&INSTANCE
/// Create a new AppHandleManager instance
fn new() -> Self {
Self {
handle: Mutex::new(None),
}
}
/// Initialize the app handle manager with an app handle.
pub fn init(&self, handle: AppHandle) {
self.init.call_once(|| {
let mut app_handle = self.inner.lock();
let mut app_handle = self.handle.lock();
if app_handle.is_none() {
*app_handle = Some(handle);
});
logging!(
info,
Type::Setup,
true,
"AppHandleManager initialized with handle"
);
}
}
/// Get the app handle if it has been initialized.
pub fn get(&self) -> Option<AppHandle> {
self.inner.lock().clone()
self.handle.lock().clone()
}
/// Get the app handle, panics if it hasn't been initialized.
pub fn get_handle(&self) -> AppHandle {
self.get().expect("AppHandle not initialized")
if let Some(handle) = self.get() {
handle
} else {
logging!(
error,
Type::Setup,
"AppHandle not initialized - ensure init() was called first"
);
std::process::exit(1)
}
}
/// Check if the app handle has been initialized.
pub fn is_initialized(&self) -> bool {
self.handle.lock().is_some()
}
#[cfg(target_os = "macos")]
pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> {
let app_handle = self.handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
app_handle
.set_activation_policy(policy)
.map_err(|e| e.to_string())
} else {
Err("AppHandle not initialized".to_string())
}
}
pub fn set_activation_policy_regular(&self) {
#[cfg(target_os = "macos")]
{
let app_handle = self.inner.lock();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Regular) {
logging!(
warn,
Type::Setup,
true,
"Failed to set regular activation policy: {}",
e
);
}
}
}
pub fn set_activation_policy_accessory(&self) {
#[cfg(target_os = "macos")]
{
let app_handle = self.inner.lock();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) {
logging!(
warn,
Type::Setup,
true,
"Failed to set accessory activation policy: {}",
e
);
}
}
}
pub fn set_activation_policy_prohibited(&self) {
#[cfg(target_os = "macos")]
{
let app_handle = self.inner.lock();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
}
}
}
#[allow(clippy::panic)]
pub fn run() {
utils::network::NetworkManager::global().init();
let _ = utils::dirs::init_portable_flag();
// 异步单例检测
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
}
}
Err(_) => {
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
"Failed to set prohibited activation policy: {}",
e
);
}
}
});
}
}
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
// Use unified singleton macro
singleton_with_logging!(AppHandleManager, INSTANCE, "AppHandleManager");
#[cfg(debug_assertions)]
let devtools = tauri_plugin_devtools::init();
/// Application initialization helper functions
mod app_init {
use super::*;
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.manage(Mutex::new(state::lightweight::LightWeightState::default()))
.setup(|app| {
logging!(info, Type::Setup, true, "开始应用初始化...");
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
#[cfg(target_os = "macos")]
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(app.config().identifier.clone());
}
let _ = app.handle().plugin(auto_start_plugin_builder.build());
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
logging!(info, Type::Setup, true, "注册深层链接...");
logging_error!(Type::System, true, app.deep_link().register_all());
}
app.deep_link().on_open_url(|event| {
AsyncHandler::spawn(move || {
let url = event.urls().first().map(|u| u.to_string());
async move {
if let Some(url) = url {
logging_error!(Type::Setup, true, resolve_scheme(url).await);
/// Initialize singleton monitoring for other instances
pub fn init_singleton_check() {
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
}
});
});
}
Err(_) => {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
);
}
}
});
}
// 窗口管理
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
let _ = app.handle().plugin(window_state_plugin);
/// Setup plugins for the Tauri builder
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
#[allow(unused_mut)]
let mut builder = builder
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init());
// 异步处理
let app_handle = app.handle().clone();
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "异步执行应用设置...");
match timeout(
Duration::from_secs(30),
resolve::resolve_setup_async(&app_handle),
)
.await
{
Ok(_) => {
logging!(info, Type::Setup, true, "应用设置成功完成");
}
Err(_) => {
logging!(
error,
Type::Setup,
true,
"应用设置超时(30秒),继续执行后续流程"
);
#[cfg(all(debug_assertions, not(feature = "tokio-trace")))]
{
builder = builder.plugin(tauri_plugin_devtools::init());
}
builder.manage(Mutex::new(state::lightweight::LightWeightState::default()))
}
/// Setup deep link handling
pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
logging!(info, Type::Setup, true, "注册深层链接...");
app.deep_link().register_all()?;
}
app.deep_link().on_open_url(|event| {
AsyncHandler::spawn(move || {
let url = event.urls().first().map(|u| u.to_string());
async move {
if let Some(url) = url {
if let Err(e) = resolve_scheme(url).await {
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
}
}
}
});
});
logging!(info, Type::Setup, true, "执行主要设置操作...");
Ok(())
}
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
AppHandleManager::global().init(app.handle().clone());
/// Setup autostart plugin
pub fn setup_autostart(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_os = "macos")]
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
#[cfg(not(target_os = "macos"))]
let auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
logging!(info, Type::Setup, true, "初始化核心句柄...");
core::handle::Handle::global().init(app.handle());
#[cfg(target_os = "macos")]
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(app.config().identifier.clone());
}
app.handle().plugin(auto_start_plugin_builder.build())?;
Ok(())
}
logging!(info, Type::Setup, true, "初始化配置...");
if let Err(e) = utils::init::init_config() {
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
/// Setup window state management
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
app.handle().plugin(window_state_plugin)?;
Ok(())
}
/// Initialize core components asynchronously
pub fn init_core_async(app_handle: tauri::AppHandle) {
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "异步执行应用设置...");
match timeout(
Duration::from_secs(30),
resolve::resolve_setup_async(&app_handle),
)
.await
{
Ok(_) => {
logging!(info, Type::Setup, true, "应用设置成功完成");
}
Err(_) => {
logging!(
error,
Type::Setup,
true,
"应用设置超时(30秒),继续执行后续流程"
);
}
}
});
}
logging!(info, Type::Setup, true, "初始化资源...");
if let Err(e) = utils::init::init_resources() {
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
}
/// Initialize core components synchronously
pub fn init_core_sync(app_handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
AppHandleManager::global().init(app_handle.clone());
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())
})
.invoke_handler(tauri::generate_handler![
// common
logging!(info, Type::Setup, true, "初始化核心句柄...");
core::handle::Handle::global().init(app_handle);
logging!(info, Type::Setup, true, "初始化配置...");
utils::init::init_config()?;
logging!(info, Type::Setup, true, "初始化资源...");
utils::init::init_resources()?;
logging!(info, Type::Setup, true, "核心组件初始化完成");
Ok(())
}
/// Generate all command handlers for the application
pub fn generate_handlers(
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![
// Common commands
cmd::get_sys_proxy,
cmd::get_auto_proxy,
cmd::open_app_dir,
@@ -231,11 +303,11 @@ pub fn run() {
cmd::get_network_interfaces,
cmd::get_system_hostname,
cmd::restart_app,
// 内核管理
// Core management
cmd::start_core,
cmd::stop_core,
cmd::restart_core,
// 启动命令
// Application lifecycle
cmd::notify_ui_ready,
cmd::update_ui_stage,
cmd::reset_ui_ready_state,
@@ -243,16 +315,16 @@ pub fn run() {
cmd::get_app_uptime,
cmd::get_auto_launch_status,
cmd::is_admin,
// 添加轻量模式相关命令
// Lightweight mode
cmd::entry_lightweight_mode,
cmd::exit_lightweight_mode,
// service 管理
// Service management
cmd::install_service,
cmd::uninstall_service,
cmd::reinstall_service,
cmd::repair_service,
cmd::is_service_available,
// clash
// Clash core commands
cmd::get_clash_info,
cmd::patch_clash_config,
cmd::patch_clash_mode,
@@ -271,7 +343,37 @@ pub fn run() {
cmd::check_dns_config_exists,
cmd::get_dns_config_content,
cmd::validate_dns_config,
// verge
cmd::get_clash_version,
cmd::get_clash_config,
cmd::force_refresh_clash_config,
cmd::update_geo_data,
cmd::upgrade_clash_core,
cmd::get_clash_rules,
cmd::update_proxy_choice,
cmd::get_proxy_providers,
cmd::get_rule_providers,
cmd::proxy_provider_health_check,
cmd::update_proxy_provider,
cmd::update_rule_provider,
cmd::get_clash_connections,
cmd::delete_clash_connection,
cmd::close_all_clash_connections,
cmd::get_group_proxy_delays,
cmd::is_clash_debug_enabled,
cmd::clash_gc,
// Logging and monitoring
cmd::get_clash_logs,
cmd::start_logs_monitoring,
cmd::stop_logs_monitoring,
cmd::clear_logs,
cmd::get_traffic_data,
cmd::get_memory_data,
cmd::get_formatted_traffic_data,
cmd::get_formatted_memory_data,
cmd::get_system_monitor_overview,
cmd::start_traffic_service,
cmd::stop_traffic_service,
// Verge configuration
cmd::get_verge_config,
cmd::patch_verge_config,
cmd::test_delay,
@@ -281,7 +383,7 @@ pub fn run() {
cmd::open_devtools,
cmd::exit_app,
cmd::get_network_interfaces_info,
// profile
// Profile management
cmd::get_profiles,
cmd::enhance_profiles,
cmd::patch_profiles_config,
@@ -295,67 +397,304 @@ pub fn run() {
cmd::read_profile_file,
cmd::save_profile_file,
cmd::get_next_update_time,
// script validation
// Script validation
cmd::script_validate_notice,
cmd::validate_script_file,
// clash api
// Clash API
cmd::clash_api_get_proxy_delay,
// backup
// Backup and WebDAV
cmd::create_webdav_backup,
cmd::save_webdav_config,
cmd::list_webdav_backup,
cmd::delete_webdav_backup,
cmd::restore_webdav_backup,
// export diagnostic info for issue reporting
// Diagnostics and system info
cmd::export_diagnostic_info,
// get system info for display
cmd::get_system_info,
// media unlock checker
// Media unlock checker
cmd::get_unlock_items,
cmd::check_media_unlock,
// light-weight model
cmd::entry_lightweight_mode,
]);
]
}
}
#[cfg(debug_assertions)]
pub fn run() {
// Setup singleton check
app_init::init_singleton_check();
// Initialize network manager
utils::network::NetworkManager::global().init();
// Initialize portable flag
let _ = utils::dirs::init_portable_flag();
// Set Linux environment variable
#[cfg(target_os = "linux")]
{
builder = builder.plugin(devtools);
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.to_uppercase();
let is_kde_desktop = desktop_env.contains("KDE");
let is_plasma_desktop = desktop_env.contains("PLASMA");
if is_kde_desktop || is_plasma_desktop {
std::env::set_var("GTK_CSD", "0");
logging!(
info,
Type::Setup,
true,
"KDE detected: Disabled GTK CSD for better titlebar stability."
);
}
}
// Macos Application Menu
#[cfg(target_os = "macos")]
{
// Temporary Achived due to cannot CMD+C/V/A
}
// Create and configure the Tauri builder
let builder = app_init::setup_plugins(tauri::Builder::default())
.setup(|app| {
logging!(info, Type::Setup, true, "开始应用初始化...");
let app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
// Setup autostart plugin
if let Err(e) = app_init::setup_autostart(app) {
logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e);
}
app.run(|app_handle, e| match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
// Setup deep links
if let Err(e) = app_init::setup_deep_links(app) {
logging!(
error,
Type::Setup,
true,
"Failed to setup deep links: {}",
e
);
}
// Setup window state management
if let Err(e) = app_init::setup_window_state(app) {
logging!(
error,
Type::Setup,
true,
"Failed to setup window state: {}",
e
);
}
// Initialize core components asynchronously
app_init::init_core_async(app.handle().clone());
logging!(info, Type::Setup, true, "执行主要设置操作...");
// Initialize core components synchronously
if let Err(e) = app_init::init_core_sync(app.handle()) {
logging!(
error,
Type::Setup,
true,
"Failed to initialize core components: {}",
e
);
return Err(e);
}
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())
})
.invoke_handler(app_init::generate_handlers());
/// Event handling helper functions
mod event_handlers {
use super::*;
/// Handle application ready/resumed events
pub fn handle_ready_resumed(app_handle: &tauri::AppHandle) {
logging!(info, Type::System, true, "应用就绪或恢复");
AppHandleManager::global().init(app_handle.clone());
#[cfg(target_os = "macos")]
{
if let Some(window) = AppHandleManager::global()
.get_handle()
.get_webview_window("main")
{
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "设置macOS窗口标题");
let _ = window.set_title("Clash Verge");
}
}
}
/// Handle application reopen events (macOS)
#[cfg(target_os = "macos")]
pub fn handle_reopen(app_handle: &tauri::AppHandle, has_visible_windows: bool) {
logging!(
info,
Type::System,
true,
"处理 macOS 应用重新打开事件: has_visible_windows={}",
has_visible_windows
);
AppHandleManager::global().init(app_handle.clone());
if !has_visible_windows {
// 当没有可见窗口时,设置为 regular 模式并显示主窗口
AppHandleManager::global().set_activation_policy_regular();
logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口");
let result = WindowManager::show_main_window();
logging!(
info,
Type::System,
true,
"窗口显示操作完成,结果: {:?}",
result
);
} else {
logging!(info, Type::System, true, "已有可见窗口,无需额外操作");
}
}
/// Handle window close requests
pub fn handle_window_close(api: &tauri::WindowEvent) {
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() {
return;
}
log::info!(target: "app", "closing window...");
if let tauri::WindowEvent::CloseRequested { api, .. } = api {
api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide();
} else {
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
}
}
}
/// Handle window focus events
pub fn handle_window_focus(focused: bool) {
let is_enable_global_hotkey = Config::verge()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if focused {
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
if let Err(e) =
hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdQ)
{
logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e);
}
if let Err(e) =
hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdW)
{
logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e);
}
}
if !is_enable_global_hotkey {
if let Err(e) = hotkey::Hotkey::global().init() {
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
}
}
return;
}
// Handle unfocused state
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
{
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+Q: {}",
e
);
}
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
{
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+W: {}",
e
);
}
}
if !is_enable_global_hotkey {
if let Err(e) = hotkey::Hotkey::global().reset() {
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
}
}
}
/// Handle window destroyed events
pub fn handle_window_destroyed() {
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
{
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+Q on destroy: {}",
e
);
}
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
{
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+W on destroy: {}",
e
);
}
}
}
}
// Build the application
let app = builder
.build(tauri::generate_context!())
.unwrap_or_else(|e| {
logging!(
error,
Type::Setup,
true,
"Failed to build Tauri application: {}",
e
);
std::process::exit(1);
});
app.run(|app_handle, e| match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
event_handlers::handle_ready_resumed(app_handle);
}
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen {
has_visible_windows,
..
} => {
if !has_visible_windows {
AppHandleManager::global().set_activation_policy_regular();
}
AppHandleManager::global().init(app_handle.clone());
event_handlers::handle_reopen(app_handle, has_visible_windows);
}
tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() {
@@ -363,7 +702,7 @@ pub fn run() {
}
}
tauri::RunEvent::Exit => {
// avoid duplicate cleanup
// Avoid duplicate cleanup
if core::handle::Handle::global().is_exiting() {
return;
}
@@ -372,82 +711,14 @@ pub fn run() {
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() {
return;
}
log::info!(target: "app", "closing window...");
api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide();
} else {
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
}
tauri::WindowEvent::CloseRequested { .. } => {
event_handlers::handle_window_close(&event);
}
tauri::WindowEvent::Focused(true) => {
#[cfg(target_os = "macos")]
{
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().register("CMD+Q", "quit")
);
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().register("CMD+W", "hide")
);
}
{
let is_enable_global_hotkey = Config::verge()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init())
}
}
}
tauri::WindowEvent::Focused(false) => {
#[cfg(target_os = "macos")]
{
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+Q")
);
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+W")
);
}
{
let is_enable_global_hotkey = Config::verge()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset())
}
}
tauri::WindowEvent::Focused(focused) => {
event_handlers::handle_window_focus(focused);
}
tauri::WindowEvent::Destroyed => {
#[cfg(target_os = "macos")]
{
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+Q")
);
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+W")
);
}
event_handlers::handle_window_destroyed();
}
_ => {}
}
+2
View File
@@ -1,4 +1,6 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
#[cfg(feature = "tokio-trace")]
console_subscriber::init();
app_lib::run();
}
+41 -18
View File
@@ -22,14 +22,23 @@ const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 添加退出轻量模式的锁,防止并发调用
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
fn with_lightweight_status<F, R>(f: F) -> R
fn with_lightweight_status<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut LightWeightState) -> R,
{
let app_handle = handle::Handle::global().app_handle().unwrap();
let state = app_handle.state::<Mutex<LightWeightState>>();
let mut guard = state.lock();
f(&mut guard)
if let Some(app_handle) = handle::Handle::global().app_handle() {
// Try to get state, but don't panic if it's not managed yet
if let Some(state) = app_handle.try_state::<Mutex<LightWeightState>>() {
let mut guard = state.lock();
Some(f(&mut guard))
} else {
// State not managed yet, return None
None
}
} else {
// App handle not available yet
None
}
}
pub fn run_once_auto_lightweight() {
@@ -62,7 +71,17 @@ pub fn run_once_auto_lightweight() {
pub fn auto_lightweight_mode_init() {
if let Some(app_handle) = handle::Handle::global().app_handle() {
let _ = app_handle.state::<Mutex<LightWeightState>>();
// Check if state is available before accessing it
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() {
logging!(
warn,
Type::Lightweight,
true,
"LightWeightState 尚未初始化,跳过自动轻量模式初始化"
);
return;
}
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto =
{ Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false);
@@ -87,23 +106,28 @@ pub fn auto_lightweight_mode_init() {
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
with_lightweight_status(|state| state.is_lightweight)
with_lightweight_status(|state| state.is_lightweight).unwrap_or(false)
}
// 设置轻量模式状态
pub fn set_lightweight_mode(value: bool) {
with_lightweight_status(|state| {
if with_lightweight_status(|state| {
state.set_lightweight_mode(value);
});
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}");
})
.is_some()
{
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}");
}
}
}
pub fn enable_auto_light_weight_mode() {
Timer::global().init().unwrap();
if let Err(e) = Timer::global().init() {
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
return;
}
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
setup_window_close_listener();
setup_webview_focus_listener();
@@ -233,10 +257,9 @@ fn setup_light_weight_timer() -> Result<()> {
// 获取task_id
let task_id = {
let mut timer_count = Timer::global().timer_count.lock();
let id = *timer_count;
*timer_count += 1;
id
Timer::global()
.timer_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
};
// 创建任务
-108
View File
@@ -1,108 +0,0 @@
use crate::config::Config;
use mihomo_api;
use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock};
use std::time::{Duration, Instant};
use tauri::http::HeaderMap;
// 缓存的最大有效期(5秒)
const CACHE_TTL: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Rate {
pub up: u64,
pub down: u64,
}
// 缓存MihomoManager实例
struct MihomoCache {
manager: mihomo_api::MihomoManager,
created_at: Instant,
server: String,
}
// 使用RwLock替代Mutex,允许多个读取操作并发进行
pub struct MihomoManager {
mihomo_cache: RwLock<Option<MihomoCache>>,
create_lock: Mutex<()>,
}
impl MihomoManager {
fn __global() -> &'static MihomoManager {
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
mihomo_cache: RwLock::new(None),
create_lock: Mutex::new(()),
});
&INSTANCE
}
pub fn global() -> mihomo_api::MihomoManager {
let instance = MihomoManager::__global();
// 尝试从缓存读取(只需读锁)
{
let cache = instance.mihomo_cache.read();
if let Some(cache_entry) = &*cache {
let (current_server, _) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
// 检查缓存是否有效
if cache_entry.server == current_server
&& cache_entry.created_at.elapsed() < CACHE_TTL
{
return cache_entry.manager.clone();
}
}
}
// 缓存无效,获取创建锁
let _create_guard = instance.create_lock.lock();
// 再次检查缓存(双重检查锁定模式)
{
let cache = instance.mihomo_cache.read();
if let Some(cache_entry) = &*cache {
let (current_server, _) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
if cache_entry.server == current_server
&& cache_entry.created_at.elapsed() < CACHE_TTL
{
return cache_entry.manager.clone();
}
}
}
// 创建新实例
let (current_server, headers) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
let manager = mihomo_api::MihomoManager::new(current_server.clone(), headers);
// 更新缓存
{
let mut cache = instance.mihomo_cache.write();
*cache = Some(MihomoCache {
manager: manager.clone(),
created_at: Instant::now(),
server: current_server,
});
}
manager
}
}
impl MihomoManager {
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
let client = { Config::clash().latest_ref().get_client_info() };
let server = format!("http://{}", client.server);
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
if let Some(secret) = client.secret {
let secret = format!("Bearer {secret}").parse().unwrap();
headers.insert("Authorization", secret);
}
Some((server, headers))
}
// 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法
}
-1
View File
@@ -1,3 +1,2 @@
pub mod lightweight;
pub mod mihomo;
pub mod sysinfo;
+13 -3
View File
@@ -32,7 +32,17 @@ impl PlatformSpecification {
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
let system_arch = System::cpu_arch();
let handler = handle::Handle::global().app_handle().unwrap();
let Some(handler) = handle::Handle::global().app_handle() else {
return Self {
system_name,
system_version,
system_kernel_version,
system_arch,
verge_version: "unknown".into(),
running_mode: "NotRunning".to_string(),
is_admin: false,
};
};
let verge_version = handler.package_info().version.to_string();
// 使用默认值避免在同步上下文中执行异步操作
@@ -52,10 +62,10 @@ impl PlatformSpecification {
}
// 异步方法来获取完整的系统信息
pub async fn new_async() -> Self {
pub fn new_sync() -> Self {
let mut info = Self::new();
let running_mode = CoreManager::global().get_running_mode().await;
let running_mode = CoreManager::global().get_running_mode();
info.running_mode = running_mode.to_string();
info
+8
View File
@@ -11,4 +11,12 @@ impl AsyncHandler {
{
async_runtime::spawn(f())
}
pub fn spawn_blocking<T, F>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
async_runtime::spawn_blocking(f)
}
}
+20 -11
View File
@@ -1,24 +1,24 @@
use crate::singleton;
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::OnceCell;
pub struct CacheEntry {
pub value: Arc<Value>,
pub expires_at: Instant,
}
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::OnceCell;
pub struct ProxyRequestCache {
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
}
impl ProxyRequestCache {
pub fn global() -> &'static Self {
static INSTANCE: once_cell::sync::OnceCell<ProxyRequestCache> =
once_cell::sync::OnceCell::new();
INSTANCE.get_or_init(|| ProxyRequestCache {
fn new() -> Self {
ProxyRequestCache {
map: DashMap::new(),
})
}
}
pub fn make_key(prefix: &str, id: &str) -> String {
@@ -49,7 +49,7 @@ impl ProxyRequestCache {
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
let new_cell = Arc::new(OnceCell::new());
self.map.insert(key_cloned.clone(), new_cell.clone());
self.map.insert(key_cloned.clone(), Arc::clone(&new_cell));
return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await;
}
}
@@ -60,6 +60,15 @@ impl ProxyRequestCache {
expires_at: Instant::now() + ttl,
};
let _ = cell.set(entry);
Arc::clone(&cell.get().unwrap().value)
// Safe to unwrap here as we just set the value
Arc::clone(
&cell
.get()
.unwrap_or_else(|| unreachable!("Cell value should exist after set"))
.value,
)
}
}
// Use singleton macro
singleton!(ProxyRequestCache, INSTANCE);
+43 -2
View File
@@ -163,8 +163,13 @@ pub fn find_target_icons(target: &str) -> Result<Option<String>> {
if matching_files.is_empty() {
Ok(None)
} else {
let first = path_to_str(matching_files.first().unwrap())?;
Ok(Some(first.to_string()))
match matching_files.first() {
Some(first_path) => {
let first = path_to_str(first_path)?;
Ok(Some(first.to_string()))
}
None => Ok(None),
}
}
}
@@ -242,3 +247,39 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
Ok(key)
}
}
#[cfg(unix)]
pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
["/var/tmp", "/tmp"]
.iter()
.map(PathBuf::from)
.find(|path| path.exists())
.or_else(|| {
std::env::var_os("HOME").and_then(|home| {
let home_config = PathBuf::from(home).join(".config");
if home_config.exists() || fs::create_dir_all(&home_config).is_ok() {
Some(home_config)
} else {
log::error!(target: "app", "Failed to create safe directory: {home_config:?}");
None
}
})
})
}
#[cfg(unix)]
pub fn ipc_path() -> Result<PathBuf> {
ensure_mihomo_safe_dir()
.map(|base_dir| base_dir.join("verge").join("verge-mihomo.sock"))
.or_else(|| {
app_home_dir()
.ok()
.map(|dir| dir.join("verge").join("verge-mihomo.sock"))
})
.ok_or_else(|| anyhow::anyhow!("Failed to determine ipc path"))
}
#[cfg(target_os = "windows")]
pub fn ipc_path() -> Result<PathBuf> {
Ok(PathBuf::from(r"\\.\pipe\verge-mihomo"))
}
+25
View File
@@ -0,0 +1,25 @@
/// Format bytes into human readable string (B, KB, MB, GB)
pub fn fmt_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let (mut val, mut unit) = (bytes as f64, 0);
while val >= 1024.0 && unit < 3 {
val /= 1024.0;
unit += 1;
}
format!("{:.1}{}", val, UNITS[unit])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fmt_bytes() {
assert_eq!(fmt_bytes(0), "0.0B");
assert_eq!(fmt_bytes(512), "512.0B");
assert_eq!(fmt_bytes(1024), "1.0KB");
assert_eq!(fmt_bytes(1536), "1.5KB");
assert_eq!(fmt_bytes(1024 * 1024), "1.0MB");
assert_eq!(fmt_bytes(1024 * 1024 * 1024), "1.0GB");
}
}
+14 -5
View File
@@ -78,14 +78,16 @@ pub fn delete_log() -> Result<()> {
verge.auto_log_clean.unwrap_or(0)
};
// 1: 1天, 2: 7天, 3: 30天, 4: 90天
let day = match auto_log_clean {
1 => 7,
2 => 30,
3 => 90,
1 => 1,
2 => 7,
3 => 30,
4 => 90,
_ => return Ok(()),
};
log::debug!(target: "app", "try to delete log files, day: {day}");
log::info!(target: "app", "try to delete log files, day: {day}");
// %Y-%m-%d to NaiveDateTime
let parse_time_str = |s: &str| {
@@ -401,7 +403,14 @@ pub fn init_scheme() -> Result<()> {
}
pub async fn startup_script() -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
return Err(anyhow::anyhow!(
"app_handle not available for startup script execution"
));
}
};
let script_path = {
let verge = Config::verge();
+2
View File
@@ -17,6 +17,7 @@ pub enum Type {
Lightweight,
Network,
ProxyMode,
Ipc,
}
impl fmt::Display for Type {
@@ -37,6 +38,7 @@ impl fmt::Display for Type {
Type::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
Type::Ipc => write!(f, "[IPC]"),
}
}
}
+2
View File
@@ -1,5 +1,6 @@
pub mod autostart;
pub mod dirs;
pub mod format;
pub mod help;
pub mod i18n;
pub mod init;
@@ -8,5 +9,6 @@ pub mod network;
pub mod notification;
pub mod resolve;
pub mod server;
pub mod singleton;
pub mod tmpl;
pub mod window_manager;
+101 -34
View File
@@ -1,14 +1,16 @@
use anyhow::Result;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response};
use std::{
sync::{Arc, Once},
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Once,
},
time::{Duration, Instant},
};
use tokio::runtime::{Builder, Runtime};
use crate::{config::Config, logging, utils::logging::Type};
use crate::{config::Config, logging, process::AsyncHandler, singleton_lazy, utils::logging::Type};
// HTTP2 相关
const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024;
@@ -24,44 +26,63 @@ const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(15);
/// 网络管理器
pub struct NetworkManager {
runtime: Arc<Runtime>,
self_proxy_client: Arc<Mutex<Option<Client>>>,
system_proxy_client: Arc<Mutex<Option<Client>>>,
no_proxy_client: Arc<Mutex<Option<Client>>>,
self_proxy_client: Mutex<Option<Client>>,
system_proxy_client: Mutex<Option<Client>>,
no_proxy_client: Mutex<Option<Client>>,
init: Once,
last_connection_error: Arc<Mutex<Option<(Instant, String)>>>,
connection_error_count: Arc<Mutex<usize>>,
last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: AtomicUsize,
}
lazy_static! {
static ref NETWORK_MANAGER: NetworkManager = NetworkManager::new();
}
// Use singleton_lazy macro to replace lazy_static!
singleton_lazy!(NetworkManager, NETWORK_MANAGER, NetworkManager::new);
impl NetworkManager {
fn new() -> Self {
// 创建专用的异步运行时,线程数限制为4个
let runtime = Builder::new_multi_thread()
let runtime = match Builder::new_multi_thread()
.worker_threads(4)
.thread_name("clash-verge-network")
.enable_io()
.enable_time()
.build()
.expect("Failed to create network runtime");
{
Ok(runtime) => runtime,
Err(e) => {
log::error!(
"Failed to create network runtime: {}. Using fallback single-threaded runtime.",
e
);
// Fallback to current thread runtime
match Builder::new_current_thread()
.enable_io()
.enable_time()
.thread_name("clash-verge-network-fallback")
.build()
{
Ok(fallback_runtime) => fallback_runtime,
Err(fallback_err) => {
log::error!(
"Failed to create fallback runtime: {}. This is critical.",
fallback_err
);
std::process::exit(1);
}
}
}
};
NetworkManager {
runtime: Arc::new(runtime),
self_proxy_client: Arc::new(Mutex::new(None)),
system_proxy_client: Arc::new(Mutex::new(None)),
no_proxy_client: Arc::new(Mutex::new(None)),
self_proxy_client: Mutex::new(None),
system_proxy_client: Mutex::new(None),
no_proxy_client: Mutex::new(None),
init: Once::new(),
last_connection_error: Arc::new(Mutex::new(None)),
connection_error_count: Arc::new(Mutex::new(0)),
last_connection_error: Mutex::new(None),
connection_error_count: AtomicUsize::new(0),
}
}
pub fn global() -> &'static Self {
&NETWORK_MANAGER
}
/// 初始化网络客户端
pub fn init(&self) {
self.init.call_once(|| {
@@ -69,7 +90,7 @@ impl NetworkManager {
logging!(info, Type::Network, true, "初始化网络管理器");
// 创建无代理客户端
let no_proxy_client = ClientBuilder::new()
let no_proxy_client = match ClientBuilder::new()
.use_rustls_tls()
.no_proxy()
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
@@ -77,9 +98,21 @@ impl NetworkManager {
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to build no_proxy client");
{
Ok(client) => client,
Err(e) => {
logging!(
error,
Type::Network,
true,
"Failed to build no_proxy client: {}",
e
);
return;
}
};
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock();
let mut no_proxy_guard = NetworkManager::global().no_proxy_client.lock();
*no_proxy_guard = Some(no_proxy_client);
logging!(info, Type::Network, true, "网络管理器初始化完成");
@@ -91,12 +124,11 @@ impl NetworkManager {
let mut last_error = self.last_connection_error.lock();
*last_error = Some((Instant::now(), error.to_string()));
let mut error_count = self.connection_error_count.lock();
*error_count += 1;
self.connection_error_count.fetch_add(1, Ordering::Relaxed);
}
fn should_reset_clients(&self) -> bool {
let error_count = *self.connection_error_count.lock();
let error_count = self.connection_error_count.load(Ordering::Relaxed);
let last_error = self.last_connection_error.lock();
if error_count > 5 {
@@ -126,10 +158,7 @@ impl NetworkManager {
let mut client = self.no_proxy_client.lock();
*client = None;
}
{
let mut error_count = self.connection_error_count.lock();
*error_count = 0;
}
self.connection_error_count.store(0, Ordering::Relaxed);
}
/// 创建带有自定义选项的HTTP请求
@@ -221,7 +250,45 @@ impl NetworkManager {
builder = builder.user_agent(version);
}
let client = builder.build().expect("Failed to build custom HTTP client");
let client = match builder.build() {
Ok(client) => client,
Err(e) => {
logging!(
error,
Type::Network,
true,
"Failed to build custom HTTP client: {}",
e
);
// Return a simple no-proxy client as fallback
match ClientBuilder::new()
.use_rustls_tls()
.no_proxy()
.timeout(DEFAULT_REQUEST_TIMEOUT)
.build()
{
Ok(fallback_client) => fallback_client,
Err(fallback_err) => {
logging!(
error,
Type::Network,
true,
"Failed to create fallback client: {}",
fallback_err
);
self.record_connection_error(&format!(
"Critical client build failure: {}",
fallback_err
));
// Return a minimal client that will likely fail but won't panic
ClientBuilder::new().build().unwrap_or_else(|_| {
// If even the most basic client fails, this is truly critical
std::process::exit(1);
})
}
}
}
};
client.get(url)
}
@@ -284,7 +351,7 @@ impl NetworkManager {
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
let url_clone = url.to_string();
let watchdog = tokio::spawn(async move {
let watchdog = AsyncHandler::spawn(move || async move {
tokio::time::sleep(Duration::from_secs(timeout_duration)).await;
let _ = cancel_tx.send(());
logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone);
+172 -125
View File
@@ -1,7 +1,7 @@
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use crate::{
config::{Config, IVerge, PrfItem},
config::{Config, PrfItem},
core::*,
logging, logging_error,
module::lightweight::{self, auto_lightweight_mode_init},
@@ -14,13 +14,8 @@ use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use percent_encoding::percent_decode_str;
use scopeguard;
use serde_yaml::Mapping;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use tauri::{AppHandle, Manager};
use tokio::net::TcpListener;
use tauri::Url;
//#[cfg(not(target_os = "linux"))]
@@ -33,7 +28,7 @@ const DEFAULT_WIDTH: u32 = 940;
const DEFAULT_HEIGHT: u32 = 700;
// 添加全局UI准备就绪标志
static UI_READY: OnceCell<Arc<RwLock<bool>>> = OnceCell::new();
static UI_READY: OnceCell<RwLock<bool>> = OnceCell::new();
// 窗口创建锁,防止并发创建窗口
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
@@ -63,18 +58,18 @@ impl Default for UiReadyState {
}
// 获取UI就绪状态细节
static UI_READY_STATE: OnceCell<Arc<UiReadyState>> = OnceCell::new();
static UI_READY_STATE: OnceCell<UiReadyState> = OnceCell::new();
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
}
fn get_ui_ready() -> &'static Arc<RwLock<bool>> {
UI_READY.get_or_init(|| Arc::new(RwLock::new(false)))
fn get_ui_ready() -> &'static RwLock<bool> {
UI_READY.get_or_init(|| RwLock::new(false))
}
fn get_ui_ready_state() -> &'static Arc<UiReadyState> {
UI_READY_STATE.get_or_init(|| Arc::new(UiReadyState::default()))
fn get_ui_ready_state() -> &'static UiReadyState {
UI_READY_STATE.get_or_init(UiReadyState::default)
}
// 更新UI准备阶段
@@ -110,27 +105,16 @@ pub fn reset_ui_ready() {
logging!(info, Type::Window, true, "UI就绪状态已重置");
}
pub async fn find_unused_port() -> Result<u16> {
match TcpListener::bind("127.0.0.1:0").await {
Ok(listener) => {
let port = listener.local_addr()?.port();
Ok(port)
}
Err(_) => {
let port = Config::verge()
.latest_ref()
.verge_mixed_port
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
log::warn!(target: "app", "use default port: {port}");
Ok(port)
}
}
}
/// 异步方式处理启动后的额外任务
pub async fn resolve_setup_async(app_handle: &AppHandle) {
let start_time = std::time::Instant::now();
logging!(info, Type::Setup, true, "开始执行异步设置任务...");
logging!(
info,
Type::Setup,
true,
"开始执行异步设置任务... 线程ID: {:?}",
std::thread::current().id()
);
if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string();
@@ -144,26 +128,26 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
logging_error!(Type::Setup, true, init::startup_script().await);
if let Err(err) = resolve_random_port_config().await {
logging!(
error,
Type::System,
true,
"Failed to resolve random port config: {}",
err
);
}
logging!(trace, Type::Config, true, "初始化配置...");
logging!(
info,
Type::Config,
true,
"开始初始化配置... 线程ID: {:?}",
std::thread::current().id()
);
logging_error!(Type::Config, true, Config::init_config().await);
logging!(info, Type::Config, true, "配置初始化完成");
// 启动时清理冗余的 Profile 文件
logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
let profiles = Config::profiles();
if let Err(e) = profiles.latest_ref().auto_cleanup() {
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
} else {
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
logging!(info, Type::Setup, true, "开始清理冗余的Profile文件...");
match Config::profiles().latest_ref().auto_cleanup() {
Ok(_) => {
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
}
Err(e) => {
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
}
}
logging!(trace, Type::Core, true, "启动核心管理器...");
@@ -172,6 +156,8 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
log::trace!(target: "app", "启动内嵌服务器...");
server::embed_server();
logging!(trace, Type::Core, true, "启动 IPC 监控服务...");
logging_error!(Type::Tray, true, tray::Tray::global().init());
if let Some(app_handle) = handle::Handle::global().app_handle() {
@@ -329,8 +315,21 @@ pub fn create_window(is_show: bool) -> bool {
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
});
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(
error,
Type::Window,
true,
"无法获取app_handle,窗口创建失败"
);
return false;
}
};
match tauri::WebviewWindowBuilder::new(
&handle::Handle::global().app_handle().unwrap(),
&app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
)
@@ -460,42 +459,82 @@ pub fn create_window(is_show: bool) -> bool {
timeout_seconds
);
// 异步监控UI状态,不影响窗口显示
tokio::spawn(async move {
let wait_result =
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
let mut check_count = 0;
while !*get_ui_ready().read() {
tokio::time::sleep(Duration::from_millis(100)).await;
check_count += 1;
// 异步监控UI状态,使用try_read避免死锁
AsyncHandler::spawn(move || async move {
logging!(
debug,
Type::Window,
true,
"启动UI状态监控线程,超时{}秒",
timeout_seconds
);
// 每2秒记录一次等待状态
if check_count % 20 == 0 {
logging!(
debug,
Type::Window,
true,
"UI加载状态检查... ({}秒)",
check_count / 10
);
}
let ui_ready_checker = || async {
let (mut check_count, mut consecutive_failures) = (0, 0);
loop {
let is_ready = get_ui_ready()
.try_read()
.map(|guard| *guard)
.unwrap_or_else(|| {
consecutive_failures += 1;
if consecutive_failures > 50 {
logging!(
warn,
Type::Window,
true,
"UI状态监控连续{}次无法获取读锁,可能存在死锁",
consecutive_failures
);
consecutive_failures = 0;
}
false
});
if is_ready {
logging!(
debug,
Type::Window,
true,
"UI状态监控检测到就绪信号,退出监控"
);
return;
}
})
.await;
consecutive_failures = 0;
tokio::time::sleep(Duration::from_millis(100)).await;
check_count += 1;
if check_count % 20 == 0 {
logging!(
debug,
Type::Window,
true,
"UI加载状态检查... ({}秒)",
check_count / 10
);
}
}
};
let wait_result = tokio::time::timeout(
Duration::from_secs(timeout_seconds),
ui_ready_checker(),
)
.await;
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI已完全加载就绪");
// 移除初始加载指示器
if let Some(window) = handle::Handle::global().get_window() {
let _ = window.eval(r#"
handle::Handle::global()
.get_window()
.map(|window| window.eval(r"
const overlay = document.getElementById('initial-loading-overlay');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 300);
}
"#);
}
"));
}
Err(_) => {
logging!(
@@ -505,7 +544,26 @@ pub fn create_window(is_show: bool) -> bool {
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
*get_ui_ready().write() = true;
get_ui_ready()
.try_write()
.map(|mut guard| {
*guard = true;
logging!(
info,
Type::Window,
true,
"超时后成功设置UI就绪状态"
);
})
.unwrap_or_else(|| {
logging!(
error,
Type::Window,
true,
"超时后无法获取UI状态写锁,可能存在严重死锁"
);
});
}
}
});
@@ -573,7 +631,17 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
create_window(false);
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = item.uid.clone().unwrap();
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
logging!(error, Type::Config, true, "Profile item missing UID");
handle::Handle::notice_message(
"import_sub_url::error",
"Profile item missing UID".to_string(),
);
return Ok(());
}
};
let _ = wrap_err!(Config::profiles().data_mut().append_item(item));
handle::Handle::notice_message("import_sub_url::ok", uid);
}
@@ -589,59 +657,26 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
Ok(())
}
async fn resolve_random_port_config() -> Result<()> {
let verge_config = Config::verge();
let clash_config = Config::clash();
let enable_random_port = verge_config
.latest_ref()
.enable_random_port
.unwrap_or(false);
let default_port = verge_config
.latest_ref()
.verge_mixed_port
.unwrap_or(clash_config.latest_ref().get_mixed_port());
let port = if enable_random_port {
find_unused_port().await.unwrap_or(default_port)
} else {
default_port
};
let port_to_save = port;
tokio::task::spawn_blocking(move || {
let verge_config_accessor = Config::verge();
let mut verge_data = verge_config_accessor.data_mut();
verge_data.patch_config(IVerge {
verge_mixed_port: Some(port_to_save),
..IVerge::default()
});
verge_data.save_file()
})
.await??; // First ? for spawn_blocking error, second for save_file Result
tokio::task::spawn_blocking(move || {
let clash_config_accessor = Config::clash(); // Extend lifetime of the accessor
let mut clash_data = clash_config_accessor.data_mut(); // Access within blocking task, made mutable
let mut mapping = Mapping::new();
mapping.insert("mixed-port".into(), port_to_save.into());
clash_data.patch_config(mapping);
clash_data.save_config()
})
.await??;
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn set_public_dns(dns_server: String) {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS configuration");
return;
}
};
log::info!(target: "app", "try to set system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("set_dns.sh");
if !script.exists() {
log::error!(target: "app", "set_dns.sh not found");
@@ -674,9 +709,21 @@ pub async fn set_public_dns(dns_server: String) {
pub async fn restore_public_dns() {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS restoration");
return;
}
};
log::info!(target: "app", "try to unset system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("unset_dns.sh");
if !script.exists() {
log::error!(target: "app", "unset_dns.sh not found");
+6 -7
View File
@@ -38,9 +38,8 @@ pub async fn check_singleton() -> Result<()> {
}
log::error!("failed to setup singleton listen server");
bail!("app exists");
} else {
Ok(())
}
Ok(())
}
/// The embed server only be used to implement singleton process
@@ -49,12 +48,12 @@ pub fn embed_server() {
let port = IVerge::get_singleton_port();
AsyncHandler::spawn(move || async move {
let visible = warp::path!("commands" / "visible").map(move || {
let visible = warp::path!("commands" / "visible").map(|| {
resolve::create_window(false);
"ok"
warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK)
});
let pac = warp::path!("commands" / "pac").map(move || {
let pac = warp::path!("commands" / "pac").map(|| {
let content = Config::verge()
.latest_ref()
.pac_file_content
@@ -70,13 +69,13 @@ pub fn embed_server() {
.body(content)
.unwrap_or_default()
});
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
async fn scheme_handler(query: QueryParam) -> Result<String, Infallible> {
logging_error!(
Type::Setup,
true,
resolve::resolve_scheme(query.param).await
);
Ok("ok")
Ok("ok".to_string())
}
let scheme = warp::path!("commands" / "scheme")
+124
View File
@@ -0,0 +1,124 @@
/// Macro to generate singleton pattern for structs
///
/// Usage:
/// ```rust,ignore
/// use crate::utils::singleton::singleton;
///
/// struct MyStruct {
/// value: i32,
/// }
/// impl MyStruct {
/// fn new() -> Self {
/// MyStruct { value: 0 }
/// }
/// }
/// singleton!(MyStruct, INSTANCE);
/// ```
#[macro_export]
macro_rules! singleton {
($struct_name:ty, $instance_name:ident) => {
static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new();
impl $struct_name {
pub fn global() -> &'static $struct_name {
$instance_name.get_or_init(|| Self::new())
}
}
};
($struct_name:ty, $instance_name:ident, $init_expr:expr) => {
static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new();
impl $struct_name {
pub fn global() -> &'static $struct_name {
$instance_name.get_or_init(|| $init_expr)
}
}
};
}
/// Macro for singleton pattern with logging
#[macro_export]
macro_rules! singleton_with_logging {
($struct_name:ty, $instance_name:ident, $struct_name_str:literal) => {
static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new();
impl $struct_name {
pub fn global() -> &'static $struct_name {
$instance_name.get_or_init(|| {
let instance = Self::new();
$crate::logging!(
info,
$crate::utils::logging::Type::Setup,
true,
concat!($struct_name_str, " initialized")
);
instance
})
}
}
};
}
/// Macro for singleton pattern with lazy initialization using a closure
/// This replaces patterns like lazy_static! or complex OnceLock initialization
#[macro_export]
macro_rules! singleton_lazy {
($struct_name:ty, $instance_name:ident, $init_closure:expr) => {
static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new();
impl $struct_name {
pub fn global() -> &'static $struct_name {
$instance_name.get_or_init($init_closure)
}
}
};
}
/// Macro for singleton pattern with lazy initialization and logging
#[macro_export]
macro_rules! singleton_lazy_with_logging {
($struct_name:ty, $instance_name:ident, $struct_name_str:literal, $init_closure:expr) => {
static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new();
impl $struct_name {
pub fn global() -> &'static $struct_name {
$instance_name.get_or_init(|| {
let instance = $init_closure();
$crate::logging!(
info,
$crate::utils::logging::Type::Setup,
true,
concat!($struct_name_str, " initialized")
);
instance
})
}
}
};
}
#[cfg(test)]
mod tests {
struct TestStruct {
value: i32,
}
impl TestStruct {
fn new() -> Self {
Self { value: 42 }
}
}
singleton!(TestStruct, TEST_INSTANCE);
#[test]
fn test_singleton_macro() {
let instance1 = TestStruct::global();
let instance2 = TestStruct::global();
assert_eq!(instance1.value, 42);
assert_eq!(instance2.value, 42);
assert!(std::ptr::eq(instance1, instance2));
}
}
@@ -1,2 +0,0 @@
# LOCAL_SOCK="/Users/tunglies/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev.dev/mihomo.sock"
LOCAL_SOCK="/Users/tunglies/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/mihomo.sock"
@@ -1,11 +0,0 @@
[package]
name = "mihomo_api"
edition = "2024"
[dependencies]
reqwest = { version = "0.12.22", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tokio = { version = "1.46.1", features = ["rt", "macros", "time"] }
[dev-dependencies]
@@ -1,147 +0,0 @@
use reqwest::{Method, header::HeaderMap};
use serde_json::{Value, json};
use std::time::Duration;
pub mod model;
pub use model::MihomoManager;
impl MihomoManager {
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
let client = reqwest::ClientBuilder::new()
.default_headers(headers)
.no_proxy()
.timeout(Duration::from_secs(15))
.pool_max_idle_per_host(5)
.pool_idle_timeout(Duration::from_secs(15))
.build()
.expect("Failed to build reqwest client");
Self {
mihomo_server,
client,
}
}
async fn send_request(
&self,
method: Method,
url: String,
data: Option<serde_json::Value>,
) -> Result<serde_json::Value, String> {
let client_response = self
.client
.request(method.clone(), &url)
.json(&data.unwrap_or(json!({})))
.send()
.await
.map_err(|e| e.to_string())?;
let response = match method {
Method::PATCH => {
let status = client_response.status();
if status.as_u16() == 204 {
json!({"code": 204})
} else {
client_response
.json::<serde_json::Value>()
.await
.map_err(|e| e.to_string())?
}
}
Method::PUT => json!(client_response.text().await.map_err(|e| e.to_string())?),
_ => client_response
.json::<serde_json::Value>()
.await
.map_err(|e| e.to_string())?,
};
Ok(response)
}
pub async fn get_refresh_proxies(&self) -> Result<Value, String> {
let url = format!("{}/proxies", self.mihomo_server);
let proxies = self.send_request(Method::GET, url, None).await?;
Ok(proxies)
}
pub async fn get_providers_proxies(&self) -> Result<Value, String> {
let url = format!("{}/providers/proxies", self.mihomo_server);
let providers_proxies = self.send_request(Method::GET, url, None).await?;
Ok(providers_proxies)
}
pub async fn close_all_connections(&self) -> Result<(), String> {
let url = format!("{}/connections", self.mihomo_server);
let response = self.send_request(Method::DELETE, url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string())
}
}
}
impl MihomoManager {
pub async fn is_mihomo_running(&self) -> Result<(), String> {
let url = format!("{}/version", self.mihomo_server);
let _response = self.send_request(Method::GET, url, None).await?;
Ok(())
}
pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> {
let url = format!("{}/configs?force=true", self.mihomo_server);
let payload = serde_json::json!({
"path": clash_config_path,
});
let _response = self.send_request(Method::PUT, url, Some(payload)).await?;
Ok(())
}
pub async fn patch_configs(&self, config: serde_json::Value) -> Result<(), String> {
let url = format!("{}/configs", self.mihomo_server);
let response = self.send_request(Method::PATCH, url, Some(config)).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string())
}
}
pub async fn test_proxy_delay(
&self,
name: &str,
test_url: Option<String>,
timeout: i32,
) -> Result<serde_json::Value, String> {
let test_url = test_url.unwrap_or("https://cp.cloudflare.com/generate_204".to_string());
let url = format!(
"{}/proxies/{}/delay?url={}&timeout={}",
self.mihomo_server, name, test_url, timeout
);
let response = self.send_request(Method::GET, url, None).await?;
Ok(response)
}
pub async fn get_connections(&self) -> Result<serde_json::Value, String> {
let url = format!("{}/connections", self.mihomo_server);
let response = self.send_request(Method::GET, url, None).await?;
Ok(response)
}
pub async fn delete_connection(&self, id: &str) -> Result<(), String> {
let url = format!("{}/connections/{}", self.mihomo_server, id);
let response = self.send_request(Method::DELETE, url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string())
}
}
}
@@ -1,5 +0,0 @@
#[derive(Clone)]
pub struct MihomoManager {
pub(crate) mihomo_server: String,
pub(crate) client: reqwest::Client,
}
@@ -1,7 +0,0 @@
use reqwest::header::HeaderMap;
#[test]
fn test_mihomo_manager_init() {
let _ = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
assert_eq!(true, true);
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "2.3.2",
"version": "2.4.0",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
@@ -0,0 +1,342 @@
import React, { Component, ErrorInfo, ReactNode } from "react";
import { Box, Typography, Button, Alert, Collapse } from "@mui/material";
import {
ErrorOutlineRounded,
RefreshRounded,
BugReportRounded,
} from "@mui/icons-material";
import { useTranslation } from "react-i18next";
interface Props {
children: ReactNode;
fallbackComponent?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
showDetails: boolean;
}
/**
* 流量统计专用错误边界组件
* 处理图表和流量统计组件的错误,提供优雅的降级体验
*/
export class TrafficErrorBoundary extends Component<Props, State> {
private retryCount = 0;
private maxRetries = 3;
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
// 更新状态以显示降级UI
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("[TrafficErrorBoundary] 捕获到组件错误:", error, errorInfo);
this.setState({
error,
errorInfo,
});
// 调用错误回调
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// 发送错误到监控系统(如果有的话)
this.reportError(error, errorInfo);
}
private reportError = (error: Error, errorInfo: ErrorInfo) => {
// 这里可以集成错误监控服务
const errorReport = {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
};
console.log("[TrafficErrorBoundary] 错误报告:", errorReport);
// TODO: 发送到错误监控服务
// sendErrorReport(errorReport);
};
private handleRetry = () => {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
console.log(
`[TrafficErrorBoundary] 尝试重试 (${this.retryCount}/${this.maxRetries})`,
);
this.setState({
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
});
} else {
console.warn("[TrafficErrorBoundary] 已达到最大重试次数");
}
};
private handleRefresh = () => {
window.location.reload();
};
private toggleDetails = () => {
this.setState((prev) => ({ showDetails: !prev.showDetails }));
};
render() {
if (this.state.hasError) {
// 如果提供了自定义降级组件,使用它
if (this.props.fallbackComponent) {
return this.props.fallbackComponent;
}
// 默认错误UI
return (
<TrafficErrorFallback
error={this.state.error}
errorInfo={this.state.errorInfo}
showDetails={this.state.showDetails}
canRetry={this.retryCount < this.maxRetries}
retryCount={this.retryCount}
maxRetries={this.maxRetries}
onRetry={this.handleRetry}
onRefresh={this.handleRefresh}
onToggleDetails={this.toggleDetails}
/>
);
}
return this.props.children;
}
}
/**
* 错误降级UI组件
*/
interface TrafficErrorFallbackProps {
error: Error | null;
errorInfo: ErrorInfo | null;
showDetails: boolean;
canRetry: boolean;
retryCount: number;
maxRetries: number;
onRetry: () => void;
onRefresh: () => void;
onToggleDetails: () => void;
}
const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
error,
errorInfo,
showDetails,
canRetry,
retryCount,
maxRetries,
onRetry,
onRefresh,
onToggleDetails,
}) => {
const { t } = useTranslation();
return (
<Box
sx={{
p: 2,
minHeight: 200,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
border: "1px dashed",
borderColor: "error.main",
borderRadius: 2,
bgcolor: "error.light",
color: "error.contrastText",
}}
>
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: "error.main" }} />
<Typography variant="h6" gutterBottom>
{t("Traffic Statistics Error")}
</Typography>
<Typography
variant="body2"
color="text.secondary"
textAlign="center"
sx={{ mb: 2 }}
>
{t(
"The traffic statistics component encountered an error and has been disabled to prevent crashes.",
)}
</Typography>
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
<Typography variant="body2">
<strong>Error:</strong> {error?.message || "Unknown error"}
</Typography>
{retryCount > 0 && (
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
{t("Retry attempts")}: {retryCount}/{maxRetries}
</Typography>
)}
</Alert>
<Box sx={{ display: "flex", gap: 1, mb: 2 }}>
{canRetry && (
<Button
variant="contained"
color="primary"
startIcon={<RefreshRounded />}
onClick={onRetry}
size="small"
>
{t("Retry")}
</Button>
)}
<Button variant="outlined" onClick={onRefresh} size="small">
{t("Refresh Page")}
</Button>
<Button
variant="text"
startIcon={<BugReportRounded />}
onClick={onToggleDetails}
size="small"
>
{showDetails ? t("Hide Details") : t("Show Details")}
</Button>
</Box>
<Collapse in={showDetails} sx={{ width: "100%", maxWidth: 600 }}>
<Box
sx={{
p: 2,
bgcolor: "background.paper",
borderRadius: 1,
border: "1px solid",
borderColor: "divider",
}}
>
<Typography variant="subtitle2" gutterBottom>
Error Details:
</Typography>
<Typography
variant="caption"
component="pre"
sx={{
whiteSpace: "pre-wrap",
wordBreak: "break-word",
fontFamily: "monospace",
fontSize: "0.75rem",
color: "text.secondary",
}}
>
{error?.stack}
</Typography>
{errorInfo?.componentStack && (
<>
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
Component Stack:
</Typography>
<Typography
variant="caption"
component="pre"
sx={{
whiteSpace: "pre-wrap",
wordBreak: "break-word",
fontFamily: "monospace",
fontSize: "0.75rem",
color: "text.secondary",
}}
>
{errorInfo.componentStack}
</Typography>
</>
)}
</Box>
</Collapse>
</Box>
);
};
/**
* 轻量级流量统计错误边界
* 用于小型流量显示组件,提供最小化的错误UI
*/
export const LightweightTrafficErrorBoundary: React.FC<{
children: ReactNode;
}> = ({ children }) => {
return (
<TrafficErrorBoundary
fallbackComponent={
<Box
sx={{
p: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: 60,
bgcolor: "error.light",
borderRadius: 1,
color: "error.contrastText",
}}
>
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="caption">Traffic data unavailable</Typography>
</Box>
}
>
{children}
</TrafficErrorBoundary>
);
};
/**
* HOC:为任何组件添加流量错误边界
*/
export function withTrafficErrorBoundary<P extends object>(
WrappedComponent: React.ComponentType<P>,
options?: {
lightweight?: boolean;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
},
) {
const WithErrorBoundaryComponent = (props: P) => {
const ErrorBoundaryComponent = options?.lightweight
? LightweightTrafficErrorBoundary
: TrafficErrorBoundary;
return (
<ErrorBoundaryComponent onError={options?.onError}>
<WrappedComponent {...props} />
</ErrorBoundaryComponent>
);
};
WithErrorBoundaryComponent.displayName = `withTrafficErrorBoundary(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithErrorBoundaryComponent;
}
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, Snackbar, useTheme } from "@mui/material";
import { deleteConnection } from "@/services/api";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
@@ -9,7 +9,7 @@ import {
alpha,
} from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
import { deleteConnection } from "@/services/api";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
const Tag = styled("span")(({ theme }) => ({
+2 -2
View File
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { Box, Typography, Paper, Stack } from "@mui/material";
import { useLockFn } from "ahooks";
import { closeAllConnections } from "@/services/api";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import {
@@ -29,7 +29,7 @@ export const ClashModeCard = () => {
`${currentMode[0].toLocaleUpperCase()}${currentMode.slice(1)} Mode Description`,
);
}
return t("Mode Description Not Available");
return t("Core communication error");
}, [currentMode]);
// 模式图标映射
+1 -1
View File
@@ -29,7 +29,7 @@ import {
} from "@mui/icons-material";
import { useNavigate } from "react-router-dom";
import { EnhancedCard } from "@/components/home/enhanced-card";
import { updateProxy, deleteConnection } from "@/services/api";
import { updateProxy, deleteConnection } from "@/services/cmds";
import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
File diff suppressed because it is too large Load Diff
@@ -1,494 +0,0 @@
import {
forwardRef,
useImperativeHandle,
useState,
useEffect,
useCallback,
useMemo,
useRef,
memo,
} from "react";
import { Box, useTheme } from "@mui/material";
import parseTraffic from "@/utils/parse-traffic";
import { useTranslation } from "react-i18next";
import { Line as ChartJsLine } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip,
Filler,
Scale,
Tick,
} from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip,
Filler,
);
// 流量数据项接口
export interface ITrafficItem {
up: number;
down: number;
timestamp?: number;
}
// 对外暴露的接口
export interface EnhancedTrafficGraphRef {
appendData: (data: ITrafficItem) => void;
toggleStyle: () => void;
}
type TimeRange = 1 | 5 | 10; // 分钟
// 数据点类型
type DataPoint = ITrafficItem & { name: string; timestamp: number };
/**
* 增强型流量图表组件
*/
export const EnhancedTrafficGraph = memo(
forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
const theme = useTheme();
const { t } = useTranslation();
// 基础状态
const [timeRange, setTimeRange] = useState<TimeRange>(10);
const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
// 数据缓冲区
const dataBufferRef = useRef<DataPoint[]>([]);
// 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => minutes * 60,
[],
);
// 最大数据点数量
const MAX_BUFFER_SIZE = useMemo(
() => getMaxPointsByTimeRange(10),
[getMaxPointsByTimeRange],
);
// 颜色配置
const colors = useMemo(
() => ({
up: theme.palette.secondary.main,
down: theme.palette.primary.main,
grid: theme.palette.divider,
tooltipBg: theme.palette.background.paper,
text: theme.palette.text.primary,
tooltipBorder: theme.palette.divider,
}),
[theme],
);
// 切换时间范围
const handleTimeRangeClick = useCallback(
(event: React.MouseEvent<SVGTextElement>) => {
event.stopPropagation();
setTimeRange((prevRange) => {
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
});
},
[],
);
// 点击图表主体或图例时切换样式
const handleToggleStyleClick = useCallback(
(event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
},
[],
);
// 初始化数据缓冲区
useEffect(() => {
const now = Date.now();
const tenMinutesAgo = now - 10 * 60 * 1000;
const initialBuffer = Array.from(
{ length: MAX_BUFFER_SIZE },
(_, index) => {
const pointTime =
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
const date = new Date(pointTime);
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(
`Initial data generation: Invalid date for timestamp ${pointTime}`,
);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
} catch (e) {
console.error(
"Error in toLocaleTimeString during initial data gen:",
e,
"Date:",
date,
"Timestamp:",
pointTime,
);
nameValue = "Err:Time";
}
return {
up: 0,
down: 0,
timestamp: pointTime,
name: nameValue,
};
},
);
dataBufferRef.current = initialBuffer;
// 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(initialBuffer.slice(-pointsToShow));
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
// 添加数据点方法
const appendData = useCallback(
(data: ITrafficItem) => {
const safeData = {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
down:
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
};
const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp);
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
} catch (e) {
console.error(
"Error in toLocaleTimeString in appendData:",
e,
"Date:",
date,
"Timestamp:",
timestamp,
);
nameValue = "Err:Time";
}
// 带时间标签的新数据点
const newPoint: DataPoint = {
...safeData,
name: nameValue,
timestamp: timestamp,
};
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer;
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow));
},
[timeRange, getMaxPointsByTimeRange],
);
// 监听时间范围变化
useEffect(() => {
const pointsToShow = getMaxPointsByTimeRange(timeRange);
if (dataBufferRef.current.length > 0) {
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
}
}, [timeRange, getMaxPointsByTimeRange]);
// 切换图表样式
const toggleStyle = useCallback(() => {
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []);
// 暴露方法给父组件
useImperativeHandle(
ref,
() => ({
appendData,
toggleStyle,
}),
[appendData, toggleStyle],
);
const formatYAxis = useCallback((value: number | string): string => {
if (typeof value !== "number") return String(value);
const [num, unit] = parseTraffic(value);
return `${num}${unit}`;
}, []);
const formatXLabel = useCallback(
(tickValue: string | number, index: number, ticks: any[]) => {
const dataPoint = displayData[index as number];
if (dataPoint && dataPoint.name) {
const parts = dataPoint.name.split(":");
return `${parts[0]}:${parts[1]}`;
}
if (typeof tickValue === "string") {
const parts = tickValue.split(":");
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
return tickValue;
}
return "";
},
[displayData],
);
// 获取当前时间范围文本
const getTimeRangeText = useCallback(() => {
return t("{{time}} Minutes", { time: timeRange });
}, [timeRange, t]);
const chartData = useMemo(() => {
const labels = displayData.map((d) => d.name);
return {
labels,
datasets: [
{
label: t("Upload"),
data: displayData.map((d) => d.up),
borderColor: colors.up,
backgroundColor: chartStyle === "area" ? colors.up : colors.up,
fill: chartStyle === "area",
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2,
},
{
label: t("Download"),
data: displayData.map((d) => d.down),
borderColor: colors.down,
backgroundColor: chartStyle === "area" ? colors.down : colors.down,
fill: chartStyle === "area",
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2,
},
],
};
}, [displayData, colors.up, colors.down, t, chartStyle]);
const chartOptions = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
animation: false as false,
scales: {
x: {
display: true,
type: "category" as const,
labels: displayData.map((d) => d.name),
ticks: {
display: true,
color: colors.text,
font: { size: 10 },
callback: function (
this: Scale,
tickValue: string | number,
index: number,
ticks: Tick[],
): string | undefined {
let labelToFormat: string | undefined = undefined;
const currentDisplayTick = ticks[index];
if (
currentDisplayTick &&
typeof currentDisplayTick.label === "string"
) {
labelToFormat = currentDisplayTick.label;
} else {
const sourceLabels = displayData.map((d) => d.name);
if (
typeof tickValue === "number" &&
tickValue >= 0 &&
tickValue < sourceLabels.length
) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === "string") {
labelToFormat = tickValue;
}
}
if (typeof labelToFormat !== "string") {
return undefined;
}
const parts: string[] = labelToFormat.split(":");
return parts.length >= 2
? `${parts[0]}:${parts[1]}`
: labelToFormat;
},
autoSkip: true,
maxTicksLimit: Math.max(
5,
Math.floor(displayData.length / (timeRange * 2)),
),
minRotation: 0,
maxRotation: 0,
},
grid: {
display: true,
drawOnChartArea: false,
drawTicks: true,
tickLength: 2,
color: colors.text,
},
},
y: {
beginAtZero: true,
ticks: {
color: colors.text,
font: { size: 10 },
callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
},
},
plugins: {
tooltip: {
enabled: true,
mode: "index" as const,
intersect: false,
backgroundColor: colors.tooltipBg,
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
},
label: (context: any): string => {
const label = context.dataset.label || "";
const value = context.parsed.y;
const [num, unit] = parseTraffic(value);
return `${label}: ${num} ${unit}/s`;
},
},
},
legend: {
display: false,
},
},
layout: {
padding: {
top: 16,
right: 7,
left: 3,
},
},
}),
[colors, t, formatYAxis, timeRange, displayData],
);
return (
<Box
sx={{
width: "100%",
height: "100%",
position: "relative",
bgcolor: "action.hover",
borderRadius: 1,
cursor: "pointer",
}}
onClick={handleToggleStyleClick}
>
<div style={{ width: "100%", height: "100%", position: "relative" }}>
{displayData.length > 0 && (
<ChartJsLine data={chartData} options={chartOptions} />
)}
<svg
width="100%"
height="100%"
style={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}}
>
<text
x="3.5%"
y="10%"
textAnchor="start"
fill={theme.palette.text.secondary}
fontSize={11}
fontWeight="bold"
onClick={handleTimeRangeClick}
style={{ cursor: "pointer", pointerEvents: "all" }}
>
{getTimeRangeText()}
</text>
<text
x="99%"
y="10%"
textAnchor="end"
fill={colors.up}
fontSize={12}
fontWeight="bold"
onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: "all" }}
>
{t("Upload")}
</text>
<text
x="99%"
y="19%"
textAnchor="end"
fill={colors.down}
fontSize={12}
fontWeight="bold"
onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: "all" }}
>
{t("Download")}
</text>
</svg>
</div>
</Box>
);
}),
);
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
+59 -200
View File
@@ -7,6 +7,7 @@ import {
useTheme,
PaletteColor,
Grid,
Box,
} from "@mui/material";
import {
ArrowUpwardRounded,
@@ -17,18 +18,19 @@ import {
CloudDownloadRounded,
} from "@mui/icons-material";
import {
EnhancedTrafficGraph,
EnhancedTrafficGraphRef,
ITrafficItem,
} from "./enhanced-traffic-graph";
EnhancedCanvasTrafficGraph,
type EnhancedCanvasTrafficGraphRef,
type ITrafficItem,
} from "./enhanced-canvas-traffic-graph";
import { useVisibility } from "@/hooks/use-visibility";
import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { createAuthSockette } from "@/utils/websocket";
import parseTraffic from "@/utils/parse-traffic";
import { isDebugEnabled, gc } from "@/services/api";
import { isDebugEnabled, gc } from "@/services/cmds";
import { ReactNode } from "react";
import { useAppData } from "@/providers/app-data-provider";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import useSWR from "swr";
interface MemoryUsage {
@@ -64,7 +66,6 @@ declare global {
// 控制更新频率
const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
// 统计卡片组件 - 使用memo优化
const CompactStatCard = memo(
@@ -160,20 +161,15 @@ export const EnhancedTrafficStats = () => {
const theme = useTheme();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
const trafficRef = useRef<EnhancedTrafficGraphRef>(null);
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null);
const pageVisible = useVisibility();
// 使用AppDataProvider
const { connections, uptime } = useAppData();
// 使用单一状态对象减少状态更新次数
const [stats, setStats] = useState({
traffic: { up: 0, down: 0 },
memory: { inuse: 0, oslimit: undefined as number | undefined },
});
// 创建一个标记来追踪最后更新时间,用于节流
const lastUpdateRef = useRef({ traffic: 0 });
// 使用增强版的统一流量数据Hook
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
useTrafficDataEnhanced();
// 是否显示流量图表
const trafficGraph = verge?.traffic_graph ?? true;
@@ -189,171 +185,7 @@ export const EnhancedTrafficStats = () => {
},
);
// 处理流量数据更新 - 使用节流控制更新频率
const handleTrafficUpdate = useCallback((event: MessageEvent) => {
try {
const data = JSON.parse(event.data) as ITrafficItem;
if (
data &&
typeof data.up === "number" &&
typeof data.down === "number"
) {
// 使用节流控制更新频率
const now = Date.now();
if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) {
try {
trafficRef.current?.appendData({
up: data.up,
down: data.down,
timestamp: now,
});
} catch {}
return;
}
lastUpdateRef.current.traffic = now;
const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down;
try {
setStats((prev) => ({
...prev,
traffic: { up: safeUp, down: safeDown },
}));
} catch {}
try {
trafficRef.current?.appendData({
up: safeUp,
down: safeDown,
timestamp: now,
});
} catch {}
}
} catch (err) {
console.error("[Traffic] 解析数据错误:", err, event.data);
}
}, []);
// 处理内存数据更新
const handleMemoryUpdate = useCallback((event: MessageEvent) => {
try {
const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") {
setStats((prev) => ({
...prev,
memory: {
inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit,
},
}));
}
} catch (err) {
console.error("[Memory] 解析数据错误:", err, event.data);
}
}, []);
// 使用 WebSocket 连接获取数据 - 合并流量和内存连接逻辑
useEffect(() => {
if (!clashInfo || !pageVisible) return;
const { server, secret = "" } = clashInfo;
if (!server) return;
// WebSocket 引用
let sockets: {
traffic: ReturnType<typeof createAuthSockette> | null;
memory: ReturnType<typeof createAuthSockette> | null;
} = {
traffic: null,
memory: null,
};
// 清理现有连接的函数
const cleanupSockets = () => {
Object.values(sockets).forEach((socket) => {
if (socket) {
socket.close();
}
});
sockets = { traffic: null, memory: null };
};
// 关闭现有连接
cleanupSockets();
// 创建新连接
console.log(
`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
);
sockets.traffic = createAuthSockette(`${server}/traffic`, secret, {
onmessage: handleTrafficUpdate,
onopen: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
},
onerror: (event) => {
console.error(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
});
console.log(
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
);
sockets.memory = createAuthSockette(`${server}/memory`, secret, {
onmessage: handleMemoryUpdate,
onopen: (event) => {
console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
},
onerror: (event) => {
console.error(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
},
onclose: (event) => {
console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
}
},
});
return cleanupSockets;
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
// 执行垃圾回收
const handleGarbageCollection = useCallback(async () => {
@@ -369,9 +201,9 @@ export const EnhancedTrafficStats = () => {
// 使用useMemo计算解析后的流量数据
const parsedData = useMemo(() => {
const [up, upUnit] = parseTraffic(stats.traffic.up);
const [down, downUnit] = parseTraffic(stats.traffic.down);
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const [uploadTotal, uploadTotalUnit] = parseTraffic(
connections.uploadTotal,
);
@@ -392,7 +224,7 @@ export const EnhancedTrafficStats = () => {
downloadTotalUnit,
connectionsCount: connections.count,
};
}, [stats, connections]);
}, [traffic, memory, connections]);
// 渲染流量图表 - 使用useMemo缓存渲染结果
const trafficGraphComponent = useMemo(() => {
@@ -411,7 +243,7 @@ export const EnhancedTrafficStats = () => {
onClick={() => trafficRef.current?.toggleStyle()}
>
<div style={{ height: "100%", position: "relative" }}>
<EnhancedTrafficGraph ref={trafficRef} />
<EnhancedCanvasTrafficGraph ref={trafficRef} />
{isDebug && (
<div
style={{
@@ -428,6 +260,10 @@ export const EnhancedTrafficStats = () => {
>
DEBUG: {!!trafficRef.current ? "图表已初始化" : "图表未初始化"}
<br />
状态: {isDataFresh ? "active" : "inactive"}
<br />
数据新鲜度: {traffic?.is_fresh ? "Fresh" : "Stale"}
<br />
{new Date().toISOString().slice(11, 19)}
</div>
)}
@@ -487,19 +323,42 @@ export const EnhancedTrafficStats = () => {
);
return (
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
{trafficGraph && (
<Grid size={12}>
{/* 流量图表区域 */}
{trafficGraphComponent}
</Grid>
)}
{/* 统计卡片区域 */}
{statCards.map((card, index) => (
<Grid key={index} size={4}>
<CompactStatCard {...card} />
</Grid>
))}
</Grid>
<TrafficErrorBoundary
onError={(error, errorInfo) => {
console.error("[EnhancedTrafficStats] 组件错误:", error, errorInfo);
}}
>
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
{trafficGraph && (
<Grid size={12}>
{/* 流量图表区域 */}
{trafficGraphComponent}
</Grid>
)}
{/* 统计卡片区域 */}
{statCards.map((card, index) => (
<Grid key={index} size={4}>
<CompactStatCard {...card} />
</Grid>
))}
{/* 数据状态指示器(调试用)*/}
{isDebug && (
<Grid size={12}>
<Box
sx={{
p: 1,
bgcolor: "action.hover",
borderRadius: 1,
fontSize: "0.75rem",
}}
>
数据状态: {isDataFresh ? "新鲜" : "过期"} | 有效数据:{" "}
{hasValidData ? "是" : "否"} | 加载中: {isLoading ? "是" : "否"}
</Box>
</Grid>
)}
</Grid>
</TrafficErrorBoundary>
);
};
+7 -29
View File
@@ -9,7 +9,7 @@ import {
useTheme,
Fade,
} from "@mui/material";
import { useState, useMemo, memo, FC, useEffect } from "react";
import { useState, useMemo, memo, FC } from "react";
import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches";
import {
ComputerRounded,
@@ -21,8 +21,6 @@ import { useVerge } from "@/hooks/use-verge";
import { useSystemState } from "@/hooks/use-system-state";
import { useSystemProxyState } from "@/hooks/use-system-proxy-state";
import { showNotice } from "@/services/noticeService";
import { getRunningMode } from "@/services/cmds";
import { mutate } from "swr";
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
@@ -142,30 +140,13 @@ export const ProxyTunCard: FC = () => {
() => localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system",
);
const [localServiceOk, setLocalServiceOk] = useState(false);
const { verge } = useVerge();
const { isAdminMode } = useSystemState();
const { indicator: systemProxyIndicator } = useSystemProxyState();
const { isAdminMode, isServiceMode } = useSystemState();
const { actualState: systemProxyActualState } = useSystemProxyState();
const { enable_tun_mode } = verge ?? {};
const updateLocalStatus = async () => {
try {
const runningMode = await getRunningMode();
const serviceStatus = runningMode === "Service";
setLocalServiceOk(serviceStatus);
mutate("isServiceAvailable", serviceStatus, false);
} catch (error) {
console.error("更新TUN状态失败:", error);
}
};
useEffect(() => {
updateLocalStatus();
}, []);
const isTunAvailable = localServiceOk || isAdminMode;
const isTunAvailable = isServiceMode || isAdminMode;
const handleError = (err: Error) => {
showNotice("error", err.message || err.toString());
@@ -174,15 +155,12 @@ export const ProxyTunCard: FC = () => {
const handleTabChange = (tab: string) => {
setActiveTab(tab);
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab);
if (tab === "tun") {
updateLocalStatus();
}
};
const tabDescription = useMemo(() => {
if (activeTab === "system") {
return {
text: systemProxyIndicator
text: systemProxyActualState
? t("System Proxy Enabled")
: t("System Proxy Disabled"),
tooltip: t("System Proxy Info"),
@@ -197,7 +175,7 @@ export const ProxyTunCard: FC = () => {
tooltip: t("TUN Mode Intercept Info"),
};
}
}, [activeTab, systemProxyIndicator, enable_tun_mode, isTunAvailable, t]);
}, [activeTab, systemProxyActualState, enable_tun_mode, isTunAvailable, t]);
return (
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
@@ -216,7 +194,7 @@ export const ProxyTunCard: FC = () => {
onClick={() => handleTabChange("system")}
icon={ComputerRounded}
label={t("System Proxy")}
hasIndicator={systemProxyIndicator}
hasIndicator={systemProxyActualState}
/>
<TabButton
isActive={activeTab === "tun"}
+111 -158
View File
@@ -10,10 +10,10 @@ import { useVerge } from "@/hooks/use-verge";
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
import { useVisibility } from "@/hooks/use-visibility";
import parseTraffic from "@/utils/parse-traffic";
import useSWRSubscription from "swr/subscription";
import { createAuthSockette } from "@/utils/websocket";
import { useTranslation } from "react-i18next";
import { isDebugEnabled, gc } from "@/services/api";
import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import useSWR from "swr";
interface MemoryUsage {
@@ -23,6 +23,18 @@ interface MemoryUsage {
// setup the traffic
export const LayoutTraffic = () => {
const { data: isDebug } = useSWR(
"clash-verge-rev-internal://isDebugEnabled",
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
if (isDebug) {
console.debug("[Traffic][LayoutTraffic] 组件正在渲染");
}
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
@@ -33,125 +45,43 @@ export const LayoutTraffic = () => {
const trafficRef = useRef<TrafficRef>(null);
const pageVisible = useVisibility();
const { data: isDebug } = useSWR(
"clash-verge-rev-internal://isDebugEnabled",
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
// 使用增强版的统一流量数据Hook
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
useTrafficDataEnhanced();
const { data: traffic = { up: 0, down: 0 } } = useSWRSubscription<
ITrafficItem,
any,
"getRealtimeTraffic" | null
>(
clashInfo && pageVisible ? "getRealtimeTraffic" : null,
(_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
// 启动流量服务
useEffect(() => {
console.log(
"[Traffic][LayoutTraffic] useEffect 触发,clashInfo:",
clashInfo,
"pageVisible:",
pageVisible,
);
if (!server) {
console.warn("[Traffic] 服务器地址为空,无法建立连接");
next(null, { up: 0, down: 0 });
return () => {};
}
// 简化条件,只要组件挂载就尝试启动服务
console.log("[Traffic][LayoutTraffic] 开始启动流量服务");
startTrafficService().catch((error) => {
console.error("[Traffic][LayoutTraffic] 启动流量服务失败:", error);
});
}, []); // 移除依赖,只在组件挂载时启动一次
console.log(`[Traffic] 正在连接: ${server}/traffic`);
const s = createAuthSockette(`${server}/traffic`, secret, {
timeout: 8000, // 8秒超时
onmessage(event) {
const data = JSON.parse(event.data) as ITrafficItem;
trafficRef.current?.appendData(data);
next(null, data);
},
onerror(event) {
console.error("[Traffic] WebSocket 连接错误", event);
this.close();
next(null, { up: 0, down: 0 });
},
onclose(event) {
console.log("[Traffic] WebSocket 连接关闭", event);
},
onopen(event) {
console.log("[Traffic] WebSocket 连接已建立");
},
// 监听数据变化,为图表添加数据点
useEffect(() => {
if (traffic?.raw && trafficRef.current) {
trafficRef.current.appendData({
up: traffic.raw.up_rate || 0,
down: traffic.raw.down_rate || 0,
});
}
}, [traffic]);
return () => {
console.log("[Traffic] 清理WebSocket连接");
try {
s.close();
} catch (e) {
console.error("[Traffic] 关闭连接时出错", e);
}
};
},
{
fallbackData: { up: 0, down: 0 },
keepPreviousData: true,
},
);
/* --------- meta memory information --------- */
// 显示内存使用情况的设置
const displayMemory = verge?.enable_memory_usage ?? true;
const { data: memory = { inuse: 0 } } = useSWRSubscription<
MemoryUsage,
any,
"getRealtimeMemory" | null
>(
clashInfo && pageVisible && displayMemory ? "getRealtimeMemory" : null,
(_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
if (!server) {
console.warn("[Memory] 服务器地址为空,无法建立连接");
next(null, { inuse: 0 });
return () => {};
}
console.log(`[Memory] 正在连接: ${server}/memory`);
const s = createAuthSockette(`${server}/memory`, secret, {
timeout: 8000, // 8秒超时
onmessage(event) {
const data = JSON.parse(event.data) as MemoryUsage;
next(null, data);
},
onerror(event) {
console.error("[Memory] WebSocket 连接错误", event);
this.close();
next(null, { inuse: 0 });
},
onclose(event) {
console.log("[Memory] WebSocket 连接关闭", event);
},
onopen(event) {
console.log("[Memory] WebSocket 连接已建立");
},
});
return () => {
console.log("[Memory] 清理WebSocket连接");
try {
s.close();
} catch (e) {
console.error("[Memory] 关闭连接时出错", e);
}
};
},
{
fallbackData: { inuse: 0 },
keepPreviousData: true,
},
);
const [up, upUnit] = parseTraffic(traffic.up);
const [down, downUnit] = parseTraffic(traffic.down);
const [inuse, inuseUnit] = parseTraffic(memory.inuse);
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const boxStyle: any = {
display: "flex",
@@ -175,55 +105,78 @@ export const LayoutTraffic = () => {
};
return (
<Box position="relative">
{trafficGraph && pageVisible && (
<div
style={{ width: "100%", height: 60, marginBottom: 6 }}
onClick={trafficRef.current?.toggleStyle}
>
<TrafficGraph ref={trafficRef} />
</div>
)}
<LightweightTrafficErrorBoundary>
<Box position="relative">
{trafficGraph && pageVisible && (
<div
style={{ width: "100%", height: 60, marginBottom: 6 }}
onClick={trafficRef.current?.toggleStyle}
>
<TrafficGraph ref={trafficRef} />
</div>
)}
<Box display="flex" flexDirection="column" gap={0.75}>
<Box title={t("Upload Speed")} {...boxStyle}>
<ArrowUpwardRounded
{...iconStyle}
color={+up > 0 ? "secondary" : "disabled"}
/>
<Typography {...valStyle} color="secondary">
{up}
</Typography>
<Typography {...unitStyle}>{upUnit}/s</Typography>
</Box>
<Box title={t("Download Speed")} {...boxStyle}>
<ArrowDownwardRounded
{...iconStyle}
color={+down > 0 ? "primary" : "disabled"}
/>
<Typography {...valStyle} color="primary">
{down}
</Typography>
<Typography {...unitStyle}>{downUnit}/s</Typography>
</Box>
{displayMemory && (
<Box display="flex" flexDirection="column" gap={0.75}>
<Box
title={t(isDebug ? "Memory Cleanup" : "Memory Usage")}
title={`${t("Upload Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
{...boxStyle}
sx={{ cursor: isDebug ? "pointer" : "auto" }}
color={isDebug ? "success.main" : "disabled"}
onClick={async () => {
isDebug && (await gc());
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography>
<Typography {...unitStyle}>{inuseUnit}</Typography>
<ArrowUpwardRounded
{...iconStyle}
color={
(traffic?.raw?.up_rate || 0) > 0 ? "secondary" : "disabled"
}
/>
<Typography {...valStyle} color="secondary">
{up}
</Typography>
<Typography {...unitStyle}>{upUnit}/s</Typography>
</Box>
)}
<Box
title={`${t("Download Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
{...boxStyle}
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<ArrowDownwardRounded
{...iconStyle}
color={
(traffic?.raw?.down_rate || 0) > 0 ? "primary" : "disabled"
}
/>
<Typography {...valStyle} color="primary">
{down}
</Typography>
<Typography {...unitStyle}>{downUnit}/s</Typography>
</Box>
{displayMemory && (
<Box
title={`${t(isDebug ? "Memory Cleanup" : "Memory Usage")} ${memory?.is_fresh ? "" : "(Stale)"} ${"usage_percent" in (memory?.formatted || {}) && memory.formatted.usage_percent ? `(${memory.formatted.usage_percent.toFixed(1)}%)` : ""}`}
{...boxStyle}
sx={{
cursor: isDebug ? "pointer" : "auto",
opacity: memory?.is_fresh ? 1 : 0.6,
}}
color={isDebug ? "success.main" : "disabled"}
onClick={async () => {
isDebug && (await gc());
}}
>
<MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography>
<Typography {...unitStyle}>{inuseUnit}</Typography>
</Box>
)}
</Box>
</Box>
</Box>
</LightweightTrafficErrorBoundary>
);
};

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