Compare commits

..

66 Commits

  • chore: updater autobuild (#5295)
    * chore(updater): configure autobuild channel and refresh suffixes
    
    * feat(updater): add channel-aware updater and UI integration
    
    * feat(updater): support channel-specific updater command
    
    * feat(updater): enable downgrade-aware updates for autobuild channel
    
    * fix(updater): tighten prerelease downgrade gating and forward target
    
    * style: prettier
  • docs(release): Assets URL correction (#5294)
    - Fix mis-spelling of assets URL to Linux RPM package
  • chore(deps): update dependency @types/node to ^24.10.0 (#5292)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • perf: select proxy (#5284)
    * perf: improve select proxy for group
    
    * chore: update
  • chore(deps): update dependency @eslint-react/eslint-plugin to ^2.3.1 (#5281)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • refactor: replace hardcoded DNS config filename with constant reference (#5280)
    * refactor: replace hardcoded DNS config filename with constant reference
    
    * refactor: remove redundant import of constants in IClashTemp template method
    
    * refactor: add conditional compilation for DEFAULT_REDIR based on OS
    
    * refactor: simplify default TPROXY port handling and remove unused trace_err macro
    
    * refactor: simplify default TPROXY port fallback logic
  • refactor: remove orphaned process cleanup functionality
    It might breaks mihomo starting.
    
    Due to potentiall process name processing, permissions verifing, permissions and safty FORCE KILL, find process faillure.
  • Refactor logging to use a centralized logging utility across the application (#5277)
    - Replaced direct log calls with a new logging macro that includes a logging type for better categorization.
    - Updated logging in various modules including `merge.rs`, `mod.rs`, `tun.rs`, `clash.rs`, `profile.rs`, `proxy.rs`, `window.rs`, `lightweight.rs`, `guard.rs`, `autostart.rs`, `dirs.rs`, `dns.rs`, `scheme.rs`, `server.rs`, and `window_manager.rs`.
    - Introduced logging types such as `Core`, `Network`, `ProxyMode`, `Window`, `Lightweight`, `Service`, and `File` to enhance log clarity and filtering.
  • refactor: reduce clone operation (#5268)
    * refactor: optimize item handling and improve profile management
    
    * refactor: update IVerge references to use references instead of owned values
    
    * refactor: update patch_verge to use data_ref for improved data handling
    
    * refactor: move handle_copy function to improve resource initialization logic
    
    * refactor: update profile handling to use references for improved memory efficiency
    
    * refactor: simplify get_item method and update profile item retrieval to use string slices
    
    * refactor: update profile validation and patching to use references for improved performance
    
    * refactor: update profile functions to use references for improved performance and memory efficiency
    
    * refactor: update profile patching functions to use references for improved memory efficiency
    
    * refactor: simplify merge function in PrfOption to enhance readability
    
    * refactor: update change_core function to accept a reference for improved memory efficiency
    
    * refactor: update PrfItem and profile functions to use references for improved memory efficiency
    
    * refactor: update resolve_scheme function to accept a reference for improved memory efficiency
    
    * refactor: update resolve_scheme function to accept a string slice for improved flexibility
    
    * refactor: simplify update_profile parameters and logic
  • chore(deps): update npm dependencies (#5278)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • refactor: convert file operations to async using tokio fs (#5267)
    * refactor: convert file operations to async using tokio fs
    
    * refactor: integrate AsyncHandler for file operations in backup processes
  • chore(deps): update dependency sass to ^1.93.3 (#5265)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • chore(deps): update dependency vitest to ^4.0.6 (#5264)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • fix: disable tun mode menu on tray when tun mode is unavailable (#4975)
    * fix: check if service installed when toggle tun mode on tray
    
    * chore: cargo fmt
    
    * fix: auto disable tun mode
    
    * docs: update UPDATELOG.md
    
    * fix: init Tun mode status
    
    * chore: update
    
    * feat: disable tun mode tray menu when tun mode is unavailable
    
    * fix: restart core when uninstall service is canceled
    
    * chore: remove check notification when toggle tun mode
    
    * chore: fix updatelog
    
    ---------
    
    Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
  • chore(deps): update dependency dayjs to v1.11.19 (#5261)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • chore(deps): update npm dependencies (#5258)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • chore(i18n): update localization files sorting and add i18n contribution guidline
    - Add i18n contribution guidline
    - Chinese (zh.json): fix TPROXY port missing translation
  • refactor: profile switch (#5197)
    * refactor: proxy refresh
    
    * fix(proxy-store): properly hydrate and filter backend provider snapshots
    
    * fix(proxy-store): add monotonic fetch guard and event bridge cleanup
    
    * fix(proxy-store): tweak fetch sequencing guard to prevent snapshot invalidation from wiping fast responses
    
    * docs: UPDATELOG.md
    
    * fix(proxy-snapshot, proxy-groups): restore last-selected proxy and group info
    
    * fix(proxy): merge static and provider entries in snapshot; fix Virtuoso viewport height
    
    * fix(proxy-groups): restrict reduced-height viewport to chain-mode column
    
    * refactor(profiles): introduce a state machine
    
    * refactor:replace state machine with reducer
    
    * refactor:introduce a profile switch worker
    
    * refactor: hooked up a backend-driven profile switch flow
    
    * refactor(profile-switch): serialize switches with async queue and enrich frontend events
    
    * feat(profiles): centralize profile switching with reducer/driver queue to fix stuck UI on rapid toggles
    
    * chore: translate comments and log messages to English to avoid encoding issues
    
    * refactor: migrate backend queue to SwitchDriver actor
    
    * fix(profile): unify error string types in validation helper
    
    * refactor(profile): make switch driver fully async and handle panics safely
    
    * refactor(cmd): move switch-validation helper into new profile_switch module
    
    * refactor(profile): modularize switch logic into profile_switch.rs
    
    * refactor(profile_switch): modularize switch handler
    
    - Break monolithic switch handler into proper module hierarchy
    - Move shared globals, constants, and SwitchScope guard to state.rs
    - Isolate queue orchestration and async task spawning in driver.rs
    - Consolidate switch pipeline and config patching in workflow.rs
    - Extract request pre-checks/YAML validation into validation.rs
    
    * refactor(profile_switch): centralize state management and add cancellation flow
    
    - Introduced SwitchManager in state.rs to unify mutex, sequencing, and SwitchScope handling.
    - Added SwitchCancellation and SwitchRequest wrappers to encapsulate cancel tokens and notifications.
    - Updated driver to allocate task IDs via SwitchManager, cancel old tokens, and queue next jobs in order.
    - Updated workflow to check cancellation and sequence at each phase, replacing global flags with manager APIs.
    
    * feat(profile_switch): integrate explicit state machine for profile switching
    
    - workflow.rs:24 now delegates each switch to SwitchStateMachine, passing an owned SwitchRequest.
      Queue cancellation and state-sequence checks are centralized inside the machine instead of scattered guards.
    - workflow.rs:176 replaces the old helper with `SwitchStateMachine::new(manager(), None, profiles).run().await`,
      ensuring manual profile patches follow the same workflow (locking, validation, rollback) as queued switches.
    - workflow.rs:180 & 275 expose `validate_profile_yaml` and `restore_previous_profile` for reuse inside the state machine.
    
    - workflow/state_machine.rs:1 introduces a dedicated state machine module.
      It manages global mutex acquisition, request/cancellation state, YAML validation, draft patching,
      `CoreManager::update_config`, failure rollback, and tray/notification side-effects.
      Transitions check for cancellations and stale sequences; completions release guards via `SwitchScope` drop.
    
    * refactor(profile-switch): integrate stage-aware panic handling
    
    - src-tauri/src/cmd/profile_switch/workflow/state_machine.rs:1
      Defines SwitchStage and SwitchPanicInfo as crate-visible, wraps each transition in with_stage(...) with catch_unwind, and propagates CmdResult<bool> to distinguish validation failures from panics while keeping cancellation semantics.
    
    - src-tauri/src/cmd/profile_switch/workflow.rs:25
      Updates run_switch_job to return Result<bool, SwitchPanicInfo>, routing timeout, validation, config, and stage panic cases separately. Reuses SwitchPanicInfo for logging/UI notifications; patch_profiles_config maps state-machine panics into user-facing error strings.
    
    - src-tauri/src/cmd/profile_switch/driver.rs:1
      Adds SwitchJobOutcome to unify workflow results: normal completions carry bool, and panics propagate SwitchPanicInfo. The driver loop now logs panics explicitly and uses AssertUnwindSafe(...).catch_unwind() to guard setup-phase panics.
    
    * refactor(profile-switch): add watchdog, heartbeat, and async timeout guards
    
    - Introduce SwitchHeartbeat for stage tracking and timing; log stage transitions with elapsed durations.
    - Add watchdog in driver to cancel stalled switches (5s heartbeat timeout).
    - Wrap blocking ops (Config::apply, tray updates, profiles_save_file_safe, etc.) with time::timeout to prevent async stalls.
    - Improve logs for stage transitions and watchdog timeouts to clarify cancellation points.
    
    * refactor(profile-switch): async post-switch tasks, early lock release, and spawn_blocking for IO
    
    * feat(profile-switch): track cleanup and coordinate pipeline
    
    - Add explicit cleanup tracking in the driver (`cleanup_profiles` map + `CleanupDone` messages) to know when background post-switch work is still running before starting a new workflow. (driver.rs:29-50)
    - Update `handle_enqueue` to detect “cleanup in progress”: same-profile retries are short-circuited; other requests collapse the pending queue, cancelling old tokens so only the latest intent survives. (driver.rs:176-247)
    - Rework scheduling helpers: `start_next_job` refuses to start while cleanup is outstanding; discarded requests release cancellation tokens; cleanup completion explicitly restarts the pipeline. (driver.rs:258-442)
    
    * feat(profile-switch): unify post-switch cleanup handling
    
    - workflow.rs (25-427) returns `SwitchWorkflowResult` (success + CleanupHandle) or `SwitchWorkflowError`.
      All failure/timeout paths stash post-switch work into a single CleanupHandle.
      Cleanup helpers (`notify_profile_switch_finished` and `close_connections_after_switch`) run inside that task for proper lifetime handling.
    
    - driver.rs (29-439) propagates CleanupHandle through `SwitchJobOutcome`, spawns a bridge to wait for completion, and blocks `start_next_job` until done.
      Direct driver-side panics now schedule failure cleanup via the shared helper.
    
    * tmp
    
    * Revert "tmp"
    
    This reverts commit e582cf4a65.
    
    * refactor: queue frontend events through async dispatcher
    
    * refactor: queue frontend switch/proxy events and throttle notices
    
    * chore: frontend debug log
    
    * fix: re-enable only ProfileSwitchFinished events - keep others suppressed for crash isolation
    
    - Re-enabled only ProfileSwitchFinished events; RefreshClash, RefreshProxy, and ProfileChanged remain suppressed (they log suppression messages)
    - Allows frontend to receive task completion notifications for UI feedback while crash isolation continues
    - src-tauri/src/core/handle.rs now only suppresses notify_profile_changed
    - Serialized emitter, frontend logging bridge, and other diagnostics unchanged
    
    * refactor: refreshClashData
    
    * refactor(proxy): stabilize proxy switch pipeline and rendering
    
    - Add coalescing buffer in notification.rs to emit only the latest proxies-updated snapshot
    - Replace nextTick with queueMicrotask in asyncQueue.ts for same-frame hydration
    - Hide auto-generated GLOBAL snapshot and preserve optional metadata in proxy-snapshot.ts
    - Introduce stable proxy rendering state in AppDataProvider (proxyTargetProfileId, proxyDisplayProfileId, isProxyRefreshPending)
    - Update proxy page to fade content during refresh and overlay status banner instead of showing incomplete snapshot
    
    * refactor(profiles): move manual activating logic to reducer for deterministic queue tracking
    
    * refactor: replace proxy-data event bridge with pure polling and simplify proxy store
    
    - Replaced the proxy-data event bridge with pure polling: AppDataProvider now fetches the initial snapshot and drives refreshes from the polled switchStatus, removing verge://refresh-* listeners (src/providers/app-data-provider.tsx).
    - Simplified proxy-store by dropping the proxies-updated listener queue and unused payload/normalizer helpers; relies on SWR/provider fetch path + calcuProxies for live updates (src/stores/proxy-store.ts).
    - Trimmed layout-level event wiring to keep only notice/show/hide subscriptions, removing obsolete refresh listeners (src/pages/_layout/useLayoutEvents.ts).
    
    * refactor(proxy): streamline proxies-updated handling and store event flow
    
    - AppDataProvider now treats `proxies-updated` as the fast path: the listener
      calls `applyLiveProxyPayload` immediately and schedules only a single fallback
      `fetchLiveProxies` ~600 ms later (replacing the old 0/250/1000/2000 cascade).
      Expensive provider/rule refreshes run in parallel via `Promise.allSettled`, and
      the multi-stage queue on profile updates completion was removed
      (src/providers/app-data-provider.tsx).
    
    - Rebuilt proxy-store to support the event flow: restored `setLive`, provider
      normalization, and an animation-frame + async queue that applies payloads without
      blocking. Exposed `applyLiveProxyPayload` so providers can push events directly
      into the store (src/stores/proxy-store.ts).
    
    * refactor: switch delay
    
    * refactor(app-data-provider): trigger getProfileSwitchStatus revalidation on profile-switch-finished
    
    - AppDataProvider now listens to `profile-switch-finished` and calls `mutate("getProfileSwitchStatus")` to immediately update state and unlock buttons (src/providers/app-data-provider.tsx).
    - Retain existing detailed timing logs for monitoring other stages.
    - Frontend success notifications remain instant; background refreshes continue asynchronously.
    
    * fix(profiles): prevent duplicate toast on page remount
    
    * refactor(profile-switch): make active switches preemptible and prevent queue piling
    
    - Add notify mechanism to SwitchCancellation to await cancellation without busy-waiting (state.rs:82)
    - Collapse pending queue to a single entry in the driver; cancel in-flight task on newer request (driver.rs:232)
    - Update handle_update_core to watch cancel token and 30s timeout; release locks, discard draft, and exit early if canceled (state_machine.rs:301)
    - Providers revalidate status immediately on profile-switch-finished events (app-data-provider.tsx:208)
    
    * refactor(core): make core reload phase controllable, reduce 0xcfffffff risk
    
    - CoreManager::apply_config now calls `reload_config_with_retry`, each attempt waits up to 5s, retries 3 times; on failure, returns error with duration logged and triggers core restart if needed (src-tauri/src/core/manager/config.rs:175, 205)
    - `reload_config_with_retry` logs attempt info on timeout or error; if error is a Mihomo connection issue, fallback to original restart logic (src-tauri/src/core/manager/config.rs:211)
    - `reload_config_once` retains original Mihomo call for retry wrapper usage (src-tauri/src/core/manager/config.rs:247)
    
    * chore(frontend-logs): downgrade routine event logs from info to debug
    
    - Logs like `emit_via_app entering spawn_blocking`, `Async emit…`, `Buffered proxies…` are now debug-level (src-tauri/src/core/notification.rs:155, :265, :309…)
    - Genuine warnings/errors (failures/timeouts) remain at warn/error
    - Core stage logs remain info to keep backend tracking visible
    
    * refactor(frontend-emit): make emit_via_app fire-and-forget async
    
    - `emit_via_app` now a regular function; spawns with `tokio::spawn` and logs a warn if `emit_to` fails, caller returns immediately (src-tauri/src/core/notification.rs:269)
    - Removed `.await` at Async emit and flush_proxies calls; only record dispatch duration and warn on failure (src-tauri/src/core/notification.rs:211, :329)
    
    * refactor(ui): restructure profile switch for event-driven speed + polling stability
    
    - Backend
      - SwitchManager maintains a lightweight event queue: added `event_sequence`, `recent_events`, and `SwitchResultEvent`; provides `push_event` / `events_after` (state.rs)
      - `handle_completion` pushes events on success/failure and keeps `last_result` (driver.rs) for frontend incremental fetch
      - New Tauri command `get_profile_switch_events(after_sequence)` exposes `events_after` (profile_switch/mod.rs → profile.rs → lib.rs)
    - Notification system
      - `NotificationSystem::process_event` only logs debug, disables WebView `emit_to`, fixes 0xcfffffff
      - Related emit/buffer functions now safe no-op, removed unused structures and warnings (notification.rs)
    - Frontend
      - services/cmds.ts defines `SwitchResultEvent` and `getProfileSwitchEvents`
      - `AppDataProvider` holds `switchEventSeqRef`, polls incremental events every 0.25s (busy) / 1s (idle); each event triggers:
          - immediate `globalMutate("getProfiles")` to refresh current profile
          - background refresh of proxies/providers/rules via `Promise.allSettled` (failures logged, non-blocking)
          - forced `mutateSwitchStatus` to correct state
      - original switchStatus effect calls `handleSwitchResult` as fallback; other toast/activation logic handled in profiles.tsx
    - Commands / API cleanup
      - removed `pub use profile_switch::*;` in cmd::mod.rs to avoid conflicts; frontend uses new command polling
    
    * refactor(frontend): optimize profile switch with optimistic updates
    
    * refactor(profile-switch): switch to event-driven flow with Profile Store
    
    - SwitchManager pushes events; frontend polls get_profile_switch_events
    - Zustand store handles optimistic profiles; AppDataProvider applies updates and background-fetches
    - UI flicker removed
    
    * fix(app-data): re-hook profile store updates during switch hydration
    
    * fix(notification): restore frontend event dispatch and non-blocking emits
    
    * fix(app-data-provider): restore proxy refresh and seed snapshot after refactor
    
    * fix: ensure switch completion events are received and handle proxies-updated
    
    * fix(app-data-provider): dedupe switch results by taskId and fix stale profile state
    
    * fix(profile-switch): ensure patch_profiles_config_by_profile_index waits for real completion and handle join failures in apply_config_with_timeout
    
    * docs: UPDATELOG.md
    
    * chore: add necessary comments
    
    * fix(core): always dispatch async proxy snapshot after RefreshClash event
    
    * fix(proxy-store, provider): handle pending snapshots and proxy profiles
    
    - Added pending snapshot tracking in proxy-store so `lastAppliedFetchId` no longer jumps on seed. Profile adoption is deferred until a qualifying fetch completes. Exposed `clearPendingProfile` for rollback support.
    - Cleared pending snapshot state whenever live payloads apply or the store resets, preventing stale optimistic profile IDs after failures.
    - In provider integration, subscribed to the pending proxy profile and fed it into target-profile derivation. Cleared it on failed switch results so hydration can advance and UI status remains accurate.
    
    * fix(proxy): re-hook tray refresh events into proxy refresh queue
    
    - Reattached listen("verge://refresh-proxy-config", …) at src/providers/app-data-provider.tsx:402 and registered it for cleanup.
    - Added matching window fallback handler at src/providers/app-data-provider.tsx:430 so in-app dispatches share the same refresh path.
    
    * fix(proxy-snapshot/proxy-groups): address review findings on snapshot placeholders
    
    - src/utils/proxy-snapshot.ts:72-95 now derives snapshot group members solely from proxy-groups.proxies, so provider ids under `use` no longer generate placeholder proxy items.
    - src/components/proxy/proxy-groups.tsx:665-677 lets the hydration overlay capture pointer events (and shows a wait cursor) so users can’t interact with snapshot-only placeholders before live data is ready.
    
    * fix(profile-switch): preserve queued requests and avoid stale connection teardown
    
    - Keep earlier queued switches intact by dropping the blanket “collapse” call: after removing duplicates for the same profile, new requests are simply appended, leaving other profiles pending (driver.rs:376). Resolves queue-loss scenario.
    - Gate connection cleanup on real successes so cancelled/stale runs no longer tear down Mihomo connections; success handler now skips close_connections_after_switch when success == false (workflow.rs:419).
    
    * fix(profile-switch, layout): improve profile validation and restore backend refresh
    
    - Hardened profile validation using `tokio::fs` with a 5s timeout and offloading YAML parsing to `AsyncHandler::spawn_blocking`, preventing slow disks or malformed files from freezing the runtime (src-tauri/src/cmd/profile_switch/validation.rs:9, 71).
    - Restored backend-triggered refresh handling by listening for `verge://refresh-clash-config` / `verge://refresh-verge-config` and invoking shared refresh services so SWR caches stay in sync with core events (src/pages/_layout/useLayoutEvents.ts:6, 45, 55).
    
    * feat(profile-switch): handle cancellations for superseded requests
    
    - Added a `cancelled` flag and constructor so superseded requests publish an explicit cancellation instead of a failure (src-tauri/src/cmd/profile_switch/state.rs:249, src-tauri/src/cmd/profile_switch/driver.rs:482)
    - Updated the profile switch effect to log cancellations as info, retain the shared `mutate` call, and skip emitting error toasts while still refreshing follow-up work (src/pages/profiles.tsx:554, src/pages/profiles.tsx:581)
    - Exposed the new flag on the TypeScript contract to keep downstream consumers type-safe (src/services/cmds.ts:20)
    
    * fix(profiles): wrap logging payload for Tauri frontend_log
    
    * fix(profile-switch): add rollback and error propagation for failed persistence
    
    - Added rollback on apply failure so Mihomo restores to the previous profile
      before exiting the success path early (state_machine.rs:474).
    - Reworked persist_profiles_with_timeout to surface timeout/join/save errors,
      convert them into CmdResult failures, and trigger rollback + error propagation
      when persistence fails (state_machine.rs:703).
    
    * fix(profile-switch): prevent mid-finalize reentrancy and lingering tasks
    
    * fix(profile-switch): preserve pending queue and surface discarded switches
    
    * fix(profile-switch): avoid draining Mihomo sockets on failed/cancelled switches
    
    * fix(app-data-provider): restore backend-driven refresh and reattach fallbacks
    
    * fix(profile-switch): queue concurrent updates and add bounded wait/backoff
    
    * fix(proxy): trigger live refresh on app start for proxy snapshot
    
    * refactor(profile-switch): split flow into layers and centralize async cleanup
    
    - Introduced `SwitchDriver` to encapsulate queue and driver logic while keeping the public Tauri command API.
    - Added workflow/cleanup helpers for notification dispatch and Mihomo connection draining, re-exported for API consistency.
    - Replaced monolithic state machine with `core.rs`, `context.rs`, and `stages.rs`, plus a thin `mod.rs` re-export layer; stage methods are now individually testable.
    - Removed legacy `workflow/state_machine.rs` and adjusted visibility on re-exported types/constants to ensure compilation.
  • chore(deps): update dependency react-i18next to v16.2.2 (#5251)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • add support x-oss-meta-subscription-userinfo (#5234)
    * add support x-oss-meta-subscription-userinfo
    
    * Update prfitem.rs
    
    match any subscription-userinfo
    
    * Update prfitem.rs
    
    改为 ends_with 更好
    
    * feat(config): enforce stricter header match for subscription usage
    
    ---------
    
    Co-authored-by: i18n <i18n.site@gmail.com>
    Co-authored-by: Slinetrac <realakayuki@gmail.com>
  • chore(deps): update npm dependencies (#5245)
    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
107 changed files with 3726 additions and 3016 deletions

View File

@@ -90,7 +90,7 @@ jobs:
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.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)
@@ -169,7 +169,8 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -197,6 +198,14 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -259,7 +268,8 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -272,6 +282,14 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -391,7 +409,8 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -404,6 +423,14 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -466,6 +493,43 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-updater-manifests:
name: Publish Updater Manifests
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: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install dependencies
run: pnpm i
- name: Publish updater manifests
run: pnpm updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish WebView2 updater manifests
run: pnpm updater-fixed-webview2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify-telegram:
name: Notify Telegram
runs-on: ubuntu-latest
@@ -475,6 +539,7 @@ jobs:
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
publish-updater-manifests,
]
steps:
- name: Checkout repository
@@ -538,7 +603,7 @@ jobs:
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.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)
@@ -551,7 +616,7 @@ jobs:
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)

View File

@@ -59,9 +59,10 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
save-if: false
cache-all-crates: false
shared-key: autobuild-shared
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -72,3 +73,10 @@ jobs:
- name: Run Clippy
working-directory: ./src-tauri
run: cargo clippy-all
- name: Run Logging Check
working-directory: ./src-tauri
shell: bash
run: |
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
clash-verge-logging-check

View File

@@ -27,6 +27,11 @@ if [ -n "$RUST_FILES" ]; then
(
cd src-tauri
cargo clippy-all
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
echo "[pre-commit] Installing clash-verge-logging-check..."
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
fi
clash-verge-logging-check
)
fi

View File

@@ -2,6 +2,10 @@
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
## Internationalization (i18n)
We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs.
## Development Setup
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:

View File

@@ -1,6 +1,45 @@
## v2.4.3
### ✨ 新增功能
感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
- 修复前端无法及时刷新操作状态
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复轻量模式托盘状态不同步
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- **Mihomo(Meta) 内核升级至 v1.19.15**
- 支持前端修改日志(最大文件大小、最大保留数量)
@@ -15,8 +54,11 @@
- 允许独立控制订阅自动更新
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
- 托盘 `打开目录` 中新增 `应用日志``内核日志`
</details>
### 🚀 优化改进
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
@@ -43,36 +85,10 @@
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
- 添加热键绑定错误的提示信息
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
</details>
## v2.4.2

81
docs/CONTRIBUTING_i18n.md Normal file
View File

@@ -0,0 +1,81 @@
# CONTRIBUTING — i18n
Thank you for considering contributing to our localization work — your help is appreciated.
Quick overview
- cvr-i18 is a CLI that helps manage simple top-level JSON locale files:
- Detect duplicated top-level keys
- Find keys missing versus a base file (default: en.json)
- Export missing entries for translators
- Reorder keys to match the base file for predictable diffs
- Operate on a directory or a single file
Get the CLI (No binary provided yet)
```bash
git clone https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
cd clash-verge-rev-i18n-cli
cargo install --path .
# or
cargo install --git https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
```
Common commands
- Show help: `cvr-i18`
- Directory (auto-detects `./locales` or `./src/locales`): `cvr-i18 -d /path/to/locales`
- Check duplicates: `cvr-i18 -k`
- Check missing keys: `cvr-i18 -m`
- Export missing keys: `cvr-i18 -m -e ./exports`
- Sort keys to base file: `cvr-i18 -s`
- Use a base file: `cvr-i18 -b base.json`
- Single file: `cvr-i18 -f locales/zh.json`
Options (short)
- `-d, --directory <DIR>`
- `-f, --file <FILE>`
- `-k, --duplicated-key`
- `-m, --missing-key`
- `-e, --export <DIR>`
- `-s, --sort`
- `-b, --base <FILE>`
Exit codes
- `0` — success (no issues)
- `1` — issues found (duplicates/missing)
- `2` — error (IO/parse/runtime)
How to contribute (recommended steps)
- Start small: fix typos, improve phrasing, or refine tone and consistency.
- Run the CLI against your locale files to detect duplicates or missing keys.
- Export starter JSONs for translators with `-m -e <DIR>`.
- Prefer incremental PRs or draft PRs; leave a comment on the issue if you want guidance.
- Open an issue to report missing strings, UI context, or localization bugs.
- Add or improve docs and tests to make future contributions easier.
PR checklist
- Keep JSON files UTF-8 encoded.
- Follow the repos locale file structure and naming conventions.
- Reorder keys to match the base file (`-s`) for minimal diffs.
- Test translations in a local dev build before opening a PR.
- Reference related issues and explain any context for translations or changes.
Notes
- The tool expects simple top-level JSON key/value maps.
- Exported JSONs are starter files for translators (fill in values, keep keys).
- Sorting keeps diffs consistent and reviewable.
Repository
https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
## Feedback & Contributions
- For tool usage issues or feedback: please open an Issue in the [repository](https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli) so it can be tracked and addressed.
- For localization contributions (translations, fixes, context notes, etc.): submit a PR or Issue in this repository and include examples, context, and testing instructions when possible.
- If you need help or a review, leave a comment on your submission requesting assistance.

View File

@@ -26,8 +26,8 @@
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint -c eslint.config.ts --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src",
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
@@ -43,7 +43,7 @@
"@mui/icons-material": "^7.3.4",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.4",
"@mui/x-data-grid": "^8.15.0",
"@mui/x-data-grid": "^8.16.0",
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
@@ -55,7 +55,7 @@
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.6",
"axios": "^1.13.1",
"dayjs": "1.11.18",
"dayjs": "1.11.19",
"foxact": "^0.2.49",
"i18next": "^25.6.0",
"js-yaml": "^4.1.0",
@@ -67,34 +67,33 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.65.0",
"react-i18next": "16.2.1",
"react-hook-form": "^7.66.0",
"react-i18next": "16.2.3",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router": "^7.9.4",
"react-router": "^7.9.5",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo",
"types-pac": "^1.0.3",
"zustand": "^5.0.8"
"types-pac": "^1.0.3"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.2.4",
"@eslint/js": "^9.38.0",
"@tauri-apps/cli": "2.9.1",
"@eslint-react/eslint-plugin": "^2.3.1",
"@eslint/js": "^9.39.0",
"@tauri-apps/cli": "2.9.2",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.9.2",
"@types/node": "^24.10.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.1.0",
"@vitejs/plugin-react-swc": "^4.2.0",
"adm-zip": "^0.5.16",
"cli-color": "^2.0.4",
"commander": "^14.0.2",
"cross-env": "^10.1.0",
"eslint": "^9.38.0",
"eslint": "^9.39.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
@@ -103,7 +102,7 @@
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^11.0.3",
"globals": "^16.4.0",
"globals": "^16.5.0",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"jiti": "^2.6.1",
@@ -111,19 +110,19 @@
"meta-json-schema": "^1.19.14",
"node-fetch": "^3.3.2",
"prettier": "^3.6.2",
"sass": "^1.93.2",
"tar": "^7.5.1",
"sass": "^1.93.3",
"tar": "^7.5.2",
"terser": "^5.44.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.12",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-monaco-editor-esm": "^2.0.2",
"vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.4"
"vitest": "^4.0.6"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"eslint --fix --max-warnings=0",
"prettier --write",
"git add"
],

931
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,6 +42,6 @@
"groupName": "github actions"
}
],
"postUpdateOptions": ["pnpmDedupe"],
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
"ignoreDeps": ["criterion"]
}

View File

@@ -5,12 +5,12 @@
* pnpm release-version <version>
*
* <version> can be:
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3-rc.1)
* - 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)
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3-autobuild.1022.r2+cc39b2)
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3-autobuild.1022.r2+a1b2c3d)
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3-deploytest.1022.r2+cc39b2)
*
* Examples:
* pnpm release-version 1.2.3
@@ -30,10 +30,12 @@
*/
import { execSync } from "child_process";
import { program } from "commander";
import fs from "fs/promises";
import process from "node:process";
import path from "path";
import { program } from "commander";
/**
* 获取当前 git 短 commit hash
* @returns {string}
@@ -73,41 +75,118 @@ function getLatestTauriCommit() {
}
/**
* 生成短时间戳格式MMDD或带 commit格式MMDD.cc39b27
* 使用 Asia/Shanghai 时区
* @param {boolean} withCommit 是否带 commit
* @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit仅当 withCommit 为 true 时有效)
* 获取 Asia/Shanghai 时区的日期片段
* @returns {string}
*/
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
function getLocalDatePart() {
const now = new Date();
const formatter = new Intl.DateTimeFormat("en-CA", {
const dateFormatter = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
});
const dateParts = Object.fromEntries(
dateFormatter.formatToParts(now).map((part) => [part.type, part.value]),
);
const parts = formatter.formatToParts(now);
const month = parts.find((part) => part.type === "month").value;
const day = parts.find((part) => part.type === "day").value;
const month = dateParts.month ?? "00";
const day = dateParts.day ?? "00";
if (withCommit) {
const gitShort = useTauriCommit
? getLatestTauriCommit()
: getGitShortCommit();
return `${month}${day}.${gitShort}`;
}
return `${month}${day}`;
}
/**
* 获取 GitHub Actions 运行编号(若存在)
* @returns {string|null}
*/
function getRunIdentifier() {
const runNumber = process.env.GITHUB_RUN_NUMBER;
if (runNumber && /^[0-9]+$/.test(runNumber)) {
const runNum = Number.parseInt(runNumber, 10);
if (!Number.isNaN(runNum)) {
const base = `r${runNum.toString(36)}`;
const attempt = process.env.GITHUB_RUN_ATTEMPT;
if (attempt && /^[0-9]+$/.test(attempt)) {
const attemptNumber = Number.parseInt(attempt, 10);
if (!Number.isNaN(attemptNumber) && attemptNumber > 1) {
return `${base}${attemptNumber.toString(36)}`;
}
}
return base;
}
}
const attempt = process.env.GITHUB_RUN_ATTEMPT;
if (attempt && /^[0-9]+$/.test(attempt)) {
const attemptNumber = Number.parseInt(attempt, 10);
if (!Number.isNaN(attemptNumber)) {
return `r${attemptNumber.toString(36)}`;
}
}
return null;
}
/**
* 生成用于自动构建类渠道的版本后缀
* @param {Object} options
* @param {boolean} [options.includeCommit=false]
* @param {"current"|"tauri"} [options.commitSource="current"]
* @param {boolean} [options.includeRun=true]
* @returns {string}
*/
function generateChannelSuffix({
includeCommit = false,
commitSource = "current",
includeRun = true,
} = {}) {
const segments = [];
const date = getLocalDatePart();
segments.push(date);
if (includeCommit) {
const commit =
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
segments.push(commit);
}
if (includeRun) {
const run = getRunIdentifier();
if (run) {
segments.push(run);
}
}
return segments.join(".");
}
/**
* 为 autobuild 渠道构建版本片段
* @param {Object} options
* @param {"current"|"tauri"} [options.commitSource="current"]
* @returns {{date: string, run: string, metadata: string}}
*/
function generateAutobuildComponents({ commitSource = "current" } = {}) {
const date = getLocalDatePart();
const run = getRunIdentifier() ?? `manual${Date.now().toString(36)}`;
const commitHash =
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
return {
date,
run,
metadata: commitHash || "nogit",
};
}
/**
* 验证版本号格式
* @param {string} version
* @returns {boolean}
*/
function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
version,
);
}
@@ -122,13 +201,14 @@ function normalizeVersion(version) {
}
/**
* 提取基础版本号(去掉所有 -tag+build 部分
* 提取基础版本号(去掉所有 pre-release 和 build metadata
* @param {string} version
* @returns {string}
*/
function getBaseVersion(version) {
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
const cleaned = version.startsWith("v") ? version.slice(1) : version;
const withoutBuild = cleaned.split("+")[0];
const [base] = withoutBuild.split("-");
return base;
}
@@ -273,17 +353,17 @@ async function main(versionArg) {
const baseVersion = getBaseVersion(currentVersion);
if (versionArg.toLowerCase() === "autobuild") {
// 格式: 2.3.0+autobuild.1004.cc39b27
// 使用 Tauri 相关的最新 commit hash
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
// 格式: 2.3.0-autobuild.1022.r2+cc39b2
const parts = generateAutobuildComponents({ commitSource: "tauri" });
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
} else if (versionArg.toLowerCase() === "autobuild-latest") {
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
const latestTauriCommit = getLatestTauriCommit();
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
// 格式: 2.3.0-autobuild.1022.r2+a1b2c3d (使用最新 Tauri 提交)
const parts = generateAutobuildComponents({ commitSource: "tauri" });
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
} else if (versionArg.toLowerCase() === "deploytest") {
// 格式: 2.3.0+deploytest.1004.cc39b27
// 使用 Tauri 相关的最新 commit hash
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
// 格式: 2.3.0-deploytest.1022.r2+cc39b2
const parts = generateAutobuildComponents({ commitSource: "tauri" });
newVersion = `${baseVersion}-deploytest.${parts.date}.${parts.run}+${parts.metadata}`;
} else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
}

View File

@@ -1,6 +1,6 @@
import axios from "axios";
import { readFileSync } from "fs";
import { log_success, log_error, log_info } from "./utils.mjs";
import { log_error, log_info, log_success } from "./utils.mjs";
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
const CHAT_ID_TEST = "@vergetest"; // 测试频道
@@ -71,6 +71,19 @@ async function sendTelegramNotification() {
.join("\n");
}
function normalizeDetailsTags(content) {
return content
.replace(
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
"\n<b>$1</b>\n",
)
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
.replace(/<\/?details>/g, "")
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
.replace(/<br\s*\/?>/g, "\n");
}
releaseContent = normalizeDetailsTags(releaseContent);
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";

View File

@@ -1,7 +1,105 @@
import fetch from "node-fetch";
import process from "node:process";
import { getOctokit, context } from "@actions/github";
import fetch from "node-fetch";
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
const SEMVER_REGEX =
/v?\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/g;
const STRICT_SEMVER_REGEX =
/^\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const stripLeadingV = (version) =>
typeof version === "string" && version.startsWith("v")
? version.slice(1)
: version;
const preferCandidate = (current, candidate) => {
if (!candidate) return current;
if (!current) return candidate;
const candidateHasPre = /[-+]/.test(candidate);
const currentHasPre = /[-+]/.test(current);
if (candidateHasPre && !currentHasPre) return candidate;
if (candidateHasPre === currentHasPre && candidate.length > current.length) {
return candidate;
}
return current;
};
const extractBestSemver = (input) => {
if (typeof input !== "string") return null;
const matches = input.match(SEMVER_REGEX);
if (!matches) return null;
return matches
.map(stripLeadingV)
.reduce((best, candidate) => preferCandidate(best, candidate), null);
};
const splitIdentifiers = (segment) =>
segment
.split(/[^0-9A-Za-z-]+/)
.map((part) => part.trim())
.filter(Boolean);
const sanitizeSuffix = (value, fallbackLabel) => {
if (!value) return fallbackLabel;
const trimmed = value.trim();
if (!trimmed) return fallbackLabel;
const [preRelease = "", metadata] = trimmed.split("+", 2);
const normalizedPre = splitIdentifiers(preRelease).join(".") || fallbackLabel;
const normalizedMeta = metadata ? splitIdentifiers(metadata).join(".") : "";
return normalizedMeta ? `${normalizedPre}+${normalizedMeta}` : normalizedPre;
};
const ensureSemverCompatibleVersion = (
version,
{ channel, releaseTag, fallbackLabel },
) => {
const trimmed = stripLeadingV(version ?? "").trim();
if (!trimmed) return null;
if (STRICT_SEMVER_REGEX.test(trimmed)) {
return trimmed;
}
if (channel === "autobuild") {
const normalizedSuffix = sanitizeSuffix(trimmed, fallbackLabel ?? channel);
const fallback = `0.0.0-${normalizedSuffix}`;
console.warn(
`[${channel}] Normalized non-semver version "${trimmed}" from release "${releaseTag}" to "${fallback}"`,
);
return fallback;
}
throw new Error(
`[${channel}] Derived version "${trimmed}" is not semver compatible for release "${releaseTag}"`,
);
};
const resolveReleaseVersion = (release) => {
const sources = [
release?.name,
release?.tag_name,
release?.body,
...(Array.isArray(release?.assets)
? release.assets.map((asset) => asset?.name)
: []),
];
return sources.reduce((best, source) => {
const candidate = extractBestSemver(source);
return preferCandidate(best, candidate);
}, null);
};
// Add stable update JSON filenames
const UPDATE_TAG_NAME = "updater";
const UPDATE_JSON_FILE = "update.json";
@@ -10,6 +108,11 @@ const UPDATE_JSON_PROXY = "update-proxy.json";
const ALPHA_TAG_NAME = "updater-alpha";
const ALPHA_UPDATE_JSON_FILE = "update.json";
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
// Add autobuild update JSON filenames
const AUTOBUILD_SOURCE_TAG_NAME = "autobuild";
const AUTOBUILD_TAG_NAME = "updater-autobuild";
const AUTOBUILD_UPDATE_JSON_FILE = "update.json";
const AUTOBUILD_UPDATE_JSON_PROXY = "update-proxy.json";
/// generate update.json
/// upload to update tag's release asset
@@ -48,12 +151,12 @@ async function resolveUpdater() {
// More flexible tag detection with regex patterns
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
// Get the latest stable tag and pre-release tag
// Get tags for known channels
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
const autobuildTag = tags.find((t) => t.name === AUTOBUILD_SOURCE_TAG_NAME);
console.log("All tags:", tags.map((t) => t.name).join(", "));
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
@@ -61,32 +164,106 @@ async function resolveUpdater() {
"Pre-release tag:",
preReleaseTag ? preReleaseTag.name : "None found",
);
console.log(
"Autobuild tag:",
autobuildTag ? autobuildTag.name : "None found",
);
console.log();
// Process stable release
if (stableTag) {
await processRelease(github, options, stableTag, false);
}
const channels = [
{
name: "stable",
tagName: stableTag?.name,
updateReleaseTag: UPDATE_TAG_NAME,
jsonFile: UPDATE_JSON_FILE,
proxyFile: UPDATE_JSON_PROXY,
prerelease: false,
},
{
name: "alpha",
tagName: preReleaseTag?.name,
updateReleaseTag: ALPHA_TAG_NAME,
jsonFile: ALPHA_UPDATE_JSON_FILE,
proxyFile: ALPHA_UPDATE_JSON_PROXY,
prerelease: true,
},
{
name: "autobuild",
tagName: autobuildTag?.name ?? AUTOBUILD_SOURCE_TAG_NAME,
updateReleaseTag: AUTOBUILD_TAG_NAME,
jsonFile: AUTOBUILD_UPDATE_JSON_FILE,
proxyFile: AUTOBUILD_UPDATE_JSON_PROXY,
prerelease: true,
},
];
// Process pre-release if found
if (preReleaseTag) {
await processRelease(github, options, preReleaseTag, true);
for (const channel of channels) {
if (!channel.tagName) {
console.log(`[${channel.name}] tag not found, skipping...`);
continue;
}
await processRelease(github, options, channel);
}
}
// Process a release (stable or alpha) and generate update files
async function processRelease(github, options, tag, isAlpha) {
if (!tag) return;
// Process a release and generate update files for the specified channel
async function processRelease(github, options, channelConfig) {
if (!channelConfig) return;
const {
tagName,
name: channelName,
updateReleaseTag,
jsonFile,
proxyFile,
prerelease,
} = channelConfig;
const channelLabel =
channelName.charAt(0).toUpperCase() + channelName.slice(1);
try {
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
tag: tagName,
});
const releaseTagName = release.tag_name ?? tagName;
const resolvedVersion = resolveReleaseVersion(release);
if (!resolvedVersion) {
throw new Error(
`[${channelName}] Failed to determine semver version from release "${releaseTagName}"`,
);
}
console.log(
`[${channelName}] Preparing update metadata from release "${releaseTagName}"`,
);
console.log(
`[${channelName}] Resolved release version: ${resolvedVersion}`,
);
const semverCompatibleVersion = ensureSemverCompatibleVersion(
resolvedVersion,
{
channel: channelName,
releaseTag: releaseTagName,
fallbackLabel: channelName,
},
);
if (semverCompatibleVersion !== resolvedVersion) {
console.log(
`[${channelName}] Normalized updater version: ${semverCompatibleVersion}`,
);
}
const updateData = {
name: tag.name,
notes: await resolveUpdateLog(tag.name).catch(() =>
version: semverCompatibleVersion,
original_version: resolvedVersion,
tag_name: releaseTagName,
notes: await resolveUpdateLog(releaseTagName).catch(() =>
resolveUpdateLogDefault().catch(() => "No changelog available"),
),
pub_date: new Date().toISOString(),
@@ -186,13 +363,15 @@ async function processRelease(github, options, tag, isAlpha) {
});
await Promise.allSettled(promises);
console.log(updateData);
console.log(`[${channelName}] Update data snapshot:`, updateData);
// maybe should test the signature as well
// delete the null field
Object.entries(updateData.platforms).forEach(([key, value]) => {
if (!value.url) {
console.log(`[Error]: failed to parse release for "${key}"`);
console.log(
`[${channelName}] [Error]: failed to parse release for "${key}"`,
);
delete updateData.platforms[key];
}
});
@@ -205,15 +384,14 @@ async function processRelease(github, options, tag, isAlpha) {
updateDataNew.platforms[key].url =
"https://download.clashverge.dev/" + value.url;
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
console.log(
`[${channelName}] [Error]: updateDataNew.platforms.${key} is null`,
);
}
});
// Get the appropriate updater release based on isAlpha flag
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
console.log(
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
releaseTag,
`[${channelName}] Processing update release target "${updateReleaseTag}"`,
);
try {
@@ -223,30 +401,28 @@ async function processRelease(github, options, tag, isAlpha) {
// Try to get the existing release
const response = await github.rest.repos.getReleaseByTag({
...options,
tag: releaseTag,
tag: updateReleaseTag,
});
updateRelease = response.data;
console.log(
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
`[${channelName}] Found existing ${updateReleaseTag} release with ID: ${updateRelease.id}`,
);
} catch (error) {
// If release doesn't exist, create it
if (error.status === 404) {
console.log(
`Release with tag ${releaseTag} not found, creating new release...`,
`[${channelName}] Release with tag ${updateReleaseTag} not found, creating new release...`,
);
const createResponse = await github.rest.repos.createRelease({
...options,
tag_name: releaseTag,
name: isAlpha
? "Auto-update Alpha Channel"
: "Auto-update Stable Channel",
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
prerelease: isAlpha,
tag_name: updateReleaseTag,
name: `Auto-update ${channelLabel} Channel`,
body: `This release contains the update information for the ${channelName} channel.`,
prerelease,
});
updateRelease = createResponse.data;
console.log(
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
`[${channelName}] Created new ${updateReleaseTag} release with ID: ${updateRelease.id}`,
);
} else {
// If it's another error, throw it
@@ -255,11 +431,8 @@ async function processRelease(github, options, tag, isAlpha) {
}
// File names based on release type
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
// Delete existing assets with these names
for (let asset of updateRelease.assets) {
for (const asset of updateRelease.assets) {
if (asset.name === jsonFile) {
await github.rest.repos.deleteReleaseAsset({
...options,
@@ -270,7 +443,12 @@ async function processRelease(github, options, tag, isAlpha) {
if (asset.name === proxyFile) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error); // do not break the pipeline
.catch((deleteError) =>
console.error(
`[${channelName}] Failed to delete existing proxy asset:`,
deleteError.message,
),
); // do not break the pipeline
}
}
@@ -290,20 +468,22 @@ async function processRelease(github, options, tag, isAlpha) {
});
console.log(
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
`[${channelName}] Successfully uploaded update files to ${updateReleaseTag}`,
);
} catch (error) {
console.error(
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
`[${channelName}] Failed to process update release:`,
error.message,
);
}
} catch (error) {
if (error.status === 404) {
console.log(`Release not found for tag: ${tag.name}, skipping...`);
console.log(
`[${channelName}] Release not found for tag: ${tagName}, skipping...`,
);
} else {
console.error(
`Failed to get release for tag: ${tag.name}`,
`[${channelName}] Failed to get release for tag: ${tagName}`,
error.message,
);
}

412
src-tauri/Cargo.lock generated
View File

@@ -58,9 +58,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
@@ -152,6 +152,12 @@ dependencies = [
"x11rb",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arraydeque"
version = "0.5.1"
@@ -479,7 +485,7 @@ dependencies = [
"http-body 0.4.6",
"hyper 0.14.32",
"itoa",
"matchit",
"matchit 0.7.3",
"memchr",
"mime",
"percent-encoding",
@@ -494,25 +500,23 @@ dependencies = [
[[package]]
name = "axum"
version = "0.7.9"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
dependencies = [
"async-trait",
"axum-core 0.4.5",
"axum-core 0.5.5",
"bytes",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"itoa",
"matchit",
"matchit 0.8.4",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_core",
"sync_wrapper 1.0.2",
"tower 0.5.2",
"tower-layer",
@@ -538,19 +542,17 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.4.5"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"futures-core",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 1.0.2",
"tower-layer",
"tower-service",
@@ -1068,18 +1070,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.50"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.50"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
dependencies = [
"anstyle",
"clap_lex",
@@ -1097,6 +1099,7 @@ version = "2.4.3"
dependencies = [
"aes-gcm",
"anyhow",
"arc-swap",
"async-trait",
"backoff",
"base64 0.22.1",
@@ -1107,17 +1110,13 @@ dependencies = [
"compact_str",
"console-subscriber",
"criterion",
"dashmap 6.1.0",
"deelevate",
"delay_timer",
"dirs 6.0.0",
"dunce",
"flexi_logger",
"futures",
"gethostname",
"getrandom 0.3.4",
"hex",
"hmac",
"isahc",
"libc",
"log",
@@ -1136,7 +1135,6 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml_ng",
"sha2 0.10.9",
"signal-hook 0.3.18",
"smartstring",
"sys-locale",
@@ -1160,6 +1158,7 @@ dependencies = [
"tauri-plugin-window-state",
"tokio",
"tokio-stream",
"url",
"users",
"warp",
"winapi",
@@ -1183,8 +1182,8 @@ dependencies = [
[[package]]
name = "clash_verge_service_ipc"
version = "2.0.20"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#c0b6e99da27e7956047d42aee104f5c33083c970"
version = "2.0.21"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#1e34c648e48f8580208ff777686092e0a94b8025"
dependencies = [
"anyhow",
"compact_str",
@@ -1294,22 +1293,23 @@ dependencies = [
[[package]]
name = "console-api"
version = "0.8.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857"
checksum = "e8599749b6667e2f0c910c1d0dff6901163ff698a52d5a39720f61b5be4b20d3"
dependencies = [
"futures-core",
"prost 0.13.5",
"prost-types 0.13.5",
"tonic 0.12.3",
"prost 0.14.1",
"prost-types 0.14.1",
"tonic 0.14.2",
"tonic-prost",
"tracing-core",
]
[[package]]
name = "console-subscriber"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01"
checksum = "fb4915b7d8dd960457a1b6c380114c2944f728e7c65294ab247ae6b6f1f37592"
dependencies = [
"console-api",
"crossbeam-channel",
@@ -1318,14 +1318,14 @@ dependencies = [
"hdrhistogram",
"humantime",
"hyper-util",
"prost 0.13.5",
"prost-types 0.13.5",
"prost 0.14.1",
"prost-types 0.14.1",
"serde",
"serde_json",
"thread_local",
"tokio",
"tokio-stream",
"tonic 0.12.3",
"tonic 0.14.2",
"tracing",
"tracing-core",
"tracing-subscriber",
@@ -3376,7 +3376,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
"windows-core 0.62.2",
]
[[package]]
@@ -3413,12 +3413,13 @@ dependencies = [
[[package]]
name = "icu_locale_core"
version = "2.0.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
dependencies = [
"displaydoc",
"litemap",
"serde",
"tinystr",
"writeable",
"zerovec",
@@ -3426,9 +3427,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc"
dependencies = [
"displaydoc",
"icu_collections",
@@ -3449,9 +3450,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4"
dependencies = [
"displaydoc",
"icu_collections",
@@ -3471,14 +3472,14 @@ checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
version = "2.0.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
dependencies = [
"displaydoc",
"icu_locale_core",
"serde",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
@@ -3792,9 +3793,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.81"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -3987,9 +3988,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
@@ -4135,6 +4136,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
@@ -4226,9 +4233,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.7.7"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40"
checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6"
dependencies = [
"num-traits",
"pxfm",
@@ -5503,9 +5510,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [
"zerovec",
]
@@ -5617,12 +5624,12 @@ dependencies = [
[[package]]
name = "prost"
version = "0.13.5"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
dependencies = [
"bytes",
"prost-derive 0.13.5",
"prost-derive 0.14.1",
]
[[package]]
@@ -5640,9 +5647,9 @@ dependencies = [
[[package]]
name = "prost-derive"
version = "0.13.5"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -5662,11 +5669,11 @@ dependencies = [
[[package]]
name = "prost-types"
version = "0.13.5"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
dependencies = [
"prost 0.13.5",
"prost 0.14.1",
]
[[package]]
@@ -6050,11 +6057,11 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "regress"
version = "0.10.4"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145bb27393fe455dd64d6cbc8d059adfa392590a45eadf079c01b11857e7b010"
checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48"
dependencies = [
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"memchr",
]
@@ -6315,9 +6322,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [
"web-time",
"zeroize",
@@ -6325,9 +6332,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.7"
version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
"ring",
"rustls-pki-types",
@@ -7137,16 +7144,16 @@ dependencies = [
[[package]]
name = "sysproxy"
version = "0.3.0"
source = "git+https://github.com/clash-verge-rev/sysproxy-rs#9fe61ca25dc5808cb6d7f13ae73a7a250ab56173"
version = "0.3.1"
source = "git+https://github.com/clash-verge-rev/sysproxy-rs#50100ab03eb802056c381f3c5009e903c67e3bac"
dependencies = [
"interfaces",
"iptools",
"log",
"thiserror 1.0.69",
"thiserror 2.0.17",
"url",
"windows 0.58.0",
"winreg 0.52.0",
"windows 0.62.2",
"winreg 0.55.0",
"xdg",
]
@@ -7990,11 +7997,12 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
dependencies = [
"displaydoc",
"serde_core",
"zerovec",
]
@@ -8243,13 +8251,12 @@ dependencies = [
[[package]]
name = "tonic"
version = "0.12.3"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
dependencies = [
"async-stream",
"async-trait",
"axum 0.7.9",
"axum 0.8.6",
"base64 0.22.1",
"bytes",
"h2 0.4.12",
@@ -8261,11 +8268,11 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
"prost 0.13.5",
"socket2 0.5.10",
"socket2 0.6.1",
"sync_wrapper 1.0.2",
"tokio",
"tokio-stream",
"tower 0.4.13",
"tower 0.5.2",
"tower-layer",
"tower-service",
"tracing",
@@ -8284,6 +8291,17 @@ dependencies = [
"tonic 0.10.2",
]
[[package]]
name = "tonic-prost"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67"
dependencies = [
"bytes",
"prost 0.14.1",
"tonic 0.14.2",
]
[[package]]
name = "tonic-web"
version = "0.10.2"
@@ -8332,11 +8350,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.12.0",
"pin-project-lite",
"slab",
"sync_wrapper 1.0.2",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -8616,9 +8638,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.20"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-segmentation"
@@ -8854,9 +8876,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.104"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
dependencies = [
"cfg-if",
"once_cell",
@@ -8865,25 +8887,11 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.108",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.54"
version = "0.4.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
dependencies = [
"cfg-if",
"js-sys",
@@ -8894,9 +8902,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.104"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -8904,22 +8912,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.104"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.108",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.104"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
dependencies = [
"unicode-ident",
]
@@ -9012,9 +9020,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.81"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -9093,8 +9101,8 @@ dependencies = [
"webview2-com-sys",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-implement",
"windows-interface",
]
[[package]]
@@ -9189,27 +9197,29 @@ dependencies = [
"windows-version",
]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-collections 0.2.0",
"windows-core 0.61.2",
"windows-future",
"windows-future 0.2.1",
"windows-link 0.1.3",
"windows-numerics",
"windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
]
[[package]]
@@ -9222,16 +9232,12 @@ dependencies = [
]
[[package]]
name = "windows-core"
version = "0.58.0"
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
"windows-core 0.62.2",
]
[[package]]
@@ -9240,13 +9246,26 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-future"
version = "0.2.1"
@@ -9255,18 +9274,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading",
"windows-threading 0.1.0",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
]
[[package]]
@@ -9280,17 +9299,6 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
@@ -9324,6 +9332,16 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]]
name = "windows-registry"
version = "0.5.3"
@@ -9335,15 +9353,6 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -9354,13 +9363,12 @@ dependencies = [
]
[[package]]
name = "windows-strings"
version = "0.1.0"
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
"windows-link 0.2.1",
]
[[package]]
@@ -9372,6 +9380,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -9498,6 +9515,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-version"
version = "0.1.7"
@@ -9714,16 +9740,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.55.0"
@@ -9767,9 +9783,9 @@ checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "wry"
@@ -9867,9 +9883,9 @@ dependencies = [
[[package]]
name = "xdg"
version = "2.5.2"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
[[package]]
name = "xkeysym"
@@ -9879,9 +9895,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xml-rs"
version = "0.8.27"
version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]]
name = "xsum"
@@ -9891,11 +9907,10 @@ checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db"
[[package]]
name = "yoke"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
@@ -9903,9 +9918,9 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
@@ -10038,9 +10053,9 @@ dependencies = [
[[package]]
name = "zerotrie"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
dependencies = [
"displaydoc",
"yoke",
@@ -10049,10 +10064,11 @@ dependencies = [
[[package]]
name = "zerovec"
version = "0.11.4"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
dependencies = [
"serde",
"yoke",
"zerofrom",
"zerovec-derive",
@@ -10060,9 +10076,9 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
@@ -10116,9 +10132,9 @@ checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
[[package]]
name = "zopfli"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",

View File

@@ -18,7 +18,6 @@ tauri-build = { version = "2.5.1", features = [] }
[dependencies]
warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.100"
dirs = "6.0"
open = "5.3.2"
log = "0.4.28"
dunce = "1.0.5"
@@ -67,11 +66,7 @@ futures = "0.3.31"
sys-locale = "0.3.2"
libc = "0.2.177"
gethostname = "1.1.0"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.3"
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
@@ -82,15 +77,17 @@ backoff = { version = "0.4.0", features = ["tokio"] }
compact_str = { version = "0.9.0", features = ["serde"] }
tauri-plugin-http = "2.5.4"
flexi_logger = "0.31.7"
console-subscriber = { version = "0.4.1", optional = true }
console-subscriber = { version = "0.5.0", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
smartstring = { version = "1.0.1", features = ["serde"] }
clash_verge_service_ipc = { version = "2.0.20", features = [
clash_verge_service_ipc = { version = "2.0.21", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.7.1"
url = "2.5.4"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"

View File

@@ -3,7 +3,6 @@ use std::hint::black_box;
use std::process;
use tokio::runtime::Runtime;
// 引入业务模型 & Draft 实现
use app_lib::config::IVerge;
use app_lib::utils::Draft as DraftNew;
@@ -17,108 +16,86 @@ fn make_draft() -> DraftNew<Box<IVerge>> {
DraftNew::from(verge)
}
/// 基准:只读 data_ref正式数据
fn bench_data_ref(c: &mut Criterion) {
c.bench_function("draft_data_ref", |b| {
b.iter(|| {
let draft = make_draft();
let data = draft.data_ref();
black_box(data.enable_auto_launch);
});
});
}
/// 基准:可写 data_mut正式数据
fn bench_data_mut(c: &mut Criterion) {
c.bench_function("draft_data_mut", |b| {
b.iter(|| {
let draft = make_draft();
let mut data = draft.data_mut();
data.enable_tun_mode = Some(true);
black_box(data.enable_tun_mode);
});
});
}
/// 基准:首次创建草稿(会触发 clone
fn bench_draft_mut_first(c: &mut Criterion) {
c.bench_function("draft_draft_mut_first", |b| {
b.iter(|| {
let draft = make_draft();
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
black_box(d.enable_auto_launch);
});
});
}
/// 基准:重复 draft_mut已存在草稿不再 clone
fn bench_draft_mut_existing(c: &mut Criterion) {
c.bench_function("draft_draft_mut_existing", |b| {
b.iter(|| {
let draft = make_draft();
{
let mut first = draft.draft_mut();
first.enable_tun_mode = Some(true);
}
let mut second = draft.draft_mut();
second.enable_tun_mode = Some(false);
black_box(second.enable_tun_mode);
});
});
}
/// 基准零拷贝读取最新视图latest_ref
fn bench_latest_ref(c: &mut Criterion) {
c.bench_function("draft_latest_ref", |b| {
b.iter(|| {
let draft = make_draft();
let latest = draft.latest_ref();
black_box(latest.enable_auto_launch);
});
});
}
/// 基准apply提交草稿
fn bench_apply(c: &mut Criterion) {
c.bench_function("draft_apply", |b| {
b.iter(|| {
let draft = make_draft();
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.apply();
});
});
}
/// 基准discard丢弃草稿
fn bench_discard(c: &mut Criterion) {
c.bench_function("draft_discard", |b| {
b.iter(|| {
let draft = make_draft();
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.discard();
});
});
}
/// 基准:异步 with_data_modify
fn bench_with_data_modify(c: &mut Criterion) {
let rt = Runtime::new().unwrap_or_else(|error| {
eprintln!("draft benchmarks require a Tokio runtime: {error}");
pub fn bench_draft(c: &mut Criterion) {
let rt = Runtime::new().unwrap_or_else(|e| {
eprintln!("Tokio runtime init failed: {e}");
process::exit(1);
});
c.bench_function("draft_with_data_modify", |b| {
let mut group = c.benchmark_group("draft");
group.sample_size(100);
group.warm_up_time(std::time::Duration::from_millis(300));
group.measurement_time(std::time::Duration::from_secs(1));
group.bench_function("data_mut", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let mut data = draft.data_mut();
data.enable_tun_mode = Some(true);
black_box(&data.enable_tun_mode);
});
});
group.bench_function("draft_mut_first", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
black_box(&d.enable_auto_launch);
});
});
group.bench_function("draft_mut_existing", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
let mut first = draft.draft_mut();
first.enable_tun_mode = Some(true);
black_box(&first.enable_tun_mode);
}
let mut second = draft.draft_mut();
second.enable_tun_mode = Some(false);
black_box(&second.enable_tun_mode);
});
});
group.bench_function("latest_ref", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let latest = draft.latest_ref();
black_box(&latest.enable_auto_launch);
});
});
group.bench_function("apply", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
draft.apply();
black_box(&draft);
});
});
group.bench_function("discard", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
draft.discard();
black_box(&draft);
});
});
group.bench_function("with_data_modify_async", |b| {
b.to_async(&rt).iter(|| async {
let draft = make_draft();
let _res: Result<(), anyhow::Error> = draft
.with_data_modify(|mut box_data| async move {
let draft = black_box(make_draft());
let _: Result<(), anyhow::Error> = draft
.with_data_modify::<_, _, _, anyhow::Error>(|mut box_data| async move {
box_data.enable_auto_launch =
Some(!box_data.enable_auto_launch.unwrap_or(false));
Ok((box_data, ()))
@@ -126,17 +103,9 @@ fn bench_with_data_modify(c: &mut Criterion) {
.await;
});
});
group.finish();
}
criterion_group!(
benches,
bench_data_ref,
bench_data_mut,
bench_draft_mut_first,
bench_draft_mut_existing,
bench_latest_ref,
bench_apply,
bench_discard,
bench_with_data_modify
);
criterion_group!(benches, bench_draft);
criterion_main!(benches);

View File

@@ -12,6 +12,7 @@ use smartstring::alias::String;
use std::path::Path;
use tauri::{AppHandle, Manager};
use tokio::fs;
use tokio::io::AsyncWriteExt;
/// 打开应用程序所在目录
#[tauri::command]
@@ -41,6 +42,20 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
open::that(url.as_str()).stringify_err()
}
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
/// 打开 Verge 最新日志
#[tauri::command]
pub async fn open_app_log() -> CmdResult<()> {
open::that(dirs::app_latest_log().stringify_err()?).stringify_err()
}
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
/// 打开 Clash 最新日志
#[tauri::command]
pub async fn open_core_log() -> CmdResult<()> {
open::that(dirs::clash_latest_log().stringify_err()?).stringify_err()
}
/// 打开/关闭开发者工具
#[tauri::command]
pub fn open_devtools(app_handle: AppHandle) {
@@ -102,7 +117,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
}
if !icon_cache_dir.exists() {
let _ = std::fs::create_dir_all(&icon_cache_dir);
let _ = fs::create_dir_all(&icon_cache_dir).await;
}
let temp_path = icon_cache_dir.join(format!("{}.downloading", name.as_str()));
@@ -126,7 +141,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
if is_image && !is_html {
{
let mut file = match std::fs::File::create(&temp_path) {
let mut file = match fs::File::create(&temp_path).await {
Ok(file) => file,
Err(_) => {
if icon_path.exists() {
@@ -135,12 +150,12 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
return Err("Failed to create temporary file".into());
}
};
std::io::copy(&mut content.as_ref(), &mut file).stringify_err()?;
file.write_all(content.as_ref()).await.stringify_err()?;
file.flush().await.stringify_err()?;
}
if !icon_path.exists() {
match std::fs::rename(&temp_path, &icon_path) {
match fs::rename(&temp_path, &icon_path).await {
Ok(_) => {}
Err(_) => {
let _ = temp_path.remove_if_exists().await;
@@ -226,7 +241,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
/// 通知UI已准备就绪
#[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> {
log::info!(target: "app", "前端UI已准备就绪");
logging!(info, Type::Cmd, "前端UI已准备就绪");
crate::utils::resolve::ui::mark_ui_ready();
Ok(())
}
@@ -234,7 +249,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
/// UI加载阶段
#[tauri::command]
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
log::info!(target: "app", "UI加载阶段更新: {}", stage.as_str());
logging!(info, Type::Cmd, "UI加载阶段更新: {}", stage.as_str());
use crate::utils::resolve::ui::UiReadyStage;
@@ -245,7 +260,12 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready,
_ => {
log::warn!(target: "app", "未知的UI加载阶段: {}", stage.as_str());
logging!(
warn,
Type::Cmd,
"Warning: 未知的UI加载阶段: {}",
stage.as_str()
);
return Err(format!("未知的UI加载阶段: {}", stage.as_str()).into());
}
};

View File

@@ -11,8 +11,8 @@ pub async fn create_local_backup() -> CmdResult<()> {
/// List local backups
#[tauri::command]
pub fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
feat::list_local_backup().stringify_err()
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
feat::list_local_backup().await.stringify_err()
}
/// Delete local backup
@@ -29,6 +29,8 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
/// Export local backup to a user selected destination
#[tauri::command]
pub fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
feat::export_local_backup(filename, destination).stringify_err()
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
feat::export_local_backup(filename, destination)
.await
.stringify_err()
}

View File

@@ -1,13 +1,16 @@
use super::CmdResult;
use crate::utils::dirs;
use crate::{
cmd::StringifyErr,
config::Config,
constants,
core::{CoreManager, handle, validate::CoreConfigValidator},
};
use crate::{config::*, feat, logging, utils::logging::Type};
use compact_str::CompactString;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
/// 复制Clash环境变量
#[tauri::command]
@@ -40,10 +43,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global()
.change_core(Some(clash_core.clone()))
.await
{
match CoreManager::global().change_core(&clash_core).await {
Ok(_) => {
// 切换内核后重启内核
match CoreManager::global().restart_core().await {
@@ -111,7 +111,7 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
log::error!(target: "app", "{}", e);
logging!(error, Type::Cmd, "{}", e);
10000u32
}
};
@@ -128,7 +128,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
// 获取DNS配置文件路径
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join("dns_config.yaml");
.join(constants::files::DNS_CONFIG);
// 保存DNS配置到文件
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
@@ -151,18 +151,16 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join("dns_config.yaml");
.join(constants::files::DNS_CONFIG);
if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found");
return Err("DNS config file not found".into());
}
let dns_yaml = tokio::fs::read_to_string(&dns_path)
.await
.stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
})?;
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
})?;
// 解析DNS配置
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
@@ -231,7 +229,7 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join("dns_config.yaml");
.join(constants::files::DNS_CONFIG);
Ok(dns_path.exists())
}
@@ -244,7 +242,7 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join("dns_config.yaml");
.join(constants::files::DNS_CONFIG);
if !fs::try_exists(&dns_path).await.stringify_err()? {
return Err("DNS config file not found".into());
@@ -257,10 +255,8 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
/// 验证DNS配置文件
#[tauri::command]
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
use crate::utils::dirs;
let app_dir = dirs::app_home_dir().stringify_err()?;
let dns_path = app_dir.join("dns_config.yaml");
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
let dns_path_str = dns_path.to_str().unwrap_or_default();
if !dns_path.exists() {

View File

@@ -16,6 +16,7 @@ pub mod runtime;
pub mod save_profile;
pub mod service;
pub mod system;
pub mod updater;
pub mod uwp;
pub mod validate;
pub mod verge;
@@ -34,6 +35,7 @@ pub use runtime::*;
pub use save_profile::*;
pub use service::*;
pub use system::*;
pub use updater::*;
pub use uwp::*;
pub use validate::*;
pub use verge::*;

View File

@@ -2,13 +2,14 @@ use super::CmdResult;
use crate::cmd::StringifyErr;
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use network_interface::NetworkInterface;
use serde_yaml_ng::Mapping;
/// get the system proxy
#[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
log::debug!(target: "app", "异步获取系统代理配置");
logging!(debug, Type::Network, "异步获取系统代理配置");
let current = AsyncProxyQuery::get_system_proxy().await;
@@ -20,14 +21,21 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
);
map.insert("bypass".into(), current.bypass.into());
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
logging!(
debug,
Type::Network,
"返回系统代理配置: enable={}, {}:{}",
current.enable,
current.host,
current.port
);
Ok(map)
}
/// 获取自动代理配置
#[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
let proxy_manager = EventDrivenProxyManager::global();
@@ -41,7 +49,13 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
map.insert("enable".into(), current.enable.into());
map.insert("url".into(), current.url.clone().into());
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
logging!(
debug,
Type::Network,
"返回自动代理配置(缓存): enable={}, url={}",
current.enable,
current.url
);
Ok(map)
}

View File

@@ -15,68 +15,19 @@ use crate::{
ret_err,
utils::{dirs, help, logging::Type},
};
use scopeguard::defer;
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
// 全局请求序列号跟踪,用于避免队列化执行
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
#[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> {
// 策略1: 尝试快速获取latest数据
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
let profiles = Config::profiles().await;
let latest = profiles.latest_ref();
IProfiles {
current: latest.current.clone(),
items: latest.items.clone(),
}
})
.await;
match latest_result {
Ok(profiles) => {
logging!(info, Type::Cmd, "快速获取配置列表成功");
return Ok(profiles);
}
Err(_) => {
logging!(warn, Type::Cmd, "快速获取配置超时(500ms)");
}
}
// 策略2: 如果快速获取失败尝试获取data()
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
let profiles = Config::profiles().await;
let data = profiles.latest_ref();
IProfiles {
current: data.current.clone(),
items: data.items.clone(),
}
})
.await;
match data_result {
Ok(profiles) => {
logging!(info, Type::Cmd, "获取draft配置列表成功");
return Ok(profiles);
}
Err(join_err) => {
logging!(
error,
Type::Cmd,
"获取draft配置任务失败或超时: {}",
join_err
);
}
}
// 策略3: fallback尝试重新创建配置
logging!(warn, Type::Cmd, "所有获取配置策略都失败尝试fallback");
Ok(IProfiles::new().await)
logging!(debug, Type::Cmd, "获取配置文件列表");
let draft = Config::profiles().await;
let latest = draft.latest_ref();
Ok((**latest).clone())
}
/// 增强配置文件
@@ -85,7 +36,7 @@ pub async fn enhance_profiles() -> CmdResult {
match feat::enhance_profiles().await {
Ok(_) => {}
Err(e) => {
log::error!(target: "app", "{}", e);
logging!(error, Type::Cmd, "{}", e);
return Err(e.to_string().into());
}
}
@@ -99,7 +50,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
// 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹
let item = match PrfItem::from_url(&url, None, None, option).await {
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
Ok(it) => {
logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置");
it
@@ -110,7 +61,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
}
};
match profiles_append_item_safe(item.clone()).await {
match profiles_append_item_safe(item).await {
Ok(_) => match profiles_save_file_safe().await {
Ok(_) => {
logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功");
@@ -145,13 +96,13 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
/// 调整profile的顺序
#[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
match profiles_reorder_safe(active_id, over_id).await {
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
log::info!(target: "app", "重新排序配置文件");
logging!(info, Type::Cmd, "重新排序配置文件");
Ok(())
}
Err(err) => {
log::error!(target: "app", "重新排序配置文件失败: {}", err);
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
Err(format!("重新排序配置文件失败: {}", err).into())
}
}
@@ -161,7 +112,7 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
/// 创建一个新的配置文件
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
match profiles_append_item_with_filedata_safe(item.clone(), file_data).await {
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
// 发送配置变更通知
if let Some(uid) = &item.uid {
@@ -180,10 +131,10 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
/// 更新配置文件
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
match feat::update_profile(index, option, Some(true)).await {
match feat::update_profile(&index, option.as_ref(), true, true).await {
Ok(_) => Ok(()),
Err(e) => {
log::error!(target: "app", "{}", e);
logging!(error, Type::Cmd, "{}", e);
Err(e.to_string().into())
}
}
@@ -194,9 +145,7 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
pub async fn delete_profile(index: String) -> CmdResult {
println!("delete_profile: {}", index);
// 使用Send-safe helper函数
let should_update = profiles_delete_item_safe(index.clone())
.await
.stringify_err()?;
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
profiles_save_file_safe().await.stringify_err()?;
if should_update {
@@ -208,7 +157,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
handle::Handle::notify_profile_changed(index);
}
Err(e) => {
log::error!(target: "app", "{}", e);
logging!(error, Type::Cmd, "{}", e);
return Err(e.to_string().into());
}
}
@@ -334,65 +283,39 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
Config::profiles()
.await
.draft_mut()
.patch_config(restore_profiles)
.patch_config(&restore_profiles)
.stringify_err()?;
Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
logging!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}");
}
});
logging!(info, Type::Cmd, "成功恢复到之前的配置");
Ok(())
}
async fn handle_success(current_sequence: u64, current_value: Option<String>) -> CmdResult<bool> {
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
logging!(
info,
Type::Cmd,
"配置更新成功,序列号: {}",
current_sequence
);
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
Config::profiles().await.apply();
handle::Handle::refresh_clash();
if let Err(e) = Tray::global().update_tooltip().await {
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
}
if let Err(e) = Tray::global().update_menu().await {
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
}
if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存配置文件失败: {e}");
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
}
if let Some(current) = &current_value {
logging!(
info,
Type::Cmd,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,);
handle::Handle::notify_profile_changed(current.clone());
}
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(true)
}
@@ -406,53 +329,31 @@ async fn handle_validation_failure(
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::error", error_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_update_error<E: std::fmt::Display>(e: E, current_sequence: u64) -> CmdResult<bool> {
logging!(
warn,
Type::Cmd,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
Config::profiles().await.discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_timeout(current_profile: Option<String>, current_sequence: u64) -> CmdResult<bool> {
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(
error,
Type::Cmd,
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
logging!(error, Type::Cmd, "{}", timeout_msg);
Config::profiles().await.discard();
if let Some(prev_profile) = current_profile {
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn perform_config_update(
current_sequence: u64,
current_value: Option<String>,
current_profile: Option<String>,
) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
"开始内核配置更新,序列号: {}",
current_sequence
);
let update_result = tokio::time::timeout(
Duration::from_secs(30),
CoreManager::global().update_config(),
@@ -460,46 +361,36 @@ async fn perform_config_update(
.await;
match update_result {
Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await,
Ok(Ok((true, _))) => handle_success(current_value).await,
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
Ok(Err(e)) => handle_update_error(e, current_sequence).await,
Err(_) => handle_timeout(current_profile, current_sequence).await,
Ok(Err(e)) => handle_update_error(e).await,
Err(_) => handle_timeout(current_profile).await,
}
}
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
if CURRENT_SWITCHING_PROFILE
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(false);
return Err("switch_in_progress".into());
}
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
// 为当前请求分配序列号
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let target_profile = profiles.current.clone();
logging!(
info,
Type::Cmd,
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence,
"开始修改配置文件目标profile: {:?}",
target_profile
);
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().await.latest_ref().current.clone();
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
@@ -509,50 +400,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
&& current_profile.as_ref() != Some(new_profile)
&& validate_new_profile(new_profile).await.is_err()
{
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
return Ok(false);
}
// 检查请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
// 更新profiles配置
logging!(
info,
Type::Cmd,
"正在更新配置草稿,序列号: {}",
current_sequence
);
let _ = Config::profiles().await.draft_mut().patch_config(&profiles);
let current_value = profiles.current.clone();
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
// 在调用内核前再次验证请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
perform_config_update(current_sequence, current_value, current_profile).await
perform_config_update(current_value, current_profile).await
}
/// 根据profile name修改profiles
@@ -572,33 +426,34 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) {
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
{
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
let new_interval = new_option.update_interval;
let old_allow_auto_update = old_profile
.option
.as_ref()
.and_then(|o| o.allow_auto_update);
let new_allow_auto_update = profile.option.as_ref().and_then(|o| o.allow_auto_update);
let new_allow_auto_update = new_option.allow_auto_update;
(old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update)
} else {
false
};
profiles_patch_item_safe(index.clone(), profile)
profiles_patch_item_safe(&index, &profile)
.await
.stringify_err()?;
// 如果更新间隔或允许自动更新变更,异步刷新定时器
if should_refresh_timer {
let index_clone = index.clone();
crate::process::AsyncHandler::spawn(move || async move {
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
if let Err(e) = crate::core::Timer::global().refresh().await {
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
crate::core::handle::Handle::notify_timer_updated(index_clone);
crate::core::handle::Handle::notify_timer_updated(index);
}
});
}
@@ -631,10 +486,15 @@ pub async fn view_profile(index: String) -> CmdResult {
/// 读取配置文件内容
#[tauri::command]
pub async fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let item = profiles_ref.get_item(&index).stringify_err()?;
let data = item.read_file().stringify_err()?;
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
PrfItem {
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
..Default::default()
}
};
let data = item.read_file().await.stringify_err()?;
Ok(data)
}

View File

@@ -12,28 +12,37 @@ use tokio::fs;
/// 保存profiles的配置
#[tauri::command]
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
if file_data.is_none() {
return Ok(());
}
let file_data = match file_data {
Some(d) => d,
None => return Ok(()),
};
// 在异步操作前完成所有文件操作
let (file_path, original_content, is_merge_file) = {
// 在异步操作前获取必要元数据并释放锁
let (rel_path, is_merge_file) = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref();
let item = profiles_guard.get_item(&index).stringify_err()?;
// 确定是否为merge类型文件
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let content = item.read_file().stringify_err()?;
let path = item.file.clone().ok_or("file field is null")?;
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
(profiles_dir.join(path.as_str()), content, is_merge)
(path, is_merge)
};
// 读取原始内容在释放profiles_guard后进行
let original_content = PrfItem {
file: Some(rel_path.clone()),
..Default::default()
}
.read_file()
.await
.stringify_err()?;
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
let file_path = profiles_dir.join(rel_path.as_str());
let file_path_str = file_path.to_string_lossy().to_string();
// 保存新的配置文件
let file_data = file_data.ok_or("file_data is None")?;
fs::write(&file_path, &file_data).await.stringify_err()?;
let file_path_str = file_path.to_string_lossy().to_string();
logging!(
info,
Type::Config,
@@ -42,102 +51,107 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
is_merge_file
);
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
if is_merge_file {
logging!(
info,
Type::Config,
"[cmd配置save] 检测到merge文件只进行语法验证"
);
match CoreConfigValidator::validate_config_file(&file_path_str, Some(true)).await {
Ok((true, _)) => {
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
// 成功后尝试更新整体配置
match CoreManager::global().update_config().await {
Ok(_) => {
// 配置更新成功,刷新前端
handle::Handle::refresh_clash();
}
Err(e) => {
logging!(
warn,
Type::Config,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
}
}
return Ok(());
}
Ok((false, error_msg)) => {
return handle_merge_file(&file_path_str, &file_path, &original_content).await;
}
handle_full_validation(&file_path_str, &file_path, &original_content).await
}
async fn restore_original(
file_path: &std::path::Path,
original_content: &str,
) -> Result<(), String> {
fs::write(file_path, original_content).await.stringify_err()
}
fn is_script_error(err: &str, file_path_str: &str) -> bool {
file_path_str.ends_with(".js")
|| err.contains("Script syntax error")
|| err.contains("Script must contain a main function")
|| err.contains("Failed to read script file")
}
async fn handle_merge_file(
file_path_str: &str,
file_path: &std::path::Path,
original_content: &str,
) -> CmdResult {
logging!(
info,
Type::Config,
"[cmd配置save] 检测到merge文件只进行语法验证"
);
match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await {
Ok((true, _)) => {
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
if let Err(e) = CoreManager::global().update_config().await {
logging!(
warn,
Type::Config,
"[cmd配置save] merge文件语法验证失败: {}",
error_msg
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
// 发送合并文件专用错误通知
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
return Ok(());
}
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
return Err(e.to_string().into());
} else {
handle::Handle::refresh_clash();
}
Ok(())
}
Ok((false, error_msg)) => {
logging!(
warn,
Type::Config,
"[cmd配置save] merge文件语法验证失败: {}",
error_msg
);
restore_original(file_path, original_content).await?;
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
Ok(())
}
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
restore_original(file_path, original_content).await?;
Err(e.to_string().into())
}
}
}
// 非merge文件使用完整验证流程
match CoreConfigValidator::validate_config_file(&file_path_str, None).await {
async fn handle_full_validation(
file_path_str: &str,
file_path: &std::path::Path,
original_content: &str,
) -> CmdResult {
match CoreConfigValidator::validate_config_file(file_path_str, None).await {
Ok((true, _)) => {
logging!(info, Type::Config, "[cmd配置save] 验证成功");
Ok(())
}
Ok((false, error_msg)) => {
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
// 智能判断错误类型
let is_script_error = file_path_str.ends_with(".js")
|| error_msg.contains("Script syntax error")
|| error_msg.contains("Script must contain a main function")
|| error_msg.contains("Failed to read script file");
restore_original(file_path, original_content).await?;
if error_msg.contains("YAML syntax error")
|| error_msg.contains("Failed to read file:")
|| (!file_path_str.ends_with(".js") && !is_script_error)
|| (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str))
{
// 普通YAML错误使用YAML通知处理
logging!(
info,
Type::Config,
"[cmd配置save] YAML配置文件验证失败发送通知"
);
let result = (false, error_msg.clone());
let result = (false, error_msg.to_owned());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理
} else if is_script_error(&error_msg, file_path_str) {
logging!(
info,
Type::Config,
"[cmd配置save] 脚本文件验证失败,发送通知"
);
let result = (false, error_msg.clone());
let result = (false, error_msg.to_owned());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
logging!(
info,
Type::Config,
@@ -150,10 +164,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
}
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
restore_original(file_path, original_content).await?;
Err(e.to_string().into())
}
}

View File

@@ -0,0 +1,149 @@
use serde::Serialize;
use tauri::{Manager, ResourceId, Runtime, webview::Webview};
use tauri_plugin_updater::UpdaterExt;
use url::Url;
use super::{CmdResult, String};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateMetadata {
rid: ResourceId,
current_version: String,
version: String,
date: Option<String>,
body: Option<String>,
raw_json: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UpdateChannel {
Stable,
Autobuild,
}
impl TryFrom<&str> for UpdateChannel {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"stable" => Ok(Self::Stable),
"autobuild" => Ok(Self::Autobuild),
other => Err(String::from(format!("Unsupported channel \"{other}\""))),
}
}
}
const CHANNEL_RELEASE_TAGS: &[(UpdateChannel, &str)] = &[
(UpdateChannel::Stable, "updater"),
(UpdateChannel::Autobuild, "updater-autobuild"),
];
const CHANNEL_ENDPOINT_TEMPLATES: &[&str] = &[
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update.json",
];
fn resolve_release_tag(channel: UpdateChannel) -> CmdResult<&'static str> {
CHANNEL_RELEASE_TAGS
.iter()
.find_map(|(entry_channel, tag)| (*entry_channel == channel).then_some(*tag))
.ok_or_else(|| {
String::from(format!(
"No release tag registered for update channel \"{channel:?}\""
))
})
}
fn resolve_channel_endpoints(channel: UpdateChannel) -> CmdResult<Vec<Url>> {
let release_tag = resolve_release_tag(channel)?;
CHANNEL_ENDPOINT_TEMPLATES
.iter()
.map(|template| {
let endpoint = template.replace("{release}", release_tag);
Url::parse(&endpoint).map_err(|err| {
String::from(format!(
"Failed to parse updater endpoint \"{endpoint}\": {err}"
))
})
})
.collect()
}
#[allow(clippy::too_many_arguments)]
#[tauri::command]
pub async fn check_update_channel<R: Runtime>(
webview: Webview<R>,
channel: String,
headers: Option<Vec<(String, String)>>,
timeout: Option<u64>,
proxy: Option<String>,
target: Option<String>,
allow_downgrades: Option<bool>,
) -> CmdResult<Option<UpdateMetadata>> {
let channel_enum = UpdateChannel::try_from(channel.as_str())?;
let endpoints = resolve_channel_endpoints(channel_enum)?;
let mut builder = webview
.updater_builder()
.endpoints(endpoints)
.map_err(|err| String::from(err.to_string()))?;
if let Some(headers) = headers {
for (key, value) in headers {
builder = builder
.header(key.as_str(), value.as_str())
.map_err(|err| String::from(err.to_string()))?;
}
}
if let Some(timeout) = timeout {
builder = builder.timeout(std::time::Duration::from_millis(timeout));
}
if let Some(proxy) = proxy {
let proxy_url = Url::parse(&proxy)
.map_err(|err| String::from(format!("Invalid proxy URL \"{proxy}\": {err}")))?;
builder = builder.proxy(proxy_url);
}
if let Some(target) = target {
builder = builder.target(target);
}
let allow_downgrades = allow_downgrades.unwrap_or(channel_enum != UpdateChannel::Stable);
if allow_downgrades {
builder = builder.version_comparator(|current, update| update.version != current);
}
let updater = builder
.build()
.map_err(|err| String::from(err.to_string()))?;
let update = updater
.check()
.await
.map_err(|err| String::from(err.to_string()))?;
let Some(update) = update else {
return Ok(None);
};
let formatted_date = update
.date
.as_ref()
.map(|date| String::from(date.to_string()));
let metadata = UpdateMetadata {
rid: webview.resources_table().add(update.clone()),
current_version: String::from(update.current_version.clone()),
version: String::from(update.version.clone()),
date: formatted_date,
body: update.body.clone().map(Into::into),
raw_json: update.raw_json.clone(),
};
Ok(Some(metadata))
}

View File

@@ -9,12 +9,12 @@ pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
let ref_data = verge.latest_ref();
ref_data.clone()
};
let verge_response = IVergeResponse::from(*verge_data);
let verge_response = IVergeResponse::from(verge_data);
Ok(verge_response)
}
/// 修改Verge配置
#[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
feat::patch_verge(payload, false).await.stringify_err()
feat::patch_verge(&payload, false).await.stringify_err()
}

View File

@@ -12,10 +12,7 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
webdav_password: Some(password),
..IVerge::default()
};
Config::verge()
.await
.draft_mut()
.patch_config(patch.clone());
Config::verge().await.draft_mut().patch_config(&patch);
Config::verge().await.apply();
// 分离数据获取和异步调用

View File

@@ -1,6 +1,8 @@
use crate::config::Config;
use crate::constants::{network, tun as tun_const};
use crate::utils::dirs::{ipc_path, path_to_str};
use crate::utils::{dirs, help};
use crate::{logging, utils::logging::Type};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Value};
@@ -40,15 +42,13 @@ impl IClashTemp {
Self(Self::guard(map))
}
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
template
}
}
}
pub fn template() -> Self {
use crate::constants::{network, tun as tun_const};
let mut map = Mapping::new();
let mut tun_config = Mapping::new();
let mut cors_map = Mapping::new();
@@ -214,9 +214,9 @@ impl IClashTemp {
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7896);
.unwrap_or(network::ports::DEFAULT_TPROXY);
if port == 0 {
port = 7896;
port = network::ports::DEFAULT_TPROXY;
}
port
}
@@ -330,7 +330,7 @@ impl IClashTemp {
.ok()
.and_then(|path| path_to_str(&path).ok().map(|s| s.into()))
.unwrap_or_else(|| {
log::error!(target: "app", "Failed to get IPC path");
logging!(error, Type::Config, "Failed to get IPC path");
crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into()
})
}

View File

@@ -1,9 +1,10 @@
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
cmd,
config::{PrfItem, profiles_append_item_safe},
constants::{files, timing},
core::{CoreManager, handle, validate::CoreConfigValidator},
enhance, logging,
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
enhance, logging, logging_error,
utils::{Draft, dirs, help, logging::Type},
};
use anyhow::{Result, anyhow};
@@ -55,6 +56,20 @@ impl Config {
pub async fn init_config() -> Result<()> {
Self::ensure_default_profile_items().await?;
// init Tun mode
if !cmd::system::is_admin().unwrap_or_default()
&& service::is_service_available().await.is_err()
{
let verge = Config::verge().await;
verge.draft_mut().enable_tun_mode = Some(false);
verge.apply();
let _ = tray::Tray::global().update_tray_display().await;
// 分离数据获取和异步调用避免Send问题
let verge_data = Config::verge().await.latest_ref().clone();
logging_error!(Type::Core, verge_data.save_file().await);
}
let validation_result = Self::generate_and_validate().await?;
if let Some((msg_type, msg_content)) = validation_result {
@@ -68,13 +83,13 @@ impl Config {
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await;
if profiles.latest_ref().get_item(&"Merge".into()).is_err() {
let merge_item = PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item.clone()).await?;
if profiles.latest_ref().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item).await?;
}
if profiles.latest_ref().get_item(&"Script".into()).is_err() {
let script_item = PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item.clone()).await?;
if profiles.latest_ref().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item).await?;
}
Ok(())
}
@@ -153,11 +168,11 @@ impl Config {
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
**Config::runtime().await.draft_mut() = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
});
};
Ok(())
}

View File

@@ -1,13 +1,17 @@
use crate::utils::{
dirs, help,
network::{NetworkManager, ProxyType},
tmpl,
use crate::{
config::profiles,
utils::{
dirs, help,
network::{NetworkManager, ProxyType},
tmpl,
},
};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::{fs, time::Duration};
use std::time::Duration;
use tokio::fs;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem {
@@ -118,26 +122,29 @@ pub struct PrfOption {
}
impl PrfOption {
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
match (one, other) {
(Some(mut a), Some(b)) => {
a.user_agent = b.user_agent.or(a.user_agent);
a.with_proxy = b.with_proxy.or(a.with_proxy);
a.self_proxy = b.self_proxy.or(a.self_proxy);
a.danger_accept_invalid_certs = b
(Some(a_ref), Some(b_ref)) => {
let mut result = a_ref.clone();
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
result.danger_accept_invalid_certs = b_ref
.danger_accept_invalid_certs
.or(a.danger_accept_invalid_certs);
a.allow_auto_update = b.allow_auto_update.or(a.allow_auto_update);
a.update_interval = b.update_interval.or(a.update_interval);
a.merge = b.merge.or(a.merge);
a.script = b.script.or(a.script);
a.rules = b.rules.or(a.rules);
a.proxies = b.proxies.or(a.proxies);
a.groups = b.groups.or(a.groups);
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
Some(a)
.or(result.danger_accept_invalid_certs);
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
result.update_interval = b_ref.update_interval.or(result.update_interval);
result.merge = b_ref.merge.clone().or(result.merge);
result.script = b_ref.script.clone().or(result.script);
result.rules = b_ref.rules.clone().or(result.rules);
result.proxies = b_ref.proxies.clone().or(result.proxies);
result.groups = b_ref.groups.clone().or(result.groups);
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
Some(result)
}
t => t.0.or(t.1),
(Some(a_ref), None) => Some(a_ref.clone()),
(None, Some(b_ref)) => Some(b_ref.clone()),
(None, None) => None,
}
}
}
@@ -145,13 +152,14 @@ impl PrfOption {
impl PrfItem {
/// From partial item
/// must contain `itype`
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
pub async fn from(item: &PrfItem, file_data: Option<String>) -> Result<PrfItem> {
if item.itype.is_none() {
bail!("type should not be null");
}
let itype = item
.itype
.as_ref()
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
match itype.as_str() {
"remote" => {
@@ -159,14 +167,16 @@ impl PrfItem {
.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
let name = item.name.as_ref();
let desc = item.desc.as_ref();
let option = item.option.as_ref();
PrfItem::from_url(url, name, desc, option).await
}
"local" => {
let name = item.name.unwrap_or_else(|| "Local File".into());
let desc = item.desc.unwrap_or_else(|| "".into());
PrfItem::from_local(name, desc, file_data, item.option).await
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
let desc = item.desc.clone().unwrap_or_else(|| "".into());
let option = item.option.as_ref();
PrfItem::from_local(name, desc, file_data, option).await
}
typ => bail!("invalid profile item type \"{typ}\""),
}
@@ -178,7 +188,7 @@ impl PrfItem {
name: String,
desc: String,
file_data: Option<String>,
option: Option<PrfOption>,
option: Option<&PrfOption>,
) -> Result<PrfItem> {
let uid = help::get_uid("L").into();
let file = format!("{uid}.yaml").into();
@@ -191,29 +201,29 @@ impl PrfItem {
let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
merge = merge_item.uid;
let merge_item = &mut PrfItem::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
script = script_item.uid;
let script_item = &mut PrfItem::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
rules = rules_item.uid;
let rules_item = &mut PrfItem::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
proxies = proxies_item.uid;
let proxies_item = &mut PrfItem::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
groups = groups_item.uid;
let groups_item = &mut PrfItem::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
Ok(PrfItem {
uid: Some(uid),
@@ -243,24 +253,23 @@ impl PrfItem {
/// create a new item from url
pub async fn from_url(
url: &str,
name: Option<String>,
desc: Option<String>,
option: Option<PrfOption>,
name: Option<&String>,
desc: Option<&String>,
option: Option<&PrfOption>,
) -> Result<PrfItem> {
let opt_ref = option.as_ref();
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
let accept_invalid_certs =
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
let allow_auto_update = opt_ref.map(|o| o.allow_auto_update.unwrap_or(true));
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval);
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
let mut merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone());
let mut rules = opt_ref.and_then(|o| o.rules.clone());
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
let mut groups = opt_ref.and_then(|o| o.groups.clone());
option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
let allow_auto_update = option.map(|o| o.allow_auto_update.unwrap_or(true));
let user_agent = option.and_then(|o| o.user_agent.clone());
let update_interval = option.and_then(|o| o.update_interval);
let timeout = option.and_then(|o| o.timeout_seconds).unwrap_or(20);
let mut merge = option.and_then(|o| o.merge.clone());
let mut script = option.and_then(|o| o.script.clone());
let mut rules = option.and_then(|o| o.rules.clone());
let mut proxies = option.and_then(|o| o.proxies.clone());
let mut groups = option.and_then(|o| o.groups.clone());
// 选择代理类型
let proxy_type = if self_proxy {
@@ -297,18 +306,27 @@ impl PrfItem {
let header = resp.headers();
// parse the Subscription UserInfo
let extra = match header.get("Subscription-Userinfo") {
Some(value) => {
let sub_info = value.to_str().unwrap_or("");
Some(PrfExtra {
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
download: help::parse_str(sub_info, "download").unwrap_or(0),
total: help::parse_str(sub_info, "total").unwrap_or(0),
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
})
let extra;
'extra: {
for (k, v) in header.iter() {
let key_lower = k.as_str().to_ascii_lowercase();
// Accept standard custom-metadata prefixes (x-amz-meta-, x-obs-meta-, x-cos-meta-, etc.).
if key_lower
.strip_suffix("subscription-userinfo")
.is_some_and(|prefix| prefix.is_empty() || prefix.ends_with('-'))
{
let sub_info = v.to_str().unwrap_or("");
extra = Some(PrfExtra {
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
download: help::parse_str(sub_info, "download").unwrap_or(0),
total: help::parse_str(sub_info, "total").unwrap_or(0),
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
});
break 'extra;
}
}
None => None,
};
extra = None;
}
// parse the Content-Disposition
let filename = match header.get("Content-Disposition") {
@@ -356,7 +374,11 @@ impl PrfItem {
let uid = help::get_uid("R").into();
let file = format!("{uid}.yaml").into();
let name = name.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()).into());
let name = name.map(|s| s.to_owned()).unwrap_or_else(|| {
filename
.map(|s| s.into())
.unwrap_or_else(|| "Remote File".into())
});
let data = resp.text_with_charset()?;
// process the charset "UTF-8 with BOM"
@@ -371,36 +393,36 @@ impl PrfItem {
}
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
merge = merge_item.uid;
let merge_item = &mut PrfItem::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
script = script_item.uid;
let script_item = &mut PrfItem::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
rules = rules_item.uid;
let rules_item = &mut PrfItem::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
proxies = proxies_item.uid;
let proxies_item = &mut PrfItem::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
groups = groups_item.uid;
let groups_item = &mut PrfItem::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
Ok(PrfItem {
uid: Some(uid),
itype: Some("remote".into()),
name: Some(name),
desc,
desc: desc.cloned(),
file: Some(file),
url: Some(url.into()),
selected: None,
@@ -537,24 +559,28 @@ impl PrfItem {
}
/// get the file data
pub fn read_file(&self) -> Result<String> {
pub async fn read_file(&self) -> Result<String> {
let file = self
.file
.clone()
.as_ref()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
let content = fs::read_to_string(path).context("failed to read the file")?;
let content = fs::read_to_string(path)
.await
.context("failed to read the file")?;
Ok(content.into())
}
/// save the file data
pub fn save_file(&self, data: String) -> Result<()> {
pub async fn save_file(&self, data: String) -> Result<()> {
let file = self
.file
.clone()
.as_ref()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
fs::write(path, data.as_bytes()).context("failed to save the file")
fs::write(path, data.as_bytes())
.await
.context("failed to save the file")
}
}

View File

@@ -3,6 +3,7 @@ use crate::utils::{
dirs::{self, PathBufExec},
help,
};
use crate::{logging, utils::logging::Type};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
@@ -31,7 +32,7 @@ pub struct CleanupResult {
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key;
$lv.$key = $rv.$key.clone();
}
};
}
@@ -67,24 +68,17 @@ impl IProfiles {
profiles
}
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
logging!(error, Type::Config, "{err}");
Self::default()
}
},
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
logging!(error, Type::Config, "{err}");
Self::default()
}
}
}
pub fn template() -> Self {
Self {
items: Some(vec![]),
..Self::default()
}
}
pub async fn save_file(&self) -> Result<()> {
help::save_yaml(
&dirs::profiles_path()?,
@@ -95,17 +89,17 @@ impl IProfiles {
}
/// 只修改currentvalid和chain
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
if let Some(current) = patch.current
if let Some(current) = &patch.current
&& 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;
if items.iter().any(|e| e.uid.as_ref() == some_uid) {
self.current = some_uid.cloned();
}
}
@@ -122,28 +116,30 @@ impl IProfiles {
}
/// find the item by the uid
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if let Some(items) = self.items.as_ref() {
let some_uid = Some(uid.clone());
pub fn get_item(&self, uid: impl AsRef<str>) -> Result<&PrfItem> {
let uid_str = uid.as_ref();
if let Some(items) = self.items.as_ref() {
for each in items.iter() {
if each.uid == some_uid {
if let Some(uid_val) = &each.uid
&& uid_val.as_str() == uid_str
{
return Ok(each);
}
}
}
bail!("failed to get the profile item \"uid:{uid}\"");
bail!("failed to get the profile item \"uid:{}\"", uid_str);
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub async fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
if item.uid.is_none() {
pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
let uid = &item.uid;
if uid.is_none() {
bail!("the uid should not be null");
}
let uid = item.uid.clone();
// save the file data
// move the field value after save
@@ -165,7 +161,7 @@ impl IProfiles {
if self.current.is_none()
&& (item.itype == Some("remote".into()) || item.itype == Some("local".into()))
{
self.current = uid;
self.current = uid.to_owned();
}
if self.items.is_none() {
@@ -173,24 +169,23 @@ impl IProfiles {
}
if let Some(items) = self.items.as_mut() {
items.push(item)
items.push(item.to_owned());
}
// self.save_file().await
Ok(())
}
/// reorder items
pub async fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
let mut old_index = None;
let mut new_index = None;
for (i, _) in items.iter().enumerate() {
if items[i].uid == Some(active_id.clone()) {
if items[i].uid.as_ref() == Some(active_id) {
old_index = Some(i);
}
if items[i].uid == Some(over_id.clone()) {
if items[i].uid.as_ref() == Some(over_id) {
new_index = Some(i);
}
}
@@ -206,11 +201,11 @@ impl IProfiles {
}
/// update the item value
pub async fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
for each in items.iter_mut() {
if each.uid == Some(uid.clone()) {
if each.uid.as_ref() == Some(uid) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
@@ -232,13 +227,13 @@ impl IProfiles {
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
// find the item
let _ = self.get_item(&uid)?;
let _ = self.get_item(uid)?;
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
@@ -247,8 +242,8 @@ impl IProfiles {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home;
each.option = PrfOption::merge(each.option.clone(), item.option);
each.home = item.home.to_owned();
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
@@ -279,10 +274,10 @@ impl IProfiles {
/// delete item
/// if delete the current then return true
pub async fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid);
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(uid);
let current = current.clone();
let item = self.get_item(&uid)?;
let item = self.get_item(uid)?;
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
@@ -330,7 +325,7 @@ impl IProfiles {
.await;
}
// delete the original uid
if current == uid {
if current == *uid {
self.current = None;
for item in items.iter() {
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
@@ -342,7 +337,7 @@ impl IProfiles {
self.items = Some(items);
self.save_file().await?;
Ok(current == uid)
Ok(current == *uid)
}
/// 获取current指向的订阅内容
@@ -433,8 +428,8 @@ impl IProfiles {
}
/// 判断profile是否是current指向的
pub fn is_current_profile_index(&self, index: String) -> bool {
self.current == Some(index)
pub fn is_current_profile_index(&self, index: &String) -> bool {
self.current.as_ref() == Some(index)
}
/// 获取所有的profiles(uid名称)
@@ -453,6 +448,18 @@ impl IProfiles {
})
}
/// 通过 uid 获取名称
pub fn get_name_by_uid(&self, uid: &String) -> Option<String> {
if let Some(items) = &self.items {
for item in items {
if item.uid.as_ref() == Some(uid) {
return item.name.clone();
}
}
}
None
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
pub async fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
let profiles_dir = dirs::app_profiles_dir()?;
@@ -491,7 +498,7 @@ impl IProfiles {
{
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}");
continue;
}
@@ -500,11 +507,15 @@ impl IProfiles {
match path.to_path_buf().remove_if_exists().await {
Ok(_) => {
deleted_files.push(file_name.into());
log::info!(target: "app", "已清理冗余文件: {file_name}");
logging!(info, Type::Config, "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}").into());
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
logging!(
warn,
Type::Config,
"Warning: 清理文件失败: {file_name} - {e}"
);
}
}
}
@@ -517,8 +528,9 @@ impl IProfiles {
failed_deletions,
};
log::info!(
target: "app",
logging!(
info,
Type::Config,
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
result.total_files,
result.deleted_files.len(),
@@ -626,14 +638,14 @@ impl IProfiles {
use crate::config::Config;
pub async fn profiles_append_item_with_filedata_safe(
item: PrfItem,
item: &PrfItem,
file_data: Option<String>,
) -> Result<()> {
let item = PrfItem::from(item, file_data).await?;
let item = &mut PrfItem::from(item, file_data).await?;
profiles_append_item_safe(item).await
}
pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -643,7 +655,7 @@ pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
.await
}
pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> {
pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -653,7 +665,7 @@ pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()
.await
}
pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -663,7 +675,7 @@ pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
.await
}
pub async fn profiles_reorder_safe(active_id: String, over_id: String) -> Result<()> {
pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -683,7 +695,7 @@ pub async fn profiles_save_file_safe() -> Result<()> {
.await
}
pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> {
pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {

View File

@@ -1,3 +1,4 @@
use crate::config::Config;
use crate::{
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
logging,
@@ -304,19 +305,17 @@ impl IVerge {
/// 配置修正后重新加载配置
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
use crate::config::Config;
let config_draft = Config::verge().await;
*config_draft.draft_mut() = Box::new(updated_config.clone());
config_draft.apply();
logging!(
info,
Type::Config,
"内存配置已强制更新新的clash_core: {:?}",
updated_config.clash_core
&updated_config.clash_core
);
let config_draft = Config::verge().await;
**config_draft.draft_mut() = updated_config;
config_draft.apply();
Ok(())
}
@@ -354,12 +353,12 @@ impl IVerge {
config
}
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
Self::template()
}
},
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
Self::template()
}
}
@@ -438,11 +437,11 @@ impl IVerge {
/// patch verge config
/// only save to file
#[allow(clippy::cognitive_complexity)]
pub fn patch_config(&mut self, patch: IVerge) {
pub fn patch_config(&mut self, patch: &IVerge) {
macro_rules! patch {
($key: tt) => {
if patch.$key.is_some() {
self.$key = patch.$key;
self.$key = patch.$key.clone();
}
};
}
@@ -697,3 +696,9 @@ impl From<IVerge> for IVergeResponse {
}
}
}
impl From<Box<IVerge>> for IVergeResponse {
fn from(verge: Box<IVerge>) -> Self {
IVergeResponse::from(*verge)
}
}

View File

@@ -5,15 +5,13 @@ pub mod network {
pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097";
pub mod ports {
#[allow(dead_code)]
#[cfg(not(target_os = "windows"))]
pub const DEFAULT_REDIR: u16 = 7895;
#[allow(dead_code)]
#[cfg(target_os = "linux")]
pub const DEFAULT_TPROXY: u16 = 7896;
pub const DEFAULT_MIXED: u16 = 7897;
pub const DEFAULT_SOCKS: u16 = 7898;
pub const DEFAULT_HTTP: u16 = 7899;
#[allow(dead_code)]
pub const DEFAULT_EXTERNAL_CONTROLLER: u16 = 9097;
#[cfg(not(feature = "verge-dev"))]
pub const SINGLETON_SERVER: u16 = 33331;
@@ -39,11 +37,8 @@ pub mod timing {
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500);
pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300);
pub const PROCESS_VERIFY_DELAY: Duration = Duration::from_millis(100);
#[allow(dead_code)]
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
#[allow(dead_code)]
pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300);
#[cfg(target_os = "windows")]
@@ -53,40 +48,16 @@ pub mod timing {
}
pub mod retry {
#[allow(dead_code)]
pub const EVENT_EMIT_THRESHOLD: u64 = 10;
#[allow(dead_code)]
pub const SWR_ERROR_RETRY: usize = 2;
}
pub mod files {
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
#[allow(dead_code)]
pub const DNS_CONFIG: &str = "dns_config.yaml";
#[allow(dead_code)]
pub const WINDOW_STATE: &str = "window_state.json";
}
pub mod process {
pub const VERGE_MIHOMO: &str = "verge-mihomo";
pub const VERGE_MIHOMO_ALPHA: &str = "verge-mihomo-alpha";
pub fn process_names() -> [&'static str; 2] {
[VERGE_MIHOMO, VERGE_MIHOMO_ALPHA]
}
#[cfg(windows)]
pub fn with_extension(name: &str) -> String {
format!("{}.exe", name)
}
#[cfg(not(windows))]
pub fn with_extension(name: &str) -> String {
name.to_string()
}
}
pub mod error_patterns {
pub const CONNECTION_ERRORS: &[&str] = &[
"Failed to create connection",

View File

@@ -1,5 +1,6 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::time::{Duration, timeout};
@@ -41,15 +42,21 @@ impl AsyncProxyQuery {
pub async fn get_auto_proxy() -> AsyncAutoproxy {
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
logging!(
debug,
Type::Network,
"异步获取自动代理成功: enable={}, url={}",
proxy.enable,
proxy.url
);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "异步获取自动代理失败: {e}");
logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}");
AsyncAutoproxy::default()
}
Err(_) => {
log::warn!(target: "app", "异步获取自动代理超时");
logging!(warn, Type::Network, "Warning: 异步获取自动代理超时");
AsyncAutoproxy::default()
}
}
@@ -59,15 +66,22 @@ impl AsyncProxyQuery {
pub async fn get_system_proxy() -> AsyncSysproxy {
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
logging!(
debug,
Type::Network,
"异步获取系统代理成功: enable={}, {}:{}",
proxy.enable,
proxy.host,
proxy.port
);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "异步获取系统代理失败: {e}");
logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}");
AsyncSysproxy::default()
}
Err(_) => {
log::warn!(target: "app", "异步获取系统代理超时");
logging!(warn, Type::Network, "Warning: 异步获取系统代理超时");
AsyncSysproxy::default()
}
}
@@ -99,7 +113,7 @@ impl AsyncProxyQuery {
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
if result != 0 {
log::debug!(target: "app", "无法打开注册表项");
logging!(debug, Type::Network, "无法打开注册表项");
return Ok(AsyncAutoproxy::default());
}
@@ -125,7 +139,7 @@ impl AsyncProxyQuery {
.position(|&x| x == 0)
.unwrap_or(url_buffer.len());
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
log::debug!(target: "app", "从注册表读取到PAC URL: {pac_url}");
logging!(debug, Type::Network, "从注册表读取到PAC URL: {pac_url}");
}
// 2. 检查自动检测设置是否启用
@@ -150,7 +164,11 @@ impl AsyncProxyQuery {
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
if pac_enabled {
log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}");
logging!(
debug,
Type::Network,
"PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}"
);
if pac_url.is_empty() && auto_detect != 0 {
pac_url = "auto-detect".into();
@@ -161,7 +179,7 @@ impl AsyncProxyQuery {
url: pac_url,
})
} else {
log::debug!(target: "app", "PAC配置未启用");
logging!(debug, Type::Network, "PAC配置未启用");
Ok(AsyncAutoproxy::default())
}
}
@@ -177,7 +195,11 @@ impl AsyncProxyQuery {
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::debug!(target: "app", "scutil output: {stdout}");
crate::logging!(
debug,
crate::utils::logging::Type::Network,
"scutil output: {stdout}"
);
let mut pac_enabled = false;
let mut pac_url = String::new();
@@ -196,7 +218,11 @@ impl AsyncProxyQuery {
}
}
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
crate::logging!(
debug,
crate::utils::logging::Type::Network,
"解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}"
);
Ok(AsyncAutoproxy {
enable: pac_enabled && !pac_url.is_empty(),
@@ -363,7 +389,11 @@ impl AsyncProxyQuery {
(proxy_server, 8080)
};
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}");
logging!(
debug,
Type::Network,
"从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}"
);
Ok(AsyncSysproxy {
enable: true,
@@ -386,7 +416,7 @@ impl AsyncProxyQuery {
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::debug!(target: "app", "scutil proxy output: {stdout}");
logging!(debug, Type::Network, "scutil proxy output: {stdout}");
let mut http_enabled = false;
let mut http_host = String::new();

View File

@@ -1,19 +1,24 @@
use crate::{config::Config, utils::dirs};
use crate::constants::files::DNS_CONFIG;
use crate::{
config::Config,
logging,
process::AsyncHandler,
utils::{dirs, logging::Type},
};
use anyhow::Error;
use arc_swap::{ArcSwap, ArcSwapOption};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use smartstring::alias::String;
use std::{
collections::HashMap,
env::{consts::OS, temp_dir},
fs,
io::Write,
path::PathBuf,
sync::Arc,
time::Duration,
};
use tokio::time::timeout;
use tokio::{fs, time::timeout};
use zip::write::SimpleFileOptions;
// 应用版本常量,来自 tauri.conf.json
@@ -51,24 +56,24 @@ impl Operation {
}
pub struct WebDavClient {
config: Arc<Mutex<Option<WebDavConfig>>>,
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
config: Arc<ArcSwapOption<WebDavConfig>>,
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
config: Arc::new(Mutex::new(None)),
clients: Arc::new(Mutex::new(HashMap::new())),
config: Arc::new(ArcSwapOption::new(None)),
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
})
}
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
// 先尝试从缓存获取
{
let clients = self.clients.lock();
if let Some(client) = clients.get(&op) {
let clients_map = self.clients.load();
if let Some(client) = clients_map.get(&op) {
return Ok(client.clone());
}
}
@@ -76,10 +81,10 @@ impl WebDavClient {
// 获取或创建配置
let config = {
// 首先检查是否已有配置
let existing_config = self.config.lock().as_ref().cloned();
let existing_config = self.config.load();
if let Some(cfg) = existing_config {
cfg
if let Some(cfg_arc) = existing_config.clone() {
(*cfg_arc).clone()
} else {
// 释放锁后获取异步配置
let verge = Config::verge().await.latest_ref().clone();
@@ -101,8 +106,8 @@ impl WebDavClient {
password: verge.webdav_password.unwrap_or_default(),
};
// 重新获取锁并存储配置
*self.config.lock() = Some(config.clone());
// 存储配置到 ArcSwapOption
self.config.store(Some(Arc::new(config.clone())));
config
}
};
@@ -138,9 +143,14 @@ impl WebDavClient {
.is_err()
{
match client.mkcol(dirs::BACKUP_DIR).await {
Ok(_) => log::info!("Successfully created backup directory"),
Ok(_) => logging!(info, Type::Backup, "Successfully created backup directory"),
Err(e) => {
log::warn!("Failed to create backup directory: {}", e);
logging!(
warn,
Type::Backup,
"Warning: Failed to create backup directory: {}",
e
);
// 清除缓存,强制下次重新尝试
self.reset();
return Err(anyhow::Error::msg(format!(
@@ -151,18 +161,19 @@ impl WebDavClient {
}
}
// 缓存客户端
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
{
let mut clients = self.clients.lock();
clients.insert(op, client.clone());
let mut map = (**self.clients.load()).clone();
map.insert(op, client.clone());
self.clients.store(map.into());
}
Ok(client)
}
pub fn reset(&self) {
*self.config.lock() = None;
self.clients.lock().clear();
self.config.store(None);
self.clients.store(Arc::new(HashMap::new()));
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
@@ -170,7 +181,7 @@ impl WebDavClient {
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into();
// 读取文件并上传,如果失败尝试一次重试
let file_content = fs::read(&file_path)?;
let file_content = fs::read(&file_path).await?;
// 添加超时保护
let upload_result = timeout(
@@ -181,7 +192,11 @@ impl WebDavClient {
match upload_result {
Err(_) => {
log::warn!("Upload timed out, retrying once");
logging!(
warn,
Type::Backup,
"Warning: Upload timed out, retrying once"
);
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -192,7 +207,11 @@ impl WebDavClient {
}
Ok(Err(e)) => {
log::warn!("Upload failed, retrying once: {e}");
logging!(
warn,
Type::Backup,
"Warning: Upload failed, retrying once: {e}"
);
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -212,7 +231,7 @@ impl WebDavClient {
let fut = async {
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
fs::write(&storage_path, &content).await?;
Ok::<(), Error>(())
};
@@ -250,18 +269,19 @@ impl WebDavClient {
}
}
pub fn create_backup() -> Result<(String, PathBuf), Error> {
pub async fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let zip_file_name: String = format!("{OS}-backup-{now}.zip").into();
let zip_path = temp_dir().join(zip_file_name.as_str());
let file = fs::File::create(&zip_path)?;
let value = zip_path.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::create(&value)).await??;
let mut zip = zip::ZipWriter::new(file);
zip.add_directory("profiles/", SimpleFileOptions::default())?;
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?;
if let Ok(mut entries) = fs::read_dir(dirs::app_profiles_dir()?).await {
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_file() {
let file_name_os = entry.file_name();
@@ -270,16 +290,16 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
let backup_path = format!("profiles/{}", file_name);
zip.start_file(backup_path, options)?;
let file_content = fs::read(&path)?;
let file_content = fs::read(&path).await?;
zip.write_all(&file_content)?;
}
}
}
zip.start_file(dirs::CLASH_CONFIG, options)?;
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
zip.write_all(fs::read(dirs::clash_path()?).await?.as_slice())?;
let mut verge_config: serde_json::Value =
serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
let verge_text = fs::read_to_string(dirs::verge_path()?).await?;
let mut verge_config: serde_json::Value = serde_yaml_ng::from_str(&verge_text)?;
if let Some(obj) = verge_config.as_object_mut() {
obj.remove("webdav_username");
obj.remove("webdav_password");
@@ -288,14 +308,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
let dns_config_path = dirs::app_home_dir()?.join(dirs::DNS_CONFIG);
let dns_config_path = dirs::app_home_dir()?.join(DNS_CONFIG);
if dns_config_path.exists() {
zip.start_file(dirs::DNS_CONFIG, options)?;
zip.write_all(fs::read(&dns_config_path)?.as_slice())?;
zip.start_file(DNS_CONFIG, options)?;
zip.write_all(fs::read(&dns_config_path).await?.as_slice())?;
}
zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
zip.write_all(fs::read(dirs::profiles_path()?).await?.as_slice())?;
zip.finish()?;
Ok((zip_file_name, zip_path))
}

View File

@@ -7,6 +7,7 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use crate::config::{Config, IVerge};
use crate::core::{async_proxy_query::AsyncProxyQuery, handle};
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use once_cell::sync::Lazy;
use smartstring::alias::String;
use sysproxy::{Autoproxy, Sysproxy};
@@ -104,14 +105,14 @@ impl EventDrivenProxyManager {
let query = QueryRequest { response_tx: tx };
if self.query_sender.send(query).is_err() {
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
logging!(error, Type::Network, "发送查询请求失败,返回缓存数据");
return self.get_auto_proxy_cached().await;
}
match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result,
_ => {
log::warn!(target: "app", "查询超时,返回缓存数据");
logging!(warn, Type::Network, "Warning: 查询超时,返回缓存数据");
self.get_auto_proxy_cached().await
}
}
@@ -134,7 +135,7 @@ impl EventDrivenProxyManager {
fn send_event(&self, event: ProxyEvent) {
if let Err(e) = self.event_sender.send(event) {
log::error!(target: "app", "发送代理事件失败: {e}");
logging!(error, Type::Network, "发送代理事件失败: {e}");
}
}
@@ -143,7 +144,7 @@ impl EventDrivenProxyManager {
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) {
log::info!(target: "app", "事件驱动代理管理器启动");
logging!(info, Type::Network, "事件驱动代理管理器启动");
// 将 mpsc 接收器包装成 Stream避免每次循环创建 future
let mut event_stream = UnboundedReceiverStream::new(event_rx);
@@ -158,7 +159,7 @@ impl EventDrivenProxyManager {
loop {
tokio::select! {
Some(event) = event_stream.next() => {
log::debug!(target: "app", "处理代理事件: {event:?}");
logging!(debug, Type::Network, "处理代理事件: {event:?}");
let event_clone = event.clone(); // 保存一份副本用于后续检查
Self::handle_event(&state, event).await;
@@ -179,13 +180,13 @@ impl EventDrivenProxyManager {
// 定时检查代理设置
let config = Self::get_proxy_config().await;
if config.guard_enabled && config.sys_enabled {
log::debug!(target: "app", "定时检查代理设置");
logging!(debug, Type::Network, "定时检查代理设置");
Self::check_and_restore_proxy(&state).await;
}
}
else => {
// 两个通道都关闭时退出
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
logging!(info, Type::Network, "事件或查询通道关闭,代理管理器停止");
break;
}
}
@@ -201,7 +202,7 @@ impl EventDrivenProxyManager {
Self::initialize_proxy_state(state).await;
}
ProxyEvent::AppStopping => {
log::info!(target: "app", "清理代理状态");
logging!(info, Type::Network, "清理代理状态");
Self::update_state_timestamp(state, |s| {
s.sys_enabled = false;
s.pac_enabled = false;
@@ -224,7 +225,7 @@ impl EventDrivenProxyManager {
}
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "初始化代理状态");
logging!(info, Type::Network, "初始化代理状态");
let config = Self::get_proxy_config().await;
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
@@ -239,11 +240,17 @@ impl EventDrivenProxyManager {
})
.await;
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
logging!(
info,
Type::Network,
"代理状态初始化完成: sys={}, pac={}",
config.sys_enabled,
config.pac_enabled
);
}
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
log::debug!(target: "app", "更新代理配置");
logging!(debug, Type::Network, "更新代理配置");
let config = Self::get_proxy_config().await;
@@ -260,7 +267,7 @@ impl EventDrivenProxyManager {
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过系统代理守卫检查");
logging!(debug, Type::Network, "应用正在退出,跳过系统代理守卫检查");
return;
}
let (sys_enabled, pac_enabled) = {
@@ -272,7 +279,7 @@ impl EventDrivenProxyManager {
return;
}
log::debug!(target: "app", "检查代理状态");
logging!(debug, Type::Network, "检查代理状态");
if pac_enabled {
Self::check_and_restore_pac_proxy(state).await;
@@ -283,7 +290,7 @@ impl EventDrivenProxyManager {
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出跳过PAC代理恢复检查");
logging!(debug, Type::Network, "应用正在退出跳过PAC代理恢复检查");
return;
}
@@ -296,9 +303,9 @@ impl EventDrivenProxyManager {
.await;
if !current.enable || current.url != expected.url {
log::info!(target: "app", "PAC代理设置异常正在恢复...");
logging!(info, Type::Network, "PAC代理设置异常正在恢复...");
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "恢复PAC代理失败: {}", e);
logging!(error, Type::Network, "恢复PAC代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
@@ -314,7 +321,7 @@ impl EventDrivenProxyManager {
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复检查");
logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复检查");
return;
}
@@ -327,9 +334,9 @@ impl EventDrivenProxyManager {
.await;
if !current.enable || current.host != expected.host || current.port != expected.port {
log::info!(target: "app", "系统代理设置异常,正在恢复...");
logging!(info, Type::Network, "系统代理设置异常,正在恢复...");
if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "恢复系统代理失败: {}", e);
logging!(error, Type::Network, "恢复系统代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
@@ -457,7 +464,7 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出跳过PAC代理恢复");
logging!(debug, Type::Network, "应用正在退出跳过PAC代理恢复");
return Ok(());
}
Self::execute_sysproxy_command(&["pac", expected_url]).await
@@ -481,7 +488,7 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复");
logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复");
return Ok(());
}
let address = format!("{}:{}", expected.host, expected.port);
@@ -502,8 +509,9 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
log::debug!(
target: "app",
logging!(
debug,
Type::Network,
"应用正在退出,取消调用 sysproxy.exe参数: {:?}",
args
);
@@ -518,14 +526,14 @@ impl EventDrivenProxyManager {
let binary_path = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "获取服务路径失败: {e}");
logging!(error, Type::Network, "获取服务路径失败: {e}");
return Err(e);
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe 不存在");
logging!(error, Type::Network, "sysproxy.exe 不存在");
}
anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist");

View File

@@ -5,10 +5,9 @@ use crate::{
singleton_with_logging, utils::logging::Type,
};
use anyhow::{Result, bail};
use parking_lot::Mutex;
use arc_swap::ArcSwap;
use smartstring::alias::String;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::{AppHandle, Manager};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
/// Enum representing all available hotkey functions
@@ -94,77 +93,64 @@ impl SystemHotkey {
}
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>,
current: ArcSwap<Vec<String>>,
}
impl Hotkey {
fn new() -> Self {
Self {
current: Arc::new(Mutex::new(Vec::new())),
current: ArcSwap::new(Arc::new(Vec::new())),
}
}
/// Execute the function associated with a hotkey function enum
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
let app_handle = app_handle.clone();
fn execute_function(function: HotkeyFunction) {
match function {
HotkeyFunction::OpenOrCloseDashboard => {
AsyncHandler::spawn(async move || {
crate::feat::open_or_close_dashboard().await;
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
notify_event(NotificationEvent::DashboardToggled).await;
});
}
HotkeyFunction::ClashModeRule => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("rule".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Rule" },
)
.await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Rule" }).await;
});
}
HotkeyFunction::ClashModeGlobal => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("global".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Global" },
)
.await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Global" }).await;
});
}
HotkeyFunction::ClashModeDirect => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("direct".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Direct" },
)
.await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Direct" }).await;
});
}
HotkeyFunction::ToggleSystemProxy => {
AsyncHandler::spawn(async move || {
feat::toggle_system_proxy().await;
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
notify_event(NotificationEvent::SystemProxyToggled).await;
});
}
HotkeyFunction::ToggleTunMode => {
AsyncHandler::spawn(async move || {
feat::toggle_tun_mode(None).await;
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
notify_event(NotificationEvent::TunModeToggled).await;
});
}
HotkeyFunction::EntryLightweightMode => {
AsyncHandler::spawn(async move || {
entry_lightweight_mode().await;
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
notify_event(NotificationEvent::LightweightModeEntered).await;
});
}
HotkeyFunction::Quit => {
AsyncHandler::spawn(async move || {
notify_event(app_handle, NotificationEvent::AppQuit).await;
notify_event(NotificationEvent::AppQuit).await;
feat::quit().await;
});
}
@@ -172,7 +158,7 @@ impl Hotkey {
HotkeyFunction::Hide => {
AsyncHandler::spawn(async move || {
feat::hide().await;
notify_event(app_handle, NotificationEvent::AppHidden).await;
notify_event(NotificationEvent::AppHidden).await;
});
}
}
@@ -224,14 +210,12 @@ impl Hotkey {
let is_quit = matches!(function, HotkeyFunction::Quit);
manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| {
let hotkey_event_owned = *hotkey_event;
let event_owned = event;
let function_owned = function;
let is_quit_owned = is_quit;
let app_handle_cloned = app_handle.clone();
AsyncHandler::spawn(move || async move {
if event_owned.state == ShortcutState::Pressed {
logging!(
@@ -242,11 +226,11 @@ impl Hotkey {
);
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = app_handle_cloned.get_webview_window("main")
if let Some(window) = handle::Handle::get_window()
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned, &app_handle_cloned);
Self::execute_function(function_owned);
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
@@ -258,14 +242,14 @@ impl Hotkey {
.unwrap_or(true);
if is_enable_global_hotkey {
Self::execute_function(function_owned, &app_handle_cloned);
Self::execute_function(function_owned);
} 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_owned, &app_handle_cloned);
Self::execute_function(function_owned);
}
}
}
@@ -288,9 +272,9 @@ impl Hotkey {
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey {
pub async fn init(&self) -> Result<()> {
pub async fn init(&self, skip: bool) -> Result<()> {
let verge = Config::verge().await;
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
let enable_global_hotkey = !skip && verge.latest_ref().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -299,10 +283,6 @@ impl Hotkey {
enable_global_hotkey
);
if !enable_global_hotkey {
return Ok(());
}
// Extract hotkeys data before async operations
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
@@ -360,7 +340,7 @@ impl Hotkey {
}
}
}
self.current.lock().clone_from(&hotkeys);
self.current.store(Arc::new(hotkeys));
} else {
logging!(debug, Type::Hotkey, "No hotkeys configured");
}
@@ -391,8 +371,8 @@ impl Hotkey {
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
// Extract current hotkeys before async operations
let current_hotkeys = self.current.lock().clone();
let old_map = Self::get_map_from_vec(&current_hotkeys);
let current_hotkeys = &*self.current.load();
let old_map = Self::get_map_from_vec(current_hotkeys);
let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map);
@@ -406,7 +386,7 @@ impl Hotkey {
}
// Update the current hotkeys after all async operations
*self.current.lock() = new_hotkeys;
self.current.store(Arc::new(new_hotkeys));
Ok(())
}

View File

@@ -19,11 +19,11 @@ impl CoreManager {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
let clash_config = Config::clash().await.latest_ref().0.clone();
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
**Config::runtime().await.draft_mut() = IRuntime {
config: Some(clash_config.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
});
};
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
handle::Handle::notice_message(error_key, error_msg);
@@ -39,25 +39,20 @@ impl CoreManager {
return Ok((true, String::new()));
}
let _permit = self
.update_semaphore
.try_acquire()
.map_err(|_| anyhow!("Config update already in progress"))?;
self.perform_config_update().await
}
fn should_update_config(&self) -> Result<bool> {
let now = Instant::now();
let mut last = self.last_update.lock();
let last = self.get_last_update();
if let Some(last_time) = *last
&& now.duration_since(last_time) < timing::CONFIG_UPDATE_DEBOUNCE
if let Some(last_time) = last
&& now.duration_since(*last_time) < timing::CONFIG_UPDATE_DEBOUNCE
{
return Ok(false);
}
*last = Some(now);
self.set_last_update(now);
Ok(true)
}

View File

@@ -1,4 +1,5 @@
use super::{CoreManager, RunningMode};
use crate::config::{Config, ConfigType, IVerge};
use crate::{
core::{
logger::CLASH_LOGGER,
@@ -41,18 +42,12 @@ impl CoreManager {
self.start_core().await
}
pub async fn change_core(&self, clash_core: Option<String>) -> Result<(), String> {
use crate::config::{Config, ConfigType, IVerge};
let core = clash_core
.as_ref()
.ok_or_else(|| "Clash core cannot be None".to_string())?;
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
return Err(format!("Invalid clash core: {}", core).into());
pub async fn change_core(&self, clash_core: &String) -> Result<(), String> {
if !IVerge::VALID_CLASH_CORES.contains(&clash_core.as_str()) {
return Err(format!("Invalid clash core: {}", clash_core).into());
}
Config::verge().await.draft_mut().clash_core = clash_core;
Config::verge().await.draft_mut().clash_core = clash_core.to_owned().into();
Config::verge().await.apply();
let verge_data = Config::verge().await.latest_ref().clone();

View File

@@ -1,12 +1,10 @@
mod config;
mod lifecycle;
mod process;
mod state;
use anyhow::Result;
use parking_lot::Mutex;
use arc_swap::{ArcSwap, ArcSwapOption};
use std::{fmt, sync::Arc, time::Instant};
use tokio::sync::Semaphore;
use crate::process::CommandChildGuard;
use crate::singleton_lazy;
@@ -30,22 +28,21 @@ impl fmt::Display for RunningMode {
#[derive(Debug)]
pub struct CoreManager {
state: Arc<Mutex<State>>,
update_semaphore: Arc<Semaphore>,
last_update: Arc<Mutex<Option<Instant>>>,
state: ArcSwap<State>,
last_update: ArcSwapOption<Instant>,
}
#[derive(Debug)]
struct State {
running_mode: Arc<RunningMode>,
child_sidecar: Option<CommandChildGuard>,
running_mode: ArcSwap<RunningMode>,
child_sidecar: ArcSwapOption<CommandChildGuard>,
}
impl Default for State {
fn default() -> Self {
Self {
running_mode: Arc::new(RunningMode::NotRunning),
child_sidecar: None,
running_mode: ArcSwap::new(Arc::new(RunningMode::NotRunning)),
child_sidecar: ArcSwapOption::new(None),
}
}
}
@@ -53,28 +50,44 @@ impl Default for State {
impl Default for CoreManager {
fn default() -> Self {
Self {
state: Arc::new(Mutex::new(State::default())),
update_semaphore: Arc::new(Semaphore::new(1)),
last_update: Arc::new(Mutex::new(None)),
state: ArcSwap::new(Arc::new(State::default())),
last_update: ArcSwapOption::new(None),
}
}
}
impl CoreManager {
pub fn get_running_mode(&self) -> Arc<RunningMode> {
Arc::clone(&self.state.lock().running_mode)
Arc::clone(&self.state.load().running_mode.load())
}
pub fn take_child_sidecar(&self) -> Option<CommandChildGuard> {
self.state
.load()
.child_sidecar
.swap(None)
.and_then(|arc| Arc::try_unwrap(arc).ok())
}
pub fn get_last_update(&self) -> Option<Arc<Instant>> {
self.last_update.load_full()
}
pub fn set_running_mode(&self, mode: RunningMode) {
self.state.lock().running_mode = Arc::new(mode);
let state = self.state.load();
state.running_mode.store(Arc::new(mode));
}
pub fn set_running_child_sidecar(&self, child: CommandChildGuard) {
self.state.lock().child_sidecar = Some(child);
let state = self.state.load();
state.child_sidecar.store(Some(Arc::new(child)));
}
pub fn set_last_update(&self, time: Instant) {
self.last_update.store(Some(Arc::new(time)));
}
pub async fn init(&self) -> Result<()> {
self.cleanup_orphaned_processes().await?;
self.start_core().await?;
Ok(())
}

View File

@@ -1,244 +0,0 @@
use super::CoreManager;
#[cfg(windows)]
use crate::process::AsyncHandler;
use crate::{
constants::{process, timing},
logging,
utils::logging::Type,
};
use anyhow::Result;
#[cfg(windows)]
use anyhow::anyhow;
impl CoreManager {
pub async fn cleanup_orphaned_processes(&self) -> Result<()> {
logging!(info, Type::Core, "Cleaning orphaned mihomo processes");
let current_pid = self
.state
.lock()
.child_sidecar
.as_ref()
.and_then(|c| c.pid());
let target_processes = process::process_names();
let process_futures = target_processes.iter().map(|&name| {
let process_name = process::with_extension(name);
self.find_processes_by_name(process_name, name)
});
let process_results = futures::future::join_all(process_futures).await;
let pids_to_kill: Vec<_> = process_results
.into_iter()
.filter_map(Result::ok)
.flat_map(|(pids, name)| {
pids.into_iter()
.filter(move |&pid| Some(pid) != current_pid)
.map(move |pid| (pid, name.clone()))
})
.collect();
if pids_to_kill.is_empty() {
return Ok(());
}
let kill_futures = pids_to_kill
.iter()
.map(|(pid, name)| self.kill_process_verified(*pid, name.clone()));
let killed_count = futures::future::join_all(kill_futures)
.await
.into_iter()
.filter(|&success| success)
.count();
if killed_count > 0 {
logging!(
info,
Type::Core,
"Cleaned {} orphaned processes",
killed_count
);
}
Ok(())
}
async fn find_processes_by_name(
&self,
process_name: String,
_target: &str,
) -> Result<(Vec<u32>, String)> {
#[cfg(windows)]
{
use std::mem;
use winapi::um::{
handleapi::CloseHandle,
tlhelp32::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
},
};
let process_name_clone = process_name.clone();
let pids = AsyncHandler::spawn_blocking(move || -> Result<Vec<u32>> {
let mut pids = Vec::new();
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
return Err(anyhow!("Failed to create process snapshot"));
}
let mut pe32: PROCESSENTRY32W = mem::zeroed();
pe32.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
if Process32FirstW(snapshot, &mut pe32) != 0 {
loop {
let end_pos = pe32
.szExeFile
.iter()
.position(|&x| x == 0)
.unwrap_or(pe32.szExeFile.len());
let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]);
if exe_file.eq_ignore_ascii_case(&process_name_clone) {
pids.push(pe32.th32ProcessID);
}
if Process32NextW(snapshot, &mut pe32) == 0 {
break;
}
}
}
CloseHandle(snapshot);
}
Ok(pids)
})
.await??;
Ok((pids, process_name))
}
#[cfg(not(windows))]
{
let cmd = if cfg!(target_os = "macos") {
"pgrep"
} else {
"pidof"
};
let output = tokio::process::Command::new(cmd)
.arg(&process_name)
.output()
.await?;
if !output.status.success() {
return Ok((Vec::new(), process_name));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let pids: Vec<u32> = stdout
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
Ok((pids, process_name))
}
}
async fn kill_process_verified(&self, pid: u32, process_name: String) -> bool {
#[cfg(windows)]
let success = {
use winapi::um::{
handleapi::CloseHandle,
processthreadsapi::{OpenProcess, TerminateProcess},
winnt::{HANDLE, PROCESS_TERMINATE},
};
AsyncHandler::spawn_blocking(move || unsafe {
let handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
if handle.is_null() {
return false;
}
let result = TerminateProcess(handle, 1) != 0;
CloseHandle(handle);
result
})
.await
.unwrap_or(false)
};
#[cfg(not(windows))]
let success = tokio::process::Command::new("kill")
.args(["-9", &pid.to_string()])
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false);
if !success {
return false;
}
tokio::time::sleep(timing::PROCESS_VERIFY_DELAY).await;
if self.is_process_running(pid).await.unwrap_or(false) {
logging!(
warn,
Type::Core,
"Process {} (PID: {}) still running after termination",
process_name,
pid
);
false
} else {
logging!(
info,
Type::Core,
"Terminated process {} (PID: {})",
process_name,
pid
);
true
}
}
async fn is_process_running(&self, pid: u32) -> Result<bool> {
#[cfg(windows)]
{
use winapi::{
shared::minwindef::DWORD,
um::{
handleapi::CloseHandle,
processthreadsapi::{GetExitCodeProcess, OpenProcess},
winnt::{HANDLE, PROCESS_QUERY_INFORMATION},
},
};
AsyncHandler::spawn_blocking(move || unsafe {
let handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if handle.is_null() {
return Ok(false);
}
let mut exit_code: DWORD = 0;
let result = GetExitCodeProcess(handle, &mut exit_code);
CloseHandle(handle);
Ok(result != 0 && exit_code == 259)
})
.await?
}
#[cfg(not(windows))]
{
let output = tokio::process::Command::new("ps")
.args(["-p", &pid.to_string()])
.output()
.await?;
Ok(output.status.success() && !output.stdout.is_empty())
}
}
}

View File

@@ -93,8 +93,7 @@ impl CoreManager {
defer! {
self.set_running_mode(RunningMode::NotRunning);
}
let mut state = self.state.lock();
if let Some(child) = state.child_sidecar.take() {
if let Some(child) = self.take_child_sidecar() {
let pid = child.pid();
drop(child);
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);

View File

@@ -1,5 +1,6 @@
use crate::{
config::Config,
core::tray,
logging, logging_error,
utils::{dirs, init::service_writer_config, logging::Type},
};
@@ -531,6 +532,7 @@ impl ServiceManager {
return Err(anyhow::anyhow!("服务不可用: {}", reason));
}
}
let _ = tray::Tray::global().update_tray_display().await;
Ok(())
}
}

View File

@@ -15,6 +15,7 @@ use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_autostart::ManagerExt;
pub struct Sysopt {
initialed: AtomicBool,
update_sysproxy: AtomicBool,
reset_sysproxy: AtomicBool,
}
@@ -84,6 +85,7 @@ async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()>
impl Default for Sysopt {
fn default() -> Self {
Sysopt {
initialed: AtomicBool::new(false),
update_sysproxy: AtomicBool::new(false),
reset_sysproxy: AtomicBool::new(false),
}
@@ -94,17 +96,22 @@ impl Default for Sysopt {
singleton_lazy!(Sysopt, SYSOPT, Sysopt::default);
impl Sysopt {
pub fn is_initialed(&self) -> bool {
self.initialed.load(Ordering::SeqCst)
}
pub fn init_guard_sysproxy(&self) -> Result<()> {
// 使用事件驱动代理管理器
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_app_started();
log::info!(target: "app", "已启用事件驱动代理守卫");
logging!(info, Type::Core, "已启用事件驱动代理守卫");
Ok(())
}
/// init the sysproxy
pub async fn update_sysproxy(&self) -> Result<()> {
self.initialed.store(true, Ordering::SeqCst);
if self
.update_sysproxy
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
@@ -224,14 +231,22 @@ impl Sysopt {
let mut sysproxy: Sysproxy = match Sysproxy::get_system_proxy() {
Ok(sp) => sp,
Err(e) => {
log::warn!(target: "app", "重置代理时获取系统代理配置失败: {e}, 使用默认配置");
logging!(
warn,
Type::Core,
"Warning: 重置代理时获取系统代理配置失败: {e}, 使用默认配置"
);
Sysproxy::default()
}
};
let mut autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap,
Err(e) => {
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
logging!(
warn,
Type::Core,
"Warning: 重置代理时获取自动代理配置失败: {e}, 使用默认配置"
);
Autoproxy::default()
}
};
@@ -265,14 +280,14 @@ impl Sysopt {
{
if is_enable {
if let Err(e) = startup_shortcut::create_shortcut().await {
log::error!(target: "app", "创建启动快捷方式失败: {e}");
logging!(error, Type::Setup, "创建启动快捷方式失败: {e}");
// 如果快捷方式创建失败,回退到原来的方法
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
} else if let Err(e) = startup_shortcut::remove_shortcut().await {
log::error!(target: "app", "删除启动快捷方式失败: {e}");
logging!(error, Type::Setup, "删除启动快捷方式失败: {e}");
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
@@ -307,11 +322,11 @@ impl Sysopt {
{
match startup_shortcut::is_shortcut_enabled() {
Ok(enabled) => {
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
logging!(info, Type::System, "快捷方式自启动状态: {enabled}");
return Ok(enabled);
}
Err(e) => {
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
logging!(error, Type::System, "检查快捷方式失败,尝试原来的方法: {e}");
}
}
}
@@ -322,11 +337,11 @@ impl Sysopt {
match autostart_manager.is_enabled() {
Ok(status) => {
log::info!(target: "app", "Auto launch status: {status}");
logging!(info, Type::System, "Auto launch status: {status}");
Ok(status)
}
Err(e) => {
log::error!(target: "app", "Failed to get auto launch status: {e}");
logging!(error, Type::System, "Failed to get auto launch status: {e}");
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
}
}

View File

@@ -1,4 +1,7 @@
use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type};
use crate::{
config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton,
utils::logging::Type,
};
use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use parking_lot::RwLock;
@@ -10,7 +13,9 @@ use std::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Duration,
};
use tokio::time::{sleep, timeout};
type TaskID = u64;
@@ -99,6 +104,12 @@ impl Timer {
items
.iter()
.filter_map(|item| {
let allow_auto_update =
item.option.as_ref()?.allow_auto_update.unwrap_or_default();
if !allow_auto_update {
return None;
}
let interval = item.option.as_ref()?.update_interval? as i64;
let updated = item.updated? as i64;
let uid = item.uid.as_ref()?;
@@ -149,7 +160,7 @@ impl Timer {
.set_maximum_parallel_runnable_num(1)
.set_frequency_count_down_by_seconds(3, 3)
.spawn_async_routine(|| async move {
logging!(info, Type::Timer, "Updating tray menu");
logging!(debug, Type::Timer, "Updating tray menu");
crate::core::tray::Tray::global()
.update_tray_display()
.await
@@ -384,7 +395,8 @@ impl Timer {
.spawn_async_routine(move || {
let uid = uid.clone();
Box::pin(async move {
Self::async_task(uid).await;
Self::wait_until_sysopt(Duration::from_millis(1000)).await;
Self::async_task(&uid).await;
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
})
.context("failed to create timer task")?;
@@ -413,13 +425,15 @@ impl Timer {
};
// Get the profile updated timestamp - now safe to await
let config_profiles = Config::profiles().await;
let profiles = config_profiles.data_ref().clone();
let items = match profiles.get_items() {
Some(i) => i,
None => {
logging!(warn, Type::Timer, "获取配置列表失败");
return None;
let items = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref();
match profiles_guard.get_items() {
Some(i) => i.clone(),
None => {
logging!(warn, Type::Timer, "获取配置列表失败");
return None;
}
}
};
@@ -468,14 +482,14 @@ impl Timer {
}
/// Async task with better error handling and logging
async fn async_task(uid: String) {
async fn async_task(uid: &String) {
let task_start = std::time::Instant::now();
logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(&uid, true);
Self::emit_update_event(uid, true);
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid);
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(uid);
logging!(
info,
Type::Timer,
@@ -484,7 +498,7 @@ impl Timer {
is_current
);
feat::update_profile(uid.clone(), None, Some(is_current)).await
feat::update_profile(uid, None, is_current, false).await
})
.await
{
@@ -509,7 +523,17 @@ impl Timer {
}
// Emit completed event
Self::emit_update_event(&uid, false);
Self::emit_update_event(uid, false);
}
async fn wait_until_sysopt(max_wait: Duration) {
let _ = timeout(max_wait, async {
while !Sysopt::global().is_initialed() {
logging!(warn, Type::Timer, "Waiting for Sysopt to be initialized...");
sleep(Duration::from_millis(30)).await;
}
})
.await;
}
}

View File

@@ -39,6 +39,8 @@ define_menu! {
core_dir => CORE_DIR, "tray_core_dir", "Core Dir",
logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir",
open_dir => OPEN_DIR, "tray_open_dir", "Open Dir",
app_log => APP_LOG, "tray_app_log", "Open App Log",
core_log => CORE_LOG, "tray_core_log", "Open Core Log",
restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core",
restart_app => RESTART_APP, "tray_restart_app", "Restart App",
verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version",

View File

@@ -2,9 +2,11 @@ use once_cell::sync::OnceCell;
use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
use tauri_plugin_mihomo::models::Proxies;
use tokio::fs;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::config::PrfSelected;
use crate::core::service;
use crate::module::lightweight;
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
@@ -25,7 +27,6 @@ use smartstring::alias::String;
use std::collections::HashMap;
use std::sync::Arc;
use std::{
fs,
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
@@ -61,8 +62,12 @@ fn should_handle_tray_click() -> bool {
*last_click = now;
true
} else {
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
now.duration_since(*last_click).as_millis());
logging!(
debug,
Type::Tray,
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
now.duration_since(*last_click).as_millis()
);
false
}
}
@@ -85,7 +90,7 @@ impl TrayState {
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
&& let Ok(icon_data) = fs::read(common_icon_path)
&& let Ok(icon_data) = fs::read(common_icon_path).await
{
return (true, icon_data);
}
@@ -122,7 +127,7 @@ impl TrayState {
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
&& let Ok(icon_data) = fs::read(sysproxy_icon_path)
&& let Ok(icon_data) = fs::read(sysproxy_icon_path).await
{
return (true, icon_data);
}
@@ -159,7 +164,7 @@ impl TrayState {
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
&& let Ok(icon_data) = fs::read(tun_icon_path)
&& let Ok(icon_data) = fs::read(tun_icon_path).await
{
return (true, icon_data);
}
@@ -206,7 +211,7 @@ singleton_lazy!(Tray, TRAY, Tray::default);
impl Tray {
pub async fn init(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘初始化");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘初始化");
return Ok(());
}
@@ -214,11 +219,15 @@ impl Tray {
match self.create_tray_from_handle(app_handle).await {
Ok(_) => {
log::info!(target: "app", "System tray created successfully");
logging!(info, Type::Tray, "System tray created successfully");
}
Err(e) => {
// Don't return error, let application continue running without tray
log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e);
logging!(
warn,
Type::Tray,
"System tray creation failed: {e}, Application will continue running without tray icon",
);
}
}
// TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白
@@ -229,7 +238,7 @@ impl Tray {
/// 更新托盘点击行为
pub async fn update_click_behavior(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘点击行为更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘点击行为更新");
return Ok(());
}
@@ -249,7 +258,7 @@ impl Tray {
/// 更新托盘菜单
pub async fn update_menu(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘菜单更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新");
return Ok(());
}
// 调整最小更新间隔,确保状态及时刷新
@@ -297,6 +306,8 @@ impl Tray {
let verge = Config::verge().await.latest_ref().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let tun_mode_available = cmd::system::is_admin().unwrap_or_default()
|| service::is_service_available().await.is_ok();
let mode = {
Config::clash()
.await
@@ -322,16 +333,21 @@ impl Tray {
Some(mode.as_str()),
*system_proxy,
*tun_mode,
tun_mode_available,
profile_uid_and_name,
is_lightweight_mode,
)
.await?,
));
log::debug!(target: "app", "托盘菜单更新成功");
logging!(debug, Type::Tray, "托盘菜单更新成功");
Ok(())
}
None => {
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray menu: tray not found"
);
Ok(())
}
}
@@ -341,7 +357,7 @@ impl Tray {
#[cfg(target_os = "macos")]
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
return Ok(());
}
@@ -350,7 +366,11 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray icon: tray not found"
);
return Ok(());
}
};
@@ -380,7 +400,7 @@ impl Tray {
#[cfg(not(target_os = "macos"))]
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
return Ok(());
}
@@ -389,7 +409,11 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray icon: tray not found"
);
return Ok(());
}
};
@@ -412,7 +436,7 @@ impl Tray {
/// 更新托盘显示状态的函数
pub async fn update_tray_display(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘显示状态更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新");
return Ok(());
}
@@ -430,7 +454,7 @@ impl Tray {
/// 更新托盘提示
pub async fn update_tooltip(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘提示更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘提示更新");
return Ok(());
}
@@ -486,7 +510,11 @@ impl Tray {
if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&tooltip));
} else {
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray tooltip: tray not found"
);
}
Ok(())
@@ -494,7 +522,7 @@ impl Tray {
pub async fn update_part(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘局部更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新");
return Ok(());
}
// self.update_menu().await?;
@@ -507,11 +535,11 @@ impl Tray {
pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘创建");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建");
return Ok(());
}
log::info!(target: "app", "正在从AppHandle创建系统托盘");
logging!(info, Type::Tray, "正在从AppHandle创建系统托盘");
// 获取图标
let icon_bytes = TrayState::get_common_tray_icon().await.1;
@@ -557,7 +585,7 @@ impl Tray {
AsyncHandler::spawn(|| async move {
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
log::debug!(target: "app", "tray event: {tray_event:?}");
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
if let TrayIconEvent::Click {
button: MouseButton::Left,
@@ -592,14 +620,13 @@ impl Tray {
});
});
tray.on_menu_event(on_menu_event);
log::info!(target: "app", "系统托盘创建成功");
Ok(())
}
// 托盘统一的状态更新函数
pub async fn update_all_states(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘状态更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新");
return Ok(());
}
@@ -645,17 +672,15 @@ async fn create_profile_menu_item(
.iter()
.map(|(profile_uid, profile_name)| {
let app_handle = app_handle.clone();
let profile_uid = profile_uid.clone();
let profile_name = profile_name.clone();
async move {
let is_current_profile = Config::profiles()
.await
.latest_ref()
.is_current_profile_index(profile_uid.clone());
.is_current_profile_index(profile_uid);
CheckMenuItem::with_id(
&app_handle,
format!("profiles_{profile_uid}"),
t(&profile_name).await,
t(profile_name).await,
true,
is_current_profile,
None::<&str>,
@@ -726,7 +751,9 @@ fn create_subcreate_proxy_menu_item(
is_selected,
None::<&str>,
)
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
.map_err(|e| {
logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e)
})
.ok()
})
.collect();
@@ -768,7 +795,12 @@ fn create_subcreate_proxy_menu_item(
let insertion_index = submenus.len();
submenus.push((group_name.into(), insertion_index, submenu));
} else {
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
logging!(
warn,
Type::Tray,
"Failed to create proxy group submenu: {}",
group_name
);
}
}
}
@@ -837,6 +869,7 @@ async fn create_tray_menu(
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
tun_mode_available: bool,
profile_uid_and_name: Vec<(String, String)>,
is_lightweight_mode: bool,
) -> Result<tauri::menu::Menu<Wry>> {
@@ -980,7 +1013,7 @@ async fn create_tray_menu(
app_handle,
MenuIds::TUN_MODE,
&texts.tun_mode,
true,
tun_mode_available,
tun_mode_enabled,
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
)?;
@@ -1034,12 +1067,34 @@ async fn create_tray_menu(
None::<&str>,
)?;
let open_app_log = &MenuItem::with_id(
app_handle,
MenuIds::APP_LOG,
&texts.app_log,
true,
None::<&str>,
)?;
let open_core_log = &MenuItem::with_id(
app_handle,
MenuIds::CORE_LOG,
&texts.core_log,
true,
None::<&str>,
)?;
let open_dir = &Submenu::with_id_and_items(
app_handle,
MenuIds::OPEN_DIR,
&texts.open_dir,
true,
&[open_app_dir, open_core_dir, open_logs_dir],
&[
open_app_dir,
open_core_dir,
open_logs_dir,
open_app_log,
open_core_log,
],
)?;
let restart_clash = &MenuItem::with_id(
@@ -1138,7 +1193,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into()).await;
}
MenuIds::DASHBOARD => {
log::info!(target: "app", "托盘菜单点击: 打开窗口");
logging!(info, Type::Tray, "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() {
return;
@@ -1155,7 +1210,11 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
MenuIds::CLOSE_ALL_CONNECTIONS => {
if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await {
log::error!(target: "app", "Failed to close all connections from tray: {err}");
logging!(
error,
Type::Tray,
"Failed to close all connections from tray: {err}"
);
}
}
MenuIds::COPY_ENV => feat::copy_clash_env().await,
@@ -1169,6 +1228,12 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
MenuIds::LOGS_DIR => {
let _ = cmd::open_logs_dir().await;
}
MenuIds::APP_LOG => {
let _ = cmd::open_app_log().await;
}
MenuIds::CORE_LOG => {
let _ = cmd::open_core_log().await;
}
MenuIds::RESTART_CLASH => feat::restart_clash_core().await,
MenuIds::RESTART_APP => feat::restart_app().await,
MenuIds::LIGHTWEIGHT_MODE => {
@@ -1202,12 +1267,25 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
.await
{
Ok(_) => {
log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name);
logging!(
info,
Type::Tray,
"切换代理成功: {} -> {}",
group_name,
proxy_name
);
let _ = handle::Handle::app_handle()
.emit("verge://refresh-proxy-config", ());
}
Err(e) => {
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
logging!(
error,
Type::Tray,
"切换代理失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
e
);
// Fallback to IPC update
if (handle::Handle::mihomo()
@@ -1216,7 +1294,13 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
.await)
.is_ok()
{
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
logging!(
info,
Type::Tray,
"代理切换回退成功: {} -> {}",
group_name,
proxy_name
);
let app_handle = handle::Handle::app_handle();
let _ = app_handle.emit("verge://force-refresh-proxies", ());
@@ -1230,7 +1314,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
// Ensure tray state update is awaited and properly handled
if let Err(e) = Tray::global().update_all_states().await {
log::warn!(target: "app", "更新托盘状态失败: {e}");
logging!(warn, Type::Tray, "Failed to update tray state: {e}");
}
});
}

View File

@@ -1,9 +1,9 @@
use anyhow::Result;
use scopeguard::defer;
use smartstring::alias::String;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri_plugin_shell::ShellExt;
use tokio::fs;
use crate::config::{Config, ConfigType};
use crate::core::handle;
@@ -33,19 +33,16 @@ impl CoreConfigValidator {
impl CoreConfigValidator {
/// 检查文件是否为脚本文件
fn is_script_file<P>(path: P) -> Result<bool>
where
P: AsRef<Path> + std::fmt::Display,
{
async fn is_script_file(path: &str) -> Result<bool> {
// 1. 先通过扩展名快速判断
if has_ext(&path, "yaml") || has_ext(&path, "yml") {
if has_ext(path, "yaml") || has_ext(path, "yml") {
return Ok(false); // YAML文件不是脚本文件
} else if has_ext(&path, "js") {
} else if has_ext(path, "js") {
return Ok(true); // JS文件是脚本文件
}
// 2. 读取文件内容
let content = match std::fs::read_to_string(&path) {
let content = match fs::read_to_string(path).await {
Ok(content) => content,
Err(err) => {
logging!(
@@ -115,11 +112,11 @@ impl CoreConfigValidator {
}
/// 只进行文件语法检查,不进行完整验证
fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
async fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
logging!(info, Type::Validate, "开始检查文件: {}", config_path);
// 读取文件内容
let content = match std::fs::read_to_string(config_path) {
let content = match fs::read_to_string(config_path).await {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {err}").into();
@@ -144,9 +141,9 @@ impl CoreConfigValidator {
}
/// 验证脚本文件语法
fn validate_script_file(path: &str) -> Result<(bool, String)> {
async fn validate_script_file(path: &str) -> Result<(bool, String)> {
// 读取脚本内容
let content = match std::fs::read_to_string(path) {
let content = match fs::read_to_string(path).await {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {err}").into();
@@ -216,14 +213,14 @@ impl CoreConfigValidator {
"检测到Merge文件仅进行语法检查: {}",
config_path
);
return Self::validate_file_syntax(config_path);
return Self::validate_file_syntax(config_path).await;
}
// 检查是否为脚本文件
let is_script = if config_path.ends_with(".js") {
true
} else {
match Self::is_script_file(config_path) {
match Self::is_script_file(config_path).await {
Ok(result) => result,
Err(err) => {
// 如果无法确定文件类型尝试使用Clash内核验证
@@ -246,7 +243,7 @@ impl CoreConfigValidator {
"检测到脚本文件使用JavaScript验证: {}",
config_path
);
return Self::validate_script_file(config_path);
return Self::validate_script_file(config_path).await;
}
// 对YAML配置文件使用Clash内核验证

View File

@@ -5,7 +5,7 @@ use crate::{
};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::fs;
use tokio::fs;
#[derive(Debug, Clone)]
pub struct ChainItem {
@@ -83,7 +83,7 @@ impl AsyncChainItemFrom for Option<ChainItem> {
match itype {
"script" => Some(ChainItem {
uid,
data: ChainType::Script(fs::read_to_string(path).ok()?.into()),
data: ChainType::Script(fs::read_to_string(path).await.ok()?.into()),
}),
"merge" => Some(ChainItem {
uid,

View File

@@ -1,3 +1,5 @@
use crate::{logging, utils::logging::Type};
use super::use_lowercase;
use serde_yaml_ng::{self, Mapping, Value};
@@ -19,7 +21,11 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
deep_merge(&mut config, &Value::from(merge));
config.as_mapping().cloned().unwrap_or_else(|| {
log::error!("Failed to convert merged config to mapping, using empty mapping");
logging!(
error,
Type::Core,
"Failed to convert merged config to mapping, using empty mapping"
);
Mapping::new()
})
}

View File

@@ -6,10 +6,14 @@ pub mod seq;
mod tun;
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
use crate::constants;
use crate::utils::dirs;
use crate::{config::Config, utils::tmpl};
use crate::{logging, utils::logging::Type};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tokio::fs;
type ResultLog = Vec<(String, String)>;
#[derive(Debug)]
@@ -136,7 +140,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&merge_uid).ok().cloned()
profiles.get_item(merge_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -153,7 +157,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&script_uid).ok().cloned()
profiles.get_item(script_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -170,7 +174,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&rules_uid).ok().cloned()
profiles.get_item(rules_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -187,7 +191,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&proxies_uid).ok().cloned()
profiles.get_item(proxies_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -204,7 +208,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&groups_uid).ok().cloned()
profiles.get_item(groups_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -221,7 +225,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&"Merge".into()).ok().cloned()
profiles.get_item("Merge").ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -238,7 +242,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&"Script".into()).ok().cloned()
profiles.get_item("Script").ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -420,14 +424,14 @@ fn apply_builtin_scripts(
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c)
.for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid);
logging!(debug, Type::Core, "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned(), "".into()) {
Ok((res_config, _)) => {
config = res_config;
}
Err(err) => {
log::error!(target: "app", "builtin script error `{err}`");
logging!(error, Type::Core, "builtin script error `{err}`");
}
}
}
@@ -437,34 +441,29 @@ fn apply_builtin_scripts(
config
}
fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
if enable_dns_settings {
use crate::utils::dirs;
use std::fs;
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
if let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join("dns_config.yaml");
if dns_path.exists()
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path)
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
if dns_path.exists()
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path).await
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
if let Some(hosts_value) = dns_config.get("hosts")
&& hosts_value.is_mapping()
{
if let Some(hosts_value) = dns_config.get("hosts")
&& hosts_value.is_mapping()
{
config.insert("hosts".into(), hosts_value.clone());
log::info!(target: "app", "apply hosts configuration");
}
config.insert("hosts".into(), hosts_value.clone());
logging!(info, Type::Core, "apply hosts configuration");
}
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
log::info!(target: "app", "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
logging!(info, Type::Core, "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
logging!(info, Type::Core, "apply dns_config.yaml");
}
}
}
@@ -540,7 +539,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
config = use_sort(config);
// dns settings
config = apply_dns_settings(config, enable_dns_settings);
config = apply_dns_settings(config, enable_dns_settings).await;
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys);

View File

@@ -2,6 +2,8 @@ use serde_yaml_ng::{Mapping, Value};
#[cfg(target_os = "macos")]
use crate::process::AsyncHandler;
#[cfg(target_os = "linux")]
use crate::{logging, utils::logging::Type};
macro_rules! revise {
($map: expr, $key: expr, $val: expr) => {
@@ -42,9 +44,10 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
if should_override {
revise!(tun_val, "stack", "mixed");
log::warn!(
target: "app",
"gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility"
logging!(
warn,
Type::Network,
"Warning: gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility"
);
}
}

View File

@@ -2,6 +2,7 @@ use crate::{
config::{Config, IVerge},
core::backup,
logging, logging_error,
process::AsyncHandler,
utils::{
dirs::{PathBufExec, app_home_dir, local_backup_dir},
logging::Type,
@@ -12,7 +13,8 @@ use chrono::Utc;
use reqwest_dav::list_cmd::ListFile;
use serde::Serialize;
use smartstring::alias::String;
use std::{fs, path::PathBuf};
use std::path::PathBuf;
use tokio::fs;
#[derive(Debug, Serialize)]
pub struct LocalBackupFile {
@@ -24,7 +26,7 @@ pub struct LocalBackupFile {
/// Create a backup and upload to WebDAV
pub async fn create_backup_and_upload_webdav() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
logging!(error, Type::Backup, "Failed to create backup: {err:#?}");
err
})?;
@@ -97,12 +99,14 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
})?;
// extract zip file
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
let value = backup_storage_path.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
let mut zip = zip::ZipArchive::new(file)?;
zip.extract(app_home_dir()?)?;
logging_error!(
Type::Backup,
super::patch_verge(
IVerge {
&IVerge {
webdav_url,
webdav_username,
webdav_password,
@@ -119,7 +123,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
/// Create a backup and save to local storage
pub async fn create_local_backup() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
logging!(
error,
Type::Backup,
@@ -131,7 +135,7 @@ pub async fn create_local_backup() -> Result<()> {
let backup_dir = local_backup_dir()?;
let target_path = backup_dir.join(file_name.as_str());
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()) {
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await {
logging!(
error,
Type::Backup,
@@ -151,12 +155,12 @@ pub async fn create_local_backup() -> Result<()> {
Ok(())
}
fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
if let Some(parent) = to.parent() {
fs::create_dir_all(parent)?;
fs::create_dir_all(parent).await?;
}
match fs::rename(&from, &to) {
match fs::rename(&from, &to).await {
Ok(_) => Ok(()),
Err(rename_err) => {
// Attempt copy + remove as fallback, covering cross-device moves
@@ -165,8 +169,11 @@ fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
Type::Backup,
"Failed to rename backup file directly, fallback to copy/remove: {rename_err:#?}"
);
fs::copy(&from, &to).map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?;
fs::copy(&from, &to)
.await
.map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?;
fs::remove_file(&from)
.await
.map_err(|err| anyhow!("Failed to remove temp backup file: {err:#?}"))?;
Ok(())
}
@@ -174,24 +181,25 @@ fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
}
/// List local backups
pub fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
let backup_dir = local_backup_dir()?;
if !backup_dir.exists() {
return Ok(vec![]);
}
let mut backups = Vec::new();
for entry in fs::read_dir(&backup_dir)? {
let entry = entry?;
let mut dir = fs::read_dir(&backup_dir).await?;
while let Some(entry) = dir.next_entry().await? {
let path = entry.path();
if !path.is_file() {
let metadata = entry.metadata().await?;
if !metadata.is_file() {
continue;
}
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
let file_name = match path.file_name().and_then(|name| name.to_str()) {
Some(name) => name,
None => continue,
};
let metadata = entry.metadata()?;
let last_modified = metadata
.modified()
.map(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339())
@@ -233,18 +241,23 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
return Err(anyhow!("Backup file not found: {}", filename));
}
let verge = Config::verge().await;
let verge_data = verge.latest_ref().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let (webdav_url, webdav_username, webdav_password) = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
(
verge.webdav_url.clone(),
verge.webdav_username.clone(),
verge.webdav_password.clone(),
)
};
let mut zip = zip::ZipArchive::new(fs::File::open(&target_path)?)?;
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
let mut zip = zip::ZipArchive::new(file)?;
zip.extract(app_home_dir()?)?;
logging_error!(
Type::Backup,
super::patch_verge(
IVerge {
&IVerge {
webdav_url,
webdav_username,
webdav_password,
@@ -258,7 +271,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
}
/// Export local backup file to user selected destination
pub fn export_local_backup(filename: String, destination: String) -> Result<()> {
pub async fn export_local_backup(filename: String, destination: String) -> Result<()> {
let backup_dir = local_backup_dir()?;
let source_path = backup_dir.join(filename.as_str());
if !source_path.exists() {
@@ -267,10 +280,11 @@ pub fn export_local_backup(filename: String, destination: String) -> Result<()>
let dest_path = PathBuf::from(destination.as_str());
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
fs::create_dir_all(parent).await?;
}
fs::copy(&source_path, &dest_path)
.await
.map(|_| ())
.map_err(|err| anyhow!("Failed to export backup file: {err:#?}"))?;
Ok(())

View File

@@ -1,7 +1,7 @@
use crate::{
config::Config,
core::{CoreManager, handle, tray},
logging_error,
logging, logging_error,
process::AsyncHandler,
utils::{self, logging::Type, resolve},
};
@@ -17,7 +17,7 @@ pub async fn restart_clash_core() {
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
logging!(error, Type::Core, "{err}");
}
}
}
@@ -30,7 +30,7 @@ pub async fn restart_app() {
"restart_app::error",
format!("Failed to cleanup resources: {err}"),
);
log::error!(target:"app", "Restart failed during cleanup: {err}");
logging!(error, Type::Core, "Restart failed during cleanup: {err}");
return;
}
@@ -50,7 +50,7 @@ fn after_change_clash_mode() {
}
}
Err(err) => {
log::error!(target: "app", "Failed to get connections: {err}");
logging!(error, Type::Core, "Failed to get connections: {err}");
}
}
});
@@ -64,7 +64,7 @@ pub async fn change_clash_mode(mode: String) {
let json_value = serde_json::json!({
"mode": mode
});
log::debug!(target: "app", "change clash mode to {mode}");
logging!(debug, Type::Core, "change clash mode to {mode}");
match handle::Handle::mihomo()
.await
.patch_base_config(&json_value)
@@ -91,7 +91,7 @@ pub async fn change_clash_mode(mode: String) {
after_change_clash_mode();
}
}
Err(err) => log::error!(target: "app", "{err}"),
Err(err) => logging!(error, Type::Core, "{err}"),
}
}
@@ -123,7 +123,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
match response {
Ok(response) => {
log::trace!(target: "app", "test_delay response: {response:#?}");
logging!(trace, Type::Network, "test_delay response: {response:#?}");
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
@@ -131,7 +131,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
}
}
Err(err) => {
log::trace!(target: "app", "test_delay error: {err:#?}");
logging!(trace, Type::Network, "test_delay error: {err:#?}");
Err(err)
}
}

View File

@@ -226,15 +226,12 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
Ok(())
}
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge()
.await
.draft_mut()
.patch_config(patch.clone());
pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.draft_mut().patch_config(patch);
let update_flags = determine_update_flags(&patch);
let update_flags = determine_update_flags(patch);
let process_flag_result: std::result::Result<(), anyhow::Error> = {
process_terminated_flags(update_flags, &patch).await?;
process_terminated_flags(update_flags, patch).await?;
Ok(())
};
@@ -245,7 +242,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.apply();
if !not_save_file {
// 分离数据获取和异步调用
let verge_data = Config::verge().await.data_mut().clone();
let verge_data = Config::verge().await.data_ref().clone();
verge_data.save_file().await?;
}
Ok(())

View File

@@ -18,36 +18,57 @@ pub async fn toggle_proxy_profile(profile_index: String) {
}
}
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Tray, "{err}");
}
}
}
async fn should_update_profile(uid: String) -> Result<Option<(String, Option<PrfOption>)>> {
async fn should_update_profile(
uid: &String,
ignore_auto_update: bool,
) -> Result<Option<(String, Option<PrfOption>)>> {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
let item = profiles.get_item(&uid)?;
let item = profiles.get_item(uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
logging!(
info,
Type::Config,
"[订阅更新] {uid} 不是远程订阅,跳过更新"
);
Ok(None)
} else if item.url.is_none() {
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL无法更新");
logging!(
warn,
Type::Config,
"Warning: [订阅更新] {uid} 缺少URL无法更新"
);
bail!("failed to get the profile item url");
} else if !item
.option
.as_ref()
.and_then(|o| o.allow_auto_update)
.unwrap_or(true)
} else if !ignore_auto_update
&& !item
.option
.as_ref()
.and_then(|o| o.allow_auto_update)
.unwrap_or(true)
{
log::info!(target: "app", "[订阅更新] {} 禁止自动更新,跳过更新", uid);
logging!(
info,
Type::Config,
"[订阅更新] {} 禁止自动更新,跳过更新",
uid
);
Ok(None)
} else {
log::info!(target: "app",
logging!(
info,
Type::Config,
"[订阅更新] {} 是远程订阅URL: {}",
uid,
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
item.url
.clone()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
);
Ok(Some((
item.url
@@ -59,79 +80,111 @@ async fn should_update_profile(uid: String) -> Result<Option<(String, Option<Prf
}
async fn perform_profile_update(
uid: String,
url: String,
opt: Option<PrfOption>,
option: Option<PrfOption>,
uid: &String,
url: &String,
opt: Option<&PrfOption>,
option: Option<&PrfOption>,
) -> Result<bool> {
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
logging!(info, Type::Config, "[订阅更新] 开始下载新的订阅内容");
let mut merged_opt = PrfOption::merge(opt, option);
let is_current = {
let profiles = Config::profiles().await;
profiles.latest_ref().is_current_profile_index(uid)
};
let profile_name = {
let profiles = Config::profiles().await;
profiles
.latest_ref()
.get_name_by_uid(uid)
.unwrap_or_default()
};
let mut last_err;
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => {
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles().await;
profiles_draft_update_item_safe(uid.clone(), item).await?;
let is_current = Some(uid.clone()) == profiles.latest_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
Ok(is_current)
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(info, Type::Config, "[订阅更新] 更新订阅配置成功");
profiles_draft_update_item_safe(uid, &mut item).await?;
return Ok(is_current);
}
Err(err) => {
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err}尝试使用Clash代理更新");
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
let original_with_proxy = merged_opt.as_ref().and_then(|o| o.with_proxy);
let original_self_proxy = merged_opt.as_ref().and_then(|o| o.self_proxy);
let mut fallback_opt = merged_opt.unwrap_or_default();
fallback_opt.with_proxy = Some(false);
fallback_opt.self_proxy = Some(true);
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => {
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
if let Some(option) = item.option.as_mut() {
option.with_proxy = original_with_proxy;
option.self_proxy = original_self_proxy;
}
let profiles = Config::profiles().await;
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.data_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
Ok(is_current)
}
Err(retry_err) => {
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{retry_err}"),
);
Err(retry_err)
}
}
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err}尝试使用Clash代理更新"
);
last_err = err;
}
}
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(true);
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(false);
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(
info,
Type::Config,
"[订阅更新] 使用 Clash代理 更新订阅配置成功"
);
profiles_draft_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err}尝试使用Clash代理更新"
);
last_err = err;
}
}
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(false);
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(true);
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(
info,
Type::Config,
"[订阅更新] 使用 系统代理 更新订阅配置成功"
);
profiles_draft_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err},尝试使用系统代理更新"
);
last_err = err;
}
}
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{profile_name} - {last_err}"),
);
Ok(is_current)
}
pub async fn update_profile(
uid: String,
option: Option<PrfOption>,
auto_refresh: Option<bool>,
uid: &String,
option: Option<&PrfOption>,
auto_refresh: bool,
ignore_auto_update: bool,
) -> Result<()> {
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true);
let url_opt = should_update_profile(uid.clone()).await?;
let url_opt = should_update_profile(uid, ignore_auto_update).await?;
let should_refresh = match url_opt {
Some((url, opt)) => {
perform_profile_update(uid.clone(), url, opt, option).await? && auto_refresh
perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh
}
None => auto_refresh,
};
@@ -146,7 +199,7 @@ pub async fn update_profile(
Err(err) => {
logging!(error, Type::Config, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
}
}
}

View File

@@ -1,6 +1,8 @@
use crate::{
config::{Config, IVerge},
core::handle,
logging,
utils::logging::Type,
};
use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt;
@@ -16,11 +18,15 @@ pub async fn toggle_system_proxy() {
&& auto_close_connection
&& let Err(err) = handle::Handle::mihomo().await.close_all_connections().await
{
log::error!(target: "app", "Failed to close all connections: {err}");
logging!(
error,
Type::ProxyMode,
"Failed to close all connections: {err}"
);
}
let patch_result = super::patch_verge(
IVerge {
&IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
},
@@ -30,7 +36,7 @@ pub async fn toggle_system_proxy() {
match patch_result {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
}
}
@@ -40,7 +46,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = enable.unwrap_or(false);
match super::patch_verge(
IVerge {
&IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
},
@@ -49,7 +55,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
}
}
@@ -104,12 +110,16 @@ pub async fn copy_clash_env() {
}
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
_ => {
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
logging!(
error,
Type::ProxyMode,
"copy_clash_env: Invalid env type! {env_type}"
);
return;
}
};
if cliboard.write_text(export_text).is_err() {
log::error!(target: "app", "Failed to write to clipboard");
logging!(error, Type::ProxyMode, "Failed to write to clipboard");
}
}

View File

@@ -14,7 +14,7 @@ pub async fn open_or_close_dashboard() {
async fn open_or_close_dashboard_internal() {
let _ = lightweight::exit_lightweight_mode().await;
let result = WindowManager::toggle_main_window().await;
log::info!(target: "app", "Window toggle result: {result:?}");
logging!(info, Type::Window, "Window toggle result: {result:?}");
}
pub async fn quit() {
@@ -47,7 +47,7 @@ pub async fn clean_async() -> bool {
let tun_task = async {
let tun_enabled = Config::verge()
.await
.data_ref()
.latest_ref()
.enable_tun_mode
.unwrap_or(false);
@@ -71,16 +71,20 @@ pub async fn clean_async() -> bool {
.await
{
Ok(Ok(_)) => {
log::info!(target: "app", "TUN模式已禁用");
logging!(info, Type::Window, "TUN模式已禁用");
true
}
Ok(Err(e)) => {
log::warn!(target: "app", "禁用TUN模式失败: {e}");
logging!(warn, Type::Window, "Warning: 禁用TUN模式失败: {e}");
// 超时不阻塞退出
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时可能系统正在关机继续退出流程");
logging!(
warn,
Type::Window,
"Warning: 禁用TUN模式超时可能系统正在关机继续退出流程"
);
true
}
}
@@ -101,7 +105,7 @@ pub async fn clean_async() -> bool {
.unwrap_or(false);
if !sys_proxy_enabled {
log::info!(target: "app", "系统代理未启用,跳过重置");
logging!(info, Type::Window, "系统代理未启用,跳过重置");
return true;
}
@@ -110,19 +114,23 @@ pub async fn clean_async() -> bool {
if is_shutting_down {
// sysproxy-rs 操作注册表(避免.exe的dll错误)
log::info!(target: "app", "检测到正在关机syspro-rs操作注册表关闭系统代理");
logging!(
info,
Type::Window,
"检测到正在关机syspro-rs操作注册表关闭系统代理"
);
match Sysproxy::get_system_proxy() {
Ok(mut sysproxy) => {
sysproxy.enable = false;
if let Err(e) = sysproxy.set_system_proxy() {
log::warn!(target: "app", "关机时关闭系统代理失败: {e}");
logging!(warn, Type::Window, "Warning: 关机时关闭系统代理失败: {e}");
} else {
log::info!(target: "app", "系统代理已关闭(通过注册表)");
logging!(info, Type::Window, "系统代理已关闭(通过注册表)");
}
}
Err(e) => {
log::warn!(target: "app", "关机时获取代理设置失败: {e}");
logging!(warn, Type::Window, "Warning: 关机时获取代理设置失败: {e}");
}
}
@@ -136,7 +144,7 @@ pub async fn clean_async() -> bool {
}
// 正常退出:使用 sysproxy.exe 重置代理
log::info!(target: "app", "sysproxy.exe重置系统代理");
logging!(info, Type::Window, "sysproxy.exe重置系统代理");
match timeout(
Duration::from_secs(2),
@@ -145,15 +153,19 @@ pub async fn clean_async() -> bool {
.await
{
Ok(Ok(_)) => {
log::info!(target: "app", "系统代理已重置");
logging!(info, Type::Window, "系统代理已重置");
true
}
Ok(Err(e)) => {
log::warn!(target: "app", "重置系统代理失败: {e}");
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
true
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时,继续退出流程");
logging!(
warn,
Type::Window,
"Warning: 重置系统代理超时,继续退出流程"
);
true
}
}
@@ -169,11 +181,11 @@ pub async fn clean_async() -> bool {
.unwrap_or(false);
if !sys_proxy_enabled {
log::info!(target: "app", "系统代理未启用,跳过重置");
logging!(info, Type::Window, "系统代理未启用,跳过重置");
return true;
}
log::info!(target: "app", "开始重置系统代理...");
logging!(info, Type::Window, "开始重置系统代理...");
match timeout(
Duration::from_millis(1500),
@@ -182,15 +194,15 @@ pub async fn clean_async() -> bool {
.await
{
Ok(Ok(_)) => {
log::info!(target: "app", "系统代理已重置");
logging!(info, Type::Window, "系统代理已重置");
true
}
Ok(Err(e)) => {
log::warn!(target: "app", "重置系统代理失败: {e}");
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
true
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时,继续退出");
logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出");
true
}
}
@@ -206,11 +218,15 @@ pub async fn clean_async() -> bool {
match timeout(stop_timeout, CoreManager::global().stop_core()).await {
Ok(_) => {
log::info!(target: "app", "core已停止");
logging!(info, Type::Window, "core已停止");
true
}
Err(_) => {
log::warn!(target: "app", "停止core超时可能系统正在关机继续退出");
logging!(
warn,
Type::Window,
"Warning: 停止core超时可能系统正在关机继续退出"
);
true
}
}
@@ -226,11 +242,11 @@ pub async fn clean_async() -> bool {
.await
{
Ok(_) => {
log::info!(target: "app", "DNS设置已恢复");
logging!(info, Type::Window, "DNS设置已恢复");
true
}
Err(_) => {
log::warn!(target: "app", "恢复DNS设置超时");
logging!(warn, Type::Window, "Warning: 恢复DNS设置超时");
false
}
}

View File

@@ -10,6 +10,9 @@ mod feat;
mod module;
mod process;
pub mod utils;
use crate::constants::files;
#[cfg(target_os = "macos")]
use crate::module::lightweight;
#[cfg(target_os = "linux")]
use crate::utils::linux;
#[cfg(target_os = "macos")]
@@ -19,6 +22,7 @@ use crate::{
process::AsyncHandler,
utils::{resolve, server},
};
use anyhow::Result;
use config::Config;
use once_cell::sync::OnceCell;
use tauri::{AppHandle, Manager};
@@ -28,11 +32,8 @@ use tauri_plugin_deep_link::DeepLinkExt;
use utils::logging::Type;
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
/// Application initialization helper functions
mod app_init {
use anyhow::Result;
use super::*;
/// Initialize singleton monitoring for other instances
@@ -91,14 +92,14 @@ mod app_init {
}
app.deep_link().on_open_url(|event| {
let url = event.urls().first().map(|u| u.to_string());
if let Some(url) = url {
AsyncHandler::spawn(|| async {
if let Err(e) = resolve::resolve_scheme(url.into()).await {
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
}
});
}
let urls = event.urls();
AsyncHandler::spawn(move || async move {
if let Some(url) = urls.first()
&& let Err(e) = resolve::resolve_scheme(url.as_ref()).await
{
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
}
});
});
Ok(())
@@ -115,7 +116,7 @@ mod app_init {
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(app.config().identifier.clone());
.app_name(&app.config().identifier);
}
app.handle().plugin(auto_start_plugin_builder.build())?;
Ok(())
@@ -125,7 +126,7 @@ mod app_init {
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_filename(files::WINDOW_STATE)
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
app.handle().plugin(window_state_plugin)?;
@@ -141,6 +142,8 @@ mod app_init {
cmd::open_logs_dir,
cmd::open_web_url,
cmd::open_core_dir,
cmd::open_app_log,
cmd::open_core_log,
cmd::get_portable_flag,
cmd::get_network_interfaces,
cmd::get_system_hostname,
@@ -171,6 +174,7 @@ mod app_init {
cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::check_update_channel,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
@@ -285,6 +289,11 @@ pub fn run() {
pub async fn handle_reopen(has_visible_windows: bool) {
handle::Handle::global().init();
if lightweight::is_in_lightweight_mode() {
lightweight::exit_lightweight_mode().await;
return;
}
if !has_visible_windows {
handle::Handle::global().set_activation_policy_regular();
let _ = WindowManager::show_main_window().await;
@@ -326,10 +335,7 @@ pub fn run() {
.register_system_hotkey(SystemHotkey::CmdW)
.await;
}
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().init().await;
}
let _ = hotkey::Hotkey::global().init(true).await;
return;
}
@@ -350,8 +356,18 @@ pub fn run() {
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
AsyncHandler::spawn(move || async move {
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
let is_enable_global_hotkey = Config::verge()
.await
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().reset();
}
});
}
}
}

View File

@@ -10,6 +10,5 @@ fn main() {
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
}
}
app_lib::run();
}

View File

@@ -43,49 +43,48 @@ impl LightweightState {
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0);
static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0);
fn set_state(new: LightweightState) {
LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
match new {
LightweightState::Normal => {
logging!(info, Type::Lightweight, "轻量模式已关闭");
}
LightweightState::In => {
logging!(info, Type::Lightweight, "轻量模式已开启");
}
LightweightState::Exiting => {
logging!(info, Type::Lightweight, "正在退出轻量模式");
}
}
}
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
#[inline]
fn get_state() -> LightweightState {
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
}
// 检查是否处于轻量模式
#[inline]
fn try_transition(from: LightweightState, to: LightweightState) -> bool {
LIGHTWEIGHT_STATE
.compare_exchange(
from.as_u8(),
to.as_u8(),
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
}
#[inline]
fn record_state_and_log(state: LightweightState) {
LIGHTWEIGHT_STATE.store(state.as_u8(), Ordering::Release);
match state {
LightweightState::Normal => logging!(info, Type::Lightweight, "轻量模式已关闭"),
LightweightState::In => logging!(info, Type::Lightweight, "轻量模式已开启"),
LightweightState::Exiting => logging!(info, Type::Lightweight, "正在退出轻量模式"),
}
}
#[inline]
pub fn is_in_lightweight_mode() -> bool {
get_state() == LightweightState::In
}
// 设置轻量模式状态(仅 Normal <-> In
async fn set_lightweight_mode(value: bool) {
let current = get_state();
if value && current != LightweightState::In {
set_state(LightweightState::In);
} else if !value && current != LightweightState::Normal {
set_state(LightweightState::Normal);
}
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
async fn refresh_lightweight_tray_state() {
if let Err(err) = Tray::global().update_tray_display().await {
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
}
}
pub async fn run_once_auto_lightweight() {
pub async fn auto_lightweight_boot() -> Result<()> {
let verge_config = Config::verge().await;
let enable_auto = verge_config
.data_mut()
@@ -96,39 +95,23 @@ pub async fn run_once_auto_lightweight() {
.enable_silent_start
.unwrap_or(false);
if !(enable_auto && is_silent_start) {
logging!(
info,
Type::Lightweight,
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
);
return;
if is_silent_start {
logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式");
let _ = entry_lightweight_mode().await;
return Ok(());
}
set_lightweight_mode(true).await;
if !enable_auto {
logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化");
return Ok(());
}
logging!(
info,
Type::Lightweight,
"非静默启动:注册自动轻量模式监听器"
);
enable_auto_light_weight_mode().await;
}
pub async fn auto_lightweight_mode_init() -> Result<()> {
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto = {
Config::verge()
.await
.latest_ref()
.enable_auto_light_weight_mode
}
.unwrap_or(false);
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_state(LightweightState::Normal);
enable_auto_light_weight_mode().await;
}
Ok(())
}
@@ -151,60 +134,33 @@ pub fn disable_auto_light_weight_mode() {
}
pub async fn entry_lightweight_mode() -> bool {
// 尝试从 Normal -> In
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::Normal as u8,
LightweightState::In as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
if !try_transition(LightweightState::Normal, LightweightState::In) {
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
refresh_lightweight_tray_state().await;
return false;
}
record_state_and_log(LightweightState::In);
WindowManager::destroy_main_window();
set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer();
// 回到 In
set_state(LightweightState::In);
refresh_lightweight_tray_state().await;
true
}
// 添加从轻量模式恢复的函数
pub async fn exit_lightweight_mode() -> bool {
// 尝试从 In -> Exiting
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::In as u8,
LightweightState::Exiting as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
if !try_transition(LightweightState::In, LightweightState::Exiting) {
logging!(
info,
Type::Lightweight,
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
refresh_lightweight_tray_state().await;
return false;
}
record_state_and_log(LightweightState::Exiting);
WindowManager::show_main_window().await;
set_lightweight_mode(false).await;
let _ = cancel_light_weight_timer();
// 回到 Normal
set_state(LightweightState::Normal);
logging!(info, Type::Lightweight, "轻量模式退出完成");
record_state_and_log(LightweightState::Normal);
refresh_lightweight_tray_state().await;
true
}
@@ -215,24 +171,31 @@ pub async fn add_light_weight_timer() {
fn setup_window_close_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = window.listen("tauri://close-requested", move |_event| {
let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
if let Err(e) = setup_light_weight_timer().await {
log::warn!("Failed to setup light weight timer: {e}");
logging!(
warn,
Type::Lightweight,
"Warning: Failed to setup light weight timer: {e}"
);
}
}));
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
});
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
}
}
fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
}
}
@@ -240,7 +203,11 @@ fn cancel_window_close_listener() {
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = window.listen("tauri://focus", move |_event| {
let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer());
logging!(
info,
@@ -248,37 +215,45 @@ fn setup_webview_focus_listener() {
"监听到窗口获得焦点,取消轻量模式计时"
);
});
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
}
}
fn cancel_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
}
}
}
async fn setup_light_weight_timer() -> Result<()> {
Timer::global().init().await?;
if let Err(e) = Timer::global().init().await {
return Err(e).context("failed to initialize timer");
}
let once_by_minutes = Config::verge()
.await
.latest_ref()
.auto_light_weight_minutes
.unwrap_or(10);
// 获取task_id
{
let timer_map = Timer::global().timer_map.read();
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建");
return Ok(());
}
}
let task_id = {
Timer::global()
.timer_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
};
// 创建任务
let task = TaskBuilder::default()
.set_task_id(task_id)
.set_maximum_parallel_runnable_num(1)
@@ -289,7 +264,6 @@ async fn setup_light_weight_timer() -> Result<()> {
})
.context("failed to create timer task")?;
// 添加任务到定时器
{
let delay_timer = Timer::global().delay_timer.write();
delay_timer
@@ -297,7 +271,6 @@ async fn setup_light_weight_timer() -> Result<()> {
.context("failed to add timer task")?;
}
// 更新任务映射
{
let mut timer_map = Timer::global().timer_map.write();
let timer_task = crate::core::timer::TimerTask {

View File

@@ -1,13 +1,20 @@
use anyhow::Result;
use tauri_plugin_shell::process::CommandChild;
use crate::{logging, utils::logging::Type};
#[derive(Debug)]
pub struct CommandChildGuard(Option<CommandChild>);
impl Drop for CommandChildGuard {
fn drop(&mut self) {
if let Err(err) = self.kill() {
log::error!(target: "app", "Failed to kill child process: {}", err);
logging!(
error,
Type::Service,
"Failed to kill child process: {}",
err
);
}
}
}

View File

@@ -1,7 +1,7 @@
#[cfg(target_os = "windows")]
use crate::{logging, utils::logging::Type};
#[cfg(target_os = "windows")]
use anyhow::{Result, anyhow};
#[cfg(target_os = "windows")]
use log::info;
#[cfg(target_os = "windows")]
use std::{os::windows::process::CommandExt, path::Path, path::PathBuf};
@@ -49,15 +49,15 @@ pub async fn create_shortcut() -> Result<()> {
.remove_if_exists()
.await
.inspect(|_| {
info!(target: "app", "成功移除旧启动快捷方式");
logging!(info, Type::Setup, "成功移除旧启动快捷方式");
})
.inspect_err(|err| {
log::error!(target: "app", "移除旧启动快捷方式失败: {err}");
logging!(error, Type::Setup, "移除旧启动快捷方式失败: {err}");
});
// 如果新快捷方式已存在,直接返回成功
if new_shortcut_path.exists() {
info!(target: "app", "启动快捷方式已存在");
logging!(info, Type::Setup, "启动快捷方式已存在");
return Ok(());
}
@@ -83,7 +83,7 @@ pub async fn create_shortcut() -> Result<()> {
return Err(anyhow!("创建快捷方式失败: {}", error_msg));
}
info!(target: "app", "成功创建启动快捷方式");
logging!(info, Type::Setup, "成功创建启动快捷方式");
Ok(())
}
@@ -102,22 +102,22 @@ pub async fn remove_shortcut() -> Result<()> {
.remove_if_exists()
.await
.inspect(|_| {
info!(target: "app", "成功删除旧启动快捷方式");
logging!(info, Type::Setup, "成功删除旧启动快捷方式");
removed_any = true;
})
.inspect_err(|err| {
log::error!(target: "app", "删除旧启动快捷方式失败: {err}");
logging!(error, Type::Setup, "删除旧启动快捷方式失败: {err}");
});
let _ = new_shortcut_path
.remove_if_exists()
.await
.inspect(|_| {
info!(target: "app", "成功删除启动快捷方式");
logging!(info, Type::Setup, "成功删除启动快捷方式");
removed_any = true;
})
.inspect_err(|err| {
log::error!(target: "app", "删除启动快捷方式失败: {err}");
logging!(error, Type::Setup, "删除启动快捷方式失败: {err}");
});
Ok(())

View File

@@ -1,4 +1,8 @@
use crate::{core::handle, logging, utils::logging::Type};
use crate::{
core::{CoreManager, handle, manager::RunningMode},
logging,
utils::logging::Type,
};
use anyhow::Result;
use async_trait::async_trait;
use once_cell::sync::OnceCell;
@@ -20,7 +24,6 @@ pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
pub static CLASH_CONFIG: &str = "config.yaml";
pub static VERGE_CONFIG: &str = "verge.yaml";
pub static PROFILE_YAML: &str = "profiles.yaml";
pub static DNS_CONFIG: &str = "dns_config.yaml";
/// init portable flag
pub fn init_portable_flag() -> Result<()> {
@@ -58,7 +61,11 @@ pub fn app_home_dir() -> Result<PathBuf> {
match app_handle.path().data_dir() {
Ok(dir) => Ok(dir.join(APP_ID)),
Err(e) => {
log::error!(target: "app", "Failed to get the app home directory: {e}");
logging!(
error,
Type::File,
"Failed to get the app home directory: {e}"
);
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
}
}
@@ -72,7 +79,11 @@ pub fn app_resources_dir() -> Result<PathBuf> {
match app_handle.path().resource_dir() {
Ok(dir) => Ok(dir.join("resources")),
Err(e) => {
log::error!(target: "app", "Failed to get the resource directory: {e}");
logging!(
error,
Type::File,
"Failed to get the resource directory: {e}"
);
Err(anyhow::anyhow!("Failed to get the resource directory"))
}
}
@@ -122,6 +133,11 @@ pub fn app_logs_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("logs"))
}
// latest verge log
pub fn app_latest_log() -> Result<PathBuf> {
Ok(app_logs_dir()?.join("latest.log"))
}
/// local backups dir
pub fn local_backup_dir() -> Result<PathBuf> {
let dir = app_home_dir()?.join(BACKUP_DIR);
@@ -167,6 +183,15 @@ pub fn service_log_dir() -> Result<PathBuf> {
Ok(log_dir)
}
pub fn clash_latest_log() -> Result<PathBuf> {
match *CoreManager::global().get_running_mode() {
RunningMode::Service => Ok(service_log_dir()?.join("service_latest.log")),
RunningMode::Sidecar | RunningMode::NotRunning => {
Ok(sidecar_log_dir()?.join("sidecar_latest.log"))
}
}
}
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
let path_str = path
.as_os_str()
@@ -211,7 +236,11 @@ pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
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:?}");
logging!(
error,
Type::File,
"Failed to create safe directory: {home_config:?}"
);
None
}
})

View File

@@ -24,22 +24,21 @@ impl<T: Clone + ToOwned> From<T> for Draft<T> {
///
/// # Methods
/// - `data_mut`: Returns a mutable reference to the committed data.
/// - `data_ref`: Returns an immutable reference to the committed data.
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
/// - `discard`: Discards the draft data and returns it if it existed.
impl<T: Clone + ToOwned> Draft<Box<T>> {
/// 正式数据视图
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
}
/// 可写正式数据
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
}
/// 返回正式数据的只读视图(不包含草稿)
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
}
/// 创建或获取草稿并返回可写引用
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
let guard = self.inner.upgradable_read();
@@ -69,17 +68,21 @@ impl<T: Clone + ToOwned> Draft<Box<T>> {
}
/// 提交草稿,返回旧正式数据
pub fn apply(&self) -> Option<Box<T>> {
let mut inner = self.inner.write();
inner
.1
.take()
.map(|draft| std::mem::replace(&mut inner.0, draft))
pub fn apply(&self) {
let guard = self.inner.upgradable_read();
if guard.1.is_none() {
return;
}
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
if let Some(draft) = guard.1.take() {
guard.0 = draft;
}
}
/// 丢弃草稿,返回被丢弃的草稿
pub fn discard(&self) -> Option<Box<T>> {
self.inner.write().1.take()
pub fn discard(&self) {
self.inner.write().1.take();
}
/// 异步修改正式数据,闭包直接获得 Box<T> 所有权
@@ -152,8 +155,7 @@ fn test_draft_box() {
}
// 5. 提交草稿
assert!(draft.apply().is_some()); // 第一次提交应有返回
assert!(draft.apply().is_none()); // 第二次提交返回 None
draft.apply();
// 正式数据已更新
{
@@ -170,8 +172,7 @@ fn test_draft_box() {
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
// 7. 丢弃草稿
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
assert!(draft.discard().is_none()); // 再次丢弃返回 None
draft.discard();
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));

View File

@@ -3,6 +3,7 @@
use crate::utils::logging::NoModuleFilter;
use crate::{
config::*,
constants,
core::handle,
logging,
process::AsyncHandler,
@@ -304,7 +305,7 @@ async fn init_dns_config() -> Result<()> {
// 检查DNS配置文件是否存在
let app_dir = dirs::app_home_dir()?;
let dns_path = app_dir.join("dns_config.yaml");
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
if !dns_path.exists() {
logging!(info, Type::Setup, "Creating default DNS config file");
@@ -364,7 +365,7 @@ async fn initialize_config_files() -> Result<()> {
if let Ok(path) = dirs::profiles_path()
&& !path.exists()
{
let template = IProfiles::template();
let template = IProfiles::default();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
@@ -429,26 +430,8 @@ pub async fn init_resources() -> Result<()> {
let src_path = res_dir.join(file);
let dest_path = app_dir.join(file);
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
match fs::copy(&src, &dest).await {
Ok(_) => {
logging!(debug, Type::Setup, "resources copied '{}'", file);
}
Err(err) => {
logging!(
error,
Type::Setup,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
err
);
}
};
};
if src_path.exists() && !dest_path.exists() {
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
handle_copy(&src_path, &dest_path, file).await;
continue;
}
@@ -458,12 +441,12 @@ pub async fn init_resources() -> Result<()> {
match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified {
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
handle_copy(&src_path, &dest_path, file).await;
}
}
_ => {
logging!(debug, Type::Setup, "failed to get modified '{}'", file);
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
handle_copy(&src_path, &dest_path, file).await;
}
};
}
@@ -563,3 +546,21 @@ pub async fn startup_script() -> Result<()> {
Ok(())
}
async fn handle_copy(src: &PathBuf, dest: &PathBuf, file: &str) {
match fs::copy(src, dest).await {
Ok(_) => {
logging!(debug, Type::Setup, "resources copied '{}'", file);
}
Err(err) => {
logging!(
error,
Type::Setup,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
err
);
}
};
}

View File

@@ -29,7 +29,6 @@ pub enum Type {
Lightweight,
Network,
ProxyMode,
// Cache,
Validate,
ClashVergeRev,
}
@@ -53,7 +52,6 @@ impl fmt::Display for Type {
Type::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
// Type::Cache => write!(f, "[Cache]"),
Type::Validate => write!(f, "[Validate]"),
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
}
@@ -82,15 +80,6 @@ macro_rules! log_err {
};
}
#[macro_export]
macro_rules! trace_err {
($result: expr, $err_str: expr) => {
if let Err(err) = $result {
log::trace!(target: "app", "{}, err {}", $err_str, err);
}
}
}
/// wrap the anyhow error
/// transform the error to String
#[macro_export]

View File

@@ -1,6 +1,7 @@
use crate::config::Config;
use anyhow::Result;
use base64::{Engine as _, engine::general_purpose};
use isahc::config::DnsCache;
use isahc::prelude::*;
use isahc::{HttpClient, config::SslOption};
use isahc::{
@@ -143,6 +144,12 @@ impl NetworkManager {
builder = builder.redirect_policy(RedirectPolicy::Follow);
// 禁用缓存,不关心连接复用
builder = builder.connection_cache_size(0);
// 禁用 DNS 缓存,避免因 DNS 变化导致的问题
builder = builder.dns_cache(DnsCache::Disable);
Ok(builder.build()?)
}
}

View File

@@ -1,6 +1,5 @@
use crate::utils::i18n::t;
use crate::{core::handle, utils::i18n::t};
use tauri::AppHandle;
use tauri_plugin_notification::NotificationExt;
pub enum NotificationEvent<'a> {
@@ -16,8 +15,10 @@ pub enum NotificationEvent<'a> {
AppHidden,
}
fn notify(app: &AppHandle, title: &str, body: &str) {
app.notification()
fn notify(title: &str, body: &str) {
let app_handle = handle::Handle::app_handle();
app_handle
.notification()
.builder()
.title(title)
.body(body)
@@ -25,49 +26,44 @@ fn notify(app: &AppHandle, title: &str, body: &str) {
.ok();
}
pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) {
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
match event {
NotificationEvent::DashboardToggled => {
notify(
&app,
&t("DashboardToggledTitle").await,
&t("DashboardToggledBody").await,
);
}
NotificationEvent::ClashModeChanged { mode } => {
notify(
&app,
&t("ClashModeChangedTitle").await,
&t_with_args("ClashModeChangedBody", mode).await,
);
}
NotificationEvent::SystemProxyToggled => {
notify(
&app,
&t("SystemProxyToggledTitle").await,
&t("SystemProxyToggledBody").await,
);
}
NotificationEvent::TunModeToggled => {
notify(
&app,
&t("TunModeToggledTitle").await,
&t("TunModeToggledBody").await,
);
}
NotificationEvent::LightweightModeEntered => {
notify(
&app,
&t("LightweightModeEnteredTitle").await,
&t("LightweightModeEnteredBody").await,
);
}
NotificationEvent::AppQuit => {
notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await);
notify(&t("AppQuitTitle").await, &t("AppQuitBody").await);
}
#[cfg(target_os = "macos")]
NotificationEvent::AppHidden => {
notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await);
notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await);
}
}
}

View File

@@ -1,20 +1,27 @@
#[cfg(target_os = "macos")]
use crate::{logging, utils::logging::Type};
pub async fn set_public_dns(dns_server: String) {
use crate::{core::handle, utils::dirs};
use crate::utils::logging::Type;
use crate::{core::handle, logging, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = handle::Handle::app_handle();
log::info!(target: "app", "try to set system dns");
logging!(info, Type::Config, "try to set system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
logging!(
error,
Type::Config,
"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");
logging!(error, Type::Config, "set_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
@@ -28,14 +35,14 @@ pub async fn set_public_dns(dns_server: String) {
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
logging!(info, Type::Config, "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
logging!(error, Type::Config, "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
logging!(error, Type::Config, "set system dns failed: {err}");
}
}
}
@@ -45,17 +52,22 @@ pub async fn restore_public_dns() {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = handle::Handle::app_handle();
log::info!(target: "app", "try to unset system dns");
logging!(info, Type::Config, "try to unset system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
logging!(
error,
Type::Config,
"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");
logging!(error, Type::Config, "unset_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
@@ -69,14 +81,14 @@ pub async fn restore_public_dns() {
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
logging!(info, Type::Config, "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
logging!(error, Type::Config, "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
logging!(error, Type::Config, "unset system dns failed: {err}");
}
}
}

View File

@@ -1,5 +1,4 @@
use anyhow::Result;
use smartstring::alias::String;
use crate::{
config::Config,
@@ -11,10 +10,7 @@ use crate::{
tray::Tray,
},
logging, logging_error,
module::{
lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
signal,
},
module::{lightweight::auto_lightweight_boot, signal},
process::AsyncHandler,
utils::{init, logging::Type, server, window_manager::WindowManager},
};
@@ -71,8 +67,7 @@ pub fn resolve_setup_async() {
tray_init,
init_timer(),
init_hotkey(),
init_auto_lightweight_mode(),
init_once_auto_lightweight(),
init_auto_lightweight_boot(),
);
});
}
@@ -103,7 +98,7 @@ pub(super) async fn resolve_setup_logger() {
logging_error!(Type::Setup, init::init_logger().await);
}
pub async fn resolve_scheme(param: String) -> Result<()> {
pub async fn resolve_scheme(param: &str) -> Result<()> {
logging_error!(Type::Setup, scheme::resolve_scheme(param).await);
Ok(())
}
@@ -125,15 +120,11 @@ pub(super) async fn init_timer() {
}
pub(super) async fn init_hotkey() {
logging_error!(Type::Setup, Hotkey::global().init().await);
logging_error!(Type::Setup, Hotkey::global().init(false).await);
}
pub(super) async fn init_once_auto_lightweight() {
run_once_auto_lightweight().await;
}
pub(super) async fn init_auto_lightweight_mode() {
logging_error!(Type::Setup, auto_lightweight_mode_init().await);
pub(super) async fn init_auto_lightweight_boot() {
logging_error!(Type::Setup, auto_lightweight_boot().await);
}
pub(super) fn init_signal() {

View File

@@ -3,17 +3,22 @@ use percent_encoding::percent_decode_str;
use smartstring::alias::String;
use tauri::Url;
use crate::{config::PrfItem, core::handle, logging, logging_error, utils::logging::Type};
use crate::{
config::{PrfItem, profiles},
core::handle,
logging, logging_error,
utils::logging::Type,
};
pub(super) async fn resolve_scheme(param: String) -> Result<()> {
log::info!(target:"app", "received deep link: {param}");
pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
logging!(info, Type::Config, "received deep link: {param}");
let param_str = if param.starts_with("[") && param.len() > 4 {
param
.get(2..param.len() - 2)
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
} else {
param.as_str()
param
};
// 解析 URL
@@ -25,10 +30,11 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name = link_parsed
let name_owned: Option<String> = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into());
.map(|(_, value)| value.into_owned().into());
let name = name_owned.as_ref();
let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url=";
@@ -43,10 +49,10 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
};
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
Some(ref url) => {
logging!(info, Type::Config, "decoded subscription url: {url}");
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
Ok(mut item) => {
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
@@ -58,7 +64,7 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
return Ok(());
}
};
let result = crate::config::profiles::profiles_append_item_safe(item).await;
let result = profiles::profiles_append_item_safe(&mut item).await;
logging_error!(
Type::Config,
"failed to import subscription url: {:?}",

View File

@@ -21,16 +21,14 @@ const MINIMAL_HEIGHT: f64 = 520.0;
pub async fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::app_handle();
let start_page = Config::verge()
.await
.latest_ref()
.start_page
.clone()
.unwrap_or_else(|| "/".into());
let config = Config::verge().await;
let latest = config.latest_ref();
let start_page = latest.start_page.as_deref().unwrap_or("/");
match tauri::WebviewWindowBuilder::new(
app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App(start_page.as_str().into()),
tauri::WebviewUrl::App(start_page.into()),
)
.title("Clash Verge")
.center()

View File

@@ -51,7 +51,11 @@ pub async fn check_singleton() -> Result<()> {
.send()
.await?;
}
log::error!("failed to setup singleton listen server");
logging!(
error,
Type::Window,
"failed to setup singleton listen server"
);
bail!("app exists");
}
Ok(())
@@ -107,9 +111,8 @@ pub fn embed_server() {
let scheme = warp::path!("commands" / "scheme")
.and(warp::query::<QueryParam>())
.map(|query: QueryParam| {
let param = query.param.clone();
tokio::task::spawn_local(async move {
logging_error!(Type::Setup, resolve::resolve_scheme(param).await);
logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await);
});
warp::reply::with_status::<std::string::String>(
"ok".to_string(),
@@ -130,7 +133,7 @@ pub fn embed_server() {
}
pub fn shutdown_embedded_server() {
log::info!("shutting down embedded server");
logging!(info, Type::Window, "shutting down embedded server");
if let Some(sender) = SHUTDOWN_SENDER.get()
&& let Some(sender) = sender.lock().take()
{

View File

@@ -58,7 +58,11 @@ fn get_window_operation_debounce() -> &'static Mutex<Instant> {
fn should_handle_window_operation() -> bool {
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) {
log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用");
logging!(
warn,
Type::Window,
"Warning: [防抖] 窗口操作已在进行中,跳过重复调用"
);
return false;
}
@@ -67,17 +71,27 @@ fn should_handle_window_operation() -> bool {
let now = Instant::now();
let elapsed = now.duration_since(*last_operation);
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
logging!(
debug,
Type::Window,
"[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
elapsed.as_millis(),
WINDOW_OPERATION_DEBOUNCE_MS
);
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
*last_operation = now;
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
logging!(info, Type::Window, "[防抖] 窗口操作被允许执行");
true
} else {
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
logging!(
warn,
Type::Window,
"Warning: [防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
elapsed.as_millis(),
WINDOW_OPERATION_DEBOUNCE_MS
);
false
}
}
@@ -359,7 +373,6 @@ impl WindowManager {
}
return WindowOperationResult::Destroyed;
}
logging!(warn, Type::Window, "窗口摧毁失败");
WindowOperationResult::Failed
}

View File

@@ -506,7 +506,7 @@ export const CurrentProxyCard = () => {
// 导航到代理页面
const goToProxies = useCallback(() => {
navigate("/");
navigate("/proxies");
}, [navigate]);
// 获取要显示的代理节点

View File

@@ -26,6 +26,7 @@ import { useServiceInstaller } from "@/hooks/useServiceInstaller";
import { getSystemInfo } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
import { useUpdateChannel } from "@/services/updateChannel";
import { version as appVersion } from "@root/package.json";
import { EnhancedCard } from "./enhanced-card";
@@ -59,6 +60,7 @@ export const SystemInfoCard = () => {
const navigate = useNavigate();
const { isAdminMode, isSidecarMode } = useSystemState();
const { installServiceAndRestartCore } = useServiceInstaller();
const [updateChannel] = useUpdateChannel();
// 系统信息状态
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
@@ -117,7 +119,7 @@ export const SystemInfoCard = () => {
timeoutId = window.setTimeout(() => {
if (verge?.auto_check_update) {
checkUpdate().catch(console.error);
checkUpdate(updateChannel).catch(console.error);
}
}, 5000);
}
@@ -126,11 +128,11 @@ export const SystemInfoCard = () => {
window.clearTimeout(timeoutId);
}
};
}, [verge?.auto_check_update, dispatchSystemState]);
}, [verge?.auto_check_update, dispatchSystemState, updateChannel]);
// 自动检查更新逻辑
useSWR(
verge?.auto_check_update ? "checkUpdate" : null,
verge?.auto_check_update ? ["checkUpdate", updateChannel] : null,
async () => {
const now = Date.now();
localStorage.setItem("last_check_update", now.toString());
@@ -138,7 +140,7 @@ export const SystemInfoCard = () => {
type: "set-last-check-update",
payload: new Date(now).toLocaleString(),
});
return await checkUpdate();
return await checkUpdate(updateChannel);
},
{
revalidateOnFocus: false,
@@ -172,7 +174,7 @@ export const SystemInfoCard = () => {
// 检查更新
const onCheckUpdate = useLockFn(async () => {
try {
const info = await checkUpdate();
const info = await checkUpdate(updateChannel);
if (!info?.available) {
showNotice("success", t("Currently on the Latest Version"));
} else {

View File

@@ -4,6 +4,7 @@ import useSWR from "swr";
import { useVerge } from "@/hooks/use-verge";
import { checkUpdateSafe } from "@/services/update";
import { useUpdateChannel } from "@/services/updateChannel";
import { DialogRef } from "../base";
import { UpdateViewer } from "../setting/mods/update-viewer";
@@ -16,12 +17,14 @@ export const UpdateButton = (props: Props) => {
const { className } = props;
const { verge } = useVerge();
const { auto_check_update } = verge || {};
const [updateChannel] = useUpdateChannel();
const viewerRef = useRef<DialogRef>(null);
const shouldCheck = auto_check_update || auto_check_update === null;
const { data: updateInfo } = useSWR(
auto_check_update || auto_check_update === null ? "checkUpdate" : null,
checkUpdateSafe,
shouldCheck ? ["checkUpdate", updateChannel] : null,
() => checkUpdateSafe(updateChannel),
{
errorRetryCount: 2,
revalidateIfStale: false,

View File

@@ -53,10 +53,13 @@ export const useCustomTheme = () => {
return;
}
if (
const preferBrowserMatchMedia =
typeof window !== "undefined" &&
typeof window.matchMedia === "function"
) {
typeof window.matchMedia === "function" &&
// Skip Tauri flow when running purely in browser.
!("__TAURI__" in window);
if (preferBrowserMatchMedia) {
return;
}

View File

@@ -341,7 +341,8 @@ export const ProfileItem = (props: Props) => {
try {
// 调用后端更新(后端会自动处理回退逻辑)
await updateProfile(itemData.uid, option);
const payload = Object.keys(option).length > 0 ? option : undefined;
await updateProfile(itemData.uid, payload);
// 更新成功,刷新列表
mutate("getProfiles");

View File

@@ -15,6 +15,7 @@ import { portableFlag } from "@/pages/_layout";
import { showNotice } from "@/services/noticeService";
import { useSetUpdateState, useUpdateState } from "@/services/states";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
import { useUpdateChannel } from "@/services/updateChannel";
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation();
@@ -26,12 +27,17 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
const updateState = useUpdateState();
const setUpdateState = useSetUpdateState();
const { addListener } = useListen();
const [updateChannel] = useUpdateChannel();
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
errorRetryCount: 2,
revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour
});
const { data: updateInfo } = useSWR(
["checkUpdate", updateChannel],
() => checkUpdate(updateChannel),
{
errorRetryCount: 2,
revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour
},
);
const [downloaded, setDownloaded] = useState(0);
const [buffer, setBuffer] = useState(0);

View File

@@ -15,6 +15,7 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
import { useUpdateChannel } from "@/services/updateChannel";
import { version } from "@root/package.json";
import { BackupViewer } from "./mods/backup-viewer";
@@ -42,10 +43,11 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const liteModeRef = useRef<DialogRef>(null);
const [updateChannel] = useUpdateChannel();
const onCheckUpdate = async () => {
try {
const info = await checkUpdate();
const info = await checkUpdate(updateChannel);
if (!info?.available) {
showNotice("success", t("Currently on the Latest Version"));
} else {

View File

@@ -1,5 +1,11 @@
import { ContentCopyRounded } from "@mui/icons-material";
import { Button, Input, MenuItem, Select } from "@mui/material";
import {
Button,
Input,
MenuItem,
Select,
SelectChangeEvent,
} from "@mui/material";
import { open } from "@tauri-apps/plugin-dialog";
import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
@@ -11,6 +17,11 @@ import { navItems } from "@/pages/_routers";
import { copyClashEnv } from "@/services/cmds";
import { supportedLanguages } from "@/services/i18n";
import { showNotice } from "@/services/noticeService";
import {
UPDATE_CHANNEL_OPTIONS,
type UpdateChannel,
useUpdateChannel,
} from "@/services/updateChannel";
import getSystem from "@/utils/get-system";
import { BackupViewer } from "./mods/backup-viewer";
@@ -69,6 +80,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
const layoutRef = useRef<DialogRef>(null);
const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const [updateChannel, setUpdateChannel] = useUpdateChannel();
const onChangeData = (patch: any) => {
mutateVerge({ ...verge, ...patch }, false);
@@ -79,6 +91,14 @@ const SettingVergeBasic = ({ onError }: Props) => {
showNotice("success", t("Copy Success"), 1000);
}, [t]);
const onUpdateChannelChange = useCallback(
(event: SelectChangeEvent<UpdateChannel>) => {
const nextChannel = event.target.value as UpdateChannel;
setUpdateChannel(nextChannel);
},
[setUpdateChannel],
);
return (
<SettingList title={t("Verge Basic Setting")}>
<ThemeViewer ref={themeRef} />
@@ -89,6 +109,21 @@ const SettingVergeBasic = ({ onError }: Props) => {
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem label={t("Update Channel")}>
<Select
size="small"
value={updateChannel}
onChange={onUpdateChannelChange}
sx={{ width: 160, "> div": { py: "7.5px" } }}
>
{UPDATE_CHANNEL_OPTIONS.map((option) => (
<MenuItem key={option.value} value={option.value}>
{t(option.labelKey)}
</MenuItem>
))}
</Select>
</SettingItem>
<SettingItem label={t("Language")}>
<GuardState
value={language ?? "en"}

View File

@@ -117,13 +117,8 @@ const ProxyControlSwitches = ({
const { uninstallServiceAndRestartCore } = useServiceUninstaller();
const { actualState: systemProxyActualState, toggleSystemProxy } =
useSystemProxyState();
const {
isServiceMode,
isTunModeAvailable,
mutateRunningMode,
mutateServiceOk,
mutateTunModeAvailable,
} = useSystemState();
const { isServiceOk, isTunModeAvailable, mutateSystemState } =
useSystemState();
const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null);
@@ -148,9 +143,7 @@ const ProxyControlSwitches = ({
const onInstallService = useLockFn(async () => {
try {
await installServiceAndRestartCore();
await mutateRunningMode();
await mutateServiceOk();
await mutateTunModeAvailable();
await mutateSystemState();
} catch (err) {
showNotice("error", (err as Error).message || String(err));
}
@@ -158,11 +151,11 @@ const ProxyControlSwitches = ({
const onUninstallService = useLockFn(async () => {
try {
await handleTunToggle(false);
if (verge?.enable_tun_mode) {
await handleTunToggle(false);
}
await uninstallServiceAndRestartCore();
await mutateRunningMode();
await mutateServiceOk();
await mutateTunModeAvailable();
await mutateSystemState();
} catch (err) {
showNotice("error", (err as Error).message || String(err));
}
@@ -198,22 +191,22 @@ const ProxyControlSwitches = ({
extraIcons={
<>
{!isTunModeAvailable && (
<TooltipIcon
title={t("TUN requires Service Mode or Admin Mode")}
icon={WarningRounded}
sx={{ color: "warning.main", ml: 1 }}
/>
<>
<TooltipIcon
title={t("TUN requires Service Mode or Admin Mode")}
icon={WarningRounded}
sx={{ color: "warning.main", ml: 1 }}
/>
<TooltipIcon
title={t("Install Service")}
icon={BuildRounded}
color="primary"
onClick={onInstallService}
sx={{ ml: 1 }}
/>
</>
)}
{!isTunModeAvailable && (
<TooltipIcon
title={t("Install Service")}
icon={BuildRounded}
color="primary"
onClick={onInstallService}
sx={{ ml: 1 }}
/>
)}
{isServiceMode && (
{isServiceOk && (
<TooltipIcon
title={t("Uninstall Service")}
icon={DeleteForeverRounded}

View File

@@ -62,7 +62,9 @@ export const useProfiles = () => {
const patchCurrent = async (value: Partial<IProfileItem>) => {
if (profiles?.current) {
await patchProfile(profiles.current, value);
mutateProfiles();
if (!value.selected) {
mutateProfiles();
}
}
};

View File

@@ -1,73 +1,99 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useVerge } from "./use-verge";
export interface SystemState {
runningMode: "Sidecar" | "Service";
isAdminMode: boolean;
isServiceOk: boolean;
}
const defaultSystemState = {
runningMode: "Sidecar",
isAdminMode: false,
isServiceOk: false,
} as SystemState;
let disablingTunMode = false;
/**
* 自定义 hook 用于获取系统运行状态
* 包括运行模式、管理员状态、系统服务是否可用
*/
export function useSystemState() {
// 获取运行模式
const {
data: runningMode = "Sidecar",
mutate: mutateRunningMode,
isLoading: runningModeLoading,
} = useSWR("getRunningMode", getRunningMode, {
suspense: false,
revalidateOnFocus: false,
});
const isSidecarMode = runningMode === "Sidecar";
const isServiceMode = runningMode === "Service";
const { t } = useTranslation();
const { verge, patchVerge } = useVerge();
// 获取管理员状态
const { data: isAdminMode = false, isLoading: isAdminLoading } = useSWR(
"isAdmin",
isAdmin,
const {
data: systemState,
mutate: mutateSystemState,
isLoading,
} = useSWR(
"getSystemState",
async () => {
const [runningMode, isAdminMode, isServiceOk] = await Promise.all([
getRunningMode(),
isAdmin(),
isServiceAvailable(),
]);
return { runningMode, isAdminMode, isServiceOk } as SystemState;
},
{
suspense: false,
revalidateOnFocus: false,
suspense: true,
refreshInterval: 30000,
fallback: defaultSystemState,
},
);
const {
data: isServiceOk = false,
mutate: mutateServiceOk,
isLoading: isServiceLoading,
} = useSWR(isServiceMode ? "isServiceAvailable" : null, isServiceAvailable, {
suspense: false,
revalidateOnFocus: false,
onSuccess: (data) => {
console.log("[useSystemState] 服务状态更新:", data);
},
onError: (error) => {
console.error("[useSystemState] 服务状态检查失败:", error);
},
// isPaused: () => !isServiceMode, // 仅在非 Service 模式下暂停请求
});
const isSidecarMode = systemState.runningMode === "Sidecar";
const isServiceMode = systemState.runningMode === "Service";
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk;
const isLoading =
runningModeLoading || isAdminLoading || (isServiceMode && isServiceLoading);
const enable_tun_mode = verge?.enable_tun_mode;
useEffect(() => {
if (enable_tun_mode === undefined) return;
const { data: isTunModeAvailable = false, mutate: mutateTunModeAvailable } =
useSWR(
["isTunModeAvailable", isAdminMode, isServiceOk],
() => isAdminMode || isServiceOk,
{
suspense: false,
revalidateOnFocus: false,
},
);
if (
!disablingTunMode &&
enable_tun_mode &&
!isTunModeAvailable &&
!isLoading
) {
disablingTunMode = true;
patchVerge({ enable_tun_mode: false })
.then(() => {
showNotice(
"info",
t("TUN Mode automatically disabled due to service unavailable"),
);
})
.catch((err) => {
console.error("[useVerge] 自动关闭TUN模式失败:", err);
showNotice("error", t("Failed to disable TUN Mode automatically"));
})
.finally(() => {
const tid = setTimeout(() => {
// 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式
disablingTunMode = false;
clearTimeout(tid);
}, 1000);
});
}
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading, t]);
return {
runningMode,
isAdminMode,
runningMode: systemState.runningMode,
isAdminMode: systemState.isAdminMode,
isServiceOk: systemState.isServiceOk,
isSidecarMode,
isServiceMode,
isServiceOk,
isTunModeAvailable,
mutateRunningMode,
mutateServiceOk,
mutateTunModeAvailable,
mutateSystemState,
isLoading,
};
}

View File

@@ -1,16 +1,8 @@
import { useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { useSystemState } from "@/hooks/use-system-state";
import { getVergeConfig, patchVergeConfig } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
export const useVerge = () => {
const { t } = useTranslation();
const { isTunModeAvailable, isServiceMode, isLoading } = useSystemState();
const disablingRef = useRef(false);
const { data: verge, mutate: mutateVerge } = useSWR(
"getVergeConfig",
async () => {
@@ -24,53 +16,6 @@ export const useVerge = () => {
mutateVerge();
};
const { enable_tun_mode } = verge ?? {};
const mutateVergeRef = useRef(mutateVerge);
const tRef = useRef(t);
const enableTunRef = useRef(enable_tun_mode);
const isLoadingRef = useRef(isLoading);
const isServiceModeRef = useRef(isServiceMode);
mutateVergeRef.current = mutateVerge;
tRef.current = t;
enableTunRef.current = enable_tun_mode;
isLoadingRef.current = isLoading;
isServiceModeRef.current = isServiceMode;
const doDisable = useCallback(async () => {
try {
if (isServiceModeRef.current === true) return;
await patchVergeConfig({ enable_tun_mode: false });
await mutateVergeRef.current?.();
showNotice(
"info",
tRef.current(
"TUN Mode automatically disabled due to service unavailable",
),
);
} catch (err) {
console.error("[useVerge] 自动关闭TUN模式失败:", err);
showNotice(
"error",
tRef.current("Failed to disable TUN Mode automatically"),
);
} finally {
disablingRef.current = false;
}
}, []);
useEffect(() => {
if (isTunModeAvailable === true) return;
if (isLoadingRef.current === true) return;
if (enableTunRef.current !== true) return;
if (isServiceModeRef.current === true) return;
if (disablingRef.current) return;
disablingRef.current = true;
void doDisable();
}, [isTunModeAvailable, doDisable]);
return {
verge,
mutateVerge,

View File

@@ -25,7 +25,7 @@ const executeWithErrorHandling = async (
};
export const useServiceInstaller = () => {
const { mutateRunningMode, mutateServiceOk } = useSystemState();
const { mutateSystemState } = useSystemState();
const installServiceAndRestartCore = useCallback(async () => {
await executeWithErrorHandling(
@@ -34,9 +34,13 @@ export const useServiceInstaller = () => {
"Service Installed Successfully",
);
await executeWithErrorHandling(() => restartCore(), "Restarting Core...");
await mutateRunningMode();
await mutateServiceOk();
}, [mutateRunningMode, mutateServiceOk]);
await executeWithErrorHandling(
() => restartCore(),
"Restarting Core...",
"Clash Core Restarted",
);
await mutateSystemState();
}, [mutateSystemState]);
return { installServiceAndRestartCore };
};

View File

@@ -25,21 +25,26 @@ const executeWithErrorHandling = async (
};
export const useServiceUninstaller = () => {
const { mutateRunningMode, mutateServiceOk } = useSystemState();
const { mutateSystemState } = useSystemState();
const uninstallServiceAndRestartCore = useCallback(async () => {
await executeWithErrorHandling(() => stopCore(), "Stopping Core...");
await executeWithErrorHandling(
() => uninstallService(),
"Uninstalling Service...",
"Service Uninstalled Successfully",
);
await executeWithErrorHandling(() => restartCore(), "Restarting Core...");
await mutateRunningMode();
await mutateServiceOk();
}, [mutateRunningMode, mutateServiceOk]);
try {
await executeWithErrorHandling(() => stopCore(), "Stopping Core...");
await executeWithErrorHandling(
() => uninstallService(),
"Uninstalling Service...",
"Service Uninstalled Successfully",
);
} catch (ignore) {
} finally {
await executeWithErrorHandling(
() => restartCore(),
"Restarting Core...",
"Clash Core Restarted",
);
await mutateSystemState();
}
}, [mutateSystemState]);
return { uninstallServiceAndRestartCore };
};

View File

@@ -21,7 +21,6 @@
"Label-Connections": "الاتصالات",
"Label-Rules": "القواعد",
"Label-Logs": "السجلات",
"Label-Test": "اختبار",
"Label-Settings": "الإعدادات",
"Proxies": "الوكلاء",
"Proxy Groups": "مجموعات الوكلاء",
@@ -44,6 +43,9 @@
"Proxy detail": "تفاصيل الوكيل",
"Profiles": "الملفات الشخصية",
"Update All Profiles": "تحديث جميع الملفات الشخصية",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "عرض تكوين وقت التشغيل",
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
"Paste": "لصق",
@@ -166,7 +168,6 @@
"Table View": "عرض الجدول",
"List View": "عرض القائمة",
"Close All": "إغلاق الكل",
"Default": "افتراضي",
"Download Speed": "سرعة التنزيل",
"Upload Speed": "سرعة الرفع",
"Host": "المضيف",
@@ -197,6 +198,8 @@
"Settings": "الإعدادات",
"System Setting": "إعدادات النظام",
"Tun Mode": "وضع TUN",
"TUN requires Service Mode": "يتطلب وضع TUN خدمة",
"Install Service": "تثبيت الخدمة ",
"Reset to Default": "إعادة تعيين إلى الافتراضي",
"Tun Mode Info": "وضع TUN (بطاقة شبكة افتراضية): يلتقط كل حركة المرور في النظام. عند تمكينه، لا حاجة لتفعيل وكيل النظام.",
"Stack": "مكدس TUN",
@@ -279,7 +282,8 @@
"Open UWP tool": "فتح أداة UWP",
"Open UWP tool Info": "منذ نظام ويندوز 8، يتم تقييد تطبيقات UWP من الوصول المباشر إلى المضيف المحلي. هذه الأداة تتيح تجاوز هذا التقييد",
"Update GeoData": "تحديث البيانات الجغرافية",
"Verge Setting": "إعدادات Verge",
"Verge Basic Setting": "الإعدادات الأساسية Verge",
"Verge Advanced Setting": "الإعدادات الأساسية Verge",
"Language": "اللغة",
"Theme Mode": "وضع السمة",
"theme.light": "سمة فاتحة",
@@ -365,6 +369,7 @@
"Profile Reactivated": "تم إعادة تنشيط الملف الشخصي",
"Only YAML Files Supported": "لا يتم دعم سوى ملفات YAML",
"Settings Applied": "تم تطبيق الإعدادات",
"Installing Service...": "جاري تثبيت الخدمة...",
"Service Installed Successfully": "تم تثبيت الخدمة بنجاح",
"Service Uninstalled Successfully": "تم إلغاء تثبيت الخدمة بنجاح",
"Proxy Daemon Duration Cannot be Less than 1 Second": "لا يمكن أن تقل مدة خادم الوكيل عن ثانية واحدة",
@@ -407,19 +412,12 @@
"Help": "مساعدة",
"About": "حول",
"Theme": "السمة",
"TUN Mode": "وضع TUN",
"Main Window": "النافذة الرئيسية",
"Group Icon": "أيقونة المجموعة",
"Menu Icon": "أيقونة القائمة",
"System Proxy Bypass": "تخطي وكيل النظام",
"PAC File": "ملف PAC",
"Web UI": "واجهة الويب",
"Hotkeys": "اختصارات لوحة المفاتيح",
"Auto Close Connection": "إغلاق الاتصال تلقائيًا",
"Enable Built-in Enhanced": "تفعيل التحسين المدمج",
"Proxy Layout Column": "عمود عرض الوكيل",
"Test List": "قائمة الاختبارات",
"Enable Random Port": "تفعيل المنفذ العشوائي",
"Verge Mixed Port": "منفذ Verge المختلط",
"Verge Socks Port": "منفذ Verge SOCKS",
"Verge Redir Port": "منفذ إعادة التوجيه لـ Verge",
@@ -429,15 +427,16 @@
"WebDAV URL": "رابط WebDAV",
"WebDAV Username": "اسم المستخدم لـ WebDAV",
"WebDAV Password": "كلمة مرور WebDAV",
"Dashboard": "لوحة التحكم",
"Restart App": "إعادة تشغيل التطبيق",
"Restart Clash Core": "إعادة تشغيل نواة Clash",
"TUN Mode": "وضع TUN",
"Copy Env": "نسخ البيئة",
"Conf Dir": "مجلد الإعدادات",
"Core Dir": "مجلد النواة",
"Logs Dir": "مجلد السجلات",
"Open Dir": "فتح المجلد",
"Restart Clash Core": "إعادة تشغيل نواة Clash",
"Restart App": "إعادة تشغيل التطبيق",
"More": "المزيد",
"Dashboard": "لوحة التحكم",
"Rule Mode": "وضع القواعد",
"Global Mode": "الوضع العالمي",
"Direct Mode": "الوضع المباشر",
@@ -454,10 +453,14 @@
"Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات",
"Core Changed Successfully": "تم تغيير النواة بنجاح",
"Failed to Change Core": "فشل تغيير النواة",
"Verge Basic Setting": "الإعدادات الأساسية Verge",
"Verge Advanced Setting": "الإعدادات الأساسية Verge",
"TUN requires Service Mode": "يتطلب وضع TUN خدمة",
"Install Service": ثبيت الخدمة ",
"Installing Service...": "جاري تثبيت الخدمة...",
"Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام"
"Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام",
"Auto Close Connection": "إغلاق الاتصال تلقائيًا",
"Default": "افتراضي",
"Enable Built-in Enhanced": فعيل التحسين المدمج",
"Enable Random Port": "تفعيل المنفذ العشوائي",
"Label-Test": "اختبار",
"Proxy Layout Column": "عمود عرض الوكيل",
"System Proxy Bypass": "تخطي وكيل النظام",
"Test List": "قائمة الاختبارات",
"Verge Setting": "إعدادات Verge"
}

View File

@@ -45,6 +45,9 @@
"Proxy detail": "Knotendetails anzeigen",
"Profiles": "Abonnement",
"Update All Profiles": "Alle Abonnements aktualisieren",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "Laufzeit-Abonnement anzeigen",
"Reactivate Profiles": "Abonnement erneut aktivieren",
"Paste": "Einfügen",
@@ -195,10 +198,11 @@
"Settings": "Einstellungen",
"System Setting": "Systemeinstellungen",
"Tun Mode": "Virtual Network Interface-Modus",
"TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus",
"Install Service": "Service installieren",
"Uninstall Service": "Dienst deinstallieren",
"Reset to Default": "Auf Standardwerte zurücksetzen",
"Tun Mode Info": "Der TUN-Modus (Virtual Network Interface) übernimmt den gesamten Systemverkehr. Wenn dieser Modus aktiviert ist, muss der Systemproxy nicht geöffnet werden.",
"TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus",
"System Proxy Enabled": "Der Systemproxy ist aktiviert. Ihre Anwendungen werden über den Proxy auf das Netzwerk zugreifen.",
"System Proxy Disabled": "Der Systemproxy ist deaktiviert. Es wird empfohlen, diesen Eintrag für die meisten Benutzer zu aktivieren.",
"TUN Mode Enabled": "Der TUN-Modus ist aktiviert. Die Anwendungen werden über die virtuelle Netzwerkschnittstelle auf das Netzwerk zugreifen.",
@@ -254,6 +258,7 @@
"Unified Delay Info": "Wenn die einheitliche Latenz aktiviert ist, werden zwei Latenztests durchgeführt, um die Latenzunterschiede zwischen verschiedenen Knotentypen aufgrund von Verbindungsaufbau und anderen Faktoren zu eliminieren.",
"Log Level": "Protokolliergrad",
"Log Level Info": "Dies wirkt sich nur auf die Kernprotokolldateien im Verzeichnis Service im Protokollverzeichnis aus.",
"Port Config": "Port-Konfiguration",
"Random Port": "Zufälliger Port",
"Mixed Port": "Mischter Proxy-Port",
"Socks Port": "SOCKS-Proxy-Port",
@@ -367,17 +372,16 @@
"Stopping Core...": "Kern wird gestoppt...",
"Restarting Core...": "Kern wird neu gestartet...",
"Installing Service...": "Service wird installiert...",
"Uninstall Service": "Dienst deinstallieren",
"Service Installed Successfully": "Service erfolgreich installiert",
"Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet",
"Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.",
"Uninstalling Service...": "Service wird deinstalliert...",
"Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...",
"Service Installed Successfully": "Service erfolgreich installiert",
"Service Uninstalled Successfully": "Service erfolgreich deinstalliert",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Das Intervall des Proxy-Daemons darf nicht weniger als 1 Sekunde betragen.",
"Invalid Bypass Format": "Ungültiges Format für die Proxy-Umgehung",
"Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.",
"Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet",
"Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar",
"Core Version Updated": "Kernversion wurde aktualisiert",
"Clash Core Restarted": "Clash-Kern wurde neu gestartet",
"GeoData Updated": "Geo-Daten wurden aktualisiert",
@@ -523,7 +527,6 @@
"Unknown": "Unbekannt",
"Auto update disabled": "Automatische Aktualisierung deaktiviert",
"Update subscription successfully": "Abonnement erfolgreich aktualisiert",
"Update failed, retrying with Clash proxy...": "Abonnement-Aktualisierung fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...",
"Update with Clash proxy successfully": "Aktualisierung mit Clash-Proxy erfolgreich",
"Update failed even with Clash proxy": "Aktualisierung auch mit Clash-Proxy fehlgeschlagen",
"Profile creation failed, retrying with Clash proxy...": "Erstellung des Abonnements fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...",
@@ -555,10 +558,9 @@
"Disallowed ISP": "Nicht zugelassener Internetdienstanbieter",
"Originals Only": "Nur Original",
"Unsupported Country/Region": "Nicht unterstütztes Land/Region",
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
"Controller address copied to clipboard": "API-Port in die Zwischenablage kopiert",
"Secret copied to clipboard": "API-Schlüssel in die Zwischenablage kopiert",
"Copy to clipboard": "Klicken Sie hier, um zu kopieren",
"Port Config": "Port-Konfiguration",
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
"Enable one-click random API port and key. Click to randomize the port and key": "Einstellsichere Zufalls-API-Port- und Schlüsselgenerierung aktivieren. Klicken Sie, um Port und Schlüssel zu randomisieren"
}

View File

@@ -59,6 +59,9 @@
"Proxy detail": "Proxy detail",
"Profiles": "Profiles",
"Update All Profiles": "Update All Profiles",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "View Runtime Config",
"Reactivate Profiles": "Reactivate Profiles",
"Paste": "Paste",
@@ -628,7 +631,6 @@
"Unknown": "Unknown",
"Auto update disabled": "Auto update disabled",
"Update subscription successfully": "Update subscription successfully",
"Update failed, retrying with Clash proxy...": "Update failed, retrying with Clash proxy...",
"Update with Clash proxy successfully": "Update with Clash proxy successfully",
"Update failed even with Clash proxy": "Update failed even with Clash proxy",
"Profile creation failed, retrying with Clash proxy...": "Profile creation failed, retrying with Clash proxy...",
@@ -713,5 +715,7 @@
"Allow Auto Update": "Allow Auto Update",
"Menu reorder mode": "Menu reorder mode",
"Unlock menu order": "Unlock menu order",
"Lock menu order": "Lock menu order"
"Lock menu order": "Lock menu order",
"Open App Log": "Open App Log",
"Open Core Log": "Open Core Log"
}

View File

@@ -45,6 +45,9 @@
"Proxy detail": "Mostrar detalles del nodo",
"Profiles": "Suscripciones",
"Update All Profiles": "Actualizar todas las suscripciones",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "Ver configuración en tiempo de ejecución",
"Reactivate Profiles": "Reactivar suscripciones",
"Paste": "Pegar",
@@ -195,10 +198,11 @@
"Settings": "Ajustes",
"System Setting": "Ajustes del sistema",
"Tun Mode": "Modo de interfaz virtual (TUN)",
"TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador",
"Install Service": "Instalar servicio",
"Uninstall Service": "Desinstalar servicio",
"Reset to Default": "Restablecer a los valores predeterminados",
"Tun Mode Info": "El modo TUN (interfaz virtual) gestiona todo el tráfico del sistema. No es necesario habilitar el proxy del sistema cuando está activado.",
"TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador",
"System Proxy Enabled": "El proxy del sistema está habilitado. Sus aplicaciones accederán a Internet a través del proxy.",
"System Proxy Disabled": "El proxy del sistema está deshabilitado. Se recomienda a la mayoría de los usuarios habilitar esta opción.",
"TUN Mode Enabled": "El modo TUN está habilitado. Las aplicaciones accederán a Internet a través de la interfaz virtual.",
@@ -254,6 +258,7 @@
"Unified Delay Info": "Al habilitar la latencia unificada, se realizarán dos pruebas de latencia para eliminar las diferencias de latencia entre diferentes tipos de nodos causadas por el handshake de conexión, etc.",
"Log Level": "Nivel de registro",
"Log Level Info": "Solo se aplica al archivo de registro del núcleo en la carpeta Service del directorio de registros.",
"Port Config": "Configuración de puerto",
"Random Port": "Puerto aleatorio",
"Mixed Port": "Puerto de proxy mixto",
"Socks Port": "Puerto de proxy SOCKS",
@@ -367,17 +372,16 @@
"Stopping Core...": "Deteniendo núcleo...",
"Restarting Core...": "Reiniciando núcleo...",
"Installing Service...": "Instalando servicio...",
"Uninstall Service": "Desinstalar servicio",
"Service Installed Successfully": "Servicio instalado con éxito",
"Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado",
"Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.",
"Uninstalling Service...": "Desinstalando servicio...",
"Waiting for service to be ready...": "Esperando a que el servicio esté listo...",
"Service Installed Successfully": "Servicio instalado con éxito",
"Service Uninstalled Successfully": "Servicio desinstalado con éxito",
"Proxy Daemon Duration Cannot be Less than 1 Second": "El intervalo de tiempo del daemon de proxy no puede ser menor de 1 segundo",
"Invalid Bypass Format": "Formato de omisión de proxy no válido",
"Waiting for service to be ready...": "Esperando a que el servicio esté listo...",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.",
"Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado",
"Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.",
"Core Version Updated": "Versión del núcleo actualizada",
"Clash Core Restarted": "Núcleo de Clash reiniciado",
"GeoData Updated": "GeoData actualizado",
@@ -523,7 +527,6 @@
"Unknown": "Desconocido",
"Auto update disabled": "La actualización automática está deshabilitada",
"Update subscription successfully": "Suscripción actualizada con éxito",
"Update failed, retrying with Clash proxy...": "Error al actualizar la suscripción. Intentando con el proxy de Clash...",
"Update with Clash proxy successfully": "Actualización con el proxy de Clash exitosa",
"Update failed even with Clash proxy": "Error al actualizar incluso con el proxy de Clash",
"Profile creation failed, retrying with Clash proxy...": "Error al crear la suscripción. Intentando con el proxy de Clash...",
@@ -555,10 +558,9 @@
"Disallowed ISP": "Proveedor de servicios de Internet no permitido",
"Originals Only": "Solo originales",
"Unsupported Country/Region": "País/región no soportado",
"Configuration saved successfully": "Configuración aleatoria guardada correctamente",
"Controller address copied to clipboard": "El puerto API se copió al portapapeles",
"Secret copied to clipboard": "La clave API se copió al portapapeles",
"Copy to clipboard": "Haz clic aquí para copiar",
"Port Config": "Configuración de puerto",
"Configuration saved successfully": "Configuración aleatoria guardada correctamente",
"Enable one-click random API port and key. Click to randomize the port and key": "Habilitar la generación de puerto y clave API aleatorios con un solo clic. Haz clic para randomizar el puerto y la clave"
}

View File

@@ -21,7 +21,6 @@
"Label-Connections": "اتصالات",
"Label-Rules": "قوانین",
"Label-Logs": "لاگ‌ها",
"Label-Test": "آزمون",
"Label-Settings": "تنظیمات",
"Proxies": "پراکسی‌ها",
"Proxy Groups": "گروه‌های پراکسی",
@@ -44,6 +43,9 @@
"Proxy detail": "جزئیات پراکسی",
"Profiles": "پروفایل‌ها",
"Update All Profiles": "به‌روزرسانی همه پروفایل‌ها",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
"Reactivate Profiles": "فعال‌سازی مجدد پروفایل‌ها",
"Paste": "چسباندن",
@@ -166,7 +168,6 @@
"Table View": "نمای جدولی",
"List View": "نمای لیستی",
"Close All": "بستن همه",
"Default": "پیش‌فرض",
"Download Speed": "سرعت دانلود",
"Upload Speed": "سرعت بارگذاری",
"Host": "میزبان",
@@ -197,6 +198,8 @@
"Settings": "تنظیمات",
"System Setting": "تنظیمات سیستم",
"Tun Mode": "Tun (کارت شبکه مجازی)",
"TUN requires Service Mode": "حالت تونل‌زنی نیاز به سرویس دارد",
"Install Service": "نصب سرویس",
"Reset to Default": "بازنشانی به پیش‌فرض",
"Tun Mode Info": "حالت Tun (NIC مجازی): تمام ترافیک سیستم را ضبط می کند، وقتی فعال باشد، نیازی به فعال کردن پروکسی سیستم نیست.",
"Stack": "انباشته Tun",
@@ -275,13 +278,13 @@
"Restart": "راه‌اندازی مجدد",
"Release Version": "نسخه نهایی",
"Alpha Version": "نسخه آلفا",
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Please enter your root password": "لطفاً رمز ریشه خود را وارد کنید",
"Grant": "اعطا",
"Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
"Update GeoData": "به‌روزرسانی GeoData",
"Verge Setting": "تنظیمات Verge",
"Verge Basic Setting": "تنظیمات پایه Verge",
"Verge Advanced Setting": "تنظیمات پیشرفته Verge",
"Language": "زبان",
"Theme Mode": "حالت تم",
"theme.light": "روشن",
@@ -368,6 +371,7 @@
"Profile Reactivated": "پروفایل مجدداً فعال شد",
"Only YAML Files Supported": "فقط فایل‌های YAML پشتیبانی می‌شوند",
"Settings Applied": "تنظیمات اعمال شد",
"Installing Service...": "در حال نصب سرویس...",
"Service Installed Successfully": "سرویس با موفقیت نصب شد",
"Service Uninstalled Successfully": "سرویس با موفقیت حذف نصب شد",
"Proxy Daemon Duration Cannot be Less than 1 Second": "مدت زمان دیمن پراکسی نمی‌تواند کمتر از 1 ثانیه باشد",
@@ -451,10 +455,9 @@
"Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد",
"Core Changed Successfully": "هسته با موفقیت تغییر کرد",
"Failed to Change Core": "تغییر هسته ناموفق بود",
"Verge Basic Setting": "تنظیمات پایه Verge",
"Verge Advanced Setting": "تنظیمات پیشرفته Verge",
"TUN requires Service Mode": "حالت تونل‌زنی نیاز به سرویس دارد",
"Install Service": "نصب سرویس",
"Installing Service...": "در حال نصب سرویس...",
"Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد"
"Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد",
"Default": "پیش‌فرض",
"Label-Test": "آزمون",
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Verge Setting": "تنظیمات Verge"
}

View File

@@ -21,40 +21,7 @@
"Label-Connections": "Koneksi",
"Label-Rules": "Aturan",
"Label-Logs": "Log",
"Label-Test": "Tes",
"Label-Settings": "Pengaturan",
"Dashboard": "Dasbor",
"Profile": "Profil",
"Help": "Bantuan",
"About": "Tentang",
"Theme": "Tema",
"Main Window": "Jendela Utama",
"Group Icon": "Ikon Grup",
"Menu Icon": "Ikon Menu",
"PAC File": "Berkas PAC",
"Web UI": "Antarmuka Web",
"Hotkeys": "Pintasan",
"Verge Mixed Port": "Port Campuran Verge",
"Verge Socks Port": "Port Socks Verge",
"Verge Redir Port": "Port Pengalihan Verge",
"Verge Tproxy Port": "Port Tproxy Verge",
"Verge Port": "Port Verge",
"Verge HTTP Enabled": "HTTP Verge Diaktifkan",
"WebDAV URL": "URL WebDAV",
"WebDAV Username": "Nama Pengguna WebDAV",
"WebDAV Password": "Kata Sandi WebDAV",
"Restart App": "Mulai Ulang Aplikasi",
"Restart Clash Core": "Mulai Ulang Core Clash",
"TUN Mode": "Mode TUN",
"Copy Env": "Salin Env",
"Conf Dir": "Direktori Konfigurasi",
"Core Dir": "Direktori Core",
"Logs Dir": "Direktori Log",
"Open Dir": "Buka Direktori",
"More": "Lainnya",
"Rule Mode": "Mode Aturan",
"Global Mode": "Mode Global",
"Direct Mode": "Mode Langsung",
"Proxies": "Proksi",
"Proxy Groups": "Grup Proksi",
"Proxy Provider": "Penyedia Proksi",
@@ -76,6 +43,9 @@
"Proxy detail": "Detail Proksi",
"Profiles": "Profil",
"Update All Profiles": "Perbarui Semua Profil",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "Lihat Konfigurasi Runtime",
"Reactivate Profiles": "Reaktivasi Profil",
"Paste": "Tempel",
@@ -198,7 +168,6 @@
"Table View": "Tampilan Tabel",
"List View": "Tampilan Daftar",
"Close All": "Tutup Semua",
"Default": "Default",
"Download Speed": "Kecepatan Unduh",
"Upload Speed": "Kecepatan Unggah",
"Host": "Host",
@@ -229,6 +198,8 @@
"Settings": "Pengaturan",
"System Setting": "Pengaturan Sistem",
"Tun Mode": "Mode Tun (NIC Virtual)",
"TUN requires Service Mode": "Mode TUN memerlukan layanan",
"Install Service": "Instal Layanan",
"Reset to Default": "Setel Ulang ke Default",
"Tun Mode Info": "Mode Tun (NIC Virtual): Menangkap semua lalu lintas sistem, saat diaktifkan, tidak perlu mengaktifkan proksi sistem.",
"Stack": "Tumpukan Tun",
@@ -312,7 +283,8 @@
"Open UWP tool": "Buka alat UWP",
"Open UWP tool Info": "Sejak Windows 8, aplikasi UWP (seperti Microsoft Store) dibatasi dari mengakses layanan jaringan host lokal secara langsung, dan alat ini dapat digunakan untuk melewati pembatasan ini",
"Update GeoData": "Perbarui GeoData",
"Verge Setting": "Pengaturan Verge",
"Verge Basic Setting": "Pengaturan Dasar Verge",
"Verge Advanced Setting": "Pengaturan Lanjutan Verge",
"Language": "Bahasa",
"Theme Mode": "Mode Tema",
"theme.light": "Terang",
@@ -399,6 +371,7 @@
"Profile Reactivated": "Profil Diaktifkan Kembali",
"Only YAML Files Supported": "Hanya File YAML yang Didukung",
"Settings Applied": "Pengaturan Diterapkan",
"Installing Service...": "Memasang Layanan...",
"Service Installed Successfully": "Layanan Berhasil Diinstal",
"Service Uninstalled Successfully": "Layanan Berhasil Dicopot",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Durasi Daemon Proksi Tidak Boleh Kurang dari 1 Detik",
@@ -437,6 +410,38 @@
"Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?",
"Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
"Failed to fetch backup files": "Gagal mengambil file cadangan",
"Profile": "Profil",
"Help": "Bantuan",
"About": "Tentang",
"Theme": "Tema",
"Main Window": "Jendela Utama",
"Group Icon": "Ikon Grup",
"Menu Icon": "Ikon Menu",
"PAC File": "Berkas PAC",
"Web UI": "Antarmuka Web",
"Hotkeys": "Pintasan",
"Verge Mixed Port": "Port Campuran Verge",
"Verge Socks Port": "Port Socks Verge",
"Verge Redir Port": "Port Pengalihan Verge",
"Verge Tproxy Port": "Port Tproxy Verge",
"Verge Port": "Port Verge",
"Verge HTTP Enabled": "HTTP Verge Diaktifkan",
"WebDAV URL": "URL WebDAV",
"WebDAV Username": "Nama Pengguna WebDAV",
"WebDAV Password": "Kata Sandi WebDAV",
"Dashboard": "Dasbor",
"Restart App": "Mulai Ulang Aplikasi",
"Restart Clash Core": "Mulai Ulang Core Clash",
"TUN Mode": "Mode TUN",
"Copy Env": "Salin Env",
"Conf Dir": "Direktori Konfigurasi",
"Core Dir": "Direktori Core",
"Logs Dir": "Direktori Log",
"Open Dir": "Buka Direktori",
"More": "Lainnya",
"Rule Mode": "Mode Aturan",
"Global Mode": "Mode Global",
"Direct Mode": "Mode Langsung",
"Enable Tray Speed": "Aktifkan Tray Speed",
"LightWeight Mode": "Mode Ringan",
"LightWeight Mode Info": "Tutup GUI dan biarkan hanya kernel yang berjalan",
@@ -450,10 +455,8 @@
"Script File Error": "Kesalahan file skrip, perubahan dibatalkan",
"Core Changed Successfully": "Inti berhasil diubah",
"Failed to Change Core": "Gagal mengubah inti",
"Verge Basic Setting": "Pengaturan Dasar Verge",
"Verge Advanced Setting": "Pengaturan Lanjutan Verge",
"TUN requires Service Mode": "Mode TUN memerlukan layanan",
"Install Service": "Instal Layanan",
"Installing Service...": "Memasang Layanan...",
"Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem"
"Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem",
"Default": "Default",
"Label-Test": "Tes",
"Verge Setting": "Pengaturan Verge"
}

View File

@@ -45,6 +45,9 @@
"Proxy detail": "ノードの詳細を表示する",
"Profiles": "プロファイル",
"Update All Profiles": "すべてのプロファイルを更新",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "実行時のプロファイルを表示",
"Reactivate Profiles": "プロファイルを再アクティブ化",
"Paste": "貼り付け",
@@ -195,10 +198,11 @@
"Settings": "設定",
"System Setting": "システム設定",
"Tun Mode": "仮想ネットワークカードモード",
"TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です",
"Install Service": "サービスをインストール",
"Uninstall Service": "サービスのアンインストール",
"Reset to Default": "デフォルト値にリセット",
"Tun Mode Info": "TUN仮想ネットワークカードモードはシステムのすべてのトラフィックを制御します。有効にすると、システムプロキシを開く必要はありません。",
"TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です",
"System Proxy Enabled": "システムプロキシが有効になっています。アプリケーションはプロキシを通じてネットワークにアクセスします。",
"System Proxy Disabled": "システムプロキシが無効になっています。ほとんどのユーザーはこのオプションをオンにすることをお勧めします。",
"TUN Mode Enabled": "TUNモードが有効になっています。アプリケーションは仮想ネットワークカードを通じてネットワークにアクセスします。",
@@ -254,6 +258,7 @@
"Unified Delay Info": "統一遅延を有効にすると、2回の遅延テストが行われ、接続ハンドシェイクなどによる異なるタイプのードの遅延差を解消します。",
"Log Level": "ログレベル",
"Log Level Info": "ログディレクトリのServiceフォルダ内のコアログファイルにのみ適用されます。",
"Port Config": "ポート設定",
"Random Port": "ランダムポート",
"Mixed Port": "混合プロキシポート",
"Socks Port": "SOCKSプロキシポート",
@@ -370,17 +375,16 @@
"Stopping Core...": "コアを停止中...",
"Restarting Core...": "コアを再起動中...",
"Installing Service...": "サービスをインストール中...",
"Uninstall Service": "サービスのアンインストール",
"Service Installed Successfully": "サービスのインストールに成功しました。",
"Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。",
"Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。",
"Uninstalling Service...": "サービスをアンインストール中...",
"Waiting for service to be ready...": "サービスの準備を待っています...",
"Service Installed Successfully": "サービスのインストールに成功しました。",
"Service Uninstalled Successfully": "サービスのアンインストールに成功しました。",
"Proxy Daemon Duration Cannot be Less than 1 Second": "プロキシデーモンの間隔は1秒以上に設定する必要があります。",
"Invalid Bypass Format": "無効なバイパス形式",
"Waiting for service to be ready...": "サービスの準備を待っています...",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。",
"Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。",
"Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。",
"Core Version Updated": "コアバージョンが更新されました。",
"Clash Core Restarted": "Clashコアが再起動されました。",
"GeoData Updated": "GeoDataが更新されました。",
@@ -526,7 +530,6 @@
"Unknown": "不明",
"Auto update disabled": "自動更新が無効になっています。",
"Update subscription successfully": "サブスクリプションの更新に成功しました。",
"Update failed, retrying with Clash proxy...": "サブスクリプションの更新に失敗しました。Clashプロキシを使用して再試行します...",
"Update with Clash proxy successfully": "Clashプロキシを使用して更新に成功しました。",
"Update failed even with Clash proxy": "Clashプロキシを使用しても更新に失敗しました。",
"Profile creation failed, retrying with Clash proxy...": "プロファイルの作成に失敗しました。Clashプロキシを使用して再試行します...",
@@ -558,12 +561,9 @@
"Disallowed ISP": "許可されていないインターネットサービスプロバイダー",
"Originals Only": "オリジナルのみ",
"Unsupported Country/Region": "サポートされていない国/地域",
"Configuration saved successfully": "ランダム設定を保存完了",
"Controller address copied to clipboard": "API ポートがクリップボードにコピーされました",
"Secret copied to clipboard": "API キーがクリップボードにコピーされました",
"Copy to clipboard": "クリックしてコピー",
"Port Config": "ポート設定",
"Configuration saved successfully": "ランダム設定を保存完了",
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください",
"Batch Operations": "バッチ操作",
"Delete Selected Profiles": "選択したプロファイルを削除",
"Deselect All": "すべての選択を解除",
@@ -571,5 +571,7 @@
"items": "アイテム",
"Select All": "すべて選択",
"Selected": "選択済み",
"Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました"
"Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました",
"Copy to clipboard": "クリックしてコピー",
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
}

View File

@@ -46,6 +46,9 @@
"Proxy detail": "프록시 상세",
"Profiles": "프로필",
"Update All Profiles": "모든 프로필 업데이트",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "런타임 설정 보기",
"Reactivate Profiles": "프로필 재활성화",
"Paste": "붙여넣기",
@@ -127,6 +130,7 @@
"Lazy": "지연 로딩",
"Timeout": "타임아웃",
"Max Failed Times": "최대 실패 횟수",
"Interface Name": "인터페이스 이름",
"Routing Mark": "라우팅 마크",
"Include All": "모든 프록시 및 제공자 포함",
"Include All Providers": "모든 제공자 포함",
@@ -198,176 +202,52 @@
"Edit Test": "테스트 편집",
"Icon": "아이콘",
"Test URL": "테스트 URL",
"Timeout (ms)": "타임아웃 (ms)",
"Expected": "예상됨",
"URL": "URL",
"Method": "메소드",
"Failed": "실패",
"Succeed": "성공",
"Settings": "설정",
"Core Config": "코어 설정",
"Clash Setting": "Clash 설정",
"Verge Setting": "Verge 설정",
"System Setting": "시스템 설정",
"Appearance": "외관",
"Experimental Features": "실험적 기능",
"Others": "기타",
"Mixed Port": "혼합 포트",
"Allow LAN": "LAN 허용",
"IPv6": "IPv6",
"Log Level": "로그 레벨",
"Core Type": "코어 유형",
"General": "일반",
"Mode": "모드",
"Tun Mode": "Tun 모드",
"Transparent Proxy": "투명 프록시",
"Specify YAML": "YAML 지정",
"Status": "상태",
"Memory Usage": "메모리 사용량",
"Stack": "스택",
"Network": "네트워크",
"MTU": "MTU",
"Auto Route": "자동 라우팅",
"Auto Detect Interface": "인터페이스 자동 감지",
"Interface Name": "인터페이스 이름",
"Endpoint Independent Nat": "엔드포인트 독립 NAT",
"Include Reserved": "예약된 IP 포함",
"Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화",
"TCP Fast Open": "TCP 빠른 열기",
"Silent Start": "자동 시작",
"TcpConcurrent": "TCP 동시성",
"MTU": "MTU",
"Service Mode": "서비스 모드",
"System Proxy": "시스템 프록시",
"Start With System": "시스템과 함께 시작",
"Set System Proxy": "시스템 프록시 설정",
"Set as System Proxy": "시스템 프록시로 설정",
"System Proxy Status": "시스템 프록시 상태",
"Start Option": "시작 옵션",
"Start Core on Start": "시작 시 코어 시작",
"Start Core with System": "시스템과 함께 코어 시작",
"Start Core with System Proxy": "시스템 프록시와 함께 코어 시작",
"Start Core with Tun": "Tun과 함께 코어 시작",
"Silent Start Option": "자동 시작 옵션",
"Hidden Window on Start": "시작 시 창 숨기기",
"Log Notice": "로그 알림",
"Warning": "경고",
"Error": "오류",
"Silent Start": "자동 시작",
"Clash Setting": "Clash 설정",
"IPv6": "IPv6",
"Log Level": "로그 레벨",
"Mixed Port": "혼합 포트",
"Verge Basic Setting": "Verge 기본 설정",
"Language": "언어",
"Theme Mode": "테마 모드",
"Tray Click Event": "트레이 클릭 이벤트",
"Show Main Window": "메인 창 표시",
"Show Tray Menu": "트레이 메뉴 표시",
"Open Config Folder": "설정 폴더 열기",
"Open Dashboard": "대시보드 열기",
"Hotkey Setting": "단축키 설정",
"Misc Setting": "기타 설정",
"Layout Setting": "레이아웃 설정",
"Update Setting": "업데이트 설정",
"Enable Hotkeys": "단축키 활성화",
"System Hotkey": "시스템 단축키",
"Hotkey Enable": "단축키 활성화",
"Require Clash Core Running": "Clash 코어 실행 필요",
"Copy Env Type": "환경 유형 복사",
"Copy Success": "복사 성공",
"Start Page": "시작 페이지",
"Startup Script": "시작 스크립트",
"Icon Group Type": "아이콘 그룹 유형",
"Always": "항상",
"On Update": "업데이트 시",
"By Traffic": "트래픽별",
"Web UI List": "웹 UI 목록",
"Installed Web UI": "설치된 웹 UI",
"Built-in Web UI": "내장 웹 UI",
"Current Config": "현재 설정",
"System Config": "시스템 설정",
"Port": "포트",
"WebUI Current Port": "웹 UI 현재 포트",
"Theme": "테마",
"Light": "라이트",
"Dark": "다크",
"Auto": "자동",
"System": "시스템",
"Proxy Item Width": "프록시 항목 너비",
"Proxy Item Height": "프록시 항목 높이",
"Compact Mode": "압축 모드",
"Git Proxy": "Git 프록시",
"Enable API": "API 활성화",
"Enable Lan": "LAN 활성화",
"Select a config file": "설정 파일 선택",
"Open Config Dir": "설정 디렉토리 열기",
"System Proxy Permission": "시스템 프록시 권한",
"System Stack Type": "시스템 스택 유형",
"Undefined stack": "정의되지 않은 스택",
"Auto Start": "자동 시작",
"Mixin": "혼합",
"Set as System Auto Proxy": "시스템 자동 프록시로 설정",
"System Auto Proxy Status": "시스템 자동 프록시 상태",
"Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)",
"Primary Color": "기본 색상",
"Layout Setting": "레이아웃 설정",
"Traffic Graph": "트래픽 그래프",
"Memory Usage": "메모리 사용량",
"Auto Delay Detection": "자동 지연 감지",
"Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다",
"Hotkey Setting": "단축키 설정",
"Filter": "필터",
"Import Subscription Successful": "구독 가져오기 성공",
"Username": "사용자 이름",
"Password": "비밀번호",
"Auth Proxy": "인증 프록시",
"Geox User": "Geox 사용자",
"Geox Password": "Geox 비밀번호",
"Log File": "로그 파일",
"Enable Clash.Meta Logs": "Clash.Meta 로그 활성화",
"Verge Log": "Verge 로그",
"Enable Verge Logs": "Verge 로그 활성화",
"Traffic Graph": "트래픽 그래프",
"Profile Token": "프로필 토큰",
"Profile User Agent": "프로필 사용자 에이전트",
"The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.",
"Profile Format": "프로필 포맷",
"The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.",
"Theme Color": "테마 색상",
"Primary Color": "기본 색상",
"Customize primary color": "기본 색상 사용자 정의",
"Danger Zone": "위험 영역",
"Reset Verge Theme": "Verge 테마 재설정",
"Inject CSS": "CSS 주입",
"Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입",
"Inject HTML": "HTML 주입",
"Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)",
"Capture": "캡처",
"Color Scheme": "색상 구성표",
"Default": "기본값",
"Pink": "분홍색",
"Red": "빨간색",
"Yellow": "노란색",
"Green": "녹색",
"Cyan": "청록색",
"Blue": "파란색",
"Purple": "보라색",
"Proxy Detail": "프록시 상세",
"Address": "주소",
"Filter": "필터",
"Check Updates on Start": "시작 시 업데이트 확인",
"For Alpha Version": "알파 버전용",
"Latest Build Version": "최신 빌드 버전",
"Check Updates": "업데이트 확인",
"Proxy Setting": "프록시 설정",
"WebDav Setting": "WebDav 설정",
"WebDav Upload": "WebDav 업로드",
"WebDav Download": "WebDav 다운로드",
"Clean Cache": "캐시 정리",
"Check Network": "네트워크 확인",
"WebDav Status": "WebDav 상태",
"WebDav URL": "WebDav URL",
"WebDav Username": "WebDav 사용자 이름",
"WebDav Password": "WebDav 비밀번호",
"Update Interval(minute)": "업데이트 간격(분)",
"Skip Cert Verify": "인증서 확인 건너뛰기",
"Import Subscription Successful": "구독 가져오기 성공",
"Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공",
"Update failed, retrying with Clash proxy...": "업데이트 실패, Clash 프록시로 재시도 중...",
"Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패",
"Theme": "테마",
"Config Validation Failed": "설정 검증 실패",
"Boot Config Validation Failed": "부팅 설정 검증 실패",
"Core Change Config Validation Failed": "코어 변경 설정 검증 실패",
"Config Validation Failed": "설정 검증 실패",
"Config Validation Process Terminated": "설정 검증 프로세스 종료됨",
"Script File Error": "스크립트 파일 오류",
"Script Syntax Error": "스크립트 구문 오류",
"Script Missing Main": "스크립트 메인 없음",
"File Not Found": "파일을 찾을 수 없음",
"Script File Error": "스크립트 파일 오류",
"Core Changed Successfully": "코어 변경 성공",
"Failed to Change Core": "코어 변경 실패",
"YAML Syntax Error": "YAML 구문 오류",
"YAML Read Error": "YAML 읽기 오류",
"YAML Mapping Error": "YAML 매핑 오류",
@@ -377,28 +257,150 @@
"Merge File Mapping Error": "병합 파일 매핑 오류",
"Merge File Key Error": "병합 파일 키 오류",
"Merge File Error": "병합 파일 오류",
"Core Changed Successfully": "코어 변경 성공",
"Failed to Change Core": "코어 변경 실패",
"Copy Success": "복사 성공",
"Copy Failed": "복사 실패",
"Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공",
"Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패",
"Failed": "실패",
"Address": "주소",
"Allow LAN": "LAN 허용",
"Always": "항상",
"Appearance": "외관",
"Auth Proxy": "인증 프록시",
"Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)",
"Auto": "자동",
"Auto Start": "자동 시작",
"Blue": "파란색",
"Built-in Web UI": "내장 웹 UI",
"By Traffic": "트래픽별",
"Cannot Import Empty Subscription URL": "빈 구독 URL을 가져올 수 없습니다",
"Profile Already Exists": "프로필이 이미 존재합니다",
"Input Subscription URL": "구독 URL 입력",
"Create Profile Successful": "프로필 생성 성공",
"Capture": "캡처",
"Check Network": "네트워크 확인",
"Check Updates": "업데이트 확인",
"Check Updates on Start": "시작 시 업데이트 확인",
"Clean Cache": "캐시 정리",
"Color Scheme": "색상 구성표",
"Compact Mode": "압축 모드",
"Copy Failed": "복사 실패",
"Core Config": "코어 설정",
"Core Type": "코어 유형",
"Create Profile Failed": "프로필 생성 실패",
"Patch Profile Successful": "프로필 패치 성공",
"Patch Profile Failed": "프로필 패치 실패",
"Delete Profile Successful": "프로필 삭제 성공",
"Create Profile Successful": "프로필 생성 성공",
"Current Config": "현재 설정",
"Customize primary color": "기본 색상 사용자 정의",
"Cyan": "청록색",
"Danger Zone": "위험 영역",
"Dark": "다크",
"Default": "기본값",
"Delete Profile Failed": "프로필 삭제 실패",
"Select Active Profile Successful": "활성 프로필 선택 성공",
"Delete Profile Successful": "프로필 삭제 성공",
"Enable API": "API 활성화",
"Enable Clash.Meta Logs": "Clash.Meta 로그 활성화",
"Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화",
"Enable Hotkeys": "단축키 활성화",
"Enable Lan": "LAN 활성화",
"Enable Verge Logs": "Verge 로그 활성화",
"Endpoint Independent Nat": "엔드포인트 독립 NAT",
"Error": "오류",
"Expected": "예상됨",
"Experimental Features": "실험적 기능",
"For Alpha Version": "알파 버전용",
"General": "일반",
"Geox Password": "Geox 비밀번호",
"Geox User": "Geox 사용자",
"Git Proxy": "Git 프록시",
"Green": "녹색",
"Hidden Window on Start": "시작 시 창 숨기기",
"Hotkey Enable": "단축키 활성화",
"Icon Group Type": "아이콘 그룹 유형",
"Include Reserved": "예약된 IP 포함",
"Inject CSS": "CSS 주입",
"Inject HTML": "HTML 주입",
"Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입",
"Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)",
"Input Subscription URL": "구독 URL 입력",
"Installed Web UI": "설치된 웹 UI",
"Latest Build Version": "최신 빌드 버전",
"Light": "라이트",
"Log File": "로그 파일",
"Log Notice": "로그 알림",
"Method": "메소드",
"Misc Setting": "기타 설정",
"Mixin": "혼합",
"Mode": "모드",
"Network": "네트워크",
"On Update": "업데이트 시",
"Open Config Dir": "설정 디렉토리 열기",
"Open Config Folder": "설정 폴더 열기",
"Open Dashboard": "대시보드 열기",
"Others": "기타",
"Patch Profile Failed": "프로필 패치 실패",
"Patch Profile Successful": "프로필 패치 성공",
"Pink": "분홍색",
"Port": "포트",
"Profile Already Exists": "프로필이 이미 존재합니다",
"Profile Format": "프로필 포맷",
"Profile Token": "프로필 토큰",
"Profile User Agent": "프로필 사용자 에이전트",
"Proxy Detail": "프록시 상세",
"Proxy Item Height": "프록시 항목 높이",
"Proxy Item Width": "프록시 항목 너비",
"Proxy Setting": "프록시 설정",
"Purple": "보라색",
"Red": "빨간색",
"Require Clash Core Running": "Clash 코어 실행 필요",
"Reset Verge Theme": "Verge 테마 재설정",
"Select Active Profile Failed": "활성 프로필 선택 실패",
"View Profile-Runtime": "프로필-런타임 보기",
"View Profile-Content": "프로필-내용 보기",
"View Profile-Original": "프로필-원본 보기",
"View Profile-Script": "프로필-스크립트 보기",
"View Profile-Merge": "프로필-병합 보기",
"Update Successful": "업데이트 성공",
"Select Active Profile Successful": "활성 프로필 선택 성공",
"Select a config file": "설정 파일 선택",
"Set System Proxy": "시스템 프록시 설정",
"Set as System Auto Proxy": "시스템 자동 프록시로 설정",
"Set as System Proxy": "시스템 프록시로 설정",
"Silent Start Option": "자동 시작 옵션",
"Skip Cert Verify": "인증서 확인 건너뛰기",
"Specify YAML": "YAML 지정",
"Start Core on Start": "시작 시 코어 시작",
"Start Core with System": "시스템과 함께 코어 시작",
"Start Core with System Proxy": "시스템 프록시와 함께 코어 시작",
"Start Core with Tun": "Tun과 함께 코어 시작",
"Start Option": "시작 옵션",
"Start With System": "시스템과 함께 시작",
"Status": "상태",
"Succeed": "성공",
"System": "시스템",
"System Auto Proxy Status": "시스템 자동 프록시 상태",
"System Config": "시스템 설정",
"System Hotkey": "시스템 단축키",
"System Proxy Permission": "시스템 프록시 권한",
"System Proxy Status": "시스템 프록시 상태",
"System Stack Type": "시스템 스택 유형",
"TCP Fast Open": "TCP 빠른 열기",
"TcpConcurrent": "TCP 동시성",
"The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.",
"The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.",
"Theme Color": "테마 색상",
"Timeout (ms)": "타임아웃 (ms)",
"Transparent Proxy": "투명 프록시",
"URL": "URL",
"Undefined stack": "정의되지 않은 스택",
"Update Failed": "업데이트 실패",
"Auto Delay Detection": "자동 지연 감지",
"Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다"
"Update Interval(minute)": "업데이트 간격(분)",
"Update Setting": "업데이트 설정",
"Update Successful": "업데이트 성공",
"Verge Log": "Verge 로그",
"Verge Setting": "Verge 설정",
"View Profile-Content": "프로필-내용 보기",
"View Profile-Merge": "프로필-병합 보기",
"View Profile-Original": "프로필-원본 보기",
"View Profile-Runtime": "프로필-런타임 보기",
"View Profile-Script": "프로필-스크립트 보기",
"Warning": "경고",
"Web UI List": "웹 UI 목록",
"WebDav Download": "WebDav 다운로드",
"WebDav Password": "WebDav 비밀번호",
"WebDav Setting": "WebDav 설정",
"WebDav Status": "WebDav 상태",
"WebDav URL": "WebDav URL",
"WebDav Upload": "WebDav 업로드",
"WebDav Username": "WebDav 사용자 이름",
"WebUI Current Port": "웹 UI 현재 포트",
"Yellow": "노란색"
}

View File

@@ -51,6 +51,9 @@
"Proxy detail": "Отображать больше сведений о прокси",
"Profiles": "Профили",
"Update All Profiles": "Обновить все профили",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "Просмотреть используемый конфиг",
"Reactivate Profiles": "Перезапустить профиль",
"Paste": "Вставить",

View File

@@ -46,6 +46,9 @@
"Proxy detail": "Vekil detayı",
"Profiles": "Profiller",
"Update All Profiles": "Tüm Profilleri Güncelle",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "Çalışma Zamanı Yapılandırmasını Görüntüle",
"Reactivate Profiles": "Profilleri Yeniden Etkinleştir",
"Paste": "Yapıştır",
@@ -508,7 +511,6 @@
"Fake IP Filter Mode": "Sahte IP Filtre Modu",
"Enable IPv6 DNS resolution": "IPv6 DNS çözümlemesini etkinleştir",
"Prefer H3": "H3'ü Tercih Et",
"DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır",
"Respect Rules": "Kurallara Uy",
"DNS connections follow routing rules": "DNS bağlantıları yönlendirme kurallarını takip eder",
"Use Hosts": "Hosts Kullan",
@@ -575,7 +577,6 @@
"Unknown": "Bilinmiyor",
"Auto update disabled": "Otomatik güncelleme devre dışı",
"Update subscription successfully": "Abonelik başarıyla güncellendi",
"Update failed, retrying with Clash proxy...": "Güncelleme başarısız oldu, Clash vekil ile yeniden deneniyor...",
"Update with Clash proxy successfully": "Clash vekil ile güncelleme başarılı",
"Update failed even with Clash proxy": "Clash vekil ile bile güncelleme başarısız oldu",
"Profile creation failed, retrying with Clash proxy...": "Profil oluşturma başarısız oldu, Clash vekil ile yeniden deneniyor...",
@@ -616,5 +617,6 @@
"items": "öğeler",
"Select All": "Tümünü Seç",
"Selected": "Seçildi",
"Selected profiles deleted successfully": "Seçili profiller başarıyla silindi"
"Selected profiles deleted successfully": "Seçili profiller başarıyla silindi",
"DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır"
}

View File

@@ -21,7 +21,6 @@
"Label-Connections": "Тоташулар",
"Label-Rules": "Кагыйдәләр",
"Label-Logs": "Логлар",
"Label-Test": "Тест",
"Label-Settings": "Көйләүләр",
"Proxies": "Прокси",
"Proxy Groups": "Прокси төркемнәре",
@@ -44,6 +43,9 @@
"Proxy detail": "Прокси турында тулы мәгълүмат",
"Profiles": "Профильләр",
"Update All Profiles": "Барлык профильләрне яңарту",
"Update Channel": "Update Channel",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "Кулланылган конфигурацияне карау",
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
"Paste": "Кую",
@@ -166,7 +168,6 @@
"Table View": "Таблица күзаллау",
"List View": "Исемлек күзаллау",
"Close All": "Барысын да ябу",
"Default": "Башлангыч",
"Download Speed": "Йөкләү тизлеге",
"Upload Speed": "Йөкләү (чыгару) тизлеге",
"Host": "Хост",
@@ -197,6 +198,8 @@
"Settings": "Көйләүләр",
"System Setting": "Система көйләүләре",
"Tun Mode": "Tun режимы (виртуаль челтәр адаптеры)",
"TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә",
"Install Service": "Хезмәтне урнаштыру",
"Reset to Default": "Башлангычка кайтару",
"Tun Mode Info": "Tun режимы бөтен системаның трафигын тотып ала. Аны кабызган очракта системалы проксины аерым кабызу таләп ителми.",
"Stack": "Стек",
@@ -280,7 +283,8 @@
"Open UWP tool": "UWP инструментын ачу",
"Open UWP tool Info": "Windows 8'дән башлап UWP кушымталары (Microsoft Store кебек) локаль хосттагы челтәр хезмәтләренә турыдан-туры тоташа алмый. Бу инструмент әлеге чикләүне әйләнеп узарга ярдәм итә",
"Update GeoData": "GeoData яңарту",
"Verge Setting": "Verge көйләүләре",
"Verge Basic Setting": "Verge Төп көйләүләр",
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр",
"Language": "Тел",
"Theme Mode": "Теманың режимы",
"theme.light": "Якты",
@@ -296,8 +300,6 @@
"Theme Setting": "Тема көйләүләре",
"Primary Color": "Төп төс",
"Secondary Color": "Икенче төс",
"Primary Text Color": "Төп текст төсе",
"Secondary Text Color": "Икенче текст төсе",
"Info Color": "Мәгълүмат төсе",
"Warning Color": "Кисәтү төсе",
"Error Color": "Хата төсе",
@@ -367,6 +369,7 @@
"Profile Reactivated": "Профиль яңадан активлаштырылды",
"Only YAML Files Supported": "Фәкать YAML-файллар гына хуплана",
"Settings Applied": "Көйләүләр кулланылды",
"Installing Service...": "Хезмәт урнаштырыла...",
"Service Installed Successfully": "Сервис уңышлы урнаштырылды",
"Service Uninstalled Successfully": "Сервис уңышлы салдырылды",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Прокси-демон эш вакыты 1 секундтан ким була алмый",
@@ -380,7 +383,6 @@
"Clash Core Restarted": "Clash ядросы яңадан башланды",
"GeoData Updated": "GeoData яңартылды",
"Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган",
"Import subscription successful": "Подписка уңышлы импортланды",
"WebDAV Server URL": "WebDAV сервер URL-ы (http(s)://)",
"Username": "Кулланучы исеме",
"Password": "Пароль",
@@ -450,10 +452,11 @@
"Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды",
"Core Changed Successfully": "Ядро уңышлы алыштырылды",
"Failed to Change Core": "Ядро алыштыру уңышсыз булды",
"Verge Basic Setting": "Verge Төп көйләүләр",
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр",
"TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә",
"Install Service": "Хезмәтне урнаштыру",
"Installing Service...": "Хезмәт урнаштырыла...",
"Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә"
"Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә",
"Default": "Башлангыч",
"Import subscription successful": "Подписка уңышлы импортланды",
"Label-Test": "Тест",
"Primary Text Color": "Төп текст төсе",
"Secondary Text Color": "Икенче текст төсе",
"Verge Setting": "Verge көйләүләре"
}

View File

@@ -59,6 +59,9 @@
"Proxy detail": "展示节点细节",
"Profiles": "订阅",
"Update All Profiles": "更新所有订阅",
"Update Channel": "更新通道",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "查看运行时订阅",
"Reactivate Profiles": "重新激活订阅",
"Paste": "粘贴",
@@ -305,7 +308,7 @@
"Socks Port": "SOCKS 代理端口",
"Http Port": "HTTP(S) 代理端口",
"Redir Port": "Redir 透明代理端口",
"TPROXY Port": "TPROXY 透明代理端口",
"Tproxy Port": "Tproxy 透明代理端口",
"Port settings saved": "端口设置已保存",
"Failed to save port settings": "端口设置保存失败",
"External": "外部控制",
@@ -426,6 +429,8 @@
"Uninstalling Service...": "卸载服务中...",
"Service Installed Successfully": "已成功安装服务",
"Service Uninstalled Successfully": "已成功卸载服务",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒",
"Invalid Bypass Format": "无效的代理绕过格式",
"Waiting for service to be ready...": "等待服务准备就绪...",
"Service not ready, retrying attempt {count}/{total}...": "服务未就绪,正在重试 {{count}}/{{total}} 次...",
"Failed to check service status, retrying attempt {count}/{total}...": "检查服务状态失败,正在重试 {{count}}/{{total}} 次...",
@@ -436,8 +441,6 @@
"Fallback core restart also failed: {message}": "后备内核重启也失败了: {{message}}",
"Service is ready and core restarted": "服务已就绪,内核已重启",
"Core restarted. Service is now available.": "内核已重启,服务现已可用",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒",
"Invalid Bypass Format": "无效的代理绕过格式",
"Clash Port Modified": "Clash 端口已修改",
"Port Conflict": "端口冲突",
"Restart Application to Apply Modifications": "重启 Verge 以应用修改",
@@ -628,7 +631,6 @@
"Unknown": "未知",
"Auto update disabled": "自动更新已禁用",
"Update subscription successfully": "订阅更新成功",
"Update failed, retrying with Clash proxy...": "订阅更新失败,尝试使用 Clash 代理更新",
"Update with Clash proxy successfully": "使用 Clash 代理更新成功",
"Update failed even with Clash proxy": "使用 Clash 代理更新也失败",
"Profile creation failed, retrying with Clash proxy...": "订阅创建失败,尝试使用 Clash 代理创建",
@@ -713,5 +715,8 @@
"Allow Auto Update": "允许自动更新",
"Menu reorder mode": "菜单排序模式",
"Unlock menu order": "解锁菜单排序",
"Lock menu order": "锁定菜单排序"
"Lock menu order": "锁定菜单排序",
"Open App Log": "应用日志",
"Open Core Log": "内核日志",
"TPROXY Port": "TPROXY 透明代理端口"
}

View File

@@ -59,6 +59,9 @@
"Proxy detail": "展示節點細節",
"Profiles": "訂閱",
"Update All Profiles": "更新所有訂閱",
"Update Channel": "更新頻道",
"Update Channel Stable": "Stable",
"Update Channel Autobuild": "Autobuild",
"View Runtime Config": "查看執行時訂閱",
"Reactivate Profiles": "重新啟用訂閱",
"Paste": "貼上",
@@ -628,7 +631,6 @@
"Unknown": "未知",
"Auto update disabled": "自動更新已停用",
"Update subscription successfully": "訂閱更新成功",
"Update failed, retrying with Clash proxy...": "訂閱更新失敗,嘗試使用 Clash 代理更新",
"Update with Clash proxy successfully": "使用 Clash 代理更新成功",
"Update failed even with Clash proxy": "使用 Clash 代理更新也失敗",
"Profile creation failed, retrying with Clash proxy...": "訂閱建立失敗,嘗試使用 Clash 代理建立",
@@ -713,5 +715,7 @@
"Allow Auto Update": "允許自動更新",
"Menu reorder mode": "選單排序模式",
"Unlock menu order": "解鎖選單排序",
"Lock menu order": "鎖定選單排序"
"Lock menu order": "鎖定選單排序",
"Open App Log": "應用程式日誌",
"Open Core Log": "內核日誌"
}

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